Files
i/docs/UI-STYLE.md
wsh5485 60b7df9015 docs: add UI-STYLE.md — platform must reuse CarLog UI
按用户要求「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 的视觉风格 + 元数据驱动。
2026-06-20 22:53:37 +08:00

623 lines
19 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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`。**直接用,不要复制**
```css
/* 颜色 */
--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 按钮
```html
<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 卡片
```html
<div class="card card-pad">
<h2 class="section-title">标题</h2>
内容
</div>
```
- `.card` — 白底 + 圆角 + 阴影
- `.card-pad` — 24px padding(默认所有卡片都用这个)
### 2.3 输入控件
```html
<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
```html
<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 文字颜色
```html
<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 布局
```html
<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 响应式
```html
<span class="mobile-only">只在手机显示</span>
<span class="desktop-only">只在桌面显示</span>
```
断点(在 `style.css`):
- `--bp-sm` 480px(小屏手机)
- `--bp-md` 768px(大屏手机/小平板)
- `--bp-lg` 1024px(平板)
- `--bp-xl` 1440px(桌面)
移动端样式(在 `style.css` 已自动处理):
- `<input>` 自动 16px(防 iOS 缩放)
- `table.data` 自动横向滚动
- `.container` 自动 padding 调整
### 2.8 表格
```html
<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 — 所有页面顶层
```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 — 移动端列表
```vue
<MobileCardList :items="data" :columns="columns" />
```
平台层如有列表(Subsystems.vue),桌面端用 `table.data`,移动端用 `<MobileCardList>` 兜底(参考 `WashesList.vue` 怎么用)。
### 3.4 StatCard.vue — 数据卡片
```vue
<StatCard icon="🚗" label="车辆数" :value="12" />
```
平台 Dashboard 用这个组件展示统计数据。
### 3.5 ChartBlock.vue — 图表
```vue
<ChartBlock title="趋势">
<Line :data="..." :options="..." />
</ChartBlock>
```
平台 Dashboard 如果有图表就用这个。
### 3.6 ConfirmDangerDialog.vue — 危险操作确认
```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(总设置)
```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 + 控件**
```vue
<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(管理页)
```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`
```vue
<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 加载中
```html
<div class="card card-pad text-soft">加载中…</div>
```
### 5.2 错误
```html
<div class="card card-pad text-danger">{{ error }}</div>
```
### 5.3 操作成功 / 失败消息(表单内)
```html
<p class="msg ok">已保存</p>
<p class="msg err">保存失败:{{ error }}</p>
```
### 5.4 提示文字(灰色小字)
```html
<p class="text-soft" style="font-size:13px; margin: 0 0 16px">
说明文字
</p>
```
---
## 6. 标题层级
```html
<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>` 自动 16pxiOS 防缩放,已在 style.css 处理)
- `<table.data>` 自动横向滚动
-`.mobile-only` / `.desktop-only` 切换桌面/移动组件
- 不要写 fixed width(用 max-width + 自适应)
- Container padding 自带响应式(不用自己写)
---
## 9. Trae 自检清单(提交前过一遍)
提交 PR 前 Trae 自己 grep 检查:
```bash
# 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` 就知道长什么样。