清理代码;新增远程共育室公开接口
This commit is contained in:
@@ -5,6 +5,7 @@ import ink.snowflake.server.controller.User
|
|||||||
import ink.snowflake.server.controller.chat
|
import ink.snowflake.server.controller.chat
|
||||||
import ink.snowflake.server.utils.plugins.configureSockets
|
import ink.snowflake.server.utils.plugins.configureSockets
|
||||||
import ink.snowflake.server.controller.ImageAnalytics
|
import ink.snowflake.server.controller.ImageAnalytics
|
||||||
|
import ink.snowflake.server.controller.Public
|
||||||
import ink.snowflake.server.controller.RemoteDebug
|
import ink.snowflake.server.controller.RemoteDebug
|
||||||
import ink.snowflake.server.controller.Traceability
|
import ink.snowflake.server.controller.Traceability
|
||||||
import ink.snowflake.server.controller.VideoAnalytics
|
import ink.snowflake.server.controller.VideoAnalytics
|
||||||
@@ -18,6 +19,7 @@ import ink.snowflake.server.utils.plugins.configureSerialization
|
|||||||
import ink.snowflake.server.utils.plugins.configureStaticPath
|
import ink.snowflake.server.utils.plugins.configureStaticPath
|
||||||
import ink.snowflake.server.utils.plugins.configureStatusPages
|
import ink.snowflake.server.utils.plugins.configureStatusPages
|
||||||
import ink.snowflake.server.utils.plugins.configureTemplating
|
import ink.snowflake.server.utils.plugins.configureTemplating
|
||||||
|
import io.ktor.http.CacheControl
|
||||||
import io.ktor.server.application.*
|
import io.ktor.server.application.*
|
||||||
import io.ktor.server.tomcat.jakarta.*
|
import io.ktor.server.tomcat.jakarta.*
|
||||||
|
|
||||||
@@ -76,4 +78,6 @@ fun Application.module() {
|
|||||||
// 业务-图片分析
|
// 业务-图片分析
|
||||||
ImageAnalytics()
|
ImageAnalytics()
|
||||||
Traceability(appConfig)
|
Traceability(appConfig)
|
||||||
|
// 业务-公开接口
|
||||||
|
Public()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package ink.snowflake.server.controller
|
||||||
|
|
||||||
|
import ink.snowflake.server.SERVER_PATH_FRP
|
||||||
|
import ink.snowflake.server.model.response.BaseResponse
|
||||||
|
import io.ktor.server.application.Application
|
||||||
|
import io.ktor.server.response.respond
|
||||||
|
import io.ktor.server.routing.get
|
||||||
|
import io.ktor.server.routing.route
|
||||||
|
import io.ktor.server.routing.routing
|
||||||
|
|
||||||
|
|
||||||
|
fun Application.Public() {
|
||||||
|
routing {
|
||||||
|
route("/silk-remote") {
|
||||||
|
get("/connectLocalDevice") {
|
||||||
|
val port = call.parameters["port"]
|
||||||
|
if (port != null) {
|
||||||
|
runAdbCommand("disconnect")
|
||||||
|
runAdbCommand("connect ${SERVER_PATH_FRP}:$port")
|
||||||
|
val url =
|
||||||
|
"https://ai.ronsunny.cn:8090/remote#!action=stream&udid=s3.ronsunny.cn%3ATTT&player=mse&ws=wss%3A%2F%2Fai.ronsunny.cn%3A8090%2Fremote%3Faction%3Dproxy-adb%26remote%3Dtcp%253A8886%26udid%3Ds3.ronsunny.cn%253ATTT"
|
||||||
|
.replace(
|
||||||
|
"TTT",
|
||||||
|
port
|
||||||
|
)
|
||||||
|
call.respond(BaseResponse(data = url))
|
||||||
|
} else {
|
||||||
|
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get("/disConnectAll") {
|
||||||
|
val result = runAdbCommand("disconnect")
|
||||||
|
call.respond(BaseResponse(data = result))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -40,65 +40,66 @@ fun Application.RemoteDebug() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
routing {
|
routing {
|
||||||
authenticate {
|
authenticate {
|
||||||
route("/remote") {
|
route("/remote") {
|
||||||
get("/connect") {
|
get("/connect") {
|
||||||
val ip = call.parameters["ip"]
|
val ip = call.parameters["ip"]
|
||||||
val port = call.parameters["port"]
|
val port = call.parameters["port"]
|
||||||
if (ip != null && port != null) {
|
if (ip != null && port != null) {
|
||||||
val result = runAdbCommand("connect $ip:$port")
|
val result = runAdbCommand("connect $ip:$port")
|
||||||
call.respond(BaseResponse(data = result))
|
|
||||||
} else {
|
|
||||||
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get("/connectLocalDevice") {
|
|
||||||
val port = call.parameters["port"]
|
|
||||||
if (port != null) {
|
|
||||||
val result = runAdbCommand("connect ${SERVER_PATH_FRP}:$port")
|
|
||||||
call.respond(BaseResponse(data = result))
|
|
||||||
} else {
|
|
||||||
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get("/disConnectAll") {
|
|
||||||
val result = runAdbCommand("disconnect")
|
|
||||||
call.respond(BaseResponse(data = result))
|
call.respond(BaseResponse(data = result))
|
||||||
}
|
} else {
|
||||||
get("/runLinuxCommand") {
|
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
||||||
val command = call.parameters["command"]
|
|
||||||
if (command != null) {
|
|
||||||
call.respond(BaseResponse(data = runCommand(command)))
|
|
||||||
} else {
|
|
||||||
call.respond(BaseResponse(status = false, message = "Linux命令不可为空", data = null))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
get("/refreshDeviceList") {
|
|
||||||
try {
|
|
||||||
val name = call.parameters["name"]
|
|
||||||
val response: HttpResponse = client.get("http://${SERVER_PATH_FRP}:65534/api/proxy/tcp")
|
|
||||||
val responseBody: String = response.bodyAsText()
|
|
||||||
val devicesInfoRequest: DevicesInfoRequest = Gson().fromJson(responseBody, DevicesInfoRequest::class.java)
|
|
||||||
val onlineDevices = devicesInfoRequest.proxies.stream()
|
|
||||||
.filter { it.status == "online" && it.conf != null && it.conf.remotePort >= 10000 && it.conf.remotePort <= 20000 }
|
|
||||||
val devices: MutableList<DeviceItemResponse> = mutableListOf()
|
|
||||||
for (data in onlineDevices) {
|
|
||||||
if (name == "null" || name == null || data.name.contains(name) && data.conf != null) {
|
|
||||||
devices.add(DeviceItemResponse(data.name, data.conf!!.remotePort))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
call.respond(BaseResponse(data = devices))
|
|
||||||
} catch (e: Exception) {
|
|
||||||
call.respond(
|
|
||||||
BaseResponse(
|
|
||||||
status = false,
|
|
||||||
message = "端口信息请求失败:${e.message}",
|
|
||||||
data = null
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
get("/connectLocalDevice") {
|
||||||
|
val port = call.parameters["port"]
|
||||||
|
if (port != null) {
|
||||||
|
val result = runAdbCommand("connect ${SERVER_PATH_FRP}:$port")
|
||||||
|
call.respond(BaseResponse(data = result))
|
||||||
|
} else {
|
||||||
|
call.respond(BaseResponse(status = false, message = "IP或端口无效", data = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get("/disConnectAll") {
|
||||||
|
val result = runAdbCommand("disconnect")
|
||||||
|
call.respond(BaseResponse(data = result))
|
||||||
|
}
|
||||||
|
get("/runLinuxCommand") {
|
||||||
|
val command = call.parameters["command"]
|
||||||
|
if (command != null) {
|
||||||
|
call.respond(BaseResponse(data = runCommand(command)))
|
||||||
|
} else {
|
||||||
|
call.respond(BaseResponse(status = false, message = "Linux命令不可为空", data = null))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
get("/refreshDeviceList") {
|
||||||
|
try {
|
||||||
|
val name = call.parameters["name"]
|
||||||
|
val response: HttpResponse = client.get("http://${SERVER_PATH_FRP}:65534/api/proxy/tcp")
|
||||||
|
val responseBody: String = response.bodyAsText()
|
||||||
|
val devicesInfoRequest: DevicesInfoRequest =
|
||||||
|
Gson().fromJson(responseBody, DevicesInfoRequest::class.java)
|
||||||
|
val onlineDevices = devicesInfoRequest.proxies.stream()
|
||||||
|
.filter { it.status == "online" && it.conf != null && it.conf.remotePort >= 10000 && it.conf.remotePort <= 20000 }
|
||||||
|
val devices: MutableList<DeviceItemResponse> = mutableListOf()
|
||||||
|
for (data in onlineDevices) {
|
||||||
|
if (name == "null" || name == null || data.name.contains(name) && data.conf != null) {
|
||||||
|
devices.add(DeviceItemResponse(data.name, data.conf!!.remotePort))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
call.respond(BaseResponse(data = devices))
|
||||||
|
} catch (e: Exception) {
|
||||||
|
call.respond(
|
||||||
|
BaseResponse(
|
||||||
|
status = false,
|
||||||
|
message = "端口信息请求失败:${e.message}",
|
||||||
|
data = null
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
webSocket("/logStream") {
|
webSocket("/logStream") {
|
||||||
send("日志系统连接成功")
|
send("日志系统连接成功")
|
||||||
|
|||||||
@@ -101,7 +101,7 @@
|
|||||||
| https | **8093** | 8883 | 8883 | ce_emqx | EMQX | MQTT TCP TLS 端口 |
|
| https | **8093** | 8883 | 8883 | ce_emqx | EMQX | MQTT TCP TLS 端口 |
|
||||||
| | | 8083 | 8083 | ce_emqx | EMQX | MQTT WS 端口 |
|
| | | 8083 | 8083 | ce_emqx | EMQX | MQTT WS 端口 |
|
||||||
| | | 8084 | 8084 | ce_emqx | EMQX | MQTT WS TLS 端口 |
|
| | | 8084 | 8084 | ce_emqx | EMQX | MQTT WS TLS 端口 |
|
||||||
| | | | | | | |
|
| | | 3000 | | ce_gitea | Gitea | 管理页面:bbit:12345678 |
|
||||||
| | | | | | | |
|
| | | | | | | |
|
||||||
| | | | | | | |
|
| | | | | | | |
|
||||||
| | | | | | | |
|
| | | | | | | |
|
||||||
|
|||||||
@@ -1,20 +0,0 @@
|
|||||||
# 灵活溯源系统 MVP
|
|
||||||
|
|
||||||
这是一个纯前端的 H5 演示版,直接打开 `index.html` 就能看效果。
|
|
||||||
|
|
||||||
## 已包含内容
|
|
||||||
|
|
||||||
- 管理员端:节点库、字段编辑、模板创建、公共资料块复用
|
|
||||||
- 业务员端:基于模板新建批次、逐节点填报、二维码预览
|
|
||||||
- 消费者端:时间轴溯源展示、企业信息、县域情况、有机证书
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
|
|
||||||
1. 直接双击打开 `index.html`
|
|
||||||
2. 或者用任意静态文件服务打开 `trace-demo` 目录
|
|
||||||
|
|
||||||
## 说明
|
|
||||||
|
|
||||||
- 演示数据保存在浏览器 `localStorage`
|
|
||||||
- 点击“重置演示数据”可以恢复默认内容
|
|
||||||
- 当前二维码是本地样式模拟,后续可替换成真实二维码
|
|
||||||
-1016
File diff suppressed because it is too large
Load Diff
@@ -1,16 +0,0 @@
|
|||||||
<!DOCTYPE html>
|
|
||||||
<html lang="zh-CN">
|
|
||||||
<head>
|
|
||||||
<meta charset="UTF-8" />
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
||||||
<title>灵活溯源系统 MVP</title>
|
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com" />
|
|
||||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin />
|
|
||||||
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;600;700;800&display=swap" rel="stylesheet" />
|
|
||||||
<link rel="stylesheet" href="./styles.css" />
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
<div id="app"></div>
|
|
||||||
<script src="./app.js"></script>
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
:root {
|
|
||||||
--bg: #f4f6f9;
|
|
||||||
--panel: #ffffff;
|
|
||||||
--panel-soft: #f9fafc;
|
|
||||||
--line: #e6ebf2;
|
|
||||||
--text: #172033;
|
|
||||||
--text-soft: #5f6b85;
|
|
||||||
--brand: #1958d6;
|
|
||||||
--brand-soft: #edf3ff;
|
|
||||||
--success: #0f8c62;
|
|
||||||
--danger: #c84242;
|
|
||||||
--radius-lg: 20px;
|
|
||||||
--radius-md: 14px;
|
|
||||||
--radius-sm: 10px;
|
|
||||||
--shadow: 0 12px 40px rgba(18, 30, 67, 0.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
* { box-sizing: border-box; }
|
|
||||||
html, body { margin: 0; min-height: 100%; font-family: "Noto Sans SC", sans-serif; background: var(--bg); color: var(--text); }
|
|
||||||
button, input, select, textarea { font: inherit; }
|
|
||||||
button { cursor: pointer; }
|
|
||||||
a { color: var(--brand); text-decoration: none; }
|
|
||||||
|
|
||||||
.system-shell { min-height: 100vh; display: grid; grid-template-columns: 220px minmax(0, 1fr); }
|
|
||||||
.main-nav {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
gap: 10px;
|
|
||||||
padding: 24px 18px;
|
|
||||||
background: linear-gradient(180deg, #14213f 0%, #1e305d 100%);
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
.brand { font-size: 22px; font-weight: 800; margin-bottom: 14px; }
|
|
||||||
.nav-btn {
|
|
||||||
border: 1px solid transparent;
|
|
||||||
background: rgba(255, 255, 255, 0.08);
|
|
||||||
color: #eef3ff;
|
|
||||||
border-radius: 14px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.nav-btn.active { background: #fff; color: var(--brand); }
|
|
||||||
.nav-btn.danger { margin-top: auto; color: #ffd2d2; border-color: rgba(255,255,255,0.1); }
|
|
||||||
|
|
||||||
.main-panel { padding: 22px; }
|
|
||||||
.content-shell { display: grid; gap: 18px; }
|
|
||||||
.content-shell.two-col, .content-shell.operator-layout { grid-template-columns: 320px minmax(0, 1fr); }
|
|
||||||
.content-shell.single-col { grid-template-columns: 1fr; }
|
|
||||||
|
|
||||||
.left-pane, .right-pane, .editor-card, .stats-card {
|
|
||||||
background: var(--panel);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: var(--radius-lg);
|
|
||||||
box-shadow: var(--shadow);
|
|
||||||
}
|
|
||||||
|
|
||||||
.left-pane, .right-pane { padding: 18px; }
|
|
||||||
.right-pane { display: grid; gap: 18px; align-content: start; }
|
|
||||||
.editor-card { padding: 18px; }
|
|
||||||
|
|
||||||
.pane-head {
|
|
||||||
display: flex;
|
|
||||||
justify-content: space-between;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
margin-bottom: 16px;
|
|
||||||
}
|
|
||||||
.pane-head h2, .timeline-head h3 { margin: 0; font-size: 20px; }
|
|
||||||
.pane-actions, .editor-actions, .template-toolbar, .query-bar, .inline-select, .sub-tabs { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; }
|
|
||||||
.muted-line { margin: 6px 0 0; color: var(--text-soft); font-size: 13px; }
|
|
||||||
|
|
||||||
.scroll-list { display: grid; gap: 12px; max-height: calc(100vh - 150px); overflow: auto; padding-right: 4px; }
|
|
||||||
.list-card {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: var(--panel-soft);
|
|
||||||
border-radius: var(--radius-md);
|
|
||||||
padding: 14px;
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
text-align: left;
|
|
||||||
color: var(--text);
|
|
||||||
}
|
|
||||||
.list-card.active { border-color: var(--brand); background: var(--brand-soft); }
|
|
||||||
.list-card span { color: var(--text-soft); font-size: 13px; }
|
|
||||||
|
|
||||||
.primary-btn, .ghost-btn, .sub-tab {
|
|
||||||
border-radius: 12px;
|
|
||||||
padding: 10px 14px;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
}
|
|
||||||
.primary-btn { background: var(--brand); color: #fff; }
|
|
||||||
.ghost-btn, .sub-tab { background: var(--panel-soft); color: var(--text); border-color: var(--line); }
|
|
||||||
.danger-btn { color: var(--danger); }
|
|
||||||
.sub-tab.active { background: var(--brand-soft); color: var(--brand); border-color: #cadeff; }
|
|
||||||
|
|
||||||
.form-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 14px; }
|
|
||||||
.form-grid.compact { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
|
||||||
.field { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
.field.full { grid-column: 1 / -1; }
|
|
||||||
.field label { font-size: 13px; font-weight: 700; color: var(--text-soft); }
|
|
||||||
.field input, .field select, .field textarea {
|
|
||||||
width: 100%;
|
|
||||||
border: 1px solid #d6deea;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff;
|
|
||||||
color: var(--text);
|
|
||||||
padding: 11px 12px;
|
|
||||||
min-height: 44px;
|
|
||||||
}
|
|
||||||
.field textarea { min-height: 110px; resize: vertical; }
|
|
||||||
.field input:disabled, .field select:disabled, .field textarea:disabled { background: #f3f5f8; color: #79859e; }
|
|
||||||
|
|
||||||
.switch { position: relative; width: 54px; height: 32px; }
|
|
||||||
.switch input { opacity: 0; width: 0; height: 0; }
|
|
||||||
.switch span {
|
|
||||||
position: absolute;
|
|
||||||
inset: 0;
|
|
||||||
background: #cdd6e4;
|
|
||||||
border-radius: 999px;
|
|
||||||
transition: 0.2s ease;
|
|
||||||
}
|
|
||||||
.switch span::before {
|
|
||||||
content: "";
|
|
||||||
position: absolute;
|
|
||||||
width: 22px;
|
|
||||||
height: 22px;
|
|
||||||
left: 5px;
|
|
||||||
top: 5px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #fff;
|
|
||||||
transition: 0.2s ease;
|
|
||||||
box-shadow: 0 4px 12px rgba(23, 32, 51, 0.18);
|
|
||||||
}
|
|
||||||
.switch input:checked + span { background: var(--brand); }
|
|
||||||
.switch input:checked + span::before { transform: translateX(22px); }
|
|
||||||
|
|
||||||
.template-toolbar { margin: 16px 0; }
|
|
||||||
.inline-select select { min-width: 180px; }
|
|
||||||
.node-strip {
|
|
||||||
display: flex;
|
|
||||||
gap: 10px;
|
|
||||||
flex-wrap: wrap;
|
|
||||||
padding: 6px 0 2px;
|
|
||||||
}
|
|
||||||
.node-pill {
|
|
||||||
display: grid;
|
|
||||||
gap: 4px;
|
|
||||||
min-width: 160px;
|
|
||||||
padding: 14px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: var(--panel-soft);
|
|
||||||
border-radius: 16px;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
.node-pill.active { border-color: var(--brand); background: var(--brand-soft); }
|
|
||||||
.pill-order { color: var(--brand); font-size: 12px; font-weight: 800; }
|
|
||||||
.pill-name { font-weight: 700; }
|
|
||||||
.pill-tag { color: var(--text-soft); font-size: 12px; }
|
|
||||||
|
|
||||||
.readonly-tip {
|
|
||||||
margin-bottom: 16px;
|
|
||||||
padding: 12px 14px;
|
|
||||||
border-radius: 12px;
|
|
||||||
background: #fff6e9;
|
|
||||||
color: #8d5a16;
|
|
||||||
border: 1px solid #f2ddbb;
|
|
||||||
}
|
|
||||||
.field-list { display: grid; gap: 12px; margin-top: 16px; }
|
|
||||||
.field-card {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 16px;
|
|
||||||
background: var(--panel-soft);
|
|
||||||
padding: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.progress-strip {
|
|
||||||
display: flex;
|
|
||||||
gap: 12px;
|
|
||||||
overflow: auto;
|
|
||||||
padding-bottom: 4px;
|
|
||||||
}
|
|
||||||
.progress-step {
|
|
||||||
min-width: 132px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: var(--panel-soft);
|
|
||||||
border-radius: 16px;
|
|
||||||
padding: 12px;
|
|
||||||
text-align: left;
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.progress-step.active { border-color: var(--brand); background: var(--brand-soft); }
|
|
||||||
.progress-step.done .step-index { background: var(--success); }
|
|
||||||
.step-index {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
border-radius: 50%;
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
background: var(--brand);
|
|
||||||
color: #fff;
|
|
||||||
font-size: 12px;
|
|
||||||
font-weight: 800;
|
|
||||||
}
|
|
||||||
.step-name { font-size: 13px; font-weight: 700; }
|
|
||||||
|
|
||||||
.chips { display: flex; flex-wrap: wrap; gap: 10px; }
|
|
||||||
.chip-check {
|
|
||||||
display: inline-flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 8px;
|
|
||||||
padding: 8px 10px;
|
|
||||||
border-radius: 999px;
|
|
||||||
background: var(--panel-soft);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
}
|
|
||||||
|
|
||||||
.consumer-tabs { display: flex; gap: 10px; margin-bottom: 16px; }
|
|
||||||
.consumer-shell {
|
|
||||||
background: linear-gradient(180deg, #fefefe 0%, #f5f7fb 100%);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
border-radius: 22px;
|
|
||||||
padding: 22px;
|
|
||||||
}
|
|
||||||
.consumer-topbar { margin-bottom: 18px; }
|
|
||||||
.consumer-topbar h2 { margin: 0; }
|
|
||||||
.consumer-topbar p { margin: 8px 0 0; color: var(--text-soft); }
|
|
||||||
|
|
||||||
.timeline-v2 { display: grid; gap: 0; }
|
|
||||||
.timeline-row { display: grid; grid-template-columns: 48px minmax(0, 1fr); gap: 14px; }
|
|
||||||
.timeline-rail {
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: center;
|
|
||||||
position: relative;
|
|
||||||
}
|
|
||||||
.timeline-dot {
|
|
||||||
width: 16px;
|
|
||||||
height: 16px;
|
|
||||||
border-radius: 50%;
|
|
||||||
background: #c7d2e7;
|
|
||||||
border: 4px solid #eef3ff;
|
|
||||||
z-index: 1;
|
|
||||||
margin-top: 18px;
|
|
||||||
}
|
|
||||||
.timeline-dot.active { background: var(--brand); }
|
|
||||||
.timeline-line {
|
|
||||||
width: 2px;
|
|
||||||
flex: 1;
|
|
||||||
background: linear-gradient(180deg, #bdd0ff 0%, #e0e8f6 100%);
|
|
||||||
margin-top: 8px;
|
|
||||||
}
|
|
||||||
.timeline-body {
|
|
||||||
margin-bottom: 18px;
|
|
||||||
padding: 18px;
|
|
||||||
border-radius: 18px;
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: #fff;
|
|
||||||
}
|
|
||||||
.timeline-head span { display: block; margin-top: 8px; color: var(--text-soft); font-size: 13px; }
|
|
||||||
.timeline-grid, .materials-grid {
|
|
||||||
display: grid;
|
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
|
||||||
gap: 12px;
|
|
||||||
margin-top: 16px;
|
|
||||||
}
|
|
||||||
.kv-card {
|
|
||||||
padding: 14px;
|
|
||||||
border-radius: 14px;
|
|
||||||
background: var(--panel-soft);
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
display: grid;
|
|
||||||
gap: 6px;
|
|
||||||
}
|
|
||||||
.kv-card span { color: var(--text-soft); font-size: 12px; }
|
|
||||||
.kv-card strong { font-size: 14px; word-break: break-word; }
|
|
||||||
.material-card {
|
|
||||||
border: 1px solid var(--line);
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18px;
|
|
||||||
padding: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.stats-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 18px; }
|
|
||||||
.stats-card {
|
|
||||||
padding: 26px;
|
|
||||||
display: grid;
|
|
||||||
gap: 14px;
|
|
||||||
}
|
|
||||||
.stats-card span { color: var(--text-soft); }
|
|
||||||
.stats-card strong { font-size: 40px; line-height: 1; }
|
|
||||||
|
|
||||||
.empty-panel {
|
|
||||||
min-height: 180px;
|
|
||||||
border: 1px dashed #cfd7e4;
|
|
||||||
border-radius: 16px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
color: var(--text-soft);
|
|
||||||
background: var(--panel-soft);
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 1080px) {
|
|
||||||
.system-shell, .content-shell.two-col, .content-shell.operator-layout { grid-template-columns: 1fr; }
|
|
||||||
.main-nav { flex-direction: row; flex-wrap: wrap; align-items: center; }
|
|
||||||
.nav-btn.danger { margin-top: 0; margin-left: auto; }
|
|
||||||
}
|
|
||||||
|
|
||||||
@media (max-width: 760px) {
|
|
||||||
.main-panel { padding: 14px; }
|
|
||||||
.form-grid, .form-grid.compact, .timeline-grid, .materials-grid, .stats-grid { grid-template-columns: 1fr; }
|
|
||||||
.timeline-row { grid-template-columns: 28px minmax(0, 1fr); gap: 10px; }
|
|
||||||
.node-pill, .progress-step { min-width: 120px; }
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user