Files
i/docs/UI-STYLE.md
T
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

19 KiB
Raw Blame History

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-sm 480px(小屏手机)
  • --bp-md 768px(大屏手机/小平板)
  • --bp-lg 1024px(平板)
  • --bp-xl 1440px(桌面)

移动端样式(在 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.vueStatCard + 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> 自动 16pxiOS 防缩放,已在 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 就知道长什么样。