diff --git a/admin-frontend-vue/src/layout/AdminLayout.vue b/admin-frontend-vue/src/layout/AdminLayout.vue index 16f60a55..082070b3 100644 --- a/admin-frontend-vue/src/layout/AdminLayout.vue +++ b/admin-frontend-vue/src/layout/AdminLayout.vue @@ -121,16 +121,16 @@ async function signOut() {
-
-
运营管理控制台
+
+ 运营管理控制台

{{ pageTitle }}

{{ userVm.username }} - {{ userVm.role }}
- 退出登录 + {{ userVm.role }} + 退出登录
@@ -150,3 +150,7 @@ async function signOut() {
+ + diff --git a/admin-frontend-vue/src/styles/main.css b/admin-frontend-vue/src/styles/main.css index 582c76d7..b250ba3a 100644 --- a/admin-frontend-vue/src/styles/main.css +++ b/admin-frontend-vue/src/styles/main.css @@ -1,14 +1,16 @@ :root { --admin-sidebar: #f8fbfe; --admin-sidebar-deep: #edf3f9; + --admin-sidebar-text: #536a81; --admin-primary: #4f78a5; + --admin-primary-strong: #2f5d8b; --admin-primary-soft: #e7f0f8; --admin-bg: #f4f7fb; --admin-border: #d8e3ee; --admin-text: #24384d; --admin-muted: #5b6f83; font-family: Inter, "SF Pro Display", "Microsoft YaHei", "PingFang SC", "Helvetica Neue", sans-serif; - /* Element Plus 主色对齐 admin.html 蓝白浅色皮肤(渐变主键 #5f85ad→#4f78a5) */ + /* Element Plus 主题对齐 admin.html 蓝白浅色皮肤(EP_ALIGN/241 单源,勿单独改色值) */ --el-color-primary: #4f78a5; --el-color-primary-light-3: #7d9abf; --el-color-primary-light-5: #a5bad2; @@ -16,7 +18,6 @@ --el-color-primary-light-8: #e1e9f0; --el-color-primary-light-9: #f0f4f8; --el-color-primary-dark-2: #3f628a; - /* 语义色/边框/圆角对齐 admin.html 蓝白浅色皮肤(EP_ALIGN 单源,勿单独改) */ --el-color-success: #4e806d; --el-color-warning: #a8793e; --el-color-danger: #b35f6a; @@ -30,51 +31,109 @@ * { box-sizing: border-box; } html, body, #app { margin: 0; min-height: 100%; height: 100%; } -body { background: var(--admin-bg); color: var(--admin-text); } +body { + color: var(--admin-text); + background: + radial-gradient(circle at 12% -16%, rgba(178, 205, 229, 0.3), transparent 34rem), + radial-gradient(circle at 100% 0%, rgba(214, 226, 239, 0.28), transparent 28rem), + var(--admin-bg); +} button, input, textarea, select { font: inherit; } +/* ===== 蓝白浅色壳层(对齐 admin.html 末层皮肤) ===== */ .admin-shell { display: flex; min-height: 100vh; background: var(--admin-bg); } -.admin-sidebar { display: flex; flex: 0 0 248px; flex-direction: column; background: linear-gradient(180deg, var(--admin-sidebar) 0%, var(--admin-sidebar-deep) 100%); color: #b9c2d5; transition: flex-basis .2s ease; } +.admin-sidebar { + display: flex; flex: 0 0 252px; flex-direction: column; + background: linear-gradient(180deg, var(--admin-sidebar) 0%, var(--admin-sidebar-deep) 100%); + color: var(--admin-sidebar-text); + border-right: 1px solid #d4e0eb; + box-shadow: 14px 0 34px -28px rgba(39, 67, 94, 0.34); + transition: flex-basis .2s ease; +} .admin-shell.is-collapsed .admin-sidebar { flex-basis: 64px; } -.admin-brand { display: flex; align-items: center; gap: 12px; min-height: 72px; padding: 16px; border-bottom: 1px solid rgba(255,255,255,.08); } -.admin-brand-logo { width: 36px; height: 36px; border-radius: 10px; object-fit: cover; background: white; } +.admin-brand { + display: flex; align-items: center; gap: 12px; + min-height: 76px; padding: 20px 18px; + border-bottom: 1px solid #d4e0eb; +} +.admin-brand-logo { + width: 36px; height: 36px; border-radius: 11px; + object-fit: cover; background: #fff; + box-shadow: 0 8px 20px rgba(79, 120, 165, 0.24); +} .admin-brand-copy { display: flex; min-width: 0; flex-direction: column; gap: 3px; } -.admin-brand-copy strong { color: #fff; font-size: 16px; } -.admin-brand-copy span { color: #929db4; font-size: 11px; white-space: nowrap; } -.admin-menu-scroll { flex: 1; padding: 14px 10px; } +.admin-brand-copy strong { color: var(--admin-text); font-size: 15px; font-weight: 600; letter-spacing: .5px; white-space: nowrap; } +.admin-brand-copy span { color: #77899b; font-size: 11px; white-space: nowrap; } + +.admin-menu-scroll { flex: 1; padding: 12px 10px; } .admin-menu { border-right: 0; background: transparent; } -.admin-menu .el-sub-menu__title, .admin-menu .el-menu-item { height: 42px; line-height: 42px; border-radius: 8px; margin: 3px 0; color: #aeb8cc; } -.admin-menu .el-sub-menu__title:hover, .admin-menu .el-menu-item:hover { background: rgba(255,255,255,.08); color: #fff; } -.admin-menu .el-menu-item.is-active { background: linear-gradient(90deg, rgba(99,102,241,.95), rgba(99,102,241,.62)); color: #fff; } +.admin-menu .el-sub-menu__title, +.admin-menu .el-menu-item { + height: 42px; line-height: 42px; margin: 3px 0; padding: 0 12px; + border-radius: 8px; color: var(--admin-sidebar-text); +} +.admin-menu .el-sub-menu__title:hover, +.admin-menu .el-menu-item:hover { color: var(--admin-primary-strong); background: rgba(79, 120, 165, 0.09); } +.admin-menu .el-menu-item.is-active { + color: var(--admin-primary-strong); font-weight: 600; + background: linear-gradient(90deg, #dce9f5, #edf4fa); + border-color: #b6cbe0; + box-shadow: inset 3px 0 0 #5f85ad, 0 8px 18px -16px rgba(79, 120, 165, 0.55); +} .admin-menu .el-menu { background: transparent; } -.menu-group-title { font-weight: 600; } -.sidebar-collapse { margin: 12px; padding: 9px 10px; border: 1px solid rgba(255,255,255,.14); border-radius: 8px; color: #aeb8cc; background: transparent; cursor: pointer; } -.sidebar-collapse:hover { color: #fff; border-color: rgba(255,255,255,.3); } +.admin-menu .el-sub-menu .el-sub-menu__icon-arrow { color: var(--admin-sidebar-text); } +.menu-group-title { font-size: 12px; font-weight: 600; letter-spacing: 1.1px; color: #72859a; } +.menu-group-title:hover { color: var(--admin-primary-strong); } + +.sidebar-collapse { + margin: 12px; padding: 9px 10px; + border: 1px solid #cbd9e6; border-radius: 8px; + color: var(--admin-sidebar-text); background: #fff; cursor: pointer; +} +.sidebar-collapse:hover { color: var(--admin-primary-strong); border-color: #95b1cb; background: #edf5fb; } .sidebar-collapse:focus-visible { outline: 2px solid var(--admin-primary); outline-offset: 2px; } + .skip-link { position: absolute; left: -9999px; width: 1px; height: 1px; overflow: hidden; } .skip-link:focus { position: fixed; left: 16px; top: 16px; z-index: 2000; width: auto; height: auto; - padding: 10px 14px; background: #fff; color: var(--admin-primary); - border-radius: 8px; box-shadow: 0 4px 16px rgba(0, 0, 0, .2); text-decoration: none; + padding: 10px 14px; background: #fff; color: var(--admin-primary-strong); + border-radius: 8px; box-shadow: 0 4px 16px rgba(39, 67, 94, 0.2); text-decoration: none; } + .admin-main { display: flex; min-width: 0; flex: 1; flex-direction: column; } -.admin-topbar { display: flex; align-items: center; justify-content: space-between; gap: 24px; min-height: 84px; padding: 18px 28px; background: #fff; border-bottom: 1px solid var(--admin-border); } -.admin-kicker { color: var(--admin-primary); font-size: 12px; font-weight: 700; letter-spacing: .08em; } -.admin-topbar h1 { margin: 5px 0 0; font-size: 22px; } -.admin-user { display: flex; align-items: center; gap: 18px; } -.admin-user-meta { display: flex; flex-direction: column; align-items: flex-end; gap: 3px; } -.admin-user-meta strong { font-size: 14px; } -.admin-user-meta span { color: var(--admin-muted); font-size: 12px; } -.admin-content { flex: 1; min-width: 0; padding: 26px 28px; overflow: auto; } +.admin-topbar { + display: flex; align-items: center; justify-content: space-between; gap: 24px; + min-height: 76px; padding: 0 32px; + background: rgba(255, 255, 255, 0.92); + border-bottom: 1px solid var(--admin-border); + box-shadow: 0 10px 28px -28px rgba(39, 67, 94, 0.42); + backdrop-filter: blur(8px); +} +.admin-kicker { color: #70859a; font-size: 10px; font-weight: 700; letter-spacing: 1.6px; } +.admin-topbar h1 { margin: 6px 0 0; font-size: 18px; font-weight: 650; color: var(--admin-text); } +.admin-user { display: flex; align-items: center; gap: 14px; } +.admin-user-meta { display: flex; align-items: flex-end; flex-direction: column; } +.admin-user-meta strong { font-size: 13px; font-weight: 600; color: var(--admin-text); } +.admin-user-role { + display: inline-flex; align-items: center; + padding: 2px 10px; border-radius: 999px; font-size: 12px; line-height: 1.7; + color: var(--admin-primary-strong); background: #e6f0f8; border: 1px solid #bfd1e2; +} +.admin-user-logout.el-button { + color: var(--admin-muted); background: #fff; border-color: #cbd9e6; +} +.admin-user-logout.el-button:hover { color: var(--admin-primary-strong); border-color: #95b1cb; background: #edf5fb; } + +.admin-content { flex: 1; min-width: 0; padding: 30px 32px 56px; overflow: auto; } .page-stack { display: flex; flex-direction: column; gap: 18px; } .page-heading { display: flex; align-items: flex-start; justify-content: space-between; gap: 16px; } -.page-heading h2 { margin: 0; font-size: 22px; } +.page-heading h2 { margin: 0; font-size: 20px; color: var(--admin-text); } .page-heading p { margin: 7px 0 0; color: var(--admin-muted); font-size: 13px; } .table-footer { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding-top: 18px; color: var(--admin-muted); font-size: 13px; } -.el-card { border-color: var(--admin-border); border-radius: 12px; } +.el-card { border-color: var(--admin-border); border-radius: 14px; box-shadow: 0 1px 2px rgba(39, 67, 94, 0.04), 0 14px 30px -24px rgba(39, 67, 94, 0.28); } .page-loading, .page-error { padding: 48px 20px; text-align: center; } .page-loading { color: var(--admin-muted); } -.page-error { color: #b91c1c; } +.page-error { color: var(--el-color-danger); } .not-found { padding: 56px 20px; } .global-error-stack { display: flex; flex-direction: column; gap: 10px; margin-bottom: 14px; } .admin-breadcrumb { margin-bottom: 14px; } @@ -82,8 +141,8 @@ button, input, textarea, select { font: inherit; } @media (max-width: 820px) { .admin-sidebar { flex-basis: 64px; } .admin-brand-copy, .admin-menu .el-sub-menu__title span, .admin-menu .el-menu-item { display: none; } - .admin-topbar { align-items: flex-start; flex-direction: column; } + .admin-topbar { align-items: flex-start; flex-direction: column; padding: 12px 18px; } .admin-user { width: 100%; justify-content: space-between; } .admin-user-meta { align-items: flex-start; } - .admin-content { padding: 18px; } + .admin-content { padding: 18px 16px 40px; } } diff --git a/admin-frontend-vue/tests/task-243.test.ts b/admin-frontend-vue/tests/task-243.test.ts new file mode 100644 index 00000000..ce8258fc --- /dev/null +++ b/admin-frontend-vue/tests/task-243.test.ts @@ -0,0 +1,69 @@ +import test from 'node:test' +import assert from 'node:assert/strict' +import { readSource } from './helpers.ts' + +// module 13 task 243:壳层换皮成 admin.html 蓝白浅色 —— 侧栏 252px 浅底、顶栏 76px、 +// 角色胶囊 + 次级退出按钮、去掉顶栏重复 logo、菜单激活态浅蓝渐变+左蓝条。 + +test('test_task_243_shell_normal_primary_path', () => { + const css = readSource('src/styles/main.css') + assert.match(css, /\.admin-sidebar\s*\{[^}]*flex:\s*0\s*0\s*252px/, '侧栏宽度应为 252px') + assert.match(css, /\.admin-topbar\s*\{[^}]*min-height:\s*76px/, '顶栏高度应为 76px') + assert.match(css, /linear-gradient\(180deg,\s*var\(--admin-sidebar\)\s*0%,/, '侧栏应为浅色渐变底') +}) + +test('test_task_243_shell_normal_variant_input', () => { + // 正常变体:菜单激活态为浅蓝渐变 + inset 蓝条,不再是靛蓝渐变。 + const css = readSource('src/styles/main.css') + assert.match(css, /inset 3px 0 0 #5f85ad/, '激活态需含左侧 3px 蓝条') + assert.match(css, /linear-gradient\(90deg,\s*#dce9f5,\s*#edf4fa\)/, '激活态需为浅蓝渐变') + assert.equal(/#6366f1|rgba\(99,\s*102,\s*241/.test(css), false, '壳层不得残留 indigo 主色') +}) + +test('test_task_243_shell_normal_repeated_operation_is_idempotent', () => { + const css = readSource('src/styles/main.css') + const first = /\.admin-topbar\s*\{[^}]*min-height:\s*76px/.test(css) + const second = /\.admin-topbar\s*\{[^}]*min-height:\s*76px/.test(css) + assert.equal(first, true) + assert.equal(first, second) +}) + +test('test_task_243_shell_boundary_empty_input', () => { + // 边界:保留折叠与窄屏适配;collapse 按钮仍在布局中。 + const css = readSource('src/styles/main.css') + assert.match(css, /is-collapsed/) + assert.match(css, /@media \(max-width:\s*820px\)/) + const layout = readSource('src/layout/AdminLayout.vue') + assert.match(layout, /sidebar-collapse/) +}) + +test('test_task_243_shell_boundary_single_item', () => { + // 边界:顶栏不再放第二个 logo;用户角色以胶囊呈现。 + const layout = readSource('src/layout/AdminLayout.vue') + assert.equal(/admin-topbar-logo/.test(layout), false, '顶栏不应再出现 logo 元素') + assert.match(layout, /admin-user-role/, '需有角色胶囊类') + assert.match(layout, /admin-user-logout/, '退出按钮需为次级胶囊化按钮') +}) + +test('test_task_243_shell_boundary_limit_or_missing_field', () => { + const css = readSource('src/styles/main.css') + assert.match(css, /\.admin-user-role\s*\{[^}]*border-radius:\s*999px/, '角色应为胶囊') + assert.match(css, /\.admin-kicker\s*\{[^}]*font-size:\s*10px/, 'kicker 应为 10px') + assert.match(css, /\.admin-brand-copy strong\s*\{[^}]*color:\s*var\(--admin-text\)/, '品牌名应使用浅色文本 token') +}) + +test('test_task_243_shell_invalid_input_rejected', () => { + // 异常:旧壳层暗色/白字样式不得残留(会令浅色侧栏不可读)。 + const css = readSource('src/styles/main.css') + assert.equal(/#aeb8cc|#929db4|#b9c2d5/.test(css), false, '壳层不得残留原深色侧栏的白灰文字色') + assert.equal(/#192132|#121827/.test(css), false, '不得残留深色侧栏底色') +}) + +test('test_task_243_shell_dependency_failure_returns_actionable_message', () => { + // 依赖失败:侧栏文本/强调色 token 已落盘并被引用(防硬编码漂移)。 + const css = readSource('src/styles/main.css') + assert.match(css, /--admin-sidebar-text:\s*#536a81/) + assert.match(css, /--admin-primary-strong:\s*#2f5d8b/) + assert.match(css, /var\(--admin-sidebar-text\)/) + assert.match(css, /var\(--admin-primary-strong\)/) +})