task-243(admin.html观感对齐): 壳层换皮蓝白浅色(浅色侧栏252px/顶栏76px/角色胶囊/次级退出)
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
|
||||
@@ -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\)/)
|
||||
})
|
||||
Reference in New Issue
Block a user