diff --git a/DESIGN-TOKENS.md b/DESIGN-TOKENS.md index f806881..d418306 100644 --- a/DESIGN-TOKENS.md +++ b/DESIGN-TOKENS.md @@ -1,6 +1,8 @@ # Design Tokens -This file maps the shipped dark visual system to its implementation source in `web/styles.css`. `DESIGN.md` is the portable design contract; the `:root` custom properties in `web/styles.css` are the runtime source of truth. +**HEL-342 定稿:** 运行时视觉来源是 `web/design-system.css` 的霜曜日间 + 黑金夜间变量(`.v-fusion` / `[data-theme="night"]`),依据 `jinniu-fusion-design`。禁止退回旧青绿 token 或叠第二套覆盖层。主题偏好只存 `localStorage.jinniu-theme`(`day` | `night`),不跟随系统、不恢复业务数据。 + +下文是历史暗色合同存档,不再作为施工依据。 ## Architecture @@ -8,7 +10,6 @@ This file maps the shipped dark visual system to its implementation source in `w - Keep shared primitives in `:root`; keep component behavior in its existing semantic rule group. - Business-component rules must not use `!important`. - Only accessibility utilities (`.sr-only`, `[hidden]`) and `prefers-reduced-motion` enforcement may force priority. -- Do not restore warm-paper, white-card, cobalt-action, cream, or light-theme aliases. New code must consume the dark semantic tokens directly. - Keep administrator and cashier portals as independent route-level surfaces even when they share tokens and component primitives. ## Typography diff --git a/scripts/hel342_shot.py b/scripts/hel342_shot.py new file mode 100644 index 0000000..51be419 --- /dev/null +++ b/scripts/hel342_shot.py @@ -0,0 +1,126 @@ +"""HEL-342: static visual shots + computed-style probe (no business data writes).""" +from __future__ import annotations + +import json +from functools import partial +from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer +from pathlib import Path +from threading import Thread + +ROOT = Path(__file__).resolve().parents[1] +WEB = ROOT / "web" +OUT = ROOT.parent / "hel342-shots" + +PROBES = [ + (".sidebar", ["width", "backgroundColor", "backdropFilter"]), + (".brand-logo-day", ["width", "height"]), + (".btn-primary", ["backgroundColor", "height", "borderRadius", "color"]), + (".stat-card.gold .stat-value, .stat-card .stat-value", ["color", "fontSize", "fontVariantNumeric"]), + (".page-head h1", ["fontSize", "fontWeight"]), + (".ds-table th", ["fontSize", "color"]), +] + + +def main() -> None: + from playwright.sync_api import sync_playwright + + OUT.mkdir(parents=True, exist_ok=True) + handler = partial(SimpleHTTPRequestHandler, directory=str(WEB)) + httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler) + Thread(target=httpd.serve_forever, daemon=True).start() + base = f"http://127.0.0.1:{httpd.server_address[1]}" + report = [] + + pages = [ + ("index", "index.html", None, None), + ("login-admin", "login-admin.html", None, None), + ("login-company", "login-company.html", None, None), + ("admin-dashboard", "admin.html", "dashboard", None), + ("admin-flows", "admin.html", "flows", None), + ("admin-settings", "admin.html", "settings", None), + ("admin-audit", "admin.html", "audit", None), + ("admin-reminders", "admin.html", "reminders", None), + ("admin-period-audit", "admin.html", "period-audit", None), + ("admin-companies", "admin.html", "companies", None), + ("admin-pair", "admin.html", "pair", None), + ("company-workspace", "company.html", "workspace", None), + ("company-transfers", "company.html", "transfers", None), + ("company-reconcile", "company.html", "reconcile", None), + ("company-upload", "company.html", "upload", None), + ] + + with sync_playwright() as p: + browser = p.chromium.launch(headless=True, args=["--no-sandbox"]) + for theme in ("day", "night"): + for width, height, tag in ((1440, 900, "1440"), (820, 900, "820"), (390, 844, "390")): + if tag != "1440" and theme == "night" and width != 390: + continue + page = browser.new_page(viewport={"width": width, "height": height}) + page.add_init_script( + f"() => {{ try {{ localStorage.setItem('jinniu-theme', '{theme}'); }} catch (e) {{}} }}" + ) + for name, html, view, _ in pages: + if tag != "1440" and name not in ("login-admin", "admin-flows", "company-transfers", "admin-dashboard"): + continue + page.route("**/app.js**", lambda route: route.abort()) + page.goto(f"{base}/{html}", wait_until="domcontentloaded") + page.evaluate( + """(args) => { + document.documentElement.classList.add('v-fusion'); + document.documentElement.setAttribute('data-theme', args.theme); + if (args.view) { + document.querySelectorAll('.app-view').forEach((el) => { + el.classList.toggle('is-active', el.dataset.page === args.view); + }); + } + }""", + {"theme": theme, "view": view}, + ) + page.wait_for_timeout(80) + shot = OUT / f"{name}-{theme}-{tag}.png" + page.screenshot(path=str(shot), full_page=False) + if tag == "1440" and name in ("login-admin", "admin-dashboard", "admin-flows"): + probe = page.evaluate( + """(sels) => sels.map(([sel, props]) => { + const el = document.querySelector(sel); + if (!el) return { sel, missing: true }; + const cs = getComputedStyle(el); + const out = { sel }; + props.forEach((p) => { out[p] = cs[p]; }); + const box = el.getBoundingClientRect(); + out.box = { w: Math.round(box.width), h: Math.round(box.height) }; + return out; + })""", + PROBES, + ) + overflow = page.evaluate( + "() => document.documentElement.scrollWidth > document.documentElement.clientWidth + 1" + ) + report.append({"page": name, "theme": theme, "overflow": overflow, "probe": probe}) + page.close() + + # 390 drawer + page = browser.new_page(viewport={"width": 390, "height": 844}) + page.add_init_script("() => { try { localStorage.setItem('jinniu-theme', 'day'); } catch (e) {} }") + page.route("**/app.js**", lambda route: route.abort()) + page.goto(f"{base}/company.html", wait_until="domcontentloaded") + page.evaluate( + """() => { + document.documentElement.classList.add('v-fusion'); + document.querySelectorAll('.app-view').forEach((el) => { + el.classList.toggle('is-active', el.dataset.page === 'transfers'); + }); + const d = document.getElementById('transferEvidenceDrawer'); + if (d) d.classList.add('is-open'); + }""" + ) + page.screenshot(path=str(OUT / "company-transfers-drawer-day-390.png"), full_page=False) + page.close() + browser.close() + httpd.shutdown() + (OUT / "probe.json").write_text(json.dumps(report, ensure_ascii=False, indent=2), encoding="utf-8") + print(f"wrote {OUT} ({len(list(OUT.glob('*.png')))} png)") + + +if __name__ == "__main__": + main() diff --git a/tests/test_company_confirm_status_color.py b/tests/test_company_confirm_status_color.py index f09bd1a..6253336 100644 --- a/tests/test_company_confirm_status_color.py +++ b/tests/test_company_confirm_status_color.py @@ -50,7 +50,7 @@ class ConfirmStatusSourceContractTests(unittest.TestCase): self.assertIn('id="workspacePendingStatus"', html) self.assertIn('id="workspaceFlowSub"', html) self.assertIn('data-view-link="reconcile"', html) - self.assertIn("app.js?v=15", html) + self.assertIn("app.js?v=16", html) # 静态初值仍为进行中(黄),由 JS 在 pending=0 时切 done self.assertRegex(html, r'class="flow-step doing"[^>]*data-view-link="reconcile"') diff --git a/tests/test_company_transfers_page.py b/tests/test_company_transfers_page.py index 123b254..91493a3 100644 --- a/tests/test_company_transfers_page.py +++ b/tests/test_company_transfers_page.py @@ -31,8 +31,8 @@ class TransfersPageSourceContractTests(unittest.TestCase): self.assertIn('id="transferEvidenceDrawer"', html) self.assertIn("期间净变动", html) self.assertNotIn("本公司往来合计", html) - self.assertIn("design-system.css?v=7", html) - self.assertIn("app.js?v=15", html) + self.assertIn("design-system.css?v=11", html) + self.assertIn("app.js?v=16", html) # 侧栏顺序:流水管理 → 转账往来 → 往来确认 flows = html.index('data-view="flows"') transfers = html.index('data-view="transfers"') diff --git a/tests/test_reminders_page.py b/tests/test_reminders_page.py index 26b30d6..5858360 100644 --- a/tests/test_reminders_page.py +++ b/tests/test_reminders_page.py @@ -37,8 +37,8 @@ class RemindersPageSourceContractTests(unittest.TestCase): self.assertIn('id="reminder-tbody"', html) self.assertIn('id="reminder-tabs"', html) self.assertIn('id="reminder-detail-drawer"', html) - self.assertIn("design-system.css?v=10", html) - self.assertIn("app.js?v=15", html) + self.assertIn("design-system.css?v=11", html) + self.assertIn("app.js?v=16", html) pending = html.index('id="pending-reminders-card"') history = html.index('id="reminder-history-card"') send = html.index('id="send-reminder-card"') @@ -135,7 +135,7 @@ class RemindersPageLayoutSmokeTests(unittest.TestCase): def test_send_flow_columns_and_no_page_overflow(self) -> None: html = (WEB / "admin.html").read_text(encoding="utf-8") - self.assertIn("app.js?v=15", html) + self.assertIn("app.js?v=16", html) with sync_playwright() as p: browser = p.chromium.launch() page = browser.new_page() diff --git a/web/admin.html b/web/admin.html index c935ce6..cb5a916 100644 --- a/web/admin.html +++ b/web/admin.html @@ -5,16 +5,26 @@ 管理端 · 金牛集团 - + + - +
@@ -60,12 +78,12 @@
-
+
往来借方总额 · 年初至今
42,040.30万元
6 家公司合计 · 明细 392 笔
-
+
往来贷方总额 · 年初至今
39,040.30万元
6 家公司合计 · 与借方同源互证
@@ -200,7 +218,7 @@
当前账期
2026-07 · 进行中
-
结账日
2026-08-29(顺延)· 距今 9 天
+
结账日
2026-08-29(顺延)· 距今 9 天
上一账期
2026-06 · 已于 07-03 结账
@@ -748,6 +766,7 @@
待提醒清单系统按流水提交、断档、待确认自动发现,点发送即送达对应公司
+
@@ -1208,6 +1227,7 @@
- + + diff --git a/web/app.js b/web/app.js index e208948..ae50eae 100644 --- a/web/app.js +++ b/web/app.js @@ -39,49 +39,20 @@ const motionQuery = window.matchMedia("(prefers-reduced-motion: reduce)"); function animateView(view, { initial = false } = {}) { if (!view || motionQuery.matches || typeof view.animate !== "function") return; - if (!initial) { - view.getAnimations().forEach((animation) => animation.cancel()); - view.animate( - [{ opacity: 0.84, transform: "translateY(5px)" }, { opacity: 1, transform: "translateY(0)" }], - { duration: 180, easing: "cubic-bezier(.22,1,.36,1)" }, + if (!initial) return; + const rows = [...view.querySelectorAll(".ds-table tbody tr")].slice(0, 8); + rows.forEach((row, index) => { + row.getAnimations().forEach((animation) => animation.cancel()); + row.animate( + [{ opacity: 0 }, { opacity: 1 }], + { duration: 300, delay: index * 45, easing: "cubic-bezier(0.23, 1, 0.32, 1)", fill: "both" }, ); - return; - } - const selectors = [ - ".page-head", - ".stat-card", - ".card", - ".notice", - ".list-row", - ".filters", - ]; - const elements = [...new Set(selectors.flatMap((selector) => [...view.querySelectorAll(selector)]))]; - - elements.forEach((element, index) => { - element.getAnimations().forEach((animation) => animation.cancel()); - const keyframes = [ - { opacity: 0, transform: `translateY(${initial ? 16 : 10}px)` }, - { opacity: 1, transform: "translateY(0)" }, - ]; - element.animate(keyframes, { - duration: 440, - delay: Math.min(index * 38, 260), - easing: "cubic-bezier(.22,1,.36,1)", - fill: "both", - }); }); } function initMotion() { if (motionQuery.matches) return; animateView($(".app-view.is-active"), { initial: true }); - - $$(".side-nav a", $("#sidebar") || document).forEach((item, index) => { - item.animate( - [{ opacity: 0, transform: "translateX(-8px)" }, { opacity: 1, transform: "translateX(0)" }], - { duration: 360, delay: 90 + index * 28, easing: "cubic-bezier(.22,1,.36,1)", fill: "both" }, - ); - }); } function recordStatus(status) { @@ -139,11 +110,23 @@ function showToast(title, detail = "", kind = "info") { } toast.append(dot, body); region.append(toast); - window.setTimeout(() => { + let remaining = 2600; + let started = Date.now(); + let timer = window.setTimeout(dismiss, remaining); + function dismiss() { toast.style.opacity = "0"; - toast.style.transition = "opacity 0.2s ease"; - window.setTimeout(() => toast.remove(), 200); - }, 4200); + toast.style.transition = "opacity 400ms cubic-bezier(0.32, 0.72, 0, 1), transform 400ms cubic-bezier(0.32, 0.72, 0, 1)"; + toast.style.transform = "translateX(12px)"; + window.setTimeout(() => toast.remove(), 400); + } + toast.addEventListener("mouseenter", () => { + window.clearTimeout(timer); + remaining -= Date.now() - started; + }); + toast.addEventListener("mouseleave", () => { + started = Date.now(); + timer = window.setTimeout(dismiss, Math.max(remaining, 0)); + }); } function toastIfLocked(result) { @@ -188,7 +171,9 @@ function showView(view) { closeNavigation({ restoreFocus: navigationWasOpen }); const activeView = $(`.app-view[data-page="${view}"]`); requestAnimationFrame(() => animateView(activeView)); - window.scrollTo({ top: 0, behavior: motionQuery.matches ? "auto" : "smooth" }); + const shell = $(".main"); + if (shell) shell.scrollTop = 0; + else window.scrollTo({ top: 0, behavior: motionQuery.matches ? "auto" : "smooth" }); if (portal === "company" && view === "transfers") { if (state.transfersKeepDetail && state.transfersDetail?.company_id) { showTransfersDetailLayer(); @@ -422,15 +407,19 @@ async function initAuthGuard() { function applyCompanyIdentity(me) { if (!me) return; - const row = $(".side-foot .user-row"); - if (row) { + const foot = $(".side-foot") || $(".side-foot .user-row"); + if (foot) { const name = me.username || (portal === "company" ? (me.company_name || "公司用户") : "系统管理员"); - const avatar = $(".avatar", row); + const avatar = $(".avatar", foot); if (avatar) avatar.textContent = name.slice(0, 1); - const nameEl = $(".user-name", row); + const nameEl = $(".user-name", foot); if (nameEl) nameEl.textContent = name; - const metaEl = $(".user-meta", row); - if (metaEl) metaEl.textContent = portal === "company" ? (me.company_name || "公司业务端") : "管理员"; + const metaEl = $(".user-meta", foot); + if (metaEl) { + metaEl.textContent = portal === "company" + ? `${me.company_name || "公司业务端"} · 仅本公司数据` + : "管理员 · 集团全量数据"; + } } // The company portal always shows the session-bound company in page copy. if (portal === "company" && me.company_name) { @@ -1814,6 +1803,7 @@ async function loadAdminRemindersPending() { if (empty) empty.style.display = ""; if (summary) summary.textContent = "0 家公司 · 0 项"; if (sendAll) { sendAll.disabled = true; sendAll.textContent = "全部一键发送"; } + syncPendingCheckAll(); return; } if (empty) empty.style.display = "none"; @@ -1831,7 +1821,7 @@ async function loadAdminRemindersPending() { row.dataset.companyId = String(item.company_id); const sentHint = item.send_count > 0 ? ` · 已提醒 ${item.send_count} 次` : ""; row.innerHTML = - '' + + '' + `${item.rule_label}` + `
${item.title}
` + `
${item.reason}${sentHint}
` + @@ -1839,6 +1829,16 @@ async function loadAdminRemindersPending() { `
`; list.append(row); }); + syncPendingCheckAll(); +} + +function syncPendingCheckAll() { + const master = $("#pending-check-all"); + if (!master) return; + const boxes = $$(".pending-check"); + const checked = boxes.filter((el) => el.checked).length; + master.checked = boxes.length > 0 && checked === boxes.length; + master.indeterminate = checked > 0 && checked < boxes.length; } async function loadAdminRemindersHistory(sourceFilter) { @@ -1932,6 +1932,15 @@ function initAdminReminders() { loadAdminRemindersPending(); loadAdminRemindersHistory("all"); + $("#pending-check-all")?.addEventListener("change", (event) => { + const on = event.target.checked; + event.target.indeterminate = false; + $$(".pending-check").forEach((box) => { box.checked = on; }); + }); + document.addEventListener("change", (event) => { + if (event.target.classList?.contains("pending-check")) syncPendingCheckAll(); + }); + $("#reminder-scan-btn")?.addEventListener("click", async () => { const response = await fetch("/api/admin/reminders/scan", { method: "POST" }).catch(() => null); if (response?.status === 401) { window.location.href = "index.html"; return; } @@ -4174,6 +4183,15 @@ function renderWorkspaceTransfersCard(summary) { ? `${formatWanHtml(pending.amount_total)} · ${pCount} 笔` : `0.00万元 · 0 笔`; } + const confirmedSplit = $("#wsTfConfirmedSplit"); + const pendingSplit = $("#wsTfPendingSplit"); + if (confirmedSplit) confirmedSplit.innerHTML = formatWanHtml(confirmed.net_change, { signed: true }); + if (pendingSplit) { + const pCount = Number(pending.count) || 0; + pendingSplit.innerHTML = pCount + ? `${formatWanHtml(pending.amount_total)} · ${pCount} 笔` + : `0.00万元 · 0 笔`; + } } async function loadTransfersSummary({ asOf } = {}) { diff --git a/web/assets/logo-day.png b/web/assets/logo-day.png new file mode 100644 index 0000000..a3783ba Binary files /dev/null and b/web/assets/logo-day.png differ diff --git a/web/assets/logo-night.png b/web/assets/logo-night.png new file mode 100644 index 0000000..9be779b Binary files /dev/null and b/web/assets/logo-night.png differ diff --git a/web/company.html b/web/company.html index 8507c28..4279745 100644 --- a/web/company.html +++ b/web/company.html @@ -5,16 +5,26 @@ 公司业务端 · 金牛集团 - + + - +
@@ -175,6 +193,22 @@
+
+
+
+ 已确认 +
+
+
计入上方合计与期间净变动
+
+
+
+ 待确认 +
+
+
单列展示,不计入已确认合计
+
+
@@ -1009,6 +1043,7 @@
- + + diff --git a/web/design-system.css b/web/design-system.css index be0bf75..1ad8ace 100644 --- a/web/design-system.css +++ b/web/design-system.css @@ -1,47 +1,161 @@ -/* ─── 金牛实业资金往来管理系统 · 共享样式 ───────────────────────────── - 方向:tech-utility(数据密集型工具)。六枚基础 token 绑定设计方向, - 状态色仅在此 :root 块内以 oklch 派生,组件一律引用变量。 */ +/* ─── 金牛实业资金往来管理系统 · 霜曜融合设计 ───────────────────────── + 日间默认写在 :root / .v-fusion;夜间只覆写变量。 + --fg/--muted/--border/--success/--warn/--danger/--info 为既有组件别名。 */ -:root { - --bg: oklch(98% 0.005 250); - --surface: oklch(100% 0 0); - --fg: oklch(22% 0.02 240); - --muted: oklch(50% 0.018 240); - --border: oklch(90% 0.008 240); - --accent: oklch(58% 0.16 145); +:root, +.v-fusion { + --bg: #f5f5f7; + --text: #1d1d1f; + --text-2: #6e6e73; + --text-3: #8e8e93; + --hairline: rgba(0, 0, 0, 0.08); + --surface: #ffffff; + --surface-2: #ffffff; + --material: rgba(255, 255, 255, 0.62); + --material-drawer: rgba(255, 255, 255, 0.86); + --fill: rgba(118, 118, 128, 0.10); + --fill-strong: rgba(118, 118, 128, 0.16); + --hover: rgba(0, 0, 0, 0.045); + --row-hover: rgba(0, 0, 0, 0.025); + --scrim: rgba(0, 0, 0, 0.24); + --accent: #c62f22; + --accent-solid: #c62f22; + --accent-soft: rgba(198, 47, 34, 0.08); + --gold: #8a6d1a; + --gold-bright: #b8933d; + --gold-soft: rgba(138, 109, 26, 0.09); + --gold-line: rgba(138, 109, 26, 0.28); + --gold-fade: rgba(138, 109, 26, 0.03); + --green: #248a3d; + --green-bg: rgba(52, 199, 89, 0.12); + --orange: #b25000; + --orange-bg: rgba(255, 159, 10, 0.14); + --red: #d70015; + --red-bg: rgba(255, 59, 48, 0.10); + --red-solid: #d70015; + --gray-bg: rgba(120, 120, 128, 0.12); + --focus-ring: #0a84ff; + --focus-glow: rgba(10, 132, 255, 0.18); + --red-line: rgba(215, 0, 21, 0.45); + --red-glow: rgba(255, 59, 48, 0.14); + --switch-off: rgba(120, 120, 128, 0.24); + --switch-on: #34c759; + --material-toast: rgba(30, 30, 32, 0.92); + --grip: rgba(0, 0, 0, 0.18); + --pushback-filter: brightness(0.98); + --shadow-seg: 0 1px 3px rgba(0, 0, 0, 0.10), 0 0 0 1px rgba(0, 0, 0, 0.03); + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.05), 0 8px 24px rgba(0, 0, 0, 0.06); + --shadow-drawer: -24px 0 64px rgba(0, 0, 0, 0.14); + --shadow-modal: 0 24px 80px rgba(0, 0, 0, 0.18); + --shadow-toast: 0 8px 24px rgba(0, 0, 0, 0.22); + --z-nav: 100; + --z-drawer: 200; + --z-modal: 300; + --z-toast: 400; + --z-tooltip: 500; + --ease-out: cubic-bezier(0.23, 1, 0.32, 1); + --ease-in-out: cubic-bezier(0.77, 0, 0.175, 1); + --ease-drawer: cubic-bezier(0.32, 0.72, 0, 1); + --glow-red: rgba(198, 47, 34, 0.05); + --glow-gold: rgba(138, 109, 26, 0.05); + --lp-brand: linear-gradient(158deg, #c13423, #a02415 52%, #7e170b); + --lp-brand-glow: radial-gradient(120% 80% at 18% 12%, rgba(255, 255, 255, 0.22), transparent 58%); + --lp-tick: var(--gold-bright); - /* 状态色 — 仅此处定义 */ - --success: oklch(55% 0.14 150); - --warn: oklch(62% 0.14 70); - --danger: oklch(55% 0.18 25); - --info: oklch(52% 0.12 240); + --fg: var(--text); + --muted: var(--text-2); + --border: var(--hairline); + --success: var(--green); + --success-soft: var(--green-bg); + --warn: var(--orange); + --warn-soft: var(--orange-bg); + --danger: var(--red); + --danger-soft: var(--red-bg); + --info: var(--gold); + --info-soft: var(--gold-soft); + --fg-soft: var(--hover); - --accent-soft: color-mix(in oklch, var(--accent) 12%, transparent); - --success-soft: color-mix(in oklch, var(--success) 12%, transparent); - --warn-soft: color-mix(in oklch, var(--warn) 14%, transparent); - --danger-soft: color-mix(in oklch, var(--danger) 11%, transparent); - --info-soft: color-mix(in oklch, var(--info) 10%, transparent); - --fg-soft: color-mix(in oklch, var(--fg) 5%, transparent); - - --font-body: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", system-ui, sans-serif; - --font-mono: "JetBrains Mono", "IBM Plex Mono", ui-monospace, "SF Mono", Menlo, monospace; - - --radius: 8px; - --radius-lg: 12px; + --font-body: -apple-system, BlinkMacSystemFont, "SF Pro SC", "PingFang SC", "Segoe UI", "Microsoft YaHei", sans-serif; + --font-mono: ui-monospace, "SF Mono", "JetBrains Mono", "IBM Plex Mono", Menlo, monospace; + --radius: 9px; + --radius-lg: 14px; + --r-pill: 999px; --side-w: 232px; } +html[data-theme="night"], +.v-fusion[data-theme="night"] { + --bg: #100f0d; + --text: #f2f0eb; + --text-2: #a9a49b; + --text-3: #8e8a82; + --hairline: rgba(255, 255, 255, 0.10); + --surface: #1b1916; + --surface-2: #2a2721; + --material: rgba(26, 24, 21, 0.62); + --material-drawer: rgba(31, 29, 25, 0.84); + --fill: rgba(255, 255, 255, 0.08); + --fill-strong: rgba(255, 255, 255, 0.13); + --hover: rgba(255, 255, 255, 0.05); + --row-hover: rgba(255, 255, 255, 0.04); + --scrim: rgba(0, 0, 0, 0.52); + --accent: #e05341; + --accent-solid: #cf4529; + --accent-soft: rgba(224, 83, 65, 0.16); + --gold: #d9b45c; + --gold-bright: #f0cd7a; + --gold-soft: rgba(217, 180, 92, 0.13); + --gold-line: rgba(217, 180, 92, 0.34); + --gold-fade: rgba(217, 180, 92, 0.04); + --green: #45cd6e; + --green-bg: rgba(69, 205, 110, 0.15); + --orange: #ffb340; + --orange-bg: rgba(255, 179, 64, 0.15); + --red: #ff6b5e; + --red-bg: rgba(255, 107, 94, 0.14); + --red-solid: #d63c2f; + --gray-bg: rgba(150, 150, 158, 0.16); + --focus-ring: #409cff; + --focus-glow: rgba(64, 156, 255, 0.30); + --red-line: rgba(255, 107, 94, 0.50); + --red-glow: rgba(255, 107, 94, 0.20); + --switch-off: rgba(120, 120, 128, 0.34); + --switch-on: #30d158; + --material-toast: rgba(54, 52, 47, 0.94); + --grip: rgba(255, 255, 255, 0.22); + --pushback-filter: brightness(0.92); + --shadow-seg: 0 1px 3px rgba(0, 0, 0, 0.50), 0 0 0 1px rgba(255, 255, 255, 0.06); + --shadow-card: 0 1px 2px rgba(0, 0, 0, 0.40), 0 8px 24px rgba(0, 0, 0, 0.35); + --shadow-drawer: -24px 0 64px rgba(0, 0, 0, 0.55); + --shadow-modal: 0 24px 80px rgba(0, 0, 0, 0.60); + --shadow-toast: 0 8px 24px rgba(0, 0, 0, 0.50); + --glow-red: rgba(224, 83, 65, 0.07); + --glow-gold: rgba(217, 180, 92, 0.06); + --lp-brand: linear-gradient(158deg, #201b14, #161310 52%, #0f0d0a); + --lp-brand-glow: radial-gradient(120% 80% at 18% 12%, rgba(217, 180, 92, 0.18), transparent 58%); + --lp-tick: var(--gold); +} + /* ─── reset ─────────────────────────────────────────────────────── */ *, *::before, *::after { box-sizing: border-box; } +html.v-fusion { height: 100%; } body { margin: 0; - background: var(--bg); + background-color: var(--bg); + background-image: + radial-gradient(1200px 620px at 100% 0%, var(--glow-red), transparent 58%), + radial-gradient(1000px 520px at 0% 100%, var(--glow-gold), transparent 58%); + background-attachment: fixed; color: var(--fg); font-family: var(--font-body); font-size: 14px; line-height: 1.55; -webkit-font-smoothing: antialiased; } +body.is-app { + height: 100%; + overflow: hidden; +} img, svg { display: block; } a { color: inherit; text-decoration: none; } button { font: inherit; cursor: pointer; } @@ -49,27 +163,55 @@ h1, h2, h3, h4 { margin: 0; line-height: 1.3; } p { margin: 0; } :focus-visible { - outline: 2px solid var(--accent); + outline: 2px solid var(--focus-ring); outline-offset: 2px; - border-radius: 4px; +} + +html[data-theme-anim], +html[data-theme-anim] body, +html[data-theme-anim] body * { + transition: color 200ms ease, background-color 200ms ease, background 200ms ease, + border-color 200ms ease, box-shadow 200ms ease, filter 200ms ease, opacity 200ms ease !important; } /* ─── 应用外壳 ──────────────────────────────────────────────────── */ -.shell { display: grid; grid-template-columns: var(--side-w) 1fr; min-height: 100vh; } +.shell { + display: grid; + grid-template-columns: var(--side-w) minmax(0, 1fr); + height: 100vh; + overflow: hidden; +} .sidebar { - background: var(--surface); + background: var(--material); + backdrop-filter: blur(24px) saturate(180%); + -webkit-backdrop-filter: blur(24px) saturate(180%); border-right: 1px solid var(--border); display: flex; flex-direction: column; - position: sticky; - top: 0; height: 100vh; + overflow: hidden; + z-index: var(--z-nav); } .side-brand { - padding: 18px 20px 16px; + display: flex; + align-items: center; + gap: 10px; + padding: 18px 16px 16px; border-bottom: 1px solid var(--border); + flex: none; } +.brand-logo { + width: 38px; + height: 38px; + border-radius: 9px; + object-fit: contain; + flex: none; +} +.brand-logo-night { display: none; } +html[data-theme="night"] .brand-logo-day { display: none; } +html[data-theme="night"] .brand-logo-night { display: block; } +.brand-text { min-width: 0; } .side-brand .brand-name { font-size: 15px; font-weight: 650; letter-spacing: -0.01em; } .side-brand .brand-sub { font-family: var(--font-mono); font-size: 11px; color: var(--muted); margin-top: 3px; letter-spacing: 0.02em; } .side-role { @@ -83,7 +225,7 @@ p { margin: 0; } } .side-role.company { background: var(--info-soft); color: var(--info); } -.side-nav { flex: 1; overflow-y: auto; padding: 12px 10px; } +.side-nav { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 12px 10px; } .side-nav .nav-group { font-family: var(--font-mono); font-size: 10.5px; letter-spacing: 0.08em; color: var(--muted); padding: 14px 10px 6px; } .side-nav a { display: flex; align-items: center; gap: 10px; @@ -96,7 +238,7 @@ p { margin: 0; } .side-nav a svg { width: 16px; height: 16px; flex: none; } .side-nav a:hover { color: var(--fg); background: var(--fg-soft); } .side-nav a.active { - color: var(--fg); + color: var(--accent); background: var(--accent-soft); font-weight: 600; } @@ -111,74 +253,130 @@ p { margin: 0; } color: var(--danger); font-weight: 600; } -.side-foot { border-top: 1px solid var(--border); padding: 12px 16px; } -.side-foot .user-row { display: flex; align-items: center; gap: 10px; min-width: 0; } -.side-foot .user-row > div { min-width: 0; } +.side-foot { border-top: 1px solid var(--border); padding: 12px 12px 16px; flex: none; } +.theme-seg { + display: grid; + grid-template-columns: 1fr 1fr; + gap: 2px; + padding: 3px; + margin-bottom: 10px; + border-radius: 9px; + background: var(--fill); + box-shadow: var(--shadow-seg); +} +.theme-seg button { + height: 26px; + border: 0; + border-radius: 7px; + background: transparent; + color: var(--text-2); + font-size: 12px; + font-weight: 600; +} +.theme-seg button.is-active { + background: var(--surface); + color: var(--text); +} +.account-box { + background: var(--fill); + border-radius: 10px; + padding: 10px; +} +.side-foot .user-row { display: flex; align-items: center; gap: 8px; min-width: 0; } +.side-foot .user-row > div { min-width: 0; flex: 1; } .side-foot .avatar { - width: 30px; height: 30px; border-radius: 50%; - background: var(--fg); color: var(--surface); + width: 28px; height: 28px; border-radius: 8px; + background: var(--gold-soft); color: var(--gold); display: grid; place-items: center; - font-size: 12px; font-weight: 600; + font-size: 12px; font-weight: 700; flex: none; } -.side-foot .user-name { font-size: 13px; font-weight: 600; white-space: nowrap; } -.side-foot .user-meta { font-family: var(--font-mono); font-size: 11px; color: var(--muted); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; } -.side-foot .logout { margin-left: auto; color: var(--muted); font-size: 12px; padding: 4px 6px; border-radius: 6px; background: none; border: 0; flex: none; white-space: nowrap; } -.side-foot .logout:hover { color: var(--danger); background: var(--danger-soft); } +.side-foot .user-name { font-size: 13px; font-weight: 600; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; } +.side-foot .user-meta { font-size: 10.5px; color: var(--text-3); overflow: hidden; text-overflow: ellipsis; white-space: nowrap; margin-top: 4px; } +.side-foot .logout, +.icon-btn.logout { + margin-left: auto; + width: 28px; height: 28px; + padding: 0; + border-radius: 7px; + border: 1px solid var(--hairline); + background: var(--surface); + color: var(--text-2); + display: inline-grid; place-items: center; + flex: none; +} +.side-foot .logout svg, +.icon-btn.logout svg { width: 15px; height: 15px; stroke-width: 1.5; stroke-linecap: round; stroke-linejoin: round; } +.side-foot .logout:hover, +.icon-btn.logout:hover { color: var(--red); background: var(--red-bg); border-color: var(--red-line); } +.side-foot .logout:active, +.icon-btn.logout:active { transform: scale(0.94); } -.main { min-width: 0; display: flex; flex-direction: column; } +.main { + min-width: 0; + min-height: 0; + height: 100vh; + overflow-y: auto; + overscroll-behavior: contain; + display: flex; + flex-direction: column; + align-items: stretch; +} .topbar { position: sticky; top: 0; z-index: 20; - background: color-mix(in oklch, var(--bg) 88%, transparent); - backdrop-filter: blur(10px); + background: var(--material); + backdrop-filter: blur(24px) saturate(180%); border-bottom: 1px solid var(--border); padding: 12px 28px; display: flex; align-items: center; gap: 16px; flex-wrap: wrap; + flex: none; } .topbar .crumb { font-family: var(--font-mono); font-size: 12px; color: var(--muted); } .topbar .crumb b { color: var(--fg); font-weight: 600; } .topbar .topbar-right { margin-left: auto; display: flex; align-items: center; gap: 10px; } -.content { padding: 24px 28px 64px; max-width: 1440px; width: 100%; margin-inline: auto; } +.content { padding: 34px 40px 96px; max-width: 1440px; width: 100%; margin-inline: auto; } /* ─── 页头 ──────────────────────────────────────────────────────── */ -.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-bottom: 20px; flex-wrap: wrap; } -.page-head h1 { font-size: 22px; font-weight: 700; letter-spacing: -0.015em; } +.page-head { display: flex; align-items: flex-end; justify-content: space-between; gap: 20px; margin-bottom: 22px; flex-wrap: wrap; } +.page-head h1 { font-size: 30px; font-weight: 700; letter-spacing: -0.022em; } .page-head .page-sub { color: var(--muted); font-size: 13px; margin-top: 5px; max-width: 72ch; } .page-head .page-actions { display: flex; gap: 8px; flex-wrap: wrap; } /* ─── 按钮 ──────────────────────────────────────────────────────── */ .btn { display: inline-flex; align-items: center; justify-content: center; gap: 7px; - padding: 8px 14px; - min-height: 34px; - border-radius: var(--radius); + padding: 0 15px; + height: 32px; + min-height: 32px; + border-radius: 9px; border: 1px solid var(--border); - background: var(--surface); + background: var(--fill); color: var(--fg); - font-size: 13px; font-weight: 550; - transition: background 0.12s ease, border-color 0.12s ease; + font-size: 13px; font-weight: 600; + transition: background 0.12s ease, border-color 0.12s ease, transform 160ms var(--ease-out); } -.btn:hover { border-color: color-mix(in oklch, var(--fg) 40%, var(--border)); background: var(--fg-soft); } -.btn:active { transform: translateY(1px); } -.btn-primary { background: var(--accent); border-color: var(--accent); color: var(--surface); } -.btn-primary:hover { background: color-mix(in oklch, var(--accent) 88%, black); border-color: color-mix(in oklch, var(--accent) 88%, black); } +.btn:hover { border-color: var(--gold-line); background: var(--hover); } +.btn:active { transform: scale(0.97); } +.btn-primary { background: var(--accent-solid); border-color: var(--accent-solid); color: #fff; } +.btn-primary:hover { background: var(--accent); border-color: var(--accent); } .btn-ghost { background: transparent; border-color: transparent; color: var(--muted); } -.btn-ghost:hover { color: var(--fg); background: var(--fg-soft); } -.btn-danger { color: var(--danger); border-color: color-mix(in oklch, var(--danger) 35%, var(--border)); } -.btn-danger:hover { background: var(--danger-soft); border-color: var(--danger); } -.btn-sm { padding: 4px 10px; min-height: 26px; font-size: 12px; border-radius: 6px; } -.btn[disabled] { opacity: 0.5; cursor: not-allowed; } -.btn-primary[disabled] { opacity: 1; background: var(--fg-soft); border-color: var(--border); color: var(--muted); } +.btn-ghost:hover { color: var(--fg); background: var(--hover); } +.btn-danger { color: var(--danger); border-color: var(--red-line); background: transparent; } +.btn-danger:hover { background: var(--red-bg); border-color: var(--red); } +.btn-sm { padding: 0 10px; height: 26px; min-height: 26px; font-size: 12px; border-radius: 7px; } +.btn[disabled] { opacity: 0.5; cursor: not-allowed; transform: none; } +.btn-primary[disabled] { opacity: 1; background: var(--fill-strong); border-color: var(--border); color: var(--muted); } /* ─── 卡片 ──────────────────────────────────────────────────────── */ .card { background: var(--surface); - border: 1px solid var(--border); - border-radius: var(--radius-lg); + border: 1px solid var(--hairline); + border-radius: 14px; padding: 18px 20px; + box-shadow: var(--shadow-card); } .card-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; margin-bottom: 14px; } .card-title { font-size: 14px; font-weight: 650; letter-spacing: -0.005em; } @@ -186,11 +384,11 @@ p { margin: 0; } /* ─── 指标卡 ────────────────────────────────────────────────────── */ .stat-card { padding: 16px 18px; } -.stat-card .stat-label { font-size: 12.5px; color: var(--muted); display: flex; align-items: center; gap: 6px; } +.stat-card .stat-label { font-size: 11.5px; color: var(--text-2); display: flex; align-items: center; gap: 6px; } .stat-card .stat-value { font-family: var(--font-mono); font-variant-numeric: tabular-nums; - font-size: 26px; font-weight: 650; + font-size: 26px; font-weight: 700; letter-spacing: -0.02em; margin-top: 6px; } @@ -198,6 +396,19 @@ p { margin: 0; } .stat-card .stat-foot { font-size: 12px; color: var(--muted); margin-top: 6px; } .stat-card.alert .stat-value { color: var(--danger); } .stat-card.warn .stat-value { color: var(--warn); } +.stat-card.gold { + background: linear-gradient(180deg, var(--gold-soft), var(--gold-fade)); + border-color: var(--gold-line); + position: relative; +} +.stat-card.gold::before { + content: ""; + position: absolute; left: 16px; right: 16px; top: 0; + height: 2px; + border-radius: 2px; + background: linear-gradient(90deg, var(--gold), transparent); +} +.stat-card.gold .stat-value { color: var(--gold); font-size: 29px; } .stat-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; } .stat-dot.danger { background: var(--danger); } .stat-dot.warn { background: var(--warn); } @@ -448,7 +659,7 @@ p { margin: 0; } .meta { font-family: var(--font-mono); font-size: 12px; color: var(--muted); } .mt-0 { margin-top: 0; } -@media (max-width: 1100px) { +@media (max-width: 1279px) { .grid-4, .grid-5 { grid-template-columns: repeat(2, minmax(0, 1fr)); } .grid-3, .grid-2-1, .grid-1-2, .grid-3-2, .grid-480-1 { grid-template-columns: minmax(0, 1fr); } .dash-master-split { @@ -474,11 +685,10 @@ p { margin: 0; } border-top: 1px solid var(--border); } } -@media (max-width: 860px) { - .shell { grid-template-columns: 1fr; } - .sidebar { position: static; height: auto; } +@media (max-width: 767px) { .grid-2 { grid-template-columns: 1fr; } - .content { padding: 16px 16px 48px; } + .content { padding: 20px 16px 64px; } + .page-head h1 { font-size: 24px; } .dash-master-head-actions { width: 100%; margin-left: 0; @@ -505,15 +715,17 @@ p { margin: 0; } /* ─── 数据表 ────────────────────────────────────────────────────── */ .table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); } .ds-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 640px; } -.ds-table th, .ds-table td { padding: 9px 14px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; } +.ds-table th, .ds-table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; } .ds-table th { - color: var(--muted); font-weight: 550; - font-family: var(--font-mono); font-size: 11px; - letter-spacing: 0.05em; - background: var(--bg); + color: var(--text-3); font-weight: 600; + font-size: 11.5px; + letter-spacing: 0.03em; + background: var(--surface); position: sticky; top: 0; } -.ds-table tbody tr:hover { background: var(--fg-soft); } +@media (hover: hover) { + .ds-table tbody tr:hover { background: var(--row-hover); } +} .ds-table tbody tr:last-child td { border-bottom: 0; } .ds-table .num-col { font-family: var(--font-mono); font-variant-numeric: tabular-nums; text-align: right; } .ds-table td.wrap, .ds-table th.wrap { white-space: normal; min-width: 180px; } @@ -527,7 +739,8 @@ p { margin: 0; } /* ─── 徽章 / 状态 ───────────────────────────────────────────────── */ .pill { display: inline-flex; align-items: center; gap: 5px; - padding: 2px 9px; + height: 22px; + padding: 0 9px; border-radius: 999px; font-size: 11.5px; font-weight: 600; white-space: nowrap; @@ -564,17 +777,18 @@ p { margin: 0; } .field > label { font-size: 12px; color: var(--muted); font-weight: 550; } .input, .select, .textarea, .field .input, .field .select, .field .textarea { padding: 7px 11px; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--surface); + border: 1px solid var(--hairline); + border-radius: 9px; + background: var(--fill); color: var(--fg); font: inherit; - font-size: 13px; + font-size: 13.5px; min-height: 34px; } .input:focus, .select:focus, .textarea:focus, .field .input:focus, .field .select:focus, .field .textarea:focus { - outline: 2px solid var(--accent-soft); - border-color: var(--accent); + outline: none; + border-color: var(--focus-ring); + box-shadow: 0 0 0 3px var(--focus-glow); } .input.num-input { font-family: var(--font-mono); font-variant-numeric: tabular-nums; } .textarea { min-height: 84px; resize: vertical; line-height: 1.55; } @@ -639,21 +853,23 @@ p { margin: 0; } /* ─── 弹窗 ──────────────────────────────────────────────────────── */ .modal-backdrop { - position: fixed; inset: 0; z-index: 50; - background: color-mix(in oklch, var(--fg) 45%, transparent); + position: fixed; inset: 0; z-index: var(--z-modal); + background: var(--scrim); display: none; align-items: center; justify-content: center; padding: 24px; } .modal-backdrop.open { display: flex; } .modal { - background: var(--surface); - border-radius: var(--radius-lg); - border: 1px solid var(--border); - width: 100%; max-width: 560px; + background: var(--material-drawer); + backdrop-filter: blur(28px); + -webkit-backdrop-filter: blur(28px); + border-radius: 16px; + border: 1px solid var(--gold-line); + width: 100%; max-width: min(520px, 94vw); max-height: 86vh; overflow-y: auto; padding: 22px 24px; - box-shadow: 0 18px 50px color-mix(in oklch, var(--fg) 25%, transparent); + box-shadow: var(--shadow-modal); } .modal.wide { max-width: 760px; } .modal-head { display: flex; align-items: center; justify-content: space-between; margin-bottom: 4px; } @@ -684,33 +900,55 @@ p { margin: 0; } font-size: 13px; background: var(--surface); } -.notice.warn { background: var(--warn-soft); border-color: color-mix(in oklch, var(--warn) 30%, transparent); } -.notice.danger { background: var(--danger-soft); border-color: color-mix(in oklch, var(--danger) 30%, transparent); } -.notice.info { background: var(--info-soft); border-color: color-mix(in oklch, var(--info) 25%, transparent); } +.notice.warn { background: var(--warn-soft); border-color: var(--orange-bg); } +.notice.danger { background: var(--danger-soft); border-color: var(--red-line); } +.notice.info { background: var(--info-soft); border-color: var(--gold-line); } .notice .n-title { font-weight: 650; } -.notice .n-body { color: color-mix(in oklch, var(--fg) 80%, var(--muted)); margin-top: 2px; font-size: 12.5px; } +.notice .n-body { color: var(--text-2); margin-top: 2px; font-size: 12.5px; } /* ─── 登录页 ────────────────────────────────────────────────────── */ .login-wrap { min-height: 100vh; display: grid; - grid-template-columns: 1fr 1fr; + grid-template-columns: minmax(440px, 46%) minmax(0, 1fr); + position: relative; +} +.login-theme { + position: absolute; + top: 18px; + right: 18px; + z-index: 2; + width: 148px; } .login-aside { - background: var(--fg); - color: var(--surface); + background: var(--lp-brand); + color: #fff; padding: 48px 56px; display: flex; flex-direction: column; + position: relative; + overflow: hidden; + min-height: 100vh; } -.login-aside .brand-mark { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; opacity: 0.65; } +.login-aside::before { + content: ""; + position: absolute; inset: 0; + background: var(--lp-brand-glow); + pointer-events: none; +} +html[data-theme="night"] .login-aside { + box-shadow: inset -1px 0 0 var(--gold-line); +} +.login-aside > * { position: relative; z-index: 1; } +.login-aside .brand-logo { width: 52px; height: 52px; margin-bottom: 18px; } +.login-aside .brand-mark { font-family: var(--font-mono); font-size: 12px; letter-spacing: 0.1em; opacity: 0.72; } .login-aside h1 { font-size: 30px; font-weight: 700; letter-spacing: -0.02em; margin-top: 18px; line-height: 1.25; } -.login-aside .aside-sub { opacity: 0.72; font-size: 14px; margin-top: 14px; max-width: 40ch; } -.login-aside .aside-list { margin-top: auto; display: flex; flex-direction: column; gap: 12px; } -.login-aside .aside-item { display: flex; gap: 10px; font-size: 13px; opacity: 0.85; align-items: baseline; } -.login-aside .aside-item .tick { font-family: var(--font-mono); color: var(--accent); } -.login-panel { display: grid; place-items: center; padding: 48px 32px; } +.login-aside .aside-sub { opacity: 0.82; font-size: 14px; margin-top: 14px; max-width: 40ch; } +.login-aside .aside-list { margin-top: auto; display: flex; flex-direction: column; gap: 12px; padding-top: 32px; } +.login-aside .aside-item { display: flex; gap: 10px; font-size: 13px; opacity: 0.92; align-items: baseline; } +.login-aside .aside-item .tick { font-family: var(--font-mono); color: var(--lp-tick); font-weight: 700; } +.login-panel { display: grid; place-items: center; padding: 72px 32px 48px; } .login-card { width: 100%; max-width: 400px; } -.login-card h2 { font-size: 20px; font-weight: 700; letter-spacing: -0.01em; } +.login-card h2 { font-size: 24px; font-weight: 750; letter-spacing: -0.01em; } .login-card .login-sub { color: var(--muted); font-size: 13px; margin: 6px 0 24px; } .role-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 8px; margin-bottom: 18px; } .role-switch button { @@ -726,24 +964,33 @@ p { margin: 0; } .role-switch button.active { border-color: var(--accent); background: var(--accent-soft); } .role-switch button.active .r-name { color: var(--accent); } .login-card .field { margin-bottom: 14px; } +.login-card .btn-primary { height: 40px; min-height: 40px; width: 100%; } +.login-notice { + margin-top: 16px; + padding: 10px 12px; + border-radius: 9px; + background: var(--gold-soft); + border: 1px solid var(--gold-line); + color: var(--gold); + font-size: 12.5px; +} .login-foot { margin-top: 16px; font-size: 12px; color: var(--muted); text-align: center; } .login-foot a { color: var(--accent); font-weight: 550; } .login-foot a:hover { text-decoration: underline; } -/* 端标识:登录页顶部大字区分管理端 / 公司端 */ .login-role-title { - font-size: 34px; - font-weight: 800; - letter-spacing: 0.02em; + font-size: 22px; + font-weight: 750; + letter-spacing: 0.01em; line-height: 1.15; margin-bottom: 6px; + color: var(--text); } .login-role-title.admin { color: var(--accent); } -.login-role-title.company { color: var(--info); } +.login-role-title.company { color: var(--accent); } .login-role-caption { color: var(--muted); font-size: 13px; margin-bottom: 22px; } -/* 管理端青绿 / 公司端品蓝侧栏色块 */ -.login-aside.company { background: var(--info); } -.login-aside.company .aside-item .tick { color: var(--surface); opacity: 0.85; } +.login-aside.company { background: var(--lp-brand); } +.login-aside.company .aside-item .tick { color: var(--lp-tick); opacity: 1; } .login-back { display: inline-flex; align-items: center; gap: 6px; margin-bottom: 20px; @@ -755,10 +1002,17 @@ p { margin: 0; } font-size: 12.5px; font-weight: 600; } .login-back:hover { border-color: var(--accent); color: var(--accent); } -.login-back.company:hover { border-color: var(--info); color: var(--info); } -@media (max-width: 900px) { +.login-back.company:hover { border-color: var(--accent); color: var(--accent); } +@media (max-width: 1279px) { + .login-wrap { grid-template-columns: minmax(380px, 42%) minmax(0, 1fr); } +} +@media (max-width: 1023px) { .login-wrap { grid-template-columns: 1fr; } - .login-aside { display: none; } + .login-aside { + min-height: 0; + padding: 28px 24px 20px; + } + .login-aside .aside-list { padding-top: 16px; } } /* ─── 空状态 ────────────────────────────────────────────────────── */ @@ -867,7 +1121,7 @@ a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, trans padding: 24px; transition: border-color 0.12s ease, transform 0.12s ease; } -.portal-card:hover { border-color: var(--accent); transform: translateY(-2px); } +.portal-card:hover { border-color: var(--gold-line); } .portal-card .pc-role { font-family: var(--font-mono); font-size: 11px; letter-spacing: 0.08em; color: var(--accent); } .portal-card h3 { font-size: 17px; font-weight: 700; margin-top: 8px; } .portal-card p { color: var(--muted); font-size: 13px; margin-top: 6px; } @@ -902,28 +1156,72 @@ a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, trans .icon-button:hover { color: var(--fg); border-color: color-mix(in oklch, var(--fg) 40%, var(--border)); background: var(--fg-soft); } .icon-button svg { width: 18px; height: 18px; } .menu-button { display: none; } -@media (max-width: 860px) { - .menu-button { display: inline-grid; } - .shell { grid-template-columns: 1fr; } - .sidebar { - position: fixed; top: 0; left: 0; bottom: 0; z-index: 40; - width: var(--side-w); height: 100dvh; - transform: translateX(-100%); - transition: transform 0.2s ease; +@media (max-width: 1023px) { + .shell { grid-template-columns: 64px minmax(0, 1fr); } + .sidebar { width: 64px; } + .side-brand { padding: 14px 13px; justify-content: center; } + .brand-text, .nav-label, .nav-group, .side-foot .user-name, .side-foot .user-meta, .side-foot .avatar { display: none; } + .side-nav a { justify-content: center; padding: 10px 0; } + .side-nav a .nav-badge { display: none; } + .theme-seg { grid-template-columns: 1fr; } + .side-foot .user-row { justify-content: center; } + .side-foot .logout { margin-left: 0; } + .content { padding: 24px 20px 72px; } +} +@media (max-width: 767px) { + .shell { + grid-template-columns: 1fr; + grid-template-rows: auto minmax(0, 1fr); } - .sidebar.is-open { transform: none; box-shadow: 0 0 48px color-mix(in oklch, var(--fg) 22%, transparent); } + .sidebar { + width: 100%; + height: auto; + max-height: none; + flex-direction: row; + align-items: center; + overflow-x: auto; + overflow-y: hidden; + border-right: 0; + border-bottom: 1px solid var(--border); + } + .side-brand { border-bottom: 0; padding: 10px 12px; } + .brand-text { display: none; } + .side-nav { + display: flex; + flex-direction: row; + overflow-x: auto; + padding: 8px 4px; + flex: 1; + } + .side-nav .nav-group { display: none; } + .side-nav a { flex: none; padding: 8px 10px; } + .nav-label { display: inline; font-size: 12.5px; } + .side-foot { + display: flex; + flex-direction: row; + align-items: center; + gap: 8px; + border-top: 0; + padding: 8px 12px; + } + .theme-seg { margin-bottom: 0; width: 92px; grid-template-columns: 1fr 1fr; } + .account-box { padding: 4px; background: transparent; } + .side-foot .user-name, .side-foot .user-meta, .side-foot .avatar { display: none; } + .main { min-height: 0; height: 100%; overflow-y: auto; } } /* ─── Toast 轻量提示 ───────────────────────────────────────────── */ -.toast-region { position: fixed; right: 16px; bottom: 16px; z-index: 300; display: grid; gap: 8px; } +.toast-region { position: fixed; right: 16px; top: 16px; z-index: var(--z-toast); display: grid; gap: 8px; } .toast { - display: flex; align-items: flex-start; gap: 9px; + display: flex; align-items: center; gap: 9px; min-width: 260px; max-width: 380px; - padding: 11px 14px; - border: 1px solid var(--border); - border-radius: var(--radius-lg); - background: var(--surface); - box-shadow: 0 12px 34px color-mix(in oklch, var(--fg) 18%, transparent); + height: 40px; + padding: 0 16px; + border: 0; + border-radius: 999px; + background: var(--material-toast); + color: #fff; + box-shadow: var(--shadow-toast); font-size: 13px; } .toast .t-dot { width: 8px; height: 8px; border-radius: 50%; flex: none; margin-top: 5px; background: var(--muted); } @@ -945,18 +1243,26 @@ a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, trans /* ─── 侧滑详情抽屉(用 modal/card token 重画) ─────────────────── */ .drawer { - position: fixed; top: 0; right: 0; bottom: 0; z-index: 60; - width: min(400px, calc(100vw - 24px)); + position: fixed; top: 0; right: 0; bottom: 0; z-index: var(--z-drawer); + width: min(480px, 94vw); display: flex; flex-direction: column; - background: var(--surface); - border-left: 1px solid var(--border); - box-shadow: -18px 0 50px color-mix(in oklch, var(--fg) 22%, transparent); + background: var(--material-drawer); + backdrop-filter: blur(28px); + -webkit-backdrop-filter: blur(28px); + border-left: 1px solid var(--gold-line); + box-shadow: var(--shadow-drawer); transform: translateX(100%); visibility: hidden; - transition: transform 0.22s ease, visibility 0.22s; + transition: transform 420ms var(--ease-drawer), visibility 420ms; } .drawer.is-open { transform: none; visibility: visible; } -.drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 20px 12px; border-bottom: 1px solid var(--border); } +.drawer-head { display: flex; align-items: flex-start; justify-content: space-between; gap: 12px; padding: 18px 20px 12px; border-bottom: 1px solid var(--border); position: relative; } +.drawer-head::before { + content: ""; + position: absolute; top: 8px; left: 50%; + width: 36px; height: 4px; margin-left: -18px; + border-radius: 999px; background: var(--grip); +} .drawer-head .d-title { font-size: 16px; font-weight: 700; margin-top: 6px; } .drawer-head .d-desc { color: var(--muted); font-size: 12.5px; margin-top: 4px; } .drawer-body { flex: 1; overflow-y: auto; padding: 16px 20px; } @@ -999,24 +1305,24 @@ body[data-portal="company"] .side-nav a[data-view="transfers"].active svg { color: var(--info); } [data-page="transfers"] .btn-primary { - background: var(--info); - border-color: var(--info); - color: var(--surface); + background: var(--accent-solid); + border-color: var(--accent-solid); + color: #fff; } [data-page="transfers"] .btn-primary:hover { - background: color-mix(in oklch, var(--info) 88%, black); - border-color: color-mix(in oklch, var(--info) 88%, black); + background: var(--accent); + border-color: var(--accent); } [data-page="transfers"] .loading-inline::before { - border-top-color: var(--info); + border-top-color: var(--accent); } [data-page="transfers"] .stat-card .stat-value.amt-in { color: var(--success); } [data-page="transfers"] .stat-card .stat-value.amt-out { color: var(--danger); } [data-page="transfers"] .ds-table tbody tr.is-pending { - background: color-mix(in oklch, var(--warn) 8%, transparent); + background: var(--orange-bg); } [data-page="transfers"] .ds-table tbody tr.is-pending:hover { - background: color-mix(in oklch, var(--warn) 14%, transparent); + background: var(--warn-soft); } .xfer-split { @@ -1032,11 +1338,11 @@ body[data-portal="company"] .side-nav a[data-view="transfers"].active svg { padding: 16px 18px; } .xfer-split-pane.confirmed { - background: color-mix(in oklch, var(--success) 6%, var(--surface)); + background: var(--green-bg); border-right: 1px solid var(--border); } .xfer-split-pane.pending { - background: color-mix(in oklch, var(--warn) 8%, var(--surface)); + background: var(--orange-bg); } .xfer-split-head { display: flex; @@ -1051,7 +1357,7 @@ body[data-portal="company"] .side-nav a[data-view="transfers"].active svg { letter-spacing: -0.02em; } .xfer-split-pane.confirmed .xfer-split-value { color: var(--success); } -.xfer-split-pane.pending .xfer-split-value { color: color-mix(in oklch, var(--warn) 80%, black); } +.xfer-split-pane.pending .xfer-split-value { color: var(--warn); } .xfer-split-note { margin-top: 6px; font-size: 12px; @@ -1084,6 +1390,83 @@ body[data-portal="company"] .side-nav a[data-view="transfers"].active svg { .xfer-split-pane.confirmed { border-right: 0; border-bottom: 1px solid var(--border); } } +/* ─── 霜曜融合补遗:证据金、空态、降级、三态勾选 ───────────────── */ +.amt-gold, .kv dd.amt-gold, .drawer-body .kv dd.amt-gold { color: var(--gold); font-weight: 750; font-size: 22px; } +.evidence-flag { + display: inline-flex; align-items: center; gap: 6px; + color: var(--gold); font-size: 11.5px; font-weight: 600; +} +.empty-action { + border: 1px dashed var(--hairline); + border-radius: 14px; + padding: 32px 20px; + text-align: center; +} +.restricted-card { background: var(--surface); border: 1px solid var(--hairline); border-radius: 14px; padding: 24px; } +.ds-check { + appearance: none; + width: 15px; height: 15px; + border: 1px solid var(--hairline); + border-radius: 4px; + background: var(--surface); + vertical-align: middle; + flex: none; +} +.ds-check:checked { + background: var(--accent); + border-color: var(--accent); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Cpath fill='none' stroke='white' stroke-width='1.8' d='M2.2 6.2l2.4 2.4 5.2-5.2'/%3E%3C/svg%3E"); + background-size: 10px 10px; + background-repeat: no-repeat; + background-position: center; +} +.ds-check:indeterminate { + background: var(--accent); + border-color: var(--accent); + background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 12 12'%3E%3Crect fill='white' x='1.5' y='5' width='9' height='2' rx='1'/%3E%3C/svg%3E"); + background-size: 10px 10px; + background-repeat: no-repeat; + background-position: center; +} +.batch-bar { display: flex; align-items: center; gap: 10px; padding: 8px 0; } +.login-page { min-height: 100vh; overflow: auto; } +.toast .t-detail { color: rgba(255,255,255,.72); } +.toast .t-dot { margin-top: 0; } + +@media (max-width: 480px) { + .grid-4, .grid-5, .grid-2, .grid-3 { grid-template-columns: 1fr; } +} + +@media (prefers-reduced-motion: reduce) { + *, *::before, *::after { + animation-duration: 0.01ms !important; + animation-iteration-count: 1 !important; + transition-duration: 0.01ms !important; + scroll-behavior: auto !important; + } + .drawer, .modal-backdrop, .toast { transition-duration: 150ms !important; } + .skeleton::after { animation: none; } +} + +@media (prefers-reduced-transparency: reduce) { + .sidebar, .topbar { background: var(--surface); backdrop-filter: none; } + .drawer, .modal { background: var(--surface); backdrop-filter: none; } + .toast { background: var(--material-toast); } +} + +@media (prefers-contrast: more) { + :root, .v-fusion { + --hairline: rgba(0, 0, 0, 0.28); + --gold: #6b5310; + --gold-line: rgba(107, 83, 16, 0.55); + } + html[data-theme="night"], .v-fusion[data-theme="night"] { + --hairline: rgba(255, 255, 255, 0.28); + --gold: #f0cd7a; + --gold-line: rgba(240, 205, 122, 0.6); + } +} + /* ─── 提醒管理:三步发送流(HEL-230) ─────────────────────────── */ .send-flow { display: grid; @@ -1291,3 +1674,4 @@ body[data-portal="company"] .side-nav a[data-view="transfers"].active svg { [data-page="period-audit"] .table-wrap { display: none; } .audit-event-cards { display: grid; gap: 10px; } } + diff --git a/web/index.html b/web/index.html index ce6ad0f..d2e6bc6 100644 --- a/web/index.html +++ b/web/index.html @@ -4,30 +4,45 @@ 金牛实业资金往来管理系统 · 入口 - + + - +
-

JINNIU GROUP · INTERCOMPANY TREASURY

+ + + +

JINNIU GROUP · INTERCOMPANY TREASURY

河南金牛实业集团有限公司 · 资金往来管理系统

集团内部公司间资金往来记账平台。银行流水导入后自动轧算往来余额,支持从集团汇总逐级穿透至银行原始流水。本系统包含管理端与公司业务端两套界面。

+ diff --git a/web/login-admin.html b/web/login-admin.html index 44b75da..6cb0d43 100644 --- a/web/login-admin.html +++ b/web/login-admin.html @@ -4,11 +4,19 @@ 登录 · 金牛实业资金往来管理系统 - + + - +
+
@@ -75,6 +90,7 @@
- + + diff --git a/web/login-company.html b/web/login-company.html index 3b213ae..cb9d590 100644 --- a/web/login-company.html +++ b/web/login-company.html @@ -4,11 +4,19 @@ 登录 · 金牛实业资金往来管理系统 - + + - +
+
@@ -75,6 +90,7 @@
- + + diff --git a/web/theme.js b/web/theme.js new file mode 100644 index 0000000..b24a830 --- /dev/null +++ b/web/theme.js @@ -0,0 +1,55 @@ +/* 霜曜 / 黑金主题:仅记忆主题偏好,不写任何业务数据。 */ +(function (global) { + var KEY = "jinniu-theme"; + + function read() { + try { + return localStorage.getItem(KEY) === "night" ? "night" : "day"; + } catch (err) { + return "day"; + } + } + + function syncButtons(theme) { + document.querySelectorAll("[data-theme-set]").forEach(function (btn) { + var on = btn.getAttribute("data-theme-set") === theme; + btn.classList.toggle("is-active", on); + btn.setAttribute("aria-pressed", on ? "true" : "false"); + }); + } + + function apply(theme, animate) { + var next = theme === "night" ? "night" : "day"; + var root = document.documentElement; + root.classList.add("v-fusion"); + if (animate) { + root.setAttribute("data-theme-anim", ""); + window.setTimeout(function () { + root.removeAttribute("data-theme-anim"); + }, 220); + } + root.setAttribute("data-theme", next); + if (document.body) document.body.setAttribute("data-theme", next); + syncButtons(next); + } + + function set(theme) { + var next = theme === "night" ? "night" : "day"; + try { + localStorage.setItem(KEY, next); + } catch (err) { + /* 无本地存储时仍切换当次会话 */ + } + apply(next, true); + } + + apply(read(), false); + document.addEventListener("click", function (event) { + var btn = event.target.closest("[data-theme-set]"); + if (!btn) return; + event.preventDefault(); + set(btn.getAttribute("data-theme-set")); + }); + + global.JinniuTheme = { apply: apply, set: set, read: read }; +})(window);