60b7df9015
按用户要求「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 的视觉风格 + 元数据驱动。
331 lines
15 KiB
Markdown
331 lines
15 KiB
Markdown
# 架构方案:单 SPA + 单库 + 表前缀
|
||
|
||
## 1. 目标
|
||
|
||
i 平台是一个「生活操作系统」:单 Vue SPA + 单 Express 进程 + 单 MySQL,按表前缀分多子系统,永远单用户。
|
||
|
||
- **不引入分布式复杂度**:不分库、不分进程、不分部署
|
||
- **数据隔离靠表前缀**:`carlog_*` / `fitness_*` / `reading_*` / ...
|
||
- **元数据驱动 UI**:subsystem 注册到 `subsystems` 表,平台前端通用渲染设置页和导航
|
||
- **永远单用户**:管什么 RBAC / 多租户
|
||
|
||
理由:个人用,数据量小;分库分进程反而增加运维复杂度而没收益。
|
||
|
||
## 2. 顶层架构
|
||
|
||
```
|
||
┌────────────────────────────────────────────────────────┐
|
||
│ Vue SPA (一个壳子) │
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────┐│
|
||
│ │ 总设置 / 子系统管理 / Dashboard (平台层) ││
|
||
│ └──────────────────────────────────────────────────────┘│
|
||
│ │
|
||
│ ┌──────────────────────────────────────────────────────┐│
|
||
│ │ 🚗 CarLog | 💪 Fitness | 📚 Reading ││
|
||
│ │ 概览/洗车/加油 | 训练/打卡/计划 | 书/笔记/进度 ││
|
||
│ └──────────────────────────────────────────────────────┘│
|
||
│ │
|
||
└────────────────────────────────────────────────────────┘
|
||
│ axios + cookie session
|
||
▼
|
||
┌────────────────────────────────────────────────────────┐
|
||
│ Express (一个进程) │
|
||
│ ├── /api/platform/* (总设置 / 子系统管理 / Dashboard) │
|
||
│ ├── /api/carlog/* (CarLog 子系统) │
|
||
│ └── /api/{future}/* (将来加的子系统) │
|
||
└────────────────────────────────────────────────────────┘
|
||
│
|
||
▼
|
||
┌────────────────────────────────────────────────────────┐
|
||
│ MySQL @ 162.14.110.130:33306 / carlog │
|
||
│ │
|
||
│ 平台表 (无前缀): │
|
||
│ users / sessions / subsystems / platform_settings │
|
||
│ │
|
||
│ CarLog 表: │
|
||
│ carlog_vehicles / carlog_wash_records / carlog_refuels │
|
||
│ carlog_charging_records / carlog_maintenance_records │
|
||
│ carlog_insurance_records / carlog_chemicals │
|
||
│ carlog_weather_snapshots / carlog_operation_logs │
|
||
│ carlog_tags / carlog_achievements / carlog_notifications│
|
||
│ ... │
|
||
│ │
|
||
│ Fitness 表 (将来): │
|
||
│ fitness_workouts / fitness_plans / fitness_measurements│
|
||
│ │
|
||
│ Reading 表 (将来): │
|
||
│ reading_books / reading_notes / reading_progress │
|
||
└────────────────────────────────────────────────────────┘
|
||
```
|
||
|
||
## 3. 物理隔离靠什么
|
||
|
||
| 隔离维度 | 做法 |
|
||
|---|---|
|
||
| 数据 | 表前缀 `{subsystem}_*`(同一 DB 内;本阶段 CarLog 还没加前缀,留给将来加第二个子系统时) |
|
||
| 路由 | 子系统自己的路径空间(`/api/carlog/*` 现有;将来 `/api/fitness/*` 等) |
|
||
| 代码 | 子系统独立目录(`server/src/subsystems/{name}/`、`client/src/views/subsystems/{name}/`) |
|
||
| 设置 | 每个子系统有自己的 settings schema(JSON Schema,存 `platform_settings` 表,key 前缀 `{name}.*`) |
|
||
| 菜单 | 每个子系统在 `subsystems` 表注册,平台层根据 `category` 分组渲染左侧导航 |
|
||
|
||
**没有** JWT / SSO / iframe / 6 端点协议 / 独立 DB — 那些都是过度设计。
|
||
|
||
## 4. Subsystem 注册表
|
||
|
||
平台层只管一张元数据表:
|
||
|
||
```sql
|
||
CREATE TABLE subsystems (
|
||
id VARCHAR(50) PRIMARY KEY, -- 'carlog' / 'fitness' / 'reading'
|
||
name VARCHAR(100) NOT NULL, -- 显示名:「洗车管理系统」
|
||
description TEXT,
|
||
icon VARCHAR(20), -- emoji 或文件名
|
||
color VARCHAR(20),
|
||
category VARCHAR(50), -- vehicle / fitness / finance / reading
|
||
version VARCHAR(20),
|
||
enabled TINYINT(1) DEFAULT 1,
|
||
sort_order INT DEFAULT 0,
|
||
settings_schema JSON, -- JSON Schema 描述这个子系统的设置项
|
||
nav_items JSON, -- [{label, icon, path}]
|
||
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
||
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
|
||
) ENGINE=InnoDB;
|
||
|
||
INSERT INTO subsystems (id, name, icon, color, category, version, settings_schema, nav_items) VALUES
|
||
('carlog', '洗车管理系统', '🚗', '#1B6EF3', 'vehicle', '2.8.0',
|
||
'{"fields":[
|
||
{"key":"weather.default_city","label":"默认城市","type":"select","options":["Beijing","Shanghai","Korla"],"default":"Korla"},
|
||
{"key":"ai.provider","label":"AI 识别 provider","type":"select","options":["openai_compat","minimax_vl"],"default":"minimax_vl"}
|
||
]}',
|
||
'[{"label":"概览","path":"/","icon":"🏠"},{"label":"洗车记录","path":"/washes","icon":"🧽"}]'
|
||
);
|
||
```
|
||
|
||
**注意**:subsystem 表里**不存业务数据**,只存「我是谁 / 我有什么设置 / 我在菜单里长啥样」。
|
||
|
||
## 5. 总设置 vs 子系统设置
|
||
|
||
### 5.1 总设置(平台级)
|
||
|
||
存 `platform_settings` 表,**不带前缀**:
|
||
|
||
| key | value | 说明 |
|
||
|---|---|---|
|
||
| `ui.theme` | auto / light / dark | UI 主题 |
|
||
| `ui.language` | zh-CN / en | 界面语言 |
|
||
| `dashboard.layout` | default / compact | Dashboard 布局 |
|
||
| `backup.enabled` | 1 / 0 | 自动备份开关 |
|
||
| `backup.path` | /path/ | 备份路径 |
|
||
|
||
UI 在 `/settings/global`,平台前端渲染。
|
||
|
||
### 5.2 子系统设置
|
||
|
||
存 `platform_settings` 表,**带 `{subsystem}.` 前缀**:
|
||
|
||
| key | 说明 |
|
||
|---|---|
|
||
| `carlog.weather.default_city` | 来自 CarLog manifest 的 settings_schema |
|
||
| `carlog.ai.provider` | 同上 |
|
||
| `carlog.grocy.url` | Grocy 实例 URL(将来从 settings 表迁过来) |
|
||
| `fitness.units.metric` | Fitness 子系统的「公制单位」开关 |
|
||
| `reading.sort.default` | Reading 子系统的默认排序 |
|
||
|
||
UI 在 `/settings/{subsystem}`,平台前端**通用渲染器**(按 settings_schema 的 type 渲染 input / select / textarea):
|
||
|
||
```js
|
||
// 通用渲染器伪代码
|
||
async function loadSubsystemSettings(subsystemId) {
|
||
const sub = await api.get(`/platform/subsystems/${subsystemId}`);
|
||
const values = await api.get(`/platform/settings?prefix=${subsystemId}.`);
|
||
return { schema: sub.settings_schema, values };
|
||
}
|
||
```
|
||
|
||
**好处**:加新子系统不用改平台前端代码,只写 settings_schema 就完事。
|
||
|
||
### 5.3 兼容现有 settings 表
|
||
|
||
CarLog 现在已经有 `settings` 表(21 个键),存了 AI / Grocy / 登录锁定等配置。**两种方案**:
|
||
|
||
- (A) **保留 `settings` 表**,平台层加 `platform_settings` 表,新子系统的设置走 platform_settings,CarLog 自己的设置继续用 `settings` 表
|
||
- (B) **迁移到 `platform_settings`**,所有子系统统一,CarLog 的 key 加 `carlog.` 前缀
|
||
|
||
**推荐 (A)**:迁移工作量小,CarLog 的现有逻辑不用改。platform_settings 主要管「平台级 UI 设置 + 将来新子系统的设置」。
|
||
|
||
## 6. 路由 / 代码目录
|
||
|
||
### 6.1 服务端
|
||
|
||
```
|
||
server/src/
|
||
├── index.js # 统一入口
|
||
├── routes/
|
||
│ ├── platform/ # 总设置 / 子系统管理 / Dashboard 聚合
|
||
│ │ ├── settings.js # GET/PUT /api/platform/settings
|
||
│ │ ├── subsystems.js # GET /api/platform/subsystems
|
||
│ │ └── dashboard.js # GET /api/platform/dashboard (跨子系统聚合)
|
||
│ └── subsystems/
|
||
│ └── carlog/
|
||
│ ├── index.js # 聚合导出 13 个 CarLog router
|
||
│ └── routes/ # 13 个 CarLog 路由文件
|
||
└── ...
|
||
```
|
||
|
||
### 6.2 前端
|
||
|
||
```
|
||
client/src/
|
||
├── AppLayout.vue # 左侧菜单按 category 分组
|
||
├── views/
|
||
│ ├── Platform/ # 平台层
|
||
│ │ ├── GlobalSettings.vue
|
||
│ │ ├── SubsystemSettings.vue # 通用渲染器
|
||
│ │ └── Subsystems.vue # 启停 + 注册
|
||
│ ├── Login.vue # i 平台统一登录
|
||
│ ├── Home.vue # Dashboard, 读 /api/platform/dashboard
|
||
│ └── subsystems/
|
||
│ └── carlog/ # 20 个 CarLog view
|
||
├── api/
|
||
│ ├── client.js # 底层 axios 实例(auth interceptor + 解包)
|
||
│ └── subsystems.js # carlogApi helper(baseURL = /api/carlog)
|
||
├── router/index.js
|
||
└── stores/
|
||
├── auth.js
|
||
└── platform.js # 总设置 / 子系统列表 / 当前激活子系统
|
||
```
|
||
|
||
### 6.3 子系统如何在平台菜单里显示
|
||
|
||
```vue
|
||
<!-- AppLayout.vue 左侧导航 -->
|
||
<template v-for="cat in groupedCategories" :key="cat">
|
||
<div class="nav-category">{{ cat.label }}</div>
|
||
<router-link v-for="item in cat.items" :key="item.path" :to="item.path">
|
||
{{ item.icon }} {{ item.label }}
|
||
</router-link>
|
||
</template>
|
||
|
||
<script setup>
|
||
import { computed } from 'vue';
|
||
import { usePlatformStore } from '../stores/platform';
|
||
const platform = usePlatformStore();
|
||
const groupedCategories = computed(() => {
|
||
const groups = {};
|
||
for (const sub of platform.subsystems.filter(s => s.enabled)) {
|
||
if (!groups[sub.category]) groups[sub.category] = { label: sub.category, items: [] };
|
||
for (const nav of (sub.nav_items || [])) {
|
||
groups[sub.category].items.push({ ...nav, subsystem: sub.id });
|
||
}
|
||
}
|
||
return Object.values(groups);
|
||
});
|
||
</script>
|
||
```
|
||
|
||
## 7. 实施步骤
|
||
|
||
### W1: 平台骨架(极简版)
|
||
|
||
1. 加 `subsystems` + `platform_settings` 两张表(migration 001_platform.sql)
|
||
2. CarLog 自己注册一条记录到 `subsystems` 表(migration 里 INSERT)
|
||
3. 加 `server/src/routes/platform/{settings,subsystems,dashboard}.js` 三个文件
|
||
4. 在 `index.js` 把这三个 router mount 到 `/api/platform`
|
||
5. 加 `client/src/views/Platform/GlobalSettings.vue`(总设置 UI)
|
||
6. 加 `client/src/views/Platform/SubsystemSettings.vue`(通用子系统设置渲染器)
|
||
7. AppLayout.vue 改成读 `platform.subsystems` 动态渲染导航
|
||
|
||
### W2: 表前缀迁移(CarLog)
|
||
|
||
把所有 CarLog 表加 `carlog_` 前缀:
|
||
|
||
```sql
|
||
RENAME TABLE vehicles TO carlog_vehicles;
|
||
RENAME TABLE wash_records TO carlog_wash_records;
|
||
RENAME TABLE refuel_records TO carlog_refuels;
|
||
RENAME TABLE charging_records TO carlog_chargings;
|
||
-- ... 其他表
|
||
```
|
||
|
||
`db.js` 加 helper:
|
||
|
||
```js
|
||
const TABLE_PREFIX = {
|
||
vehicles: 'carlog_vehicles',
|
||
wash_records: 'carlog_wash_records',
|
||
};
|
||
export function tableName(name) {
|
||
return TABLE_PREFIX[name] || name;
|
||
}
|
||
```
|
||
|
||
### W3+(暂不规划)
|
||
|
||
用户还没决定下一子系统做什么。本阶段只做:
|
||
|
||
1. ✅ 平台基座(subsystems 表 + platform_settings + 3 个平台路由 + 3 个平台前端 + 元数据驱动菜单)
|
||
2. ✅ CarLog 子系统化(目录迁移 + 加 `/api/carlog/` 前缀 + 注册到 subsystems 表)
|
||
|
||
将来加新子系统时按这个流程:
|
||
1. 写子系统的表(带 `{subsystem}_` 前缀)+ 路由 + 前端
|
||
2. 注册到 `subsystems` 表
|
||
3. 平台菜单自动出现新子系统入口
|
||
4. 通用设置渲染器自动支持新子系统的 settings
|
||
5. 验证端到端流程
|
||
|
||
## 7.5 UI 设计原则
|
||
|
||
**平台层 UI 100% 复用 CarLog 现有 UI,不另起设计。**
|
||
|
||
CarLog 已经有完整设计系统:
|
||
- 设计令牌:`client/src/style.css` 顶部 `:root`(颜色 / 圆角 / 字体 / 阴影 / 响应式断点)
|
||
- 工具类:60+ 个(`.btn` / `.card` / `.input` / `.pill` / `.flex` / `.mt-3` / `.mobile-only` ...)
|
||
- 组件:`AppLayout` / `AppHeader` / `StatCard` / `ChartBlock` / `MobileCardList` / `ConfirmDangerDialog`
|
||
|
||
**平台层新页面(GlobalSettings / SubsystemSettings / Subsystems / Dashboard)必须**:
|
||
- ✅ 用 `<AppLayout>` 包整个页面
|
||
- ✅ 颜色 / 字体 / 间距全部走 CSS 变量(`var(--xxx)`)或工具类
|
||
- ✅ 复用现有组件(`StatCard` / `MobileCardList` / `ConfirmDangerDialog`)
|
||
- ✅ 跟现有 `Settings.vue` 视觉一致(卡片 + form + 按钮 + 消息)
|
||
|
||
**禁止**:
|
||
- ❌ 引入新 UI 库(ant-design / element-plus / naive-ui / vuetify)
|
||
- ❌ 写新 CSS 变量(除非有明确理由)
|
||
- ❌ inline `style="color: ..."`
|
||
- ❌ 自定义 button / card 样式
|
||
- ❌ 另起设计语言
|
||
|
||
完整规范见 [docs/UI-STYLE.md](UI-STYLE.md)(设计令牌 + 工具类速查 + 4 个平台 view 完整模板 + Mavis review 验收点)。
|
||
|
||
## 8. 不做的事
|
||
|
||
- ❌ JWT / SSO / 跨域 auth
|
||
- ❌ iframe 嵌入子系统
|
||
- ❌ 子系统独立部署 / 独立 DB
|
||
- ❌ RBAC / 多用户 / 多租户
|
||
- ❌ manifest + 6 端点协议
|
||
- ❌ SSR / 原生 app
|
||
- ❌ Marketplace / 插件化
|
||
- ❌ 跨子系统 widget(等 5+ 子系统再做)
|
||
|
||
## 9. 风险 & 缓解
|
||
|
||
| 风险 | 缓解 |
|
||
|---|---|
|
||
| 表前缀迁移漏表 | 写 Python 脚本扫描所有 SQL 提取表名检查 |
|
||
| 子系统相互依赖(A 写 B 的表) | 代码 review + lint 规则禁止跨前缀 SQL |
|
||
| 表越来越多 DB 卡 | 单用户不可能;几万条再分库 |
|
||
| 总设置 vs 子系统设置混淆 | key 命名约定:总设置无前缀,子系统 `{sub}.{key}` |
|
||
|
||
## 10. 立即动手清单
|
||
|
||
1. **本周**写 migration 001_platform.sql(subsystems 表 + platform_settings 表 + seed CarLog)
|
||
2. **本周**写平台路由:settings / subsystems / dashboard 三个文件
|
||
3. **本周**写平台前端:GlobalSettings + SubsystemSettings + 改 AppLayout 动态菜单
|
||
4. **下下周**做表前缀迁移 + 改 server SQL
|
||
5. **下下周**跑完整测试套件 + 手动 E2E
|
||
|
||
---
|
||
|
||
> 简单胜过复杂。元数据驱动(settings_schema / nav_items 两个 JSON 字段)覆盖 80% 场景,剩下的真要 iframe / SSO 时再说。 |