按用户要求「UI 统一」新建 UI 设计规范 docs/UI-STYLE.md (623 行):
- §0 不做的事 (不引新 UI 库 / 不写新 CSS 变量 / 不另起设计)
- §1 设计令牌 (CSS 变量速查: bg/card/text/accent/brand/green/warn/danger)
- §2 工具类速查 (btn/card/input/pill/text-*/flex/gap/mt-*/mobile-only)
- §3 布局组件 (AppLayout / AppHeader / StatCard / MobileCardList / ChartBlock)
- §4 4 个平台 view 完整模板 (GlobalSettings / SubsystemSettings / Subsystems / Dashboard)
- §5 错误 / 消息样式
- §6 标题层级
- §7 主题色使用规则
- §8 移动端注意事项
- §9 Trae 自检清单 (4 条 grep 命令)
- §10 Mavis review 验收点 (12 项)
DEV-PLAN.md 更新:
- Task 2.4 / 2.5 / 2.6 / 2.7 顶部加 ⚠️ UI-STYLE.md 引用 + 强制要求清单
- 6.2.6b 新增「平台前端 UI 规范」子节 (12 项检查 + 3 条 grep 自检)
ARCHITECTURE.md 更新:
- 新增 §7.5 UI 设计原则 + 引用 UI-STYLE.md
README.md:
- 文档列表加 UI-STYLE.md
核心原则: 平台层新页面 = Settings.vue 的视觉风格 + 元数据驱动。
19 KiB
UI 设计规范 — 平台层必须 100% 复用 CarLog UI
核心原则:用户很喜欢 CarLog 现有 UI。平台层新页面(GlobalSettings / SubsystemSettings / Subsystems / Dashboard)必须严格复用现有设计系统,不另起设计,不引入新依赖,不写新 CSS 变量。
一切样式从
client/src/style.css的 CSS 变量 + 工具类 + 现有组件取。
0. 不做的事(明确边界)
- ❌ 不引入新 UI 库(ant-design / element-plus / naive-ui / vuetify 都不行)
- ❌ 不写新的 CSS 变量(除非有明确理由并记录)
- ❌ 不写 styled-component / CSS module / scoped 颜色
- ❌ 不另起一套设计语言(不要"平台层用 X 配色 / Y 字体")
- ❌ 不要 inline
style="color: #xxx"(用.text-brand/.text-danger等类) - ❌ 不要自定义 button 样式(用
.btn+ 变体)
1. 设计令牌(CSS 变量)
全部定义在 client/src/style.css 顶部 :root。直接用,不要复制:
/* 颜色 */
--bg: #E8F4F9; /* 页面背景(淡蓝) */
--bg-soft: #F2F8FB; /* 卡片 hover / 二级背景 */
--card: #FFFFFF; /* 卡片背景 */
--text: #0F2233; /* 主文字(深蓝近黑) */
--text-soft: #5A6F80; /* 次要文字 */
--text-mute: #8A9CAB; /* 辅助文字 / 描述 */
--line: #E1ECF2; /* 分隔线 / 输入框边框 */
--accent: #0F2233; /* 主按钮背景(深蓝近黑) */
--accent-soft: #1A3A55; /* 主按钮 hover */
--brand: #1E5B8A; /* 蓝色 brand(链接 / 强调) */
--brand-soft: #2C7AB0;
--green: #4DBA9A; /* 成功 */
--warn: #E8A33D; /* 警告 */
--danger: #D9695C; /* 错误 */
--info: #5AA8D8; /* 信息 */
/* 圆角 */
--radius: 14px;
--radius-sm: 8px;
--radius-lg: 22px;
--pill: 999px;
/* 字体 */
--font: 'Outfit', system-ui, -apple-system, 'PingFang SC', 'Microsoft YaHei', sans-serif;
/* 阴影 */
--card-shadow: 0 2px 8px rgba(40, 80, 110, 0.06);
--card-shadow-hover: 0 4px 16px rgba(40, 80, 110, 0.10);
2. 工具类速查表
全部在 client/src/style.css,直接 <div class="card card-pad"> 这样用:
2.1 按钮
<button class="btn btn-primary">主操作</button>
<button class="btn btn-ghost">次操作</button>
<button class="btn btn-danger">危险</button>
<button class="btn btn-danger-outline btn-sm">小危险</button>
<button class="btn btn-primary btn-sm">小主</button>
| 类 | 用途 |
|---|---|
.btn |
基础(inline-flex / 圆角 / 过渡) |
.btn-primary |
主操作(深蓝背景白字) |
.btn-ghost |
次要(透明 + 边框) |
.btn-danger |
危险(红背景白字) |
.btn-danger-outline |
危险但次要(红边框) |
.btn-sm |
小尺寸 |
2.2 卡片
<div class="card card-pad">
<h2 class="section-title">标题</h2>
内容
</div>
.card— 白底 + 圆角 + 阴影.card-pad— 24px padding(默认所有卡片都用这个)
2.3 输入控件
<div>
<label class="label">字段名</label>
<input class="input" />
</div>
<div>
<label class="label">下拉</label>
<select class="select">
<option>A</option>
</select>
</div>
<div>
<label class="label">多行</label>
<textarea class="textarea"></textarea>
</div>
<div class="form-check">
<input type="checkbox" id="x" />
<label for="x">启用</label>
</div>
2.4 标签 / Pill
<span class="pill pill-blue">新</span>
<span class="pill pill-green">完成</span>
<span class="pill pill-warn">待办</span>
<span class="pill pill-danger">错误</span>
<span class="pill pill-gray">默认</span>
2.5 文字颜色
<p class="text">普通</p>
<p class="text-soft">次要</p>
<p class="text-mute">辅助</p>
<p class="text-brand">蓝色强调</p>
<p class="text-green">成功</p>
<p class="text-danger">错误</p>
2.6 布局
<div class="flex items-center gap-3">
<span>左</span>
<span>中</span>
<span>右</span>
</div>
<div class="flex-col gap-2 mt-4">
<div>上</div>
<div>下</div>
</div>
<div class="row mt-3">
<button>保存</button>
<button>取消</button>
</div>
| 类 | 效果 |
|---|---|
.flex / .flex-col |
flex 布局 |
.items-center / .justify-between / .justify-center |
flex 对齐 |
.gap-2 / gap-3 / gap-4 / gap-6 |
gap 8/12/16/24px |
.mt-2/3/4/6 / .mb-2/3/4/6 |
margin-top/bottom 8/12/16/24px |
.row |
flex 横向 + gap |
2.7 响应式
<span class="mobile-only">只在手机显示</span>
<span class="desktop-only">只在桌面显示</span>
断点(在 style.css):
--bp-sm480px(小屏手机)--bp-md768px(大屏手机/小平板)--bp-lg1024px(平板)--bp-xl1440px(桌面)
移动端样式(在 style.css 已自动处理):
<input>自动 16px(防 iOS 缩放)table.data自动横向滚动.container自动 padding 调整
2.8 表格
<table class="data">
<thead>
<tr><th>列1</th><th>列2</th></tr>
</thead>
<tbody>
<tr><td>A</td><td>B</td></tr>
</tbody>
</table>
- 自动 hover 高亮(
tr:hover td) - 移动端自动横向滚动
3. 布局组件(必须复用)
3.1 AppLayout.vue — 所有页面顶层
<template>
<AppLayout>
<h1 class="title">页面标题</h1>
<p class="text-soft" style="margin: 4px 0 24px; font-size:14px">副标题</p>
<div class="card card-pad">
内容
</div>
</AppLayout>
</template>
<script setup>
import AppLayout from '../../components/AppLayout.vue';
</script>
所有平台层 view 必须用 <AppLayout> 包起来(Settings.vue / WashesList.vue 等都这么做)。
3.2 AppHeader.vue — 顶栏
现有 AppHeader.vue 已经包含左侧菜单 + 用户信息 + 主题切换。平台层不需要新写顶栏,复用现有的。
3.3 MobileCardList.vue — 移动端列表
<MobileCardList :items="data" :columns="columns" />
平台层如有列表(Subsystems.vue),桌面端用 table.data,移动端用 <MobileCardList> 兜底(参考 WashesList.vue 怎么用)。
3.4 StatCard.vue — 数据卡片
<StatCard icon="🚗" label="车辆数" :value="12" />
平台 Dashboard 用这个组件展示统计数据。
3.5 ChartBlock.vue — 图表
<ChartBlock title="趋势">
<Line :data="..." :options="..." />
</ChartBlock>
平台 Dashboard 如果有图表就用这个。
3.6 ConfirmDangerDialog.vue — 危险操作确认
<ConfirmDangerDialog
:open="showConfirm"
title="删除子系统?"
message="此操作不可恢复"
@confirm="onConfirm"
@cancel="showConfirm = false"
/>
平台层启停 / 删除子系统前用这个。
3.7 AiFallbackModal.vue — AI 失败兜底
平台层暂不需要(无 AI 集成)。保留供将来用。
4. 平台层 view 模板(参考 Settings.vue)
4.1 GlobalSettings.vue(总设置)
<template>
<AppLayout>
<h1 class="title">总设置</h1>
<p class="text-soft" style="margin: 4px 0 24px; font-size:14px">
全局 UI / 备份 / Dashboard 配置
</p>
<div v-if="loading" class="card card-pad text-soft">加载中…</div>
<div v-else-if="error" class="card card-pad text-danger">{{ error }}</div>
<div v-else>
<section class="card card-pad mb-4">
<h2 class="section-title">界面</h2>
<form @submit.prevent="save" class="form">
<div>
<label class="label">主题</label>
<select v-model="form.theme" class="select">
<option value="auto">跟随系统</option>
<option value="light">浅色</option>
<option value="dark">深色</option>
</select>
</div>
<div class="mt-3">
<label class="label">语言</label>
<select v-model="form.language" class="select">
<option value="zh-CN">简体中文</option>
<option value="en">English</option>
</select>
</div>
<div class="mt-3">
<label class="label">Dashboard 布局</label>
<select v-model="form.dashboardLayout" class="select">
<option value="default">默认</option>
<option value="compact">紧凑</option>
</select>
</div>
<p v-if="msg" class="msg ok mt-3">{{ msg }}</p>
<div class="row mt-3">
<button class="btn btn-primary" :disabled="busy">
{{ busy ? '保存中…' : '保存' }}
</button>
</div>
</form>
</section>
<section class="card card-pad">
<h2 class="section-title">备份</h2>
<form @submit.prevent="save" class="form">
<div>
<label class="label">启用自动备份</label>
<input type="checkbox" v-model="form.backupEnabled" />
</div>
<div class="mt-3">
<label class="label">备份路径</label>
<input v-model="form.backupPath" class="input" placeholder="/path/to/backup" />
</div>
<div class="row mt-3">
<button class="btn btn-primary" :disabled="busy">
{{ busy ? '保存中…' : '保存' }}
</button>
</div>
</form>
</section>
</div>
</AppLayout>
</template>
4.2 SubsystemSettings.vue(通用渲染器)
跟 GlobalSettings 同结构,每个字段用 <div> 包 label + 控件:
<template>
<AppLayout>
<h1 class="title">{{ subsystemName }} 设置</h1>
<p class="text-soft" style="margin: 4px 0 24px; font-size:14px">
子系统配置(保存到 platform_settings 表)
</p>
<div v-if="loading" class="card card-pad text-soft">加载中…</div>
<div v-else-if="error" class="card card-pad text-danger">{{ error }}</div>
<div v-else class="card card-pad">
<form @submit.prevent="save" class="form">
<div v-for="field in schema.fields" :key="field.key" class="mt-3">
<label class="label">{{ field.label }}</label>
<input v-if="field.type === 'string'"
v-model="values[field.key]"
class="input" />
<input v-else-if="field.type === 'password'"
v-model="values[field.key]"
type="password" class="input" />
<input v-else-if="field.type === 'number'"
v-model.number="values[field.key]"
type="number" class="input" />
<input v-else-if="field.type === 'boolean'"
v-model="values[field.key]"
type="checkbox" />
<textarea v-else-if="field.type === 'textarea'"
v-model="values[field.key]"
class="textarea" />
<select v-else-if="field.type === 'select'"
v-model="values[field.key]"
class="select">
<option v-for="opt in field.options" :key="opt" :value="opt">{{ opt }}</option>
</select>
<span v-else class="text-warn">未支持的字段类型: {{ field.type }}</span>
</div>
<p v-if="msg" class="msg ok mt-3">{{ msg }}</p>
<div class="row mt-3">
<button class="btn btn-primary" :disabled="busy">
{{ busy ? '保存中…' : '保存' }}
</button>
</div>
</form>
</div>
</AppLayout>
</template>
4.3 Subsystems.vue(管理页)
<template>
<AppLayout>
<h1 class="title">子系统管理</h1>
<p class="text-soft" style="margin: 4px 0 24px; font-size:14px">
启停 / 注册子系统
</p>
<div v-if="loading" class="card card-pad text-soft">加载中…</div>
<div v-else-if="error" class="card card-pad text-danger">{{ error }}</div>
<div v-else class="card card-pad">
<!-- 桌面端表格 -->
<table class="data desktop-only">
<thead>
<tr>
<th>图标</th>
<th>名称</th>
<th>类别</th>
<th>版本</th>
<th>启用</th>
</tr>
</thead>
<tbody>
<tr v-for="sub in subsystems" :key="sub.id">
<td>{{ sub.icon }}</td>
<td><strong>{{ sub.name }}</strong><br /><span class="text-mute sm">{{ sub.description }}</span></td>
<td><span class="pill pill-blue">{{ sub.category }}</span></td>
<td>{{ sub.version }}</td>
<td>
<input type="checkbox" :checked="sub.enabled" @change="toggle(sub, $event.target.checked)" />
</td>
</tr>
</tbody>
</table>
<!-- 移动端卡片列表 -->
<MobileCardList
class="mobile-only"
:items="subsystems"
:columns="[
{ key: 'icon', label: '', render: r => r.icon },
{ key: 'name', label: '名称', render: r => r.name },
{ key: 'enabled', label: '启用', render: r => r.enabled ? '✅' : '❌' },
]"
/>
</div>
</AppLayout>
</template>
<script setup>
import AppLayout from '../../components/AppLayout.vue';
import MobileCardList from '../../components/MobileCardList.vue';
</script>
4.4 Dashboard.vue(替换现有 Home.vue)
参考现有 Home.vue 用 StatCard + ChartBlock:
<template>
<AppLayout>
<h1 class="title">Dashboard</h1>
<p class="text-soft" style="margin: 4px 0 24px; font-size:14px">
i 平台概览
</p>
<div class="dashboard-grid">
<StatCard icon="🚗" label="车辆" :value="data.carlog?.vehicles_count ?? 0" />
<StatCard icon="🧽" label="本月洗车" :value="data.carlog?.this_month?.washes ?? 0" />
<StatCard icon="💰" label="本月洗车支出"
:value="`¥${data.carlog?.this_month?.wash_cost ?? 0}`" />
<StatCard icon="⛽" label="本月加油 (升)"
:value="data.carlog?.this_month?.refuel_liters ?? 0" />
</div>
</AppLayout>
</template>
<script setup>
import AppLayout from '../../components/AppLayout.vue';
import StatCard from '../../components/StatCard.vue';
</script>
<style scoped>
.dashboard-grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 16px;
}
</style>
5. 错误 / 消息样式
5.1 加载中
<div class="card card-pad text-soft">加载中…</div>
5.2 错误
<div class="card card-pad text-danger">{{ error }}</div>
5.3 操作成功 / 失败消息(表单内)
<p class="msg ok">已保存</p>
<p class="msg err">保存失败:{{ error }}</p>
5.4 提示文字(灰色小字)
<p class="text-soft" style="font-size:13px; margin: 0 0 16px">
说明文字
</p>
6. 标题层级
<h1 class="title">页面大标题</h1>
<h2 class="section-title">卡片内分节标题</h2>
<h3 class="subsection-title">小节标题</h3>
class="title"— 页面级(h1)class="section-title"— 卡片内分节(h2)class="subsection-title"— 小节(h3)
(参考 Settings.vue 里 <h1 class="title"> + <h2 class="section-title"> 的用法)
7. 主题色使用规则
| 场景 | 用什么 |
|---|---|
| 主操作按钮 | .btn-primary(深蓝近黑) |
| 次要按钮 | .btn-ghost |
| 危险操作(删除/禁用) | .btn-danger / .btn-danger-outline |
| 链接 / 强调文字 | .text-brand |
| 成功提示 | .text-green / .pill-green |
| 警告 | .text-warn(无对应工具类但 CSS 变量有 var(--warn)) / .pill-warn |
| 错误 | .text-danger / .pill-danger |
| 描述文字 | .text-soft |
| 占位 / 不可点 | .text-mute |
8. 移动端注意事项
- 所有
<input>自动 16px(iOS 防缩放,已在 style.css 处理) <table.data>自动横向滚动- 用
.mobile-only/.desktop-only切换桌面/移动组件 - 不要写 fixed width(用 max-width + 自适应)
- Container padding 自带响应式(不用自己写)
9. Trae 自检清单(提交前过一遍)
提交 PR 前 Trae 自己 grep 检查:
# 1. 没有任何 inline style 写颜色
grep -rn "style=\"color:" client/src/views/Platform/
# 期望: 没有输出
# 2. 没有任何引入新 UI 库
grep -rn "ant-design\|element-plus\|naive-ui\|vuetify\|@mui" client/src/
# 期望: 没有输出
# 3. 所有平台 view 用了 AppLayout
grep -L "AppLayout" client/src/views/Platform/*.vue
# 期望: 没有输出(每个文件都 import 了)
# 4. 没有写新的 CSS 变量(除非 style.css 里没有的)
grep -rn "^ --" client/src/views/Platform/
# 期望: 没有输出(变量定义在 style.css,不在 view 里)
10. Mavis review 时会看的 UI 验收点
我(reviewer)会按这个清单看:
- 所有平台 view 顶部用
<AppLayout>包(看 template 第一行) - 颜色 / 字体 / 间距全部走 CSS 变量(grep
style="color"应该没结果) - 没有引入新 UI 库(package.json + 静态分析)
- 没有新加 CSS 变量(除非 style.css 里没的)
- 按钮用
.btn+ 变体,不是<button class="my-btn"> - 卡片用
.card.card-pad,不是自定义 background + border-radius - 输入用
.input/.select/.textarea+.label - Pill 用
.pill+ 颜色变体 - 移动端用
.mobile-only/.desktop-only切换 - 加载/错误用
.card.card-pad.text-soft/text-danger - 消息用
.msg.ok/.msg.err - 跟现有
Settings.vue视觉一致(颜色 / 间距 / 圆角)
任何一项不通过都要改完再合并。
总结:所有平台层新页面 = Settings.vue 的视觉风格 + 元数据驱动。看
client/src/views/Settings.vue就知道长什么样。