B-229: 登录页改为桌面双区布局并收紧三档响应式
把居中手机卡改回横向双区,端口选择复用分段控件节奏,避免再叠加一套登录样式。 Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -0,0 +1,75 @@
|
||||
"""Static contracts for the B-229 login desktop layout."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
import re
|
||||
import unittest
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WEB = ROOT / "web"
|
||||
|
||||
|
||||
class LoginFrontendContractTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.html = (WEB / "index.html").read_text(encoding="utf-8")
|
||||
cls.css = (WEB / "styles.css").read_text(encoding="utf-8")
|
||||
cls.js = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
|
||||
def test_dual_zone_markup_and_auth_controls(self) -> None:
|
||||
self.assertIn('class="entry-context"', self.html)
|
||||
self.assertIn('class="entry-form-wrap"', self.html)
|
||||
self.assertIn('id="loginForm"', self.html)
|
||||
self.assertIn('id="togglePassword"', self.html)
|
||||
self.assertIn('id="changePassword"', self.html)
|
||||
self.assertIn('id="loginError"', self.html)
|
||||
self.assertIn('name="username"', self.html)
|
||||
self.assertIn('name="password"', self.html)
|
||||
self.assertIn('name="new_password"', self.html)
|
||||
self.assertIn('name="confirm_password"', self.html)
|
||||
self.assertIn('value="admin"', self.html)
|
||||
self.assertIn('value="company"', self.html)
|
||||
self.assertIn("总账管理端", self.html)
|
||||
self.assertIn("公司业务端", self.html)
|
||||
self.assertIn("服务状态", self.html)
|
||||
self.assertIn("当前账期", self.html)
|
||||
self.assertNotIn("entry-card", self.html)
|
||||
self.assertNotIn('value="group-admin"', self.html)
|
||||
self.assertNotIn('value="demo123456"', self.html)
|
||||
|
||||
def test_login_css_lives_in_one_section(self) -> None:
|
||||
self.assertIn("minmax(460px, 520px)", self.css)
|
||||
self.assertIn("width: min(440px, 100%)", self.css)
|
||||
self.assertIn("@media (max-width: 1023px)", self.css)
|
||||
self.assertIn("@media (max-width: 720px)", self.css)
|
||||
self.assertIn("minmax(400px, 440px)", self.css)
|
||||
self.assertNotIn(".entry-card", self.css)
|
||||
self.assertNotIn("0 0 60px rgba(55, 235, 137, 0.06)", self.css)
|
||||
login_block = self.css.split("/* Login */", 1)[1]
|
||||
after_login = login_block.split("/* ---- B-44", 1)[0] if "/* ---- B-44" in login_block else login_block
|
||||
self.assertEqual(1, after_login.count(".entry-shell { width: 100%;"))
|
||||
self.assertEqual(3, len(re.findall(r"\.entry-shell \{", after_login)))
|
||||
later = self.css.split("/* ---- B-44 intercompany balances ---- */", 1)[-1]
|
||||
self.assertNotIn(".entry-shell", later)
|
||||
self.assertNotIn(".entry-form {", later)
|
||||
self.assertNotIn(".role-switch {", later)
|
||||
|
||||
def test_role_switch_reuses_segmented_rhythm(self) -> None:
|
||||
self.assertIn(".role-switch { display: flex; gap: 4px; padding: 4px;", self.css)
|
||||
self.assertIn("min-height: 36px", self.css)
|
||||
self.assertIn('role="radiogroup"', self.html)
|
||||
self.assertIn("class=\"sr-only\"", self.html)
|
||||
|
||||
def test_auth_javascript_untouched(self) -> None:
|
||||
self.assertIn("function initEntry()", self.js)
|
||||
self.assertIn('fetch("/api/login"', self.js)
|
||||
self.assertIn('fetch("/api/password/change"', self.js)
|
||||
self.assertIn("must_change_password", self.js)
|
||||
self.assertIn("#togglePassword", self.js)
|
||||
self.assertIn("进入总账管理端", self.js)
|
||||
self.assertIn("进入公司业务端", self.js)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -0,0 +1,222 @@
|
||||
"""Chromium layout regression for the B-229 login page.
|
||||
|
||||
Measures dual-zone geometry, form width, overflow and overlap at the
|
||||
accepted viewports. Screenshots are written next to other visual evidence.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import base64
|
||||
import json
|
||||
import subprocess
|
||||
import sys
|
||||
import tempfile
|
||||
import threading
|
||||
import time
|
||||
import unittest
|
||||
from http.server import ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
from urllib.request import urlopen
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
if str(Path(__file__).resolve().parent) not in sys.path:
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent))
|
||||
|
||||
from test_b44_layout import QuietHandler, _Cdp, chrome_bin # noqa: E402
|
||||
|
||||
SCREEN_DIR = ROOT / "screenshots"
|
||||
LOGIN_URL = "/web/index.html"
|
||||
MEASURE = r"""
|
||||
(() => {
|
||||
const box = (el) => {
|
||||
if (!el) return null;
|
||||
const r = el.getBoundingClientRect();
|
||||
return {
|
||||
left: r.left, right: r.right, top: r.top, bottom: r.bottom,
|
||||
width: r.width, height: r.height
|
||||
};
|
||||
};
|
||||
const overlapArea = (a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
const dx = Math.min(a.right, b.right) - Math.max(a.left, b.left);
|
||||
const dy = Math.min(a.bottom, b.bottom) - Math.max(a.top, b.top);
|
||||
if (dx <= 0 || dy <= 0) return 0;
|
||||
return Math.round(dx * dy);
|
||||
};
|
||||
const context = document.querySelector(".entry-context");
|
||||
const wrap = document.querySelector(".entry-form-wrap");
|
||||
const form = document.querySelector(".entry-form");
|
||||
const role = document.querySelector(".role-switch");
|
||||
const spans = [...document.querySelectorAll(".role-switch span")];
|
||||
const brand = document.querySelector("#product-name");
|
||||
const status = document.querySelector(".entry-status");
|
||||
const statement = document.querySelector(".entry-statement h1");
|
||||
const ctx = box(context);
|
||||
const wr = box(wrap);
|
||||
const fm = box(form);
|
||||
const sp = spans.map(box);
|
||||
const clipped = (el) => Boolean(el && el.scrollWidth > el.clientWidth + 1);
|
||||
return {
|
||||
viewport: window.innerWidth,
|
||||
viewportH: window.innerHeight,
|
||||
grid: getComputedStyle(document.querySelector(".entry-shell")).gridTemplateColumns,
|
||||
contextBox: ctx,
|
||||
wrapBox: wr,
|
||||
formBox: fm,
|
||||
formWidth: fm ? Math.round(fm.width) : 0,
|
||||
roleHeight: role ? Math.round(role.getBoundingClientRect().height) : 0,
|
||||
roleHorizontal: sp.length === 2 && sp[0].right <= sp[1].left + 2,
|
||||
dualZone: Boolean(ctx && wr && ctx.right <= wr.left + 2 && Math.abs(ctx.top - wr.top) < 48),
|
||||
stacked: Boolean(ctx && wr && ctx.bottom <= wr.top + 8),
|
||||
overlap: overlapArea(ctx, fm),
|
||||
pageOverflowX: document.documentElement.scrollWidth - window.innerWidth,
|
||||
formBottom: fm ? fm.bottom : 0,
|
||||
formFitsViewport: Boolean(fm && fm.top >= -1 && fm.bottom <= window.innerHeight + 2),
|
||||
brandClipped: clipped(brand),
|
||||
statementClipped: clipped(statement),
|
||||
statusText: (status?.textContent || "").replace(/\s+/g, " ").trim(),
|
||||
periodText: (document.querySelector(".entry-facts")?.textContent || "").replace(/\s+/g, " ").trim(),
|
||||
hasToggle: Boolean(document.querySelector("#togglePassword")),
|
||||
hasError: Boolean(document.querySelector("#loginError")),
|
||||
changeHidden: document.querySelector("#changePassword")?.hidden === true,
|
||||
};
|
||||
})()
|
||||
"""
|
||||
|
||||
VIEWPORTS = (
|
||||
(1440, 900, "desktop", "b229-login-1440.png"),
|
||||
(1366, 768, "desktop", "b229-login-1366.png"),
|
||||
(1024, 768, "desktop", "b229-login-1024.png"),
|
||||
(768, 1024, "compact", "b229-login-768.png"),
|
||||
(375, 812, "mobile", "b229-login-375.png"),
|
||||
)
|
||||
|
||||
|
||||
class LoginLayoutTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
cls.chrome = chrome_bin()
|
||||
cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), QuietHandler)
|
||||
cls.http_thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True)
|
||||
cls.http_thread.start()
|
||||
cls.port = cls.httpd.server_address[1]
|
||||
cls.tmp = tempfile.TemporaryDirectory(prefix="b229-login-", ignore_cleanup_errors=True)
|
||||
cls.proc = subprocess.Popen(
|
||||
[
|
||||
str(cls.chrome),
|
||||
"--headless=new",
|
||||
"--disable-gpu",
|
||||
"--no-first-run",
|
||||
"--disable-extensions",
|
||||
"--hide-scrollbars",
|
||||
"--remote-debugging-port=0",
|
||||
f"--user-data-dir={cls.tmp.name}",
|
||||
"about:blank",
|
||||
],
|
||||
stdout=subprocess.DEVNULL,
|
||||
stderr=subprocess.DEVNULL,
|
||||
)
|
||||
port_file = Path(cls.tmp.name) / "DevToolsActivePort"
|
||||
deadline = time.time() + 15
|
||||
listing = None
|
||||
while time.time() < deadline:
|
||||
if port_file.exists() and port_file.stat().st_size:
|
||||
text = port_file.read_text(encoding="utf-8").strip().splitlines()
|
||||
if text:
|
||||
cls.debug_port = int(text[0])
|
||||
try:
|
||||
listing = json.loads(
|
||||
urlopen(f"http://127.0.0.1:{cls.debug_port}/json/list", timeout=5).read()
|
||||
)
|
||||
except Exception:
|
||||
listing = None
|
||||
if listing and any(item.get("type") == "page" for item in listing):
|
||||
break
|
||||
time.sleep(0.05)
|
||||
else:
|
||||
raise RuntimeError("Chrome DevTools port not ready")
|
||||
page = next(item for item in listing if item.get("type") == "page")
|
||||
cls.cdp = _Cdp(page["webSocketDebuggerUrl"])
|
||||
cls.cdp.call("Runtime.enable")
|
||||
cls.cdp.call("Page.enable")
|
||||
SCREEN_DIR.mkdir(exist_ok=True)
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
if getattr(cls, "cdp", None):
|
||||
cls.cdp.close()
|
||||
if getattr(cls, "proc", None):
|
||||
cls.proc.terminate()
|
||||
try:
|
||||
cls.proc.wait(timeout=5)
|
||||
except Exception:
|
||||
cls.proc.kill()
|
||||
cls.proc.wait(timeout=5)
|
||||
time.sleep(0.2)
|
||||
if getattr(cls, "httpd", None):
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
if getattr(cls, "tmp", None):
|
||||
cls.tmp.cleanup()
|
||||
|
||||
def _open(self, width: int, height: int) -> dict:
|
||||
self.cdp.call(
|
||||
"Emulation.setDeviceMetricsOverride",
|
||||
{
|
||||
"width": width,
|
||||
"height": height,
|
||||
"deviceScaleFactor": 1,
|
||||
"mobile": width <= 720,
|
||||
},
|
||||
)
|
||||
self.cdp.call("Page.navigate", {"url": f"http://127.0.0.1:{self.port}{LOGIN_URL}"})
|
||||
time.sleep(0.4)
|
||||
deadline = time.time() + 8
|
||||
last = None
|
||||
while time.time() < deadline:
|
||||
result = self.cdp.call("Runtime.evaluate", {"expression": MEASURE, "returnByValue": True})
|
||||
last = result.get("result", {}).get("value")
|
||||
if last and last.get("formWidth"):
|
||||
return last
|
||||
time.sleep(0.1)
|
||||
raise TimeoutError(f"login metrics not ready: {last}")
|
||||
|
||||
def _shot(self, name: str) -> None:
|
||||
raw = self.cdp.call("Page.captureScreenshot", {"format": "png", "captureBeyondViewport": False})
|
||||
(SCREEN_DIR / name).write_bytes(base64.b64decode(raw["data"]))
|
||||
|
||||
def test_login_viewports(self) -> None:
|
||||
for width, height, mode, shot in VIEWPORTS:
|
||||
with self.subTest(width=width, height=height, mode=mode):
|
||||
metrics = self._open(width, height)
|
||||
self._shot(shot)
|
||||
self.assertEqual(width, metrics["viewport"], metrics)
|
||||
self.assertEqual(0, metrics["overlap"], metrics)
|
||||
self.assertLessEqual(metrics["pageOverflowX"], 1, metrics)
|
||||
self.assertFalse(metrics["brandClipped"], metrics)
|
||||
self.assertFalse(metrics["statementClipped"], metrics)
|
||||
self.assertTrue(metrics["roleHorizontal"], metrics)
|
||||
self.assertLessEqual(metrics["roleHeight"], 52, metrics)
|
||||
self.assertIn("运行中", metrics["statusText"], metrics)
|
||||
self.assertIn("2026 年 7 月", metrics["periodText"], metrics)
|
||||
self.assertTrue(metrics["hasToggle"], metrics)
|
||||
self.assertTrue(metrics["hasError"], metrics)
|
||||
self.assertTrue(metrics["changeHidden"], metrics)
|
||||
if mode == "desktop":
|
||||
self.assertTrue(metrics["dualZone"], metrics)
|
||||
self.assertFalse(metrics["stacked"], metrics)
|
||||
self.assertGreaterEqual(metrics["formWidth"], 420, metrics)
|
||||
self.assertLessEqual(metrics["formWidth"], 460, metrics)
|
||||
self.assertTrue(metrics["formFitsViewport"], metrics)
|
||||
elif mode == "compact":
|
||||
self.assertTrue(metrics["dualZone"], metrics)
|
||||
self.assertFalse(metrics["stacked"], metrics)
|
||||
self.assertGreaterEqual(metrics["formWidth"], 360, metrics)
|
||||
self.assertLessEqual(metrics["formWidth"], 440, metrics)
|
||||
else:
|
||||
self.assertTrue(metrics["stacked"], metrics)
|
||||
self.assertFalse(metrics["dualZone"], metrics)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
+17
-10
@@ -11,19 +11,31 @@
|
||||
<!--
|
||||
THESIS: 登录页只完成身份确认,并明确区分总账管理端与公司业务端。
|
||||
OWN-WORLD: 深黑身份场景、石墨玻璃表单和荧光绿当前状态,延续双端工作台的材料语言。
|
||||
STORY: 居中集团登录卡:先确认集团身份与系统名称,再选择工作端口登录。
|
||||
FIRST VIEWPORT: 一张悬浮玻璃登录卡居于氛围光中央,集团名称置顶,端口选择、账号表单与系统事实依次排列。
|
||||
STORY: 桌面双区入口:左侧确认集团身份、系统名称与账期状态,右侧选择工作端口后登录。
|
||||
FIRST VIEWPORT: 横向双区;左侧入口信息,右侧 420-460px 登录表单。端口选择为紧凑分段控件。
|
||||
FORM: 用户参考图锁定的深色玻璃财务工作台,Operate 模式;seed key fe8a50aa。
|
||||
FINISH: unreviewed and undocumented is unfinished; this build ends with the finish review, the verdict, and DESIGN.md
|
||||
-->
|
||||
<main class="entry-shell">
|
||||
<section class="entry-card" aria-labelledby="login-title">
|
||||
<section class="entry-context" aria-labelledby="product-name">
|
||||
<div class="entry-brand"><span class="brand-mark">金</span><span><strong id="product-name">河南金牛实业集团</strong><small>集团资金往来管理系统</small></span></div>
|
||||
<p class="entry-status"><span>服务状态</span><b>运行中</b></p>
|
||||
<div class="entry-statement">
|
||||
<h1>一笔往来,追溯到双方银行凭证。</h1>
|
||||
<p>选择与账号一致的工作端口后进入对应工作台。总账管理端查看集团全貌,公司业务端只处理本公司账务。</p>
|
||||
</div>
|
||||
<dl class="entry-facts">
|
||||
<div><dt>全局起算日</dt><dd>2026.01.01</dd></div>
|
||||
<div><dt>当前账期</dt><dd>2026 年 7 月</dd></div>
|
||||
<div><dt>覆盖银行</dt><dd>已对接 6 家</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
<section class="entry-form-wrap" aria-labelledby="login-title">
|
||||
<form class="entry-form" id="loginForm">
|
||||
<header><h2 id="login-title">登录</h2><p>请选择与账号一致的工作端口</p></header>
|
||||
<div class="role-switch" role="radiogroup" aria-label="工作端口">
|
||||
<label><input type="radio" name="role" value="admin" checked /><span><svg><use href="icons.svg#shield-check"/></svg><b>总账管理端</b><small>集团管理员</small></span></label>
|
||||
<label><input type="radio" name="role" value="company" /><span><svg><use href="icons.svg#building"/></svg><b>公司业务端</b><small>公司出纳</small></span></label>
|
||||
<label><input type="radio" name="role" value="admin" checked /><span><svg><use href="icons.svg#shield-check"/></svg><b>总账管理端</b><small class="sr-only">集团管理员</small></span></label>
|
||||
<label><input type="radio" name="role" value="company" /><span><svg><use href="icons.svg#building"/></svg><b>公司业务端</b><small class="sr-only">公司出纳</small></span></label>
|
||||
</div>
|
||||
<label class="field"><span>账号</span><input name="username" autocomplete="username" required /></label>
|
||||
<label class="field"><span>密码</span><span class="password-field"><input name="password" type="password" autocomplete="current-password" required /><button type="button" class="inside-icon" id="togglePassword" aria-label="显示密码" title="显示密码"><svg><use href="icons.svg#eye"/></svg></button></span></label>
|
||||
@@ -36,11 +48,6 @@
|
||||
<label class="check-field"><input type="checkbox" checked />记住本次登录</label>
|
||||
<button class="button primary wide" type="submit"><span id="loginAction">进入总账管理端</span><svg><use href="icons.svg#chevron-right"/></svg></button>
|
||||
</form>
|
||||
<dl class="entry-facts">
|
||||
<div><dt>全局起算日</dt><dd>2026.01.01</dd></div>
|
||||
<div><dt>当前账期</dt><dd>2026 年 7 月</dd></div>
|
||||
<div><dt>覆盖银行</dt><dd>已对接 6 家</dd></div>
|
||||
</dl>
|
||||
</section>
|
||||
</main>
|
||||
<script src="app.js"></script>
|
||||
|
||||
+50
-51
@@ -435,32 +435,59 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; }
|
||||
|
||||
/* Login */
|
||||
.entry-page { overflow-x: hidden; }
|
||||
.entry-shell { min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1.15fr) minmax(430px, 0.85fr); }
|
||||
.entry-context { position: relative; min-height: 100vh; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(32px, 5vw, 76px); border-right: 1px solid var(--color-line); background: #080808; }
|
||||
.entry-context::after { content: ""; position: absolute; inset: 12% 8% 12% auto; width: 1px; background: rgba(55, 235, 137, 0.32); box-shadow: 0 0 38px rgba(55, 235, 137, 0.45); }
|
||||
.entry-brand { display: flex; align-items: center; gap: 12px; }
|
||||
.entry-brand > span:last-child { display: flex; flex-direction: column; }
|
||||
.entry-shell { width: 100%; max-width: none; min-height: 100vh; display: grid; grid-template-columns: minmax(0, 1fr) minmax(460px, 520px); margin: 0; padding: 0; }
|
||||
.entry-context { position: relative; min-width: 0; min-height: 100vh; display: flex; flex-direction: column; justify-content: space-between; padding: clamp(28px, 4vw, 56px) clamp(28px, 4vw, 64px); border-right: 1px solid var(--color-line); background: var(--color-bg-soft); }
|
||||
.entry-brand { display: flex; align-items: center; gap: 12px; min-width: 0; }
|
||||
.entry-brand > span:last-child { display: flex; flex-direction: column; min-width: 0; }
|
||||
.entry-brand strong { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
|
||||
.entry-brand small { color: var(--color-ink-muted); }
|
||||
.entry-statement { max-width: 680px; margin: 80px 0; }
|
||||
.entry-statement h1 { max-width: 640px; font-size: clamp(38px, 5vw, 70px); line-height: 1.12; letter-spacing: 0; }
|
||||
.entry-statement p { margin-top: 20px; color: var(--color-ink-muted); }
|
||||
.entry-facts { display: grid; grid-template-columns: repeat(3, 1fr); gap: 12px; max-width: 670px; }
|
||||
.entry-facts div { padding: 15px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: var(--color-surface-muted); }
|
||||
.entry-facts dt { color: var(--color-ink-muted); }
|
||||
.entry-facts dd { margin-top: 4px; color: var(--color-primary); }
|
||||
.entry-form-wrap { min-height: 100vh; display: grid; place-items: center; padding: 40px; background: var(--color-bg); }
|
||||
.entry-form { width: min(420px, 100%); display: grid; gap: 18px; padding: 28px; border: 1px solid var(--color-line); border-radius: var(--radius-xl); background: var(--color-surface); box-shadow: var(--shadow-panel); backdrop-filter: blur(24px); }
|
||||
.entry-form header h2 { font-size: 25px; }
|
||||
.entry-status { display: flex; align-items: center; gap: 8px; margin: 14px 0 0; color: var(--color-ink-soft); font-size: 12px; }
|
||||
.entry-status span { color: var(--color-ink-muted); }
|
||||
.entry-status b { color: var(--color-ink); font-weight: 700; }
|
||||
.entry-status b::before { content: ""; display: inline-block; width: 7px; height: 7px; margin-right: 6px; border-radius: 50%; background: var(--color-primary); vertical-align: 1px; }
|
||||
.entry-statement { max-width: 640px; margin: 48px 0 auto; }
|
||||
.entry-statement h1 { max-width: 620px; font-size: clamp(30px, 3.4vw, 48px); line-height: 1.18; letter-spacing: 0; }
|
||||
.entry-statement p { margin-top: 12px; color: var(--color-ink-muted); max-width: 520px; }
|
||||
.entry-facts { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 10px; max-width: 640px; }
|
||||
.entry-facts div { min-width: 0; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-sm); background: var(--color-surface-muted); }
|
||||
.entry-facts dt { color: var(--color-ink-muted); font-size: 11px; }
|
||||
.entry-facts dd { margin-top: 4px; color: var(--color-primary); overflow-wrap: anywhere; }
|
||||
.entry-form-wrap { min-width: 0; min-height: 100vh; display: grid; place-items: center; padding: 28px 24px; background: var(--color-bg); }
|
||||
.entry-form { width: min(440px, 100%); display: grid; gap: 11px; padding: 22px 22px 20px; border: 1px solid var(--color-line); border-radius: var(--radius-lg); background: var(--color-surface); box-shadow: var(--shadow-low); backdrop-filter: blur(18px); }
|
||||
.entry-form header h2 { font-size: 20px; }
|
||||
.entry-form header p { margin-top: 4px; font-size: 12px; }
|
||||
.entry-form header p, .entry-note { color: var(--color-ink-muted); }
|
||||
.role-switch { display: grid; grid-template-columns: 1fr 1fr; gap: 9px; }
|
||||
.role-switch label { cursor: pointer; }
|
||||
.role-switch input { position: absolute; opacity: 0; }
|
||||
.role-switch span { min-height: 83px; display: grid; grid-template-columns: 30px 1fr; align-content: center; gap: 1px 9px; padding: 12px; border: 1px solid var(--color-line); border-radius: var(--radius-md); background: rgba(0, 0, 0, 0.14); transition: border-color var(--duration-fast), background var(--duration-fast), transform var(--duration-fast); }
|
||||
.role-switch svg { grid-row: 1 / 3; align-self: center; color: var(--color-ink-muted); }
|
||||
.role-switch small { color: var(--color-ink-muted); }
|
||||
.role-switch input:checked + span { border-color: rgba(55, 235, 137, 0.4); background: var(--color-primary-wash); transform: translateY(-1px); }
|
||||
.entry-form .field { gap: 4px; }
|
||||
.role-switch { display: flex; gap: 4px; padding: 4px; border: 1px solid var(--color-line); border-radius: 13px; background: rgba(0, 0, 0, 0.18); }
|
||||
.role-switch label { position: relative; flex: 1; min-width: 0; cursor: pointer; }
|
||||
.role-switch input { position: absolute; opacity: 0; width: 1px; height: 1px; }
|
||||
.role-switch span { min-height: 36px; display: flex; align-items: center; justify-content: center; gap: 6px; padding: 0 8px; border: 0; border-radius: 9px; background: transparent; color: var(--color-ink-muted); transition: color var(--duration-fast), background var(--duration-fast); }
|
||||
.role-switch svg { width: 15px; height: 15px; flex: 0 0 auto; }
|
||||
.role-switch b { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px; }
|
||||
.role-switch input:checked + span { background: var(--color-primary-wash); color: var(--color-ink); }
|
||||
.role-switch input:checked + span svg { color: var(--color-primary); }
|
||||
.entry-note { font-size: 10px; }
|
||||
.role-switch input:focus-visible + span { outline: 2px solid var(--color-primary-strong); outline-offset: 2px; }
|
||||
.entry-note { font-size: 12px; }
|
||||
|
||||
@media (max-width: 1023px) {
|
||||
.entry-shell { grid-template-columns: minmax(0, 1fr) minmax(400px, 440px); }
|
||||
.entry-context { padding: 24px 20px; }
|
||||
.entry-statement { margin: 28px 0 auto; }
|
||||
.entry-statement h1 { font-size: clamp(24px, 3.6vw, 34px); }
|
||||
.entry-facts { grid-template-columns: 1fr; max-width: none; }
|
||||
.entry-form-wrap { padding: 20px 16px; }
|
||||
.entry-form { width: min(400px, 100%); padding: 18px; gap: 10px; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
.entry-shell { grid-template-columns: 1fr; }
|
||||
.entry-context { min-height: auto; padding: 22px 16px 16px; border-right: 0; border-bottom: 1px solid var(--color-line); }
|
||||
.entry-statement { margin: 18px 0 14px; }
|
||||
.entry-statement h1 { font-size: 24px; }
|
||||
.entry-facts { grid-template-columns: 1fr; }
|
||||
.entry-form-wrap { min-height: auto; padding: 16px 16px 32px; align-content: start; }
|
||||
.entry-form { width: 100%; padding: 16px; }
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
.app-shell { grid-template-columns: 88px minmax(0, 1fr); }
|
||||
@@ -497,9 +524,6 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; }
|
||||
.subject-strip { grid-template-columns: repeat(3, 1fr); }
|
||||
.work-progress { grid-template-columns: 1fr 24px 1fr; row-gap: 16px; }
|
||||
.work-progress i:nth-of-type(2) { display: none; }
|
||||
.entry-shell { grid-template-columns: 1fr; }
|
||||
.entry-context { min-height: 58vh; border-right: 0; border-bottom: 1px solid var(--color-line); }
|
||||
.entry-form-wrap { min-height: auto; }
|
||||
}
|
||||
|
||||
@media (max-width: 720px) {
|
||||
@@ -573,12 +597,6 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; }
|
||||
.form-grid { grid-template-columns: 1fr; }
|
||||
.toast-region { right: 12px; bottom: 12px; left: 12px; }
|
||||
.toast { min-width: 0; max-width: none; }
|
||||
.entry-context { min-height: auto; padding: 30px 22px; }
|
||||
.entry-statement { margin: 54px 0 42px; }
|
||||
.entry-statement h1 { font-size: 39px; }
|
||||
.entry-facts { grid-template-columns: 1fr; }
|
||||
.entry-form-wrap { padding: 32px 16px 48px; }
|
||||
.entry-form { padding: 22px; }
|
||||
}
|
||||
|
||||
@media (max-width: 460px) {
|
||||
@@ -586,7 +604,6 @@ main { width: min(1560px, 100%); margin: 0 auto; padding: 12px 32px 58px; }
|
||||
.metric-copy { min-height: 52px; }
|
||||
.metric-label { font-size: 11px; }
|
||||
.metric-value { font-size: 23px; }
|
||||
.role-switch { grid-template-columns: 1fr; }
|
||||
}
|
||||
|
||||
@media (max-width: 1180px) {
|
||||
@@ -718,24 +735,6 @@ body::before { content: ""; position: fixed; inset: 0; z-index: 0; pointer-event
|
||||
.table-scroll { padding: 2px 14px 8px; }
|
||||
.table-summary { margin: 0 14px; }
|
||||
|
||||
/* ===== 登录页重做:居中集团登录卡 ===== */
|
||||
.entry-shell { display: grid; grid-template-columns: 1fr; place-items: center; min-height: 100vh; padding: 48px 20px; }
|
||||
.entry-card { position: relative; width: min(470px, 100%); overflow: hidden; border: 1px solid rgba(255, 255, 255, 0.13); border-radius: var(--radius-xl); background: linear-gradient(165deg, rgba(36, 36, 36, 0.9) 0%, rgba(16, 16, 16, 0.92) 60%, rgba(10, 10, 10, 0.94) 100%); box-shadow: var(--shadow-panel), 0 0 60px rgba(55, 235, 137, 0.06); backdrop-filter: blur(28px) saturate(125%); }
|
||||
.entry-card::before { content: ""; position: absolute; inset: 0; background: radial-gradient(150% 110% at 50% 118%, rgba(255, 255, 255, 0.07), transparent 55%), linear-gradient(165deg, rgba(255, 255, 255, 0.12) 0%, rgba(255, 255, 255, 0.03) 42%, transparent 70%); pointer-events: none; }
|
||||
.entry-card::after { content: ""; position: absolute; top: 0; left: 18%; right: 18%; height: 2px; background: linear-gradient(90deg, transparent, rgba(55, 235, 137, 0.65), transparent); }
|
||||
.entry-brand { flex-direction: column; justify-content: center; gap: 15px; min-height: 0; padding: 36px 28px 24px; border-bottom: 1px solid var(--color-line); text-align: center; }
|
||||
.entry-brand .brand-mark { width: 54px; height: 54px; border-radius: 17px; font-size: 26px; }
|
||||
.entry-brand > span:last-child { align-items: center; }
|
||||
.entry-brand strong { font-size: 21px; letter-spacing: 0.03em; }
|
||||
.entry-brand small { margin-top: 4px; font-size: 12px; letter-spacing: 0.08em; }
|
||||
.entry-form { width: 100%; gap: 16px; padding: 24px 28px 10px; border: 0; background: transparent; box-shadow: none; backdrop-filter: none; }
|
||||
.entry-form header { text-align: center; }
|
||||
.entry-form header h2 { font-size: 22px; }
|
||||
.entry-form header p { margin-top: 4px; color: var(--color-ink-muted); font-size: 12px; }
|
||||
.entry-facts { max-width: none; gap: 10px; padding: 16px 28px 28px; }
|
||||
.entry-facts div { padding: 12px; text-align: center; }
|
||||
.entry-facts dd { margin-top: 2px; font-size: 12px; }
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
*, *::before, *::after { scroll-behavior: auto !important; animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user