feat(web): apply Lingniu brand assets

This commit is contained in:
lingniu
2026-07-16 09:17:04 +08:00
parent 61c1bbc365
commit c3b0996698
8 changed files with 112 additions and 17 deletions

View File

@@ -52,10 +52,10 @@ export function AuthGate({ children }: { children: ReactNode }) {
};
if (session.isPending) {
return <div className="v2-auth-screen"><div className="v2-auth-card"><i className="v2-auth-spinner" /><strong>访</strong></div></div>;
return <div className="v2-auth-screen"><div className="v2-auth-card"><img className="v2-auth-logo" src="/brand-logo.svg" alt="灵牛智能" /><i className="v2-auth-spinner" /><strong>访</strong></div></div>;
}
if (!session.data) {
return <div className="v2-auth-screen"><form className="v2-auth-card" onSubmit={login}><div className="v2-auth-mark"></div><h1></h1><p>访</p><label><span>访</span><input autoFocus required type="password" autoComplete="current-password" value={draftToken} onChange={(event) => setDraftToken(event.target.value)} placeholder="Bearer token" /></label>{attempted && session.error ? <em>{session.error.message}</em> : null}<button type="submit" disabled={!draftToken.trim()}></button></form></div>;
return <div className="v2-auth-screen"><form className="v2-auth-card" onSubmit={login}><img className="v2-auth-logo" src="/brand-logo.svg" alt="灵牛智能" /><h1></h1><p>访</p><label><span>访</span><input autoFocus required type="password" autoComplete="current-password" value={draftToken} onChange={(event) => setDraftToken(event.target.value)} placeholder="Bearer token" /></label>{attempted && session.error ? <em>{session.error.message}</em> : null}<button type="submit" disabled={!draftToken.trim()}></button></form></div>;
}
return <AuthContext.Provider value={{ session: session.data, logout }}>{children}</AuthContext.Provider>;
}

View File

@@ -4,7 +4,7 @@ import indexSource from '../../index.html?raw';
const bootScript = indexSource.match(/<script>\s*([\s\S]*?vehicle-platform:ready[\s\S]*?)<\/script>/)?.[1];
function mountBootShell() {
document.body.innerHTML = '<div id="root"><div id="platform-boot" role="status"><div><span class="platform-boot-mark">车</span><strong>车辆数据中台</strong><small>正在加载工作台…</small></div></div></div>';
document.body.innerHTML = '<div id="root"><div id="platform-boot" role="status"><div><img class="platform-boot-logo" src="/brand-logo.svg" alt="灵牛智能"><strong>车辆数据中台</strong><small>正在加载工作台…</small></div></div></div>';
expect(bootScript).toBeTruthy();
Function(bootScript!)();
}

View File

@@ -99,9 +99,9 @@ function Sidebar() {
return (
<aside className={`v2-sidebar${collapsed ? ' is-collapsed' : ''}`}>
<div className="v2-brand">
<span className="v2-brand-mark"><IconBox size="large" /></span>
<strong></strong>
<div className="v2-brand" aria-label="灵牛智能车辆数据中台">
<img className="v2-brand-logo" src="/brand-logo.svg" alt="灵牛智能" />
<img className="v2-brand-symbol" src="/brand-mark.svg" alt="" aria-hidden="true" />
</div>
<nav className="v2-navigation" aria-label="主导航">
{navigation.map(({ to, label, icon: Icon }) => (

View File

@@ -15,6 +15,8 @@ describe('V2 production entry', () => {
test('renders a static boot shell and recovers when the React entry never becomes ready', () => {
expect(indexSource).toContain('id="platform-boot"');
expect(indexSource).toContain('rel="icon" type="image/svg+xml" href="/brand-mark.svg"');
expect(indexSource).toContain('class="platform-boot-logo" src="/brand-logo.svg"');
expect(indexSource).toContain("window.addEventListener('error', fail, true)");
expect(indexSource).toContain("window.addEventListener('unhandledrejection', fail)");
expect(indexSource).toContain('window.setTimeout(fail, 10_000)');

View File

@@ -20,7 +20,8 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-auth-screen { display: grid; min-height: 100vh; place-items: center; background: radial-gradient(circle at 50% 10%, #edf4ff 0, #f4f7fb 42%, #eef2f7 100%); padding: 20px; }
.v2-auth-card { display: flex; width: min(390px, 100%); flex-direction: column; align-items: stretch; border: 1px solid var(--v2-border); border-radius: 16px; background: #fff; padding: 34px; box-shadow: 0 22px 70px rgba(21, 32, 51, .12); }
.v2-auth-card > strong { margin-top: 12px; text-align: center; }.v2-auth-mark { display: grid; width: 46px; height: 46px; margin: 0 auto 16px; place-items: center; border-radius: 13px; background: var(--v2-blue); color: #fff; font-size: 20px; font-weight: 800; box-shadow: 0 9px 20px rgba(18, 104, 243, .22); }
.v2-auth-card > strong { margin-top: 12px; text-align: center; }
.v2-auth-logo { display: block; width: 150px; height: auto; margin: 0 auto 18px; }
.v2-auth-card h1 { margin: 0; text-align: center; font-size: 22px; }.v2-auth-card p { margin: 10px 0 24px; color: var(--v2-muted); text-align: center; font-size: 12px; line-height: 1.7; }
.v2-auth-card label { display: flex; flex-direction: column; gap: 7px; color: #58667a; font-size: 12px; font-weight: 600; }.v2-auth-card input { height: 40px; border: 1px solid #d7e0ec; border-radius: 8px; padding: 0 11px; outline: 0; }.v2-auth-card input:focus { border-color: #8bb6fb; box-shadow: 0 0 0 3px rgba(18,104,243,.08); }
.v2-auth-card em { margin-top: 9px; color: var(--v2-red); font-size: 11px; font-style: normal; }.v2-auth-card button { height: 40px; margin-top: 18px; border: 0; border-radius: 8px; background: var(--v2-blue); color: #fff; cursor: pointer; font-weight: 700; }.v2-auth-card button:disabled { opacity: .45; cursor: not-allowed; }
@@ -28,9 +29,9 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-shell { height: 100vh; height: 100dvh; overflow: hidden; background: var(--v2-bg); }
.v2-sidebar { position: fixed; inset: 0 auto 0 0; z-index: 50; contain: layout paint; display: flex; width: 188px; flex-direction: column; border-right: 1px solid var(--v2-border); background: #fff; }
.v2-brand { display: flex; height: 64px; align-items: center; gap: 11px; border-bottom: 1px solid var(--v2-border); padding: 0 16px; white-space: nowrap; overflow: hidden; }
.v2-brand strong { font-size: 16px; letter-spacing: -.02em; }
.v2-brand-mark { display: grid; width: 34px; height: 34px; flex: 0 0 34px; place-items: center; border-radius: 9px; background: var(--v2-blue); color: #fff; box-shadow: 0 6px 14px rgba(18, 104, 243, .22); }
.v2-brand { display: flex; height: 64px; align-items: center; border-bottom: 1px solid var(--v2-border); padding: 0 16px; white-space: nowrap; overflow: hidden; }
.v2-brand-logo { display: block; width: 140px; height: auto; flex: 0 0 auto; }
.v2-brand-symbol { display: none; width: 36px; height: auto; flex: 0 0 36px; }
.v2-navigation { display: flex; flex: 1; flex-direction: column; gap: 4px; padding: 14px 10px; }
.v2-nav-item { position: relative; display: flex; height: 44px; align-items: center; gap: 12px; border-radius: 8px; padding: 0 14px; color: #59677c; text-decoration: none; white-space: nowrap; overflow: hidden; font-size: 13px; font-weight: 600; transition: background .16s, color .16s; }
.v2-nav-item:hover { background: #f7f9fc; color: var(--v2-text); }
@@ -61,8 +62,10 @@ button, a { -webkit-tap-highlight-color: transparent; }
.v2-content { min-width: 0; min-height: 0; flex: 1; overflow: auto; }
.v2-sidebar.is-collapsed { width: 68px; }
.v2-sidebar.is-collapsed + .v2-main { margin-left: 68px; }
.v2-sidebar.is-collapsed .v2-brand strong, .v2-sidebar.is-collapsed .v2-nav-label, .v2-sidebar.is-collapsed .v2-collapse span { display: none; }
.v2-sidebar.is-collapsed .v2-brand { padding: 0 17px; }
.v2-sidebar.is-collapsed .v2-nav-label, .v2-sidebar.is-collapsed .v2-collapse span { display: none; }
.v2-sidebar.is-collapsed .v2-brand { justify-content: center; padding: 0; }
.v2-sidebar.is-collapsed .v2-brand-logo { display: none; }
.v2-sidebar.is-collapsed .v2-brand-symbol { display: block; }
.v2-sidebar.is-collapsed .v2-nav-item { justify-content: center; padding: 0; }
.v2-sidebar.is-collapsed .v2-collapse { justify-content: center; padding: 0; transform: rotate(180deg); }
@@ -925,8 +928,10 @@ button, a { -webkit-tap-highlight-color: transparent; }
@media (max-width: 900px) {
.v2-sidebar { width: 68px; }
.v2-main { margin-left: 68px; }
.v2-brand strong, .v2-nav-label, .v2-collapse span { display: none; }
.v2-brand { padding: 0 17px; }
.v2-nav-label, .v2-collapse span { display: none; }
.v2-brand { justify-content: center; padding: 0; }
.v2-brand-logo { display: none; }
.v2-brand-symbol { display: block; }
.v2-nav-item { justify-content: center; padding: 0; }
.v2-filterbar { grid-template-columns: 1fr 1fr; }
.v2-search-field { grid-column: 1 / -1; }