task-243(admin.html观感对齐): 壳层换皮蓝白浅色(浅色侧栏252px/顶栏76px/角色胶囊/次级退出)

This commit is contained in:
2026-09-05 20:52:15 +08:00
parent 50ba594ad1
commit 3ba84de784
3 changed files with 166 additions and 34 deletions
@@ -121,16 +121,16 @@ async function signOut() {
<section class="admin-main">
<header class="admin-topbar">
<div>
<div class="admin-kicker">运营管理控制台</div>
<div class="admin-topbar-title">
<span class="admin-kicker">运营管理控制台</span>
<h1>{{ pageTitle }}</h1>
</div>
<div class="admin-user">
<div class="admin-user-meta">
<strong>{{ userVm.username }}</strong>
<span v-if="userVm.role">{{ userVm.role }}</span>
</div>
<el-button text type="danger" @click="signOut">退出登录</el-button>
<span v-if="userVm.role" class="admin-user-role">{{ userVm.role }}</span>
<el-button class="admin-user-logout" @click="signOut">退出登录</el-button>
</div>
</header>
<main id="admin-content" class="admin-content">
@@ -150,3 +150,7 @@ async function signOut() {
</section>
</div>
</template>
<style scoped>
.admin-topbar-title { min-width: 0; }
</style>
+89 -30
View File
@@ -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; }
}
+69
View File
@@ -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\)/)
})