From 7d0363d2752e53ee744b3063a8c9ec456d576809 Mon Sep 17 00:00:00 2001 From: chick Date: Tue, 21 Jul 2026 23:14:37 +0800 Subject: [PATCH] feat(ui): topbar settings, hide dev badge, premium fleet cards Collapse the redundant left workspace nav (overview is already the home surface) and move Settings into the top bar. Hide placeholder `dev` version badges. Restyle fleet cards with softer elevation, status summary chips, hover sheen, and reduced-motion-safe entrance motion. --- apps/web/src/app-shell.integration.test.tsx | 25 +- apps/web/src/app-shell.tsx | 45 ++-- apps/web/src/fleet/fleet-page.tsx | 4 +- apps/web/src/styles.css | 274 ++++++++++++++------ 4 files changed, 238 insertions(+), 110 deletions(-) diff --git a/apps/web/src/app-shell.integration.test.tsx b/apps/web/src/app-shell.integration.test.tsx index 609b7fe..05f7dcf 100644 --- a/apps/web/src/app-shell.integration.test.tsx +++ b/apps/web/src/app-shell.integration.test.tsx @@ -91,7 +91,8 @@ describe('React AppShell and Fleet vertical slice', () => { 'http://bravo.example:8080', ), ).toBeTruthy(); - expect(screen.getByText('版本 0.1.0')).toBeTruthy(); + expect(screen.getByLabelText('控制台版本 0.1.0')).toBeTruthy(); + expect(screen.getByText('v0.1.0')).toBeTruthy(); expect( consoleError.mock.calls.some((call) => call.some((argument) => String(argument).includes('unique "key" prop')), @@ -192,7 +193,7 @@ describe('React AppShell and Fleet vertical slice', () => { expect(within(bravo).getByText('需要认证')).toBeTruthy(); }); - it('keeps Jobs and Audit routes compatible without advertising them in global navigation', async () => { + it('keeps Jobs and Audit routes compatible without left nav, settings in top bar only', async () => { const jobsDataSource: JobsDataSource = { load: vi.fn().mockResolvedValue(emptyPage) }; const { rerender } = render( { ); expect(await screen.findByText(/没有任务符合当前查询/i)).toBeTruthy(); - const navigation = screen.getByRole('navigation', { name: '全局导航' }); - expect(within(navigation).queryByRole('link', { name: '任务' })).toBeNull(); - expect(within(navigation).queryByRole('link', { name: '审计' })).toBeNull(); - expect(within(navigation).getByRole('link', { name: '实例总览' })).toBeTruthy(); - expect(within(navigation).getByRole('link', { name: '设置' })).toBeTruthy(); + expect(screen.queryByRole('navigation', { name: '全局导航' })).toBeNull(); + expect(screen.queryByRole('link', { name: '任务' })).toBeNull(); + expect(screen.queryByRole('link', { name: '审计' })).toBeNull(); + expect(screen.queryByRole('link', { name: '实例总览' })).toBeNull(); + expect(screen.getByRole('link', { name: '设置' }).getAttribute('href')).toBe( + '/settings/system', + ); const auditDataSource: AuditDataSource = { load: vi.fn().mockResolvedValue(emptyPage) }; rerender( @@ -220,6 +223,14 @@ describe('React AppShell and Fleet vertical slice', () => { expect(await screen.findByText(/没有审计事件符合当前查询/i)).toBeTruthy(); }); + it('hides placeholder dev version badge in the top bar', () => { + render( + snapshot)} />, + ); + expect(screen.queryByText(/版本 dev|vdev/i)).toBeNull(); + expect(screen.queryByLabelText(/控制台版本/)).toBeNull(); + }); + it('loads route-owned instance context so overview-card navigation opens detail management', async () => { const instanceDataSource: InstanceDataSource = { get: vi.fn().mockResolvedValue({ diff --git a/apps/web/src/app-shell.tsx b/apps/web/src/app-shell.tsx index 78709f6..1ba3001 100644 --- a/apps/web/src/app-shell.tsx +++ b/apps/web/src/app-shell.tsx @@ -399,6 +399,13 @@ function Page({ ); } +function displayConsoleVersion(version: string | undefined): string | undefined { + if (!version) return undefined; + const trimmed = version.trim(); + if (!trimmed || trimmed.toLowerCase() === 'dev' || trimmed === '0.0.0') return undefined; + return trimmed; +} + export function AppShell({ pathname, version = 'dev', @@ -585,8 +592,9 @@ export function AppShell({ eventStreamClient ?? defaultEventStreamClient, ); const currentSection = section(route); + const consoleVersion = displayConsoleVersion(version); return ( -
+
跳到主要内容 @@ -594,26 +602,23 @@ export function AppShell({ 多实例 SimAdmin 管理台 - 控制台 - 版本 {version} +
+ 控制台 + {consoleVersion ? ( + + v{consoleVersion} + + ) : null} + + 设置 + +
-
- +
{routeInstanceId && instanceLoading ? (

正在加载实例…

diff --git a/apps/web/src/fleet/fleet-page.tsx b/apps/web/src/fleet/fleet-page.tsx index 964d8d5..b98a2d0 100644 --- a/apps/web/src/fleet/fleet-page.tsx +++ b/apps/web/src/fleet/fleet-page.tsx @@ -497,8 +497,8 @@ export function FleetPage({
-

实例总览

-

查找、对比并管理所有 SimAdmin 实例。

+

实例舰队

+

资源优先总览,查找并管理全部 SimAdmin 实例。

diff --git a/apps/web/src/styles.css b/apps/web/src/styles.css index ed60669..222a8b2 100644 --- a/apps/web/src/styles.css +++ b/apps/web/src/styles.css @@ -13,8 +13,14 @@ --border: #ded3bd; --danger: #c95e55; --warning: #f5c31c; - --radius: 16px; + --radius: 18px; + --radius-sm: 12px; --space: clamp(0.75rem, 2vw, 1.5rem); + --shadow: 0 10px 30px rgba(121, 79, 39, 0.1); + --shadow-hover: 0 18px 40px rgba(121, 79, 39, 0.14); + --ease-out: cubic-bezier(0.16, 1, 0.3, 1); + --accent: #19c8b9; + --accent-strong: #0f8f86; font-synthesis: none; } * { @@ -178,34 +184,56 @@ p { margin-right: 0.35rem; border-radius: 50%; background: var(--mint); + box-shadow: 0 0 0 0 rgba(25, 200, 185, 0.45); + animation: pulse-dot 2.4s var(--ease-out) infinite; +} +.topbar-actions { + display: flex; + align-items: center; + gap: 0.55rem; + margin-left: auto; + flex-wrap: wrap; +} +.topbar-settings { + min-height: 2.5rem; + display: inline-flex; + align-items: center; + padding: 0.4rem 0.95rem; + border: 1px solid var(--border); + border-radius: 999px; + color: var(--brown); + background: var(--surface-raised); + text-decoration: none; + font-weight: 800; + box-shadow: 0 2px 0 #d4c9b4; + transition: + transform 160ms var(--ease-out), + box-shadow 160ms var(--ease-out), + background 160ms var(--ease-out); +} +.topbar-settings:hover { + transform: translateY(-1px); + background: #fff; +} +.topbar-settings[aria-current='page'] { + color: #fff; + border-color: var(--mint-dark); + background: var(--mint); + box-shadow: 0 4px 0 var(--mint-dark); } .app-layout { display: grid; - grid-template-columns: 13rem minmax(0, 1fr); + grid-template-columns: minmax(0, 1fr); gap: var(--space); min-height: calc(100vh - 4.5rem); padding: var(--space); } +.app-layout-single { + grid-template-columns: minmax(0, 1fr); +} .global-navigation { - align-self: start; - position: sticky; - top: 6rem; - padding: 1rem; - border: 1px solid #e9dfca; - border-radius: 20px; - background: var(--surface); - box-shadow: 0 10px 28px rgba(121, 79, 39, 0.1); + display: none; } -.global-navigation::before { - content: '工作区'; - display: block; - padding: 0.35rem 0.6rem 0.75rem; - color: var(--muted); - font-size: 0.76rem; - font-weight: 800; - letter-spacing: 0.12em; -} -.global-navigation ul, .instance-context ul { list-style: none; padding: 0; @@ -213,7 +241,6 @@ p { display: grid; gap: 0.35rem; } -.global-navigation a, .instance-context nav a { display: block; padding: 0.7rem 0.8rem; @@ -229,11 +256,13 @@ a[aria-current='page'] { } main { min-width: 0; - padding: clamp(0.5rem, 2vw, 1.25rem); + padding: clamp(0.75rem, 2vw, 1.4rem); border: 1px solid #ebe1ce; border-radius: 22px; - background: rgba(255, 253, 245, 0.88); - box-shadow: 0 16px 40px rgba(121, 79, 39, 0.09); + background: + linear-gradient(180deg, rgba(255, 253, 245, 0.96), rgba(255, 253, 245, 0.88)), + var(--surface-raised); + box-shadow: var(--shadow); } main > section > header, .fleet-heading { @@ -280,30 +309,48 @@ small { } .fleet-card-grid { display: grid; - grid-template-columns: repeat(auto-fit, minmax(min(100%, 18rem), 1fr)); - gap: 1rem; + grid-template-columns: repeat(auto-fit, minmax(min(100%, 19rem), 1fr)); + gap: 1.1rem; } .fleet-status-summary { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 1px; + gap: 0.75rem; margin: 1rem 0; - overflow: hidden; - border: 1px solid var(--border); - border-radius: 6px; - background: var(--border); } -.fleet-status-summary div { - display: flex; - justify-content: space-between; - padding: 0.75rem 1rem; - background: var(--surface-raised); +.fleet-status-summary > div { + display: grid; + gap: 0.25rem; + padding: 0.9rem 1rem; + border: 1px solid var(--border); + border-radius: 14px; + background: + linear-gradient(180deg, rgba(255, 253, 245, 0.98), rgba(247, 243, 223, 0.85)); + box-shadow: 0 6px 16px rgba(121, 79, 39, 0.06); + animation: rise-in 420ms var(--ease-out) both; +} +.fleet-status-summary > div:nth-child(2) { + animation-delay: 40ms; +} +.fleet-status-summary > div:nth-child(3) { + animation-delay: 80ms; +} +.fleet-status-summary strong { + font-size: 1.4rem; + color: var(--brown); + letter-spacing: -0.03em; } .fleet-card-entry-label { - display: block; + display: inline-flex; + align-items: center; margin-top: 0.55rem; + padding: 0.2rem 0.55rem; + border-radius: 999px; font-weight: 800; - color: #187f77; + font-size: 0.78rem; + color: #0f6f68; + background: rgba(25, 200, 185, 0.14); + transition: background 160ms var(--ease-out), transform 160ms var(--ease-out); } .fleet-card meter { width: min(7rem, 45%); @@ -312,12 +359,51 @@ small { } .fleet-card { position: relative; + isolation: isolate; min-width: 0; - padding: 1rem; - border: 1px solid var(--border); - border-radius: var(--radius); - background: var(--surface-raised); - box-shadow: 0 7px 18px rgba(121, 79, 39, 0.09); + overflow: hidden; + padding: 1.05rem 1.1rem 1rem; + border: 1px solid color-mix(in srgb, var(--border) 80%, #fff); + border-radius: calc(var(--radius) + 2px); + background: + radial-gradient(120% 90% at 0% 0%, rgba(25, 200, 185, 0.1), transparent 55%), + linear-gradient(165deg, #fffef9 0%, #fff9ec 52%, #f7f2e2 100%); + box-shadow: var(--shadow); + transition: + transform 220ms var(--ease-out), + box-shadow 220ms var(--ease-out), + border-color 220ms var(--ease-out); + animation: rise-in 460ms var(--ease-out) both; +} +.fleet-card::before { + content: ''; + position: absolute; + inset: 0 auto 0 0; + width: 4px; + background: linear-gradient(180deg, var(--mint), color-mix(in srgb, var(--mint) 20%, var(--brown))); + opacity: 0.9; +} +.fleet-card::after { + content: ''; + position: absolute; + inset: 0; + z-index: -1; + background: linear-gradient(120deg, transparent 30%, rgba(255, 255, 255, 0.45) 48%, transparent 65%); + transform: translateX(-120%); + transition: transform 700ms var(--ease-out); + pointer-events: none; +} +.fleet-card:hover { + transform: translateY(-3px); + border-color: color-mix(in srgb, var(--mint) 35%, var(--border)); + box-shadow: var(--shadow-hover); +} +.fleet-card:hover::after { + transform: translateX(120%); +} +.fleet-card:hover .fleet-card-entry-label { + background: rgba(25, 200, 185, 0.22); + transform: translateX(2px); } .fleet-card > header { display: flex; @@ -327,28 +413,47 @@ small { } .fleet-card h2 { margin: 0 0 0.2rem; - font-size: 1.15rem; + font-size: 1.18rem; + letter-spacing: -0.02em; } .fleet-card code { color: var(--muted); overflow-wrap: anywhere; + font-size: 0.82rem; } .fleet-card-metrics { margin: 1rem 0; + display: grid; + gap: 0.55rem; } .fleet-card-metrics div { display: flex; justify-content: space-between; + align-items: center; gap: 1rem; - padding: 0.35rem 0; - border-bottom: 1px dashed var(--border); + padding: 0.45rem 0.55rem; + border: 1px solid transparent; + border-radius: 12px; + background: rgba(255, 253, 245, 0.55); +} +.fleet-card-metrics dt { + color: var(--muted); + font-size: 0.78rem; + font-weight: 700; +} +.fleet-card-metrics dd { + margin: 0; + font-weight: 800; + color: var(--brown); + font-variant-numeric: tabular-nums; } .fleet-card-sms { margin: 0.8rem 0; - padding: 0.75rem; - border: 1px solid #d8e8d4; + padding: 0.8rem 0.85rem; + border: 1px solid color-mix(in srgb, #b7d8b2 70%, var(--border)); border-radius: 14px; - background: #f5faf1; + background: + linear-gradient(180deg, rgba(245, 250, 241, 0.98), rgba(236, 246, 230, 0.9)); } .fleet-card-sms h3 { margin: 0 0 0.55rem; @@ -388,8 +493,9 @@ small { .capability-tags span { padding: 0.2rem 0.55rem; border-radius: 999px; - color: #187f77; - background: #d8f5ec; + color: #0f6f68; + background: rgba(25, 200, 185, 0.14); + border: 1px solid rgba(25, 200, 185, 0.18); font-size: 0.78rem; font-weight: 700; } @@ -407,6 +513,25 @@ small { border-radius: 999px; font-weight: 700; } +@keyframes rise-in { + from { + opacity: 0; + transform: translateY(10px); + } + to { + opacity: 1; + transform: translateY(0); + } +} +@keyframes pulse-dot { + 0%, + 100% { + box-shadow: 0 0 0 0 rgba(25, 200, 185, 0.45); + } + 50% { + box-shadow: 0 0 0 6px rgba(25, 200, 185, 0); + } +} .danger-button { color: #a4423b; border-color: #db938d; @@ -1028,19 +1153,13 @@ main ul[aria-label='已配置实例'] { display: block; padding: 0.65rem; } + .topbar-actions { + width: 100%; + justify-content: flex-end; + } .global-navigation { - position: static; - padding: 0.55rem; - margin-bottom: 0.65rem; - overflow-x: auto; - } - .global-navigation::before { display: none; } - .global-navigation ul { - display: flex; - min-width: max-content; - } main { padding: 0.8rem; border-radius: 16px; @@ -1224,10 +1343,10 @@ main ul[aria-label='已配置实例'] { .fleet-card.is-selected { - border-color: color-mix(in srgb, var(--accent, #f2a93b) 55%, var(--border)); + border-color: color-mix(in srgb, var(--mint) 45%, var(--border)); box-shadow: - 0 0 0 1px color-mix(in srgb, var(--accent, #f2a93b) 35%, transparent), - var(--shadow); + 0 0 0 1px color-mix(in srgb, var(--mint) 28%, transparent), + var(--shadow-hover); } .fleet-card header { display: grid; @@ -1281,23 +1400,6 @@ main ul[aria-label='已配置实例'] { margin: 0; font-size: 0.925rem; } -.fleet-status-summary { - display: grid; - grid-template-columns: repeat(3, minmax(0, 1fr)); - gap: 0.75rem; - margin: 0.75rem 0 1rem; -} -.fleet-status-summary > div { - padding: 0.85rem 1rem; - border: 1px solid var(--border); - border-radius: 0.9rem; - background: var(--surface-raised, #fff); - display: grid; - gap: 0.25rem; -} -.fleet-status-summary strong { - font-size: 1.35rem; -} @media (max-width: 720px) { .fleet-status-summary { grid-template-columns: 1fr; @@ -1345,3 +1447,13 @@ main ul[aria-label='已配置实例'] { color: var(--danger, #b45309); font-size: 0.85rem; } + + +/* Card entrance stagger (respects reduced-motion block above) */ +.fleet-card-grid .fleet-card:nth-child(1) { animation-delay: 0ms; } +.fleet-card-grid .fleet-card:nth-child(2) { animation-delay: 40ms; } +.fleet-card-grid .fleet-card:nth-child(3) { animation-delay: 80ms; } +.fleet-card-grid .fleet-card:nth-child(4) { animation-delay: 120ms; } +.fleet-card-grid .fleet-card:nth-child(5) { animation-delay: 160ms; } +.fleet-card-grid .fleet-card:nth-child(6) { animation-delay: 200ms; } +.fleet-card-grid .fleet-card:nth-child(n + 7) { animation-delay: 220ms; }