diff --git a/DOCKER_DEPLOY.md b/DOCKER_DEPLOY.md index 165d811..4626e5a 100644 --- a/DOCKER_DEPLOY.md +++ b/DOCKER_DEPLOY.md @@ -182,6 +182,7 @@ tools/build_image.sh <提交号> <镜像tag> 该脚本的行为约束: - 先 `git fetch`,再把提交号解析为完整 SHA,解析失败立即中止,绝不使用本地脏状态或服务器旧目录; +- 构建前读取当前线上容器镜像的 `org.opencontainers.image.revision`,用 Git 祖先关系确认候选提交包含线上全部历史;落后 `main`、旁支或错误提交会直接退出,并打印线上提交、候选提交、文件差异和将丢失的提交; - 镜像 tag 必须以 `-<提交短号7位>` 结尾(如 `hel234-cefc869`),禁止 `latest`、`rollback-*`; - 通过 `git archive <提交> | ssh 部署机 docker build -` 流式构建,服务器上不存在构建用工作树; - 构建后回读镜像 label 里的 `org.opencontainers.image.revision`,与预期提交不一致则删除镜像并中止; diff --git a/frontend/m/css/features.css b/frontend/m/css/features.css index 671da68..baf3745 100644 --- a/frontend/m/css/features.css +++ b/frontend/m/css/features.css @@ -3192,8 +3192,7 @@ .m-sys-grid div { padding: 12px; border-radius: 8px; - background: var(--surface); - box-shadow: var(--elevation-card); + background: var(--surface-muted); } .m-sys-grid span { @@ -3209,16 +3208,424 @@ color: var(--text-primary); } -.m-sys-account-actions { - display: grid; +.m-sys-home { + padding: 0 0 12px; + display: flex; + flex-direction: column; gap: 8px; - margin-top: 16px; } -.m-page[data-page^="system/"] .m-btn-primary { - margin-bottom: 8px; +.m-sys-body { + padding: 12px 12px 20px; + display: flex; + flex-direction: column; + gap: 12px; +} + +.m-sys-home .m-card, +.m-sys-body .m-card { + margin-left: 0; + margin-right: 0; +} + +.m-sys-profile-card { + display: flex; + gap: 12px; + align-items: flex-start; + padding: 14px; +} + +.m-sys-avatar { + flex: 0 0 auto; + width: 48px; + height: 48px; + border-radius: 50%; + display: flex; + align-items: center; + justify-content: center; + background: var(--action-soft); + color: var(--action); + font-size: var(--font-size-page-title); + font-weight: 600; +} + +.m-sys-profile-card strong { + display: block; + font-size: var(--font-size-card-title); + color: var(--text-primary); +} + +.m-sys-profile-meta { + margin: 4px 0 8px; + font-size: var(--font-size-caption); + color: var(--text-secondary); +} + +.m-sys-badges { + display: flex; + flex-wrap: wrap; + gap: 6px; +} + +.m-sys-badge { + display: inline-flex; + align-items: center; + height: 22px; + padding: 0 8px; + border-radius: 999px; + font-size: var(--font-size-aux); + font-weight: 600; + background: var(--surface-muted); + color: var(--text-secondary); +} + +.m-sys-badge--admin { + background: var(--action-soft); + color: var(--action); +} + +.m-sys-badge--ok { + background: var(--market-down-soft); + color: var(--market-down); +} + +.m-sys-group-title { + margin: 4px 0 0; + font-size: var(--font-size-caption); + font-weight: 600; + color: var(--text-tertiary); +} + +.m-sys-list { + padding: 0; + overflow: hidden; +} + +.m-sys-row { + width: 100%; + display: flex; + align-items: center; + gap: 12px; + padding: 10px 12px; + border: 0; + background: transparent; + color: inherit; + text-align: left; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.m-sys-row + .m-sys-row { + border-top: 1px solid var(--border); +} + +.m-sys-row:active { + background: var(--surface-hover); +} + +.m-sys-row-icon { + flex: 0 0 auto; + width: 36px; + height: 36px; + border-radius: 10px; + display: inline-flex; + align-items: center; + justify-content: center; + background: var(--action-soft); + color: var(--action); +} + +.m-sys-row--danger .m-sys-row-icon { + background: var(--market-up-soft); + color: var(--market-up); +} + +.m-sys-row-body { + flex: 1; + min-width: 0; +} + +.m-sys-row-body strong { + display: block; + font-size: var(--font-size-body); + font-weight: 600; + color: var(--text-primary); +} + +.m-sys-row-body small { + display: block; + margin-top: 2px; + font-size: var(--font-size-caption); + color: var(--text-secondary); +} + +.m-sys-row-chevron { + flex: 0 0 auto; + color: var(--text-tertiary); +} + +.m-sys-foot { + margin: 8px 0 0; + text-align: center; + font-size: var(--font-size-caption); + color: var(--text-tertiary); +} + +.m-sys-notice { + padding: 12px; + border-radius: 12px; + background: var(--surface-muted); +} + +.m-sys-notice p { + margin: 0; + font-size: var(--font-size-label); + color: var(--text-secondary); + line-height: 1.55; +} + +.m-sys-notice .m-sys-badges { + margin-top: 8px; +} + +.m-sys-section { + padding: 12px; +} + +.m-sys-section > strong, +.m-sys-section-title { + display: block; + margin-bottom: 10px; + font-size: var(--font-size-card-title); + color: var(--text-primary); +} + +.m-sys-hint { + margin: 8px 0 0; + font-size: var(--font-size-caption); + color: var(--text-tertiary); + line-height: 1.45; +} + +.m-sys-status-list { + display: grid; + gap: 8px; +} + +.m-sys-status-item { + display: flex; + align-items: center; + justify-content: space-between; + gap: 8px; + font-size: var(--font-size-body); + color: var(--text-primary); +} + +.m-sys-dot { + width: 8px; + height: 8px; + border-radius: 50%; + background: var(--text-tertiary); +} + +.m-sys-dot--ok { + background: var(--market-down); +} + +.m-sys-switch-row { + display: flex; + align-items: center; + justify-content: space-between; + gap: 12px; +} + +.m-sys-switch-row strong { + display: block; + font-size: var(--font-size-body); + margin-bottom: 0; +} + +.m-btn-outline, +.m-btn-outline-danger { + width: 100%; + height: 44px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + border-radius: 8px; + background: var(--surface); + font-size: var(--font-size-body); + font-weight: 600; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.m-btn-outline { + border: 1px solid var(--action); + color: var(--action); +} + +.m-btn-outline:active { + background: var(--action-soft); +} + +.m-btn-outline-danger { + border: 1px solid var(--market-up); + color: var(--market-up); +} + +.m-btn-outline-danger:active { + background: var(--market-up-soft); +} + +.m-btn-outline:disabled, +.m-btn-outline-danger:disabled { + opacity: 0.45; + cursor: default; + pointer-events: none; +} + +.m-sys-model-card .m-sys-badges, +.m-sys-user-row .m-sys-badges { + margin-top: 6px; +} + +.m-sys-model-card { + display: flex; + align-items: flex-start; + justify-content: space-between; + gap: 8px; + width: 100%; + padding: 12px; + border: 0; + background: transparent; + text-align: left; + cursor: pointer; +} + +.m-sys-model-card + .m-sys-model-card { + border-top: 1px solid var(--border); +} + +.m-sys-model-card:active { + background: var(--surface-hover); +} + +.m-sys-user-row { + display: flex; + align-items: center; + gap: 10px; + padding: 12px; +} + +.m-sys-user-row + .m-sys-user-row { + border-top: 1px solid var(--border); +} + +.m-sys-user-row .m-btn-outline { + width: auto; + height: 32px; + padding: 0 12px; + flex: 0 0 auto; +} + +.m-sys-pair { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.m-sheet-root.is-dialog .m-sheet { + display: none; +} + +.m-dialog { + position: absolute; + left: 50%; + top: 42%; + width: calc(100% - 48px); + max-width: 320px; + transform: translate(-50%, -46%) scale(0.96); + border-radius: 12px; + background: var(--surface); + box-shadow: var(--elevation-float); + padding: 18px 16px 14px; + z-index: 3; + opacity: 0; + transition: opacity var(--motion-enter) var(--ease-enter), + transform var(--motion-enter) var(--ease-enter); +} + +.m-sheet-root.is-open .m-dialog { + opacity: 1; + transform: translate(-50%, -50%) scale(1); +} + +.m-dialog h2 { + margin: 0 0 8px; + font-size: var(--font-size-card-title); + color: var(--text-primary); + text-align: center; +} + +.m-dialog p { + margin: 0 0 16px; + font-size: var(--font-size-label); + color: var(--text-secondary); + line-height: 1.5; + text-align: center; +} + +.m-dialog-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; +} + +.m-dialog-actions .m-btn-outline, +.m-dialog-actions .m-btn-outline-danger, +.m-dialog-actions .m-btn-primary { + height: 40px; +} + +.m-sys-sheet-actions { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 8px; + margin-top: 12px; +} + +.m-sys-test-row { + display: flex; + align-items: center; + gap: 10px; + margin: 8px 0 12px; +} + +.m-sys-test-row .m-btn-outline { + width: auto; + flex: 0 0 auto; + height: 36px; + padding: 0 12px; +} + +.m-sys-test-row [data-model-test-status] { + flex: 1; + font-size: var(--font-size-caption); + color: var(--text-secondary); +} + +.m-sys-hero { + padding: 14px; +} + +.m-sys-hero strong { + display: block; + font-size: var(--font-size-page-title); + margin-bottom: 6px; } .m-page[data-page^="system/"] .m-card { - margin-bottom: 12px; + margin-bottom: 0; } diff --git a/frontend/m/js/pages.js b/frontend/m/js/pages.js index 8e78515..6f353dc 100644 --- a/frontend/m/js/pages.js +++ b/frontend/m/js/pages.js @@ -91,6 +91,14 @@ "sticky-note": '', bell: '', lock: '', + user: '', + gem: '', + users: '', + "sliders-horizontal": '', + sun: '', + moon: '', + "arrow-left-right": '', + "log-out": '', }; function icon(name, size) { @@ -191,6 +199,10 @@ account: null, admin: null, adminTab: "market", + models: [], + accounts: [], + editingModelId: "", + editingUserId: "", }, }; @@ -1594,11 +1606,43 @@ /* ---------------------------------------------------------------- helpers shared by new pages */ + function bindConfirmAction(onConfirm) { + const ok = document.querySelector("[data-confirm-ok]"); + if (ok && typeof onConfirm === "function") { + ok.addEventListener("click", function () { + closeSheet(); + onConfirm(); + }); + } + } + + function openCenteredDialog(content) { + const root = ensureSheetRoot(); + sheetToken += 1; + root.classList.add("is-dialog"); + root.innerHTML = + '
' + + '"; + global.requestAnimationFrame(function () { root.classList.add("is-open"); }); + } + function openConfirmSheet(title, body, options) { const opts = options || {}; const confirmLabel = opts.confirmLabel || "确定"; const cancelLabel = opts.cancelLabel || "取消"; const danger = Boolean(opts.danger); + if (opts.centered) { + openCenteredDialog( + "

" + escapeHtml(title) + "

" + + (body ? "

" + escapeHtml(body) + "

" : "") + + '
' + + '" + + '" + + "
" + ); + bindConfirmAction(opts.onConfirm); + return; + } openSheet( '

' + escapeHtml(title) + '

' + '
' + @@ -1610,13 +1654,7 @@ '', { detail: false } ); - const ok = document.querySelector('[data-confirm-ok]'); - if (ok && typeof opts.onConfirm === 'function') { - ok.addEventListener('click', function () { - closeSheet(); - opts.onConfirm(); - }); - } + bindConfirmAction(opts.onConfirm); } function nextSeq() { @@ -3123,6 +3161,7 @@ function openSheet(content, opts) { const root = ensureSheetRoot(); sheetToken += 1; + root.classList.remove("is-dialog"); root.innerHTML = '
' + '