HEL-351: 修复夜间下拉/日期弹层、一次性初始密码领取与缩放滚动条
夜间控件改走现有 token 与 color-scheme,日期弹层锚定触发器并可翻转; 创建公司账号改为一次性可复制口令窗口,列表与后续接口不再回明文。 Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: multica-agent <github@multica.ai>
This commit is contained in:
co-authored by
Cursor
multica-agent
parent
9d06445d81
commit
acc3c29c49
@@ -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=17", html)
|
||||
self.assertIn("app.js?v=18", html)
|
||||
# 静态初值仍为进行中(黄),由 JS 在 pending=0 时切 done
|
||||
self.assertRegex(html, r'class="flow-step doing"[^>]*data-view-link="reconcile"')
|
||||
|
||||
|
||||
@@ -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=12", html)
|
||||
self.assertIn("app.js?v=17", html)
|
||||
self.assertIn("design-system.css?v=13", html)
|
||||
self.assertIn("app.js?v=18", html)
|
||||
# 侧栏顺序:流水管理 → 转账往来 → 往来确认
|
||||
flows = html.index('data-view="flows"')
|
||||
transfers = html.index('data-view="transfers"')
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
"""HEL-351: 夜间控件、一次性初始密码交付、缩放滚动条契约。"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import threading
|
||||
import unittest
|
||||
from functools import partial
|
||||
from http.server import SimpleHTTPRequestHandler, ThreadingHTTPServer
|
||||
from pathlib import Path
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
WEB = ROOT / "web"
|
||||
|
||||
try:
|
||||
from playwright.sync_api import sync_playwright
|
||||
except ImportError: # pragma: no cover
|
||||
sync_playwright = None
|
||||
|
||||
|
||||
def _prepare_chrome_libs() -> Path | None:
|
||||
candidates = [ROOT / ".chrome-libs" / "lib"]
|
||||
for lib_dir in candidates:
|
||||
if (lib_dir / "libatk-1.0.so.0").exists():
|
||||
current = os.environ.get("LD_LIBRARY_PATH", "")
|
||||
prefix = str(lib_dir)
|
||||
if prefix not in current.split(":"):
|
||||
os.environ["LD_LIBRARY_PATH"] = (
|
||||
f"{prefix}:{current}" if current else prefix
|
||||
)
|
||||
return lib_dir
|
||||
return None
|
||||
|
||||
|
||||
def _chromium_available() -> bool:
|
||||
if not sync_playwright:
|
||||
return False
|
||||
_prepare_chrome_libs()
|
||||
try:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
|
||||
browser.close()
|
||||
return True
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
|
||||
class Hel351SourceContractTests(unittest.TestCase):
|
||||
def test_night_select_and_scrollbar_use_tokens(self) -> None:
|
||||
css = (WEB / "design-system.css").read_text(encoding="utf-8")
|
||||
self.assertIn("color-scheme: light", css)
|
||||
self.assertIn("color-scheme: dark", css)
|
||||
self.assertIn("scrollbar-color: var(--gold) var(--surface-2)", css)
|
||||
self.assertIn("html[data-theme=\"night\"] ::-webkit-scrollbar-thumb", css)
|
||||
self.assertIn("background-color: var(--gold)", css)
|
||||
self.assertIn(".select option", css)
|
||||
self.assertIn("background-color: var(--surface)", css)
|
||||
self.assertIn("html[data-theme=\"night\"] .ds-dp-day.is-selected", css)
|
||||
self.assertIn("color: var(--gold)", css)
|
||||
self.assertNotIn("overflow: hidden; /* HEL-351", css)
|
||||
|
||||
def test_tabs_and_table_split_overflow_axes(self) -> None:
|
||||
css = (WEB / "design-system.css").read_text(encoding="utf-8")
|
||||
tabs = css[css.index(".tabs {") : css.index(".tabs button")]
|
||||
self.assertIn("overflow-x: auto", tabs)
|
||||
self.assertIn("overflow-y: hidden", tabs)
|
||||
wrap = css[css.index(".table-wrap {") : css.index(".table-wrap.dash-master-scroll")]
|
||||
self.assertIn("overflow-x: auto", wrap)
|
||||
self.assertIn("overflow-y: hidden", wrap)
|
||||
self.assertIn(".table-wrap.dash-master-scroll", css)
|
||||
self.assertIn("overflow-y: auto", css[css.index(".table-wrap.dash-master-scroll") :][:180])
|
||||
|
||||
def test_admin_delivers_once_password_window(self) -> None:
|
||||
html = (WEB / "admin.html").read_text(encoding="utf-8")
|
||||
self.assertIn('id="credentialDialog"', html)
|
||||
self.assertIn('id="cred-pass"', html)
|
||||
self.assertIn('id="cred-copy-pass"', html)
|
||||
self.assertIn("datepicker.js?v=1", html)
|
||||
self.assertNotIn("初始密码由管理员统一发放", html)
|
||||
js = (WEB / "app.js").read_text(encoding="utf-8")
|
||||
self.assertIn("function showOnceCredentials(", js)
|
||||
self.assertIn("function wipeCredentials(", js)
|
||||
self.assertNotIn("初始密码已生成(仅此一次显示):${result.initial_password}", js)
|
||||
self.assertNotIn("临时密码已生成(仅此一次):${result.initial_password}", js)
|
||||
self.assertIn('openModal("credentialDialog")', js)
|
||||
|
||||
def test_datepicker_follows_trigger_and_flips(self) -> None:
|
||||
js = (WEB / "datepicker.js").read_text(encoding="utf-8")
|
||||
css = (WEB / "design-system.css").read_text(encoding="utf-8")
|
||||
self.assertIn("visualViewport", js)
|
||||
self.assertIn("getBoundingClientRect", js)
|
||||
self.assertIn("rect.top - gap - height", js)
|
||||
self.assertIn('addEventListener("scroll", position, true)', js)
|
||||
block = css[css.index(".ds-datepicker {") : css.index(".ds-datepicker[hidden]")]
|
||||
self.assertIn("position: fixed", block)
|
||||
self.assertIn("z-index: var(--z-tooltip)", block)
|
||||
|
||||
|
||||
@unittest.skipUnless(sync_playwright, "playwright 未安装,跳过浏览器探针")
|
||||
@unittest.skipUnless(_chromium_available(), "chromium 无法启动,跳过浏览器探针")
|
||||
class Hel351BrowserProbeTests(unittest.TestCase):
|
||||
@classmethod
|
||||
def setUpClass(cls) -> None:
|
||||
handler = partial(SimpleHTTPRequestHandler, directory=str(WEB))
|
||||
cls.httpd = ThreadingHTTPServer(("127.0.0.1", 0), handler)
|
||||
cls.port = cls.httpd.server_address[1]
|
||||
cls.thread = threading.Thread(target=cls.httpd.serve_forever, daemon=True)
|
||||
cls.thread.start()
|
||||
cls.base = f"http://127.0.0.1:{cls.port}"
|
||||
|
||||
@classmethod
|
||||
def tearDownClass(cls) -> None:
|
||||
cls.httpd.shutdown()
|
||||
cls.httpd.server_close()
|
||||
|
||||
def _open(self, page, *, night: bool, width: int = 1440, zoom: float = 1) -> None:
|
||||
page.set_viewport_size({"width": width, "height": 900})
|
||||
theme_attr = 'data-theme="night"' if night else 'data-theme="day"'
|
||||
page.set_content(
|
||||
f"""<!doctype html>
|
||||
<html lang="zh-CN" class="v-fusion" {theme_attr}>
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="stylesheet" href="{self.base}/design-system.css"/>
|
||||
</head>
|
||||
<body class="is-app" {theme_attr}>
|
||||
<div class="tabs" id="probe-tabs">
|
||||
<button class="active">全部</button>
|
||||
<button>流水断档</button>
|
||||
<button>单边匹配</button>
|
||||
<button>科目确认</button>
|
||||
<button>起算区间校准</button>
|
||||
<button>银行账户登记</button>
|
||||
<button>公司手工记录</button>
|
||||
<button>重开审批</button>
|
||||
</div>
|
||||
<select class="select" id="probe-select">
|
||||
<option>河南金牛农业科技发展有限公司</option>
|
||||
<option>河南金牛煤业有限公司</option>
|
||||
</select>
|
||||
<input class="input" id="probe-date" type="date" value="2026-09-01" />
|
||||
<script src="{self.base}/datepicker.js"></script>
|
||||
</body></html>""",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
page.wait_for_function("() => window.JinniuDatePicker")
|
||||
if zoom != 1:
|
||||
page.evaluate(f"() => {{ document.body.style.zoom = '{zoom}'; }}")
|
||||
|
||||
def test_night_select_is_not_white_on_white(self) -> None:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
self._open(page, night=True)
|
||||
colors = page.evaluate(
|
||||
"""() => {
|
||||
const sel = document.getElementById('probe-select');
|
||||
const cs = getComputedStyle(sel);
|
||||
const opt = getComputedStyle(sel.options[0]);
|
||||
return {
|
||||
scheme: getComputedStyle(document.documentElement).colorScheme,
|
||||
bg: cs.backgroundColor,
|
||||
color: cs.color,
|
||||
optionBg: opt.backgroundColor,
|
||||
optionColor: opt.color,
|
||||
};
|
||||
}"""
|
||||
)
|
||||
browser.close()
|
||||
self.assertEqual("dark", colors["scheme"])
|
||||
self.assertNotEqual("rgb(255, 255, 255)", colors["bg"])
|
||||
self.assertNotEqual("rgb(255, 255, 255)", colors["color"])
|
||||
self.assertNotEqual("rgb(255, 255, 255)", colors["optionBg"])
|
||||
|
||||
def test_tabs_have_no_vertical_scrollbar_at_zoom(self) -> None:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
|
||||
page = browser.new_page(viewport={"width": 390, "height": 800})
|
||||
self._open(page, night=False, width=390, zoom=1.25)
|
||||
metrics = page.evaluate(
|
||||
"""() => {
|
||||
const tabs = document.getElementById('probe-tabs');
|
||||
const cs = getComputedStyle(tabs);
|
||||
return {
|
||||
overflowX: cs.overflowX,
|
||||
overflowY: cs.overflowY,
|
||||
clientHeight: tabs.clientHeight,
|
||||
scrollHeight: tabs.scrollHeight,
|
||||
clientWidth: tabs.clientWidth,
|
||||
scrollWidth: tabs.scrollWidth,
|
||||
};
|
||||
}"""
|
||||
)
|
||||
browser.close()
|
||||
self.assertEqual("auto", metrics["overflowX"])
|
||||
self.assertEqual("hidden", metrics["overflowY"])
|
||||
self.assertLessEqual(metrics["scrollHeight"] - metrics["clientHeight"], 1)
|
||||
|
||||
def test_datepicker_anchors_and_night_gold(self) -> None:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 900})
|
||||
self._open(page, night=True)
|
||||
page.click("#probe-date")
|
||||
page.wait_for_selector("#ds-datepicker:not([hidden])")
|
||||
box = page.evaluate(
|
||||
"""() => {
|
||||
const input = document.getElementById('probe-date');
|
||||
const panel = document.getElementById('ds-datepicker');
|
||||
const ir = input.getBoundingClientRect();
|
||||
const pr = panel.getBoundingClientRect();
|
||||
const selected = panel.querySelector('.ds-dp-day.is-selected');
|
||||
const cs = selected ? getComputedStyle(selected) : null;
|
||||
return {
|
||||
inputBottom: ir.bottom,
|
||||
inputLeft: ir.left,
|
||||
panelTop: pr.top,
|
||||
panelLeft: pr.left,
|
||||
panelRight: pr.right,
|
||||
viewportWidth: window.innerWidth,
|
||||
selectedColor: cs && cs.color,
|
||||
selectedBg: cs && cs.backgroundColor,
|
||||
hidden: panel.hidden,
|
||||
};
|
||||
}"""
|
||||
)
|
||||
browser.close()
|
||||
self.assertFalse(box["hidden"])
|
||||
self.assertLess(abs(box["panelTop"] - box["inputBottom"]), 24)
|
||||
self.assertLess(abs(box["panelLeft"] - box["inputLeft"]), 24)
|
||||
self.assertLess(box["panelRight"], box["viewportWidth"])
|
||||
self.assertIsNotNone(box["selectedColor"])
|
||||
self.assertNotEqual("rgb(255, 255, 255)", box["selectedBg"])
|
||||
self.assertIn("217", box["selectedColor"])
|
||||
|
||||
def test_datepicker_flips_when_near_bottom(self) -> None:
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch(headless=True, args=["--no-sandbox"])
|
||||
page = browser.new_page(viewport={"width": 1440, "height": 500})
|
||||
page.set_content(
|
||||
f"""<!doctype html>
|
||||
<html lang="zh-CN" class="v-fusion" data-theme="night">
|
||||
<head>
|
||||
<meta charset="UTF-8"/>
|
||||
<link rel="stylesheet" href="{self.base}/design-system.css"/>
|
||||
</head>
|
||||
<body class="is-app" data-theme="night" style="min-height:100vh">
|
||||
<input class="input" id="probe-date" type="date" value="2026-09-01" style="position:fixed;left:24px;bottom:12px;width:180px" />
|
||||
<script src="{self.base}/datepicker.js"></script>
|
||||
</body></html>""",
|
||||
wait_until="domcontentloaded",
|
||||
)
|
||||
page.wait_for_function("() => window.JinniuDatePicker")
|
||||
page.click("#probe-date")
|
||||
page.wait_for_selector("#ds-datepicker:not([hidden])")
|
||||
box = page.evaluate(
|
||||
"""() => {
|
||||
const input = document.getElementById('probe-date');
|
||||
const panel = document.getElementById('ds-datepicker');
|
||||
const ir = input.getBoundingClientRect();
|
||||
const pr = panel.getBoundingClientRect();
|
||||
return { inputTop: ir.top, panelBottom: pr.bottom, panelTop: pr.top };
|
||||
}"""
|
||||
)
|
||||
browser.close()
|
||||
self.assertLess(box["panelBottom"], box["inputTop"] + 2)
|
||||
self.assertGreater(box["inputTop"] - box["panelBottom"], 0)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
@@ -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=12", html)
|
||||
self.assertIn("app.js?v=17", html)
|
||||
self.assertIn("design-system.css?v=13", html)
|
||||
self.assertIn("app.js?v=18", 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=17", html)
|
||||
self.assertIn("app.js?v=18", html)
|
||||
with sync_playwright() as p:
|
||||
browser = p.chromium.launch()
|
||||
page = browser.new_page()
|
||||
|
||||
@@ -540,6 +540,9 @@ class ServerAuthMatrixTests(unittest.TestCase):
|
||||
status, _, data = self.admin.get("/api/admin/users")
|
||||
self.assertEqual(200, status)
|
||||
bodies.append(data)
|
||||
status, _, data = self.admin.get("/api/admin/companies")
|
||||
self.assertEqual(200, status)
|
||||
bodies.append(data)
|
||||
status, _, data = self.admin.get("/api/admin/audit-log?limit=100")
|
||||
self.assertEqual(200, status)
|
||||
bodies.append(data)
|
||||
@@ -548,7 +551,9 @@ class ServerAuthMatrixTests(unittest.TestCase):
|
||||
status, _, data = self.cashier_a.get("/api/batches")
|
||||
bodies.append(data)
|
||||
for body in bodies:
|
||||
self.assertNotIn("password_hash", body.decode("utf-8"))
|
||||
text = body.decode("utf-8")
|
||||
self.assertNotIn("password_hash", text)
|
||||
self.assertNotIn("initial_password", text)
|
||||
|
||||
def test_audit_log_contains_no_plaintext_passwords(self) -> None:
|
||||
connection = connect(self.db_path)
|
||||
|
||||
+33
-5
@@ -11,7 +11,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
<link rel="stylesheet" href="design-system.css?v=13" />
|
||||
</head>
|
||||
<body class="is-app" data-portal="admin">
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
@@ -512,7 +512,7 @@
|
||||
</div>
|
||||
<div class="table-foot" style="border: 1px solid var(--border); border-radius: 0 0 var(--radius-lg) var(--radius-lg);">
|
||||
<span id="companyFoot">共 0 家公司</span>
|
||||
<span class="meta">登录账号初始密码由管理员统一发放</span>
|
||||
<span class="meta">创建账号时生成一次性随机密码,仅当时显示一次</span>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
@@ -876,7 +876,7 @@
|
||||
<span class="modal-title">新增成员公司</span>
|
||||
<button type="button" class="modal-close" data-close="companyDialog" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<p class="modal-sub">登记后系统将生成公司端登录账号,初始密码由管理员统一发放。</p>
|
||||
<p class="modal-sub">登记后可同时生成公司端登录账号。系统会发一次性随机初始密码,仅创建成功时显示一次,请当场复制后交给出纳;首次登录必须改密。没有全公司通用默认密码。</p>
|
||||
<form id="companyForm" novalidate>
|
||||
<div class="field" style="margin-bottom: 12px;">
|
||||
<label for="f-name">公司全称</label>
|
||||
@@ -899,7 +899,7 @@
|
||||
<input class="input" id="f-cashier" name="cashier" type="text" required />
|
||||
</div>
|
||||
</div>
|
||||
<p class="hint" style="margin-top: 10px;">创建后生成随机初始密码,仅显示一次,首次登录必须修改。</p>
|
||||
<p class="hint" style="margin-top: 10px;">创建成功后弹出一次性口令窗口,可复制账号和密码。关闭后无法再次查看明文,列表与日志也不会保存密码。</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn" data-close="companyDialog">取消</button>
|
||||
<button type="submit" class="btn btn-primary">创建公司与账号</button>
|
||||
@@ -908,6 +908,33 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 一次性初始密码领取(关闭后从页面清除明文) -->
|
||||
<div class="modal-backdrop" id="credentialDialog">
|
||||
<div class="modal" role="dialog" aria-labelledby="cred-title" aria-modal="true">
|
||||
<div class="modal-head">
|
||||
<span class="modal-title" id="cred-title">一次性初始密码</span>
|
||||
<button type="button" class="modal-close" data-close="credentialDialog" aria-label="关闭">×</button>
|
||||
</div>
|
||||
<p class="modal-sub" id="cred-sub">请立即复制并交给出纳。关闭后无法再次查看明文。</p>
|
||||
<div class="cred-box">
|
||||
<div class="cred-row">
|
||||
<span class="cred-label">登录账号</span>
|
||||
<span class="cred-value" id="cred-user"></span>
|
||||
<button type="button" class="btn btn-sm" id="cred-copy-user">复制</button>
|
||||
</div>
|
||||
<div class="cred-row">
|
||||
<span class="cred-label">初始密码</span>
|
||||
<span class="cred-value" id="cred-pass"></span>
|
||||
<button type="button" class="btn btn-sm btn-primary" id="cred-copy-pass">复制密码</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="cred-warn">对方首次登录必须修改密码。重置密码同样只展示这一次。</p>
|
||||
<div class="modal-actions">
|
||||
<button type="button" class="btn btn-primary" data-close="credentialDialog">我已抄录</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 公司详情弹窗 -->
|
||||
<div class="modal-backdrop" id="modal-detail">
|
||||
<div class="modal wide">
|
||||
@@ -1228,6 +1255,7 @@
|
||||
</div>
|
||||
<div class="toast-region" id="toastRegion" aria-live="polite"></div>
|
||||
<script src="theme.js?v=1"></script>
|
||||
<script src="app.js?v=17"></script>
|
||||
<script src="datepicker.js?v=1"></script>
|
||||
<script src="app.js?v=18"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
+81
-10
@@ -129,6 +129,50 @@ function showToast(title, detail = "", kind = "info") {
|
||||
});
|
||||
}
|
||||
|
||||
function copyText(value, button) {
|
||||
const text = String(value || "");
|
||||
const done = () => {
|
||||
if (!button) return;
|
||||
const previous = button.textContent;
|
||||
button.textContent = "已复制";
|
||||
window.setTimeout(() => { button.textContent = previous; }, 1400);
|
||||
};
|
||||
const fallback = () => {
|
||||
const area = document.createElement("textarea");
|
||||
area.value = text;
|
||||
area.setAttribute("readonly", "");
|
||||
area.style.position = "fixed";
|
||||
area.style.left = "-9999px";
|
||||
document.body.append(area);
|
||||
area.select();
|
||||
try { document.execCommand("copy"); } catch (err) { /* ignore */ }
|
||||
area.remove();
|
||||
done();
|
||||
};
|
||||
if (navigator.clipboard && navigator.clipboard.writeText) {
|
||||
navigator.clipboard.writeText(text).then(done).catch(fallback);
|
||||
} else {
|
||||
fallback();
|
||||
}
|
||||
}
|
||||
|
||||
function wipeCredentials() {
|
||||
const user = $("#cred-user");
|
||||
const pass = $("#cred-pass");
|
||||
if (user) user.textContent = "";
|
||||
if (pass) pass.textContent = "";
|
||||
}
|
||||
|
||||
function showOnceCredentials({ title, subtitle, username, password }) {
|
||||
const titleEl = $("#cred-title");
|
||||
const subEl = $("#cred-sub");
|
||||
if (titleEl && title) titleEl.textContent = title;
|
||||
if (subEl && subtitle) subEl.textContent = subtitle;
|
||||
if ($("#cred-user")) $("#cred-user").textContent = username || "";
|
||||
if ($("#cred-pass")) $("#cred-pass").textContent = password || "";
|
||||
openModal("credentialDialog");
|
||||
}
|
||||
|
||||
function toastIfLocked(result) {
|
||||
const msg = String(result?.message || "");
|
||||
if (!(result?.year_month || /已结账锁定/.test(msg))) return false;
|
||||
@@ -2675,9 +2719,24 @@ function initAdmin() {
|
||||
}));
|
||||
$("#auditCompany")?.addEventListener("change", filterAuditRows);
|
||||
|
||||
document.querySelectorAll("[data-close]").forEach((el) => el.addEventListener("click", () => closeModal(el.dataset.close)));
|
||||
document.querySelectorAll(".modal-backdrop").forEach((bd) => bd.addEventListener("click", (e) => { if (e.target === bd) bd.classList.remove("open"); }));
|
||||
document.addEventListener("keydown", (e) => { if (e.key === "Escape") document.querySelectorAll(".modal-backdrop.open").forEach((m) => m.classList.remove("open")); });
|
||||
document.querySelectorAll("[data-close]").forEach((el) => el.addEventListener("click", () => {
|
||||
closeModal(el.dataset.close);
|
||||
if (el.dataset.close === "credentialDialog") wipeCredentials();
|
||||
}));
|
||||
$("#cred-copy-user")?.addEventListener("click", () => copyText($("#cred-user")?.textContent, $("#cred-copy-user")));
|
||||
$("#cred-copy-pass")?.addEventListener("click", () => copyText($("#cred-pass")?.textContent, $("#cred-copy-pass")));
|
||||
document.querySelectorAll(".modal-backdrop").forEach((bd) => bd.addEventListener("click", (e) => {
|
||||
if (e.target !== bd) return;
|
||||
bd.classList.remove("open");
|
||||
if (bd.id === "credentialDialog") wipeCredentials();
|
||||
}));
|
||||
document.addEventListener("keydown", (e) => {
|
||||
if (e.key !== "Escape") return;
|
||||
document.querySelectorAll(".modal-backdrop.open").forEach((m) => {
|
||||
m.classList.remove("open");
|
||||
if (m.id === "credentialDialog") wipeCredentials();
|
||||
});
|
||||
});
|
||||
|
||||
async function submitAuditResult(row) {
|
||||
const decision = state.auditDecision;
|
||||
@@ -2889,11 +2948,17 @@ function initAdmin() {
|
||||
closeModal("companyDialog");
|
||||
form.reset();
|
||||
await loadAdminCompanies();
|
||||
showToast(
|
||||
accountCreated ? "公司与账号已创建" : "公司已创建",
|
||||
accountCreated ? `账号 ${result.username} 的初始密码已生成(仅此一次显示):${result.initial_password},首次登录必须修改` : "可稍后在账号管理中创建公司账号",
|
||||
"success",
|
||||
);
|
||||
if (accountCreated) {
|
||||
showOnceCredentials({
|
||||
title: "一次性初始密码",
|
||||
subtitle: "请立即复制并交给出纳。关闭后无法再次查看明文。",
|
||||
username: result.username,
|
||||
password: result.initial_password,
|
||||
});
|
||||
showToast("公司与账号已创建", "请在一次性口令窗口复制初始密码,关闭后无法再看", "success");
|
||||
} else {
|
||||
showToast("公司已创建", "可稍后在账号管理中创建公司账号", "success");
|
||||
}
|
||||
});
|
||||
|
||||
function openCompanyDetail(companyId) {
|
||||
@@ -2933,8 +2998,14 @@ function initAdmin() {
|
||||
if (!response || !response.ok) { showToast("重置失败", result?.message || "请稍后重试", "danger"); return; }
|
||||
$("#btn-reset-pwd").disabled = true;
|
||||
$("#btn-reset-pwd").textContent = "已重置";
|
||||
$("#d-login-meta").textContent = `临时密码已生成(仅此一次):${result.initial_password},首次登录必须修改`;
|
||||
showToast("密码已重置", "临时密码仅本次显示,首次登录必须修改", "success");
|
||||
$("#d-login-meta").textContent = "临时密码已在一次性窗口展示,关闭后无法再查看。";
|
||||
showOnceCredentials({
|
||||
title: "临时密码已生成",
|
||||
subtitle: "仅此一次显示。旧会话已失效,首次登录必须修改。",
|
||||
username: user.username,
|
||||
password: result.initial_password,
|
||||
});
|
||||
showToast("密码已重置", "请在一次性口令窗口复制后交给出纳", "success");
|
||||
});
|
||||
|
||||
const remind = $("#cs-remind");
|
||||
|
||||
+3
-2
@@ -11,7 +11,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
<link rel="stylesheet" href="design-system.css?v=13" />
|
||||
</head>
|
||||
<body class="is-app" data-portal="company">
|
||||
<a class="skip-link" href="#main-content">跳到主要内容</a>
|
||||
@@ -1044,6 +1044,7 @@
|
||||
|
||||
<div class="toast-region" id="toastRegion" aria-live="polite"></div>
|
||||
<script src="theme.js?v=1"></script>
|
||||
<script src="app.js?v=17"></script>
|
||||
<script src="datepicker.js?v=1"></script>
|
||||
<script src="app.js?v=18"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -0,0 +1,268 @@
|
||||
/* 锚定在日期输入旁的页内日历。不引入第三方库;滚动/缩放后跟随触发器。 */
|
||||
(function (global) {
|
||||
var WEEK = ["日", "一", "二", "三", "四", "五", "六"];
|
||||
var panel = null;
|
||||
var grid = null;
|
||||
var titleEl = null;
|
||||
var activeInput = null;
|
||||
var viewYear = 0;
|
||||
var viewMonth = 0;
|
||||
var bound = false;
|
||||
|
||||
function pad(n) {
|
||||
return n < 10 ? "0" + n : String(n);
|
||||
}
|
||||
|
||||
function toISO(year, month, day) {
|
||||
return year + "-" + pad(month + 1) + "-" + pad(day);
|
||||
}
|
||||
|
||||
function parseISO(value) {
|
||||
var match = /^(\d{4})-(\d{2})-(\d{2})$/.exec(String(value || "").trim());
|
||||
if (!match) return null;
|
||||
var year = Number(match[1]);
|
||||
var month = Number(match[2]) - 1;
|
||||
var day = Number(match[3]);
|
||||
var date = new Date(year, month, day);
|
||||
if (date.getFullYear() !== year || date.getMonth() !== month || date.getDate() !== day) {
|
||||
return null;
|
||||
}
|
||||
return date;
|
||||
}
|
||||
|
||||
function todayISO() {
|
||||
var now = new Date();
|
||||
return toISO(now.getFullYear(), now.getMonth(), now.getDate());
|
||||
}
|
||||
|
||||
function ensurePanel() {
|
||||
if (panel) return;
|
||||
panel = document.createElement("div");
|
||||
panel.className = "ds-datepicker";
|
||||
panel.id = "ds-datepicker";
|
||||
panel.hidden = true;
|
||||
panel.setAttribute("role", "dialog");
|
||||
panel.setAttribute("aria-label", "选择日期");
|
||||
panel.innerHTML =
|
||||
'<div class="ds-dp-head">' +
|
||||
'<button type="button" class="ds-dp-nav" data-dp-nav="-1" aria-label="上一月">‹</button>' +
|
||||
'<div class="ds-dp-title"></div>' +
|
||||
'<button type="button" class="ds-dp-nav" data-dp-nav="1" aria-label="下一月">›</button>' +
|
||||
"</div>" +
|
||||
'<div class="ds-dp-week">' + WEEK.map(function (d) { return "<span>" + d + "</span>"; }).join("") + "</div>" +
|
||||
'<div class="ds-dp-grid" role="grid"></div>' +
|
||||
'<div class="ds-dp-foot">' +
|
||||
'<button type="button" data-dp-today>今天</button>' +
|
||||
'<button type="button" data-dp-clear>清除</button>' +
|
||||
"</div>";
|
||||
document.body.appendChild(panel);
|
||||
titleEl = panel.querySelector(".ds-dp-title");
|
||||
grid = panel.querySelector(".ds-dp-grid");
|
||||
panel.addEventListener("mousedown", function (event) {
|
||||
event.preventDefault();
|
||||
});
|
||||
panel.addEventListener("click", function (event) {
|
||||
var nav = event.target.closest("[data-dp-nav]");
|
||||
if (nav) {
|
||||
shiftMonth(Number(nav.getAttribute("data-dp-nav")));
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-dp-today]")) {
|
||||
commit(todayISO());
|
||||
return;
|
||||
}
|
||||
if (event.target.closest("[data-dp-clear]")) {
|
||||
commit("");
|
||||
return;
|
||||
}
|
||||
var dayBtn = event.target.closest("[data-iso]");
|
||||
if (dayBtn && !dayBtn.disabled) commit(dayBtn.getAttribute("data-iso"));
|
||||
});
|
||||
}
|
||||
|
||||
function enhance(input) {
|
||||
if (!input || input.dataset.dsDate === "1") return;
|
||||
input.dataset.dsDate = "1";
|
||||
if (input.type === "date") {
|
||||
var value = input.value;
|
||||
try {
|
||||
input.type = "text";
|
||||
} catch (err) {
|
||||
/* keep native type if the engine refuses */
|
||||
}
|
||||
if (value) input.value = value;
|
||||
}
|
||||
input.setAttribute("inputmode", "numeric");
|
||||
input.setAttribute("autocomplete", "off");
|
||||
input.setAttribute("spellcheck", "false");
|
||||
input.setAttribute("placeholder", input.getAttribute("placeholder") || "YYYY-MM-DD");
|
||||
if (!input.getAttribute("pattern")) input.setAttribute("pattern", "\\d{4}-\\d{2}-\\d{2}");
|
||||
if (!input.classList.contains("num-input")) input.classList.add("num-input");
|
||||
input.setAttribute("aria-haspopup", "dialog");
|
||||
}
|
||||
|
||||
function scan() {
|
||||
document.querySelectorAll('input[type="date"]').forEach(enhance);
|
||||
}
|
||||
|
||||
function render() {
|
||||
if (!grid || !titleEl) return;
|
||||
titleEl.textContent = viewYear + "年" + pad(viewMonth + 1) + "月";
|
||||
var first = new Date(viewYear, viewMonth, 1);
|
||||
var start = first.getDay();
|
||||
var selected = activeInput ? parseISO(activeInput.value) : null;
|
||||
var selectedISO = selected ? toISO(selected.getFullYear(), selected.getMonth(), selected.getDate()) : "";
|
||||
var today = todayISO();
|
||||
var min = activeInput && activeInput.min ? activeInput.min : "";
|
||||
var max = activeInput && activeInput.max ? activeInput.max : "";
|
||||
var html = "";
|
||||
var cursor = new Date(viewYear, viewMonth, 1 - start);
|
||||
for (var i = 0; i < 42; i += 1) {
|
||||
var iso = toISO(cursor.getFullYear(), cursor.getMonth(), cursor.getDate());
|
||||
var other = cursor.getMonth() !== viewMonth;
|
||||
var disabled = (min && iso < min) || (max && iso > max);
|
||||
var cls = "ds-dp-day";
|
||||
if (other) cls += " is-other";
|
||||
if (iso === today) cls += " is-today";
|
||||
if (iso === selectedISO) cls += " is-selected";
|
||||
html +=
|
||||
'<button type="button" class="' + cls + '" data-iso="' + iso + '"' +
|
||||
(disabled ? " disabled" : "") +
|
||||
' aria-label="' + iso + '"' +
|
||||
(iso === selectedISO ? ' aria-current="date"' : "") +
|
||||
">" + cursor.getDate() + "</button>";
|
||||
cursor.setDate(cursor.getDate() + 1);
|
||||
}
|
||||
grid.innerHTML = html;
|
||||
position();
|
||||
}
|
||||
|
||||
function viewportBox() {
|
||||
var vv = global.visualViewport;
|
||||
if (vv) {
|
||||
return {
|
||||
left: vv.offsetLeft,
|
||||
top: vv.offsetTop,
|
||||
width: vv.width,
|
||||
height: vv.height,
|
||||
};
|
||||
}
|
||||
return { left: 0, top: 0, width: global.innerWidth, height: global.innerHeight };
|
||||
}
|
||||
|
||||
function position() {
|
||||
if (!panel || panel.hidden || !activeInput) return;
|
||||
var rect = activeInput.getBoundingClientRect();
|
||||
var view = viewportBox();
|
||||
var gap = 6;
|
||||
var width = panel.offsetWidth || 280;
|
||||
var height = panel.offsetHeight || 320;
|
||||
var left = rect.left;
|
||||
var top = rect.bottom + gap;
|
||||
if (top + height > view.top + view.height - 8 && rect.top - gap - height >= view.top + 8) {
|
||||
top = rect.top - gap - height;
|
||||
}
|
||||
if (left + width > view.left + view.width - 8) {
|
||||
left = Math.max(view.left + 8, rect.right - width);
|
||||
}
|
||||
if (left < view.left + 8) left = view.left + 8;
|
||||
if (top < view.top + 8) top = view.top + 8;
|
||||
panel.style.left = Math.round(left) + "px";
|
||||
panel.style.top = Math.round(top) + "px";
|
||||
}
|
||||
|
||||
function openPicker(input) {
|
||||
if (!input || input.disabled) return;
|
||||
enhance(input);
|
||||
ensurePanel();
|
||||
activeInput = input;
|
||||
var parsed = parseISO(input.value) || new Date();
|
||||
viewYear = parsed.getFullYear();
|
||||
viewMonth = parsed.getMonth();
|
||||
panel.hidden = false;
|
||||
render();
|
||||
input.setAttribute("aria-expanded", "true");
|
||||
}
|
||||
|
||||
function closePicker() {
|
||||
if (!panel || panel.hidden) return;
|
||||
panel.hidden = true;
|
||||
if (activeInput) activeInput.setAttribute("aria-expanded", "false");
|
||||
activeInput = null;
|
||||
}
|
||||
|
||||
function shiftMonth(delta) {
|
||||
viewMonth += delta;
|
||||
while (viewMonth < 0) {
|
||||
viewMonth += 12;
|
||||
viewYear -= 1;
|
||||
}
|
||||
while (viewMonth > 11) {
|
||||
viewMonth -= 12;
|
||||
viewYear += 1;
|
||||
}
|
||||
render();
|
||||
}
|
||||
|
||||
function commit(iso) {
|
||||
if (!activeInput) return;
|
||||
var input = activeInput;
|
||||
input.value = iso;
|
||||
input.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
input.dispatchEvent(new Event("change", { bubbles: true }));
|
||||
closePicker();
|
||||
input.focus({ preventScroll: true });
|
||||
}
|
||||
|
||||
function onPointer(event) {
|
||||
var input = event.target.closest && event.target.closest('input[type="date"], input[data-ds-date="1"]');
|
||||
if (input) {
|
||||
if (event.type === "mousedown" && event.button === 0) event.preventDefault();
|
||||
openPicker(input);
|
||||
return;
|
||||
}
|
||||
if (panel && !panel.hidden && !panel.contains(event.target)) closePicker();
|
||||
}
|
||||
|
||||
function onKey(event) {
|
||||
if (event.key === "Escape") {
|
||||
closePicker();
|
||||
return;
|
||||
}
|
||||
var input = event.target.closest && event.target.closest('input[data-ds-date="1"]');
|
||||
if (!input) return;
|
||||
if (event.key === "ArrowDown" || event.key === "Enter" || event.key === " ") {
|
||||
event.preventDefault();
|
||||
openPicker(input);
|
||||
}
|
||||
}
|
||||
|
||||
function bind() {
|
||||
if (bound) return;
|
||||
bound = true;
|
||||
document.addEventListener("mousedown", onPointer, true);
|
||||
document.addEventListener("focusin", function (event) {
|
||||
var input = event.target.closest && event.target.closest('input[type="date"]');
|
||||
if (input) openPicker(input);
|
||||
});
|
||||
document.addEventListener("keydown", onKey);
|
||||
document.addEventListener("scroll", position, true);
|
||||
global.addEventListener("resize", position);
|
||||
if (global.visualViewport) {
|
||||
global.visualViewport.addEventListener("resize", position);
|
||||
global.visualViewport.addEventListener("scroll", position);
|
||||
}
|
||||
if (typeof MutationObserver === "function") {
|
||||
new MutationObserver(scan).observe(document.documentElement, { childList: true, subtree: true });
|
||||
}
|
||||
scan();
|
||||
}
|
||||
|
||||
if (document.readyState === "loading") {
|
||||
document.addEventListener("DOMContentLoaded", bind);
|
||||
} else {
|
||||
bind();
|
||||
}
|
||||
|
||||
global.JinniuDatePicker = { open: openPicker, close: closePicker, scan: scan };
|
||||
})(window);
|
||||
+246
-9
@@ -61,6 +61,7 @@
|
||||
--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);
|
||||
color-scheme: light;
|
||||
|
||||
--fg: var(--text);
|
||||
--muted: var(--text-2);
|
||||
@@ -134,6 +135,9 @@ html[data-theme="night"],
|
||||
--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);
|
||||
color-scheme: dark;
|
||||
scrollbar-color: var(--gold) var(--surface-2);
|
||||
scrollbar-width: thin;
|
||||
}
|
||||
|
||||
/* ─── reset ─────────────────────────────────────────────────────── */
|
||||
@@ -225,7 +229,7 @@ html[data-theme="night"] .brand-logo-night { display: block; }
|
||||
}
|
||||
.side-role.company { background: var(--info-soft); color: var(--info); }
|
||||
|
||||
.side-nav { flex: 1; overflow-y: auto; overscroll-behavior: contain; padding: 12px 10px; }
|
||||
.side-nav { flex: 1; overflow-x: hidden; 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;
|
||||
@@ -316,6 +320,7 @@ html[data-theme="night"] .brand-logo-night { display: block; }
|
||||
min-width: 0;
|
||||
min-height: 0;
|
||||
height: 100vh;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
overscroll-behavior: contain;
|
||||
display: flex;
|
||||
@@ -478,7 +483,8 @@ html[data-theme="night"] .brand-logo-night { display: block; }
|
||||
.dash-company-list {
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dash-company-loading,
|
||||
.dash-company-empty {
|
||||
@@ -606,7 +612,8 @@ html[data-theme="night"] .brand-logo-night { display: block; }
|
||||
border-radius: 0;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
overflow: auto;
|
||||
overflow-x: auto;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.dash-detail-empty {
|
||||
flex: 1;
|
||||
@@ -713,7 +720,16 @@ html[data-theme="night"] .brand-logo-night { display: block; }
|
||||
}
|
||||
|
||||
/* ─── 数据表 ────────────────────────────────────────────────────── */
|
||||
.table-wrap { overflow-x: auto; border: 1px solid var(--border); border-radius: var(--radius-lg); background: var(--surface); }
|
||||
.table-wrap {
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
border: 1px solid var(--border);
|
||||
border-radius: var(--radius-lg);
|
||||
background: var(--surface);
|
||||
}
|
||||
.table-wrap.dash-master-scroll {
|
||||
overflow-y: auto;
|
||||
}
|
||||
.ds-table { width: 100%; border-collapse: collapse; font-size: 13px; min-width: 640px; }
|
||||
.ds-table th, .ds-table td { padding: 12px 16px; text-align: left; border-bottom: 1px solid var(--border); white-space: nowrap; }
|
||||
.ds-table th {
|
||||
@@ -784,19 +800,71 @@ html[data-theme="night"] .brand-logo-night { display: block; }
|
||||
font: inherit;
|
||||
font-size: 13.5px;
|
||||
min-height: 34px;
|
||||
color-scheme: inherit;
|
||||
}
|
||||
.input:focus, .select:focus, .textarea:focus, .field .input:focus, .field .select:focus, .field .textarea:focus {
|
||||
outline: none;
|
||||
border-color: var(--focus-ring);
|
||||
box-shadow: 0 0 0 3px var(--focus-glow);
|
||||
}
|
||||
.input:disabled, .select:disabled, .textarea:disabled,
|
||||
.field .input:disabled, .field .select:disabled, .field .textarea:disabled {
|
||||
color: var(--text-3);
|
||||
background: var(--fill);
|
||||
opacity: 0.72;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.select option,
|
||||
.select optgroup {
|
||||
background-color: var(--surface);
|
||||
color: var(--text);
|
||||
}
|
||||
.select option:checked,
|
||||
.select option:hover {
|
||||
background-color: var(--accent-soft);
|
||||
color: var(--text);
|
||||
}
|
||||
.select option:disabled {
|
||||
color: var(--text-3);
|
||||
}
|
||||
.input.num-input { font-family: var(--font-mono); font-variant-numeric: tabular-nums; }
|
||||
.textarea { min-height: 84px; resize: vertical; line-height: 1.55; }
|
||||
.field .hint { font-size: 11.5px; color: var(--muted); }
|
||||
.field .hint.error { color: var(--danger); }
|
||||
|
||||
/* 关闭原生日历层,改由 ds-datepicker 锚定在触发器旁 */
|
||||
input[type="date"]::-webkit-calendar-picker-indicator {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
display: none;
|
||||
}
|
||||
.input[type="date"],
|
||||
.field .input[type="date"],
|
||||
.input[data-ds-date="1"],
|
||||
.field .input[data-ds-date="1"] {
|
||||
padding-right: 34px;
|
||||
background-color: var(--fill);
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%236e6e73' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='3.5' width='11' height='10' rx='1.5'/%3E%3Cpath d='M2.5 6.5h11M6 2v3M10 2v3'/%3E%3C/svg%3E");
|
||||
background-repeat: no-repeat;
|
||||
background-position: right 10px center;
|
||||
background-size: 16px 16px;
|
||||
}
|
||||
html[data-theme="night"] .input[type="date"],
|
||||
html[data-theme="night"] .field .input[type="date"],
|
||||
html[data-theme="night"] .input[data-ds-date="1"],
|
||||
html[data-theme="night"] .field .input[data-ds-date="1"] {
|
||||
background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='16' height='16' fill='none' stroke='%23d9b45c' stroke-width='1.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Crect x='2.5' y='3.5' width='11' height='10' rx='1.5'/%3E%3Cpath d='M2.5 6.5h11M6 2v3M10 2v3'/%3E%3C/svg%3E");
|
||||
}
|
||||
|
||||
/* ─── Tabs ──────────────────────────────────────────────────────── */
|
||||
.tabs { display: flex; gap: 2px; border-bottom: 1px solid var(--border); margin-bottom: 16px; overflow-x: auto; }
|
||||
.tabs {
|
||||
display: flex;
|
||||
gap: 2px;
|
||||
border-bottom: 1px solid var(--border);
|
||||
margin-bottom: 16px;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.tabs button {
|
||||
background: none; border: 0;
|
||||
padding: 9px 14px;
|
||||
@@ -867,7 +935,7 @@ html[data-theme="night"] .brand-logo-night { display: block; }
|
||||
border-radius: 16px;
|
||||
border: 1px solid var(--gold-line);
|
||||
width: 100%; max-width: min(520px, 94vw);
|
||||
max-height: 86vh; overflow-y: auto;
|
||||
max-height: 86vh; overflow-x: hidden; overflow-y: auto;
|
||||
padding: 22px 24px;
|
||||
box-shadow: var(--shadow-modal);
|
||||
}
|
||||
@@ -1190,6 +1258,7 @@ a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, trans
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
overflow-x: auto;
|
||||
overflow-y: hidden;
|
||||
padding: 8px 4px;
|
||||
flex: 1;
|
||||
}
|
||||
@@ -1207,7 +1276,7 @@ a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, trans
|
||||
.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; }
|
||||
.main { min-height: 0; height: 100%; overflow-x: hidden; overflow-y: auto; }
|
||||
}
|
||||
|
||||
/* ─── Toast 轻量提示 ───────────────────────────────────────────── */
|
||||
@@ -1265,7 +1334,7 @@ a.flow-step.doing:hover { background: color-mix(in oklch, var(--warn) 16%, trans
|
||||
}
|
||||
.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; }
|
||||
.drawer-body { flex: 1; overflow-x: hidden; overflow-y: auto; padding: 16px 20px; }
|
||||
.drawer-body .kv { grid-template-columns: auto 1fr; gap: 8px 18px; }
|
||||
.drawer-body .kv dt { color: var(--muted); white-space: nowrap; }
|
||||
.drawer-body .kv dd { text-align: right; word-break: break-all; }
|
||||
@@ -1429,7 +1498,7 @@ body[data-portal="company"] .side-nav a[data-view="transfers"].active svg {
|
||||
background-position: center;
|
||||
}
|
||||
.batch-bar { display: flex; align-items: center; gap: 10px; padding: 8px 0; }
|
||||
.login-page { min-height: 100vh; overflow: auto; }
|
||||
.login-page { min-height: 100vh; overflow-x: hidden; overflow-y: auto; }
|
||||
.toast .t-detail { color: rgba(255,255,255,.72); }
|
||||
.toast .t-dot { margin-top: 0; }
|
||||
|
||||
@@ -1675,3 +1744,171 @@ body[data-portal="company"] .side-nav a[data-view="transfers"].active svg {
|
||||
.audit-event-cards { display: grid; gap: 10px; }
|
||||
}
|
||||
|
||||
/* ─── 夜间滚动条:金色滑块 + 深色轨道(HEL-351) ─────────────── */
|
||||
html[data-theme="night"] ::-webkit-scrollbar {
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
html[data-theme="night"] ::-webkit-scrollbar-track {
|
||||
background: var(--surface-2);
|
||||
}
|
||||
html[data-theme="night"] ::-webkit-scrollbar-thumb {
|
||||
background-color: var(--gold);
|
||||
border: 2px solid var(--surface-2);
|
||||
border-radius: 999px;
|
||||
background-clip: padding-box;
|
||||
}
|
||||
html[data-theme="night"] ::-webkit-scrollbar-thumb:hover {
|
||||
background-color: var(--gold-bright);
|
||||
}
|
||||
html[data-theme="night"] ::-webkit-scrollbar-corner {
|
||||
background: var(--surface);
|
||||
}
|
||||
|
||||
/* ─── 日期选择弹层:锚定触发器,夜间黑金点缀 ───────────────── */
|
||||
.ds-datepicker {
|
||||
position: fixed;
|
||||
z-index: var(--z-tooltip);
|
||||
width: 280px;
|
||||
padding: 12px;
|
||||
background: var(--material-drawer);
|
||||
backdrop-filter: blur(28px);
|
||||
-webkit-backdrop-filter: blur(28px);
|
||||
border: 1px solid var(--hairline);
|
||||
border-radius: 16px;
|
||||
box-shadow: var(--shadow-modal);
|
||||
color: var(--text);
|
||||
}
|
||||
html[data-theme="night"] .ds-datepicker {
|
||||
background: var(--surface);
|
||||
border-color: var(--gold-line);
|
||||
}
|
||||
.ds-datepicker[hidden] { display: none !important; }
|
||||
.ds-dp-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 8px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.ds-dp-title {
|
||||
font-size: 13.5px;
|
||||
font-weight: 650;
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
html[data-theme="night"] .ds-dp-title { color: var(--gold); }
|
||||
.ds-dp-nav {
|
||||
width: 28px;
|
||||
height: 28px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: var(--fill);
|
||||
color: var(--text-2);
|
||||
}
|
||||
.ds-dp-nav:hover { background: var(--hover); color: var(--text); }
|
||||
.ds-dp-week,
|
||||
.ds-dp-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(7, 1fr);
|
||||
gap: 2px;
|
||||
}
|
||||
.ds-dp-week span {
|
||||
height: 28px;
|
||||
display: grid;
|
||||
place-items: center;
|
||||
font-size: 11px;
|
||||
font-weight: 600;
|
||||
color: var(--text-3);
|
||||
}
|
||||
.ds-dp-day {
|
||||
height: 32px;
|
||||
border: 0;
|
||||
border-radius: 8px;
|
||||
background: transparent;
|
||||
color: var(--text);
|
||||
font-size: 13px;
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-family: var(--font-mono);
|
||||
}
|
||||
.ds-dp-day:hover { background: var(--hover); }
|
||||
.ds-dp-day.is-other { color: var(--text-3); }
|
||||
.ds-dp-day.is-today {
|
||||
box-shadow: inset 0 0 0 1px var(--accent);
|
||||
color: var(--accent);
|
||||
}
|
||||
.ds-dp-day.is-selected {
|
||||
background: var(--accent-soft);
|
||||
color: var(--accent);
|
||||
font-weight: 700;
|
||||
}
|
||||
.ds-dp-day.is-selected.is-today { box-shadow: none; }
|
||||
html[data-theme="night"] .ds-dp-day.is-today {
|
||||
box-shadow: inset 0 0 0 1px var(--gold-line);
|
||||
color: var(--gold);
|
||||
}
|
||||
html[data-theme="night"] .ds-dp-day.is-selected {
|
||||
background: var(--gold-soft);
|
||||
color: var(--gold);
|
||||
}
|
||||
.ds-dp-day:disabled {
|
||||
color: var(--text-3);
|
||||
opacity: 0.45;
|
||||
cursor: not-allowed;
|
||||
}
|
||||
.ds-dp-foot {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
margin-top: 8px;
|
||||
padding-top: 8px;
|
||||
border-top: 1px solid var(--hairline);
|
||||
}
|
||||
.ds-dp-foot button {
|
||||
height: 26px;
|
||||
padding: 0 10px;
|
||||
border: 0;
|
||||
border-radius: 7px;
|
||||
background: transparent;
|
||||
color: var(--text-2);
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
}
|
||||
.ds-dp-foot button:hover { background: var(--hover); color: var(--text); }
|
||||
|
||||
/* ─── 一次性初始密码领取 ─────────────────────────────────────── */
|
||||
.cred-box {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
padding: 12px 14px;
|
||||
border: 1px solid var(--gold-line);
|
||||
border-radius: 14px;
|
||||
background: var(--gold-soft);
|
||||
}
|
||||
.cred-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
min-width: 0;
|
||||
}
|
||||
.cred-row .cred-label {
|
||||
flex: none;
|
||||
width: 72px;
|
||||
font-size: 12px;
|
||||
font-weight: 600;
|
||||
color: var(--text-2);
|
||||
}
|
||||
.cred-row .cred-value {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-family: var(--font-mono);
|
||||
font-variant-numeric: tabular-nums;
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
color: var(--text);
|
||||
word-break: break-all;
|
||||
}
|
||||
.cred-warn {
|
||||
margin-top: 12px;
|
||||
font-size: 12.5px;
|
||||
color: var(--text-2);
|
||||
}
|
||||
|
||||
|
||||
+1
-1
@@ -10,7 +10,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
<link rel="stylesheet" href="design-system.css?v=13" />
|
||||
</head>
|
||||
<body class="login-page">
|
||||
<div class="portal-wrap">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
<link rel="stylesheet" href="design-system.css?v=13" />
|
||||
</head>
|
||||
<body class="login-page" data-role="admin">
|
||||
<div class="login-wrap">
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
document.documentElement.classList.add("v-fusion");
|
||||
})();
|
||||
</script>
|
||||
<link rel="stylesheet" href="design-system.css?v=12" />
|
||||
<link rel="stylesheet" href="design-system.css?v=13" />
|
||||
</head>
|
||||
<body class="login-page" data-role="company">
|
||||
<div class="login-wrap">
|
||||
|
||||
Reference in New Issue
Block a user