docs: add frontend framework design (10 sections: architecture, modules, layout, components, templates, dataflow, conventions)

This commit is contained in:
2026-05-19 10:35:49 +08:00
parent f0366ccc47
commit 339695c851
@@ -0,0 +1,665 @@
# CraftLabs 前端框架设计 v1.0
> 基于设计体系 `2026-05-19-design-system.md` 提炼的前端工程框架规范
> 目标:统一架构、约束风格、加速开发、降低交接成本
> 日期:2026-05-19
---
## 目录
1. [框架概述](#1-框架概述)
2. [技术栈](#2-技术栈)
3. [工程结构](#3-工程结构)
4. [核心模块](#4-核心模块)
5. [布局系统](#5-布局系统)
6. [组件体系](#6-组件体系)
7. [页面模板](#7-页面模板)
8. [数据流](#8-数据流)
9. [开发规范](#9-开发规范)
10. [构建与部署](#10-构建与部署)
---
## 1. 框架概述
### 1.1 定位
CraftLabs 前端框架是基于 Vue 3 + Element Plus 的企业级 B2B 授权运营平台前端方案。在 Element Plus 之上注入品牌设计 Token,提供开箱即用的布局、组件、页面模板和开发约束。
### 1.2 核心原则
| 原则 | 说明 |
|------|------|
| **约定优于配置** | 路由、鉴权、API 封装、错误处理均有默认行为,开发者只需关注业务 |
| **Token 驱动** | 所有视觉属性通过 CSS 变量控制,换肤只需改 `theme.css` |
| **组件隔离** | 每个 `.vue` 文件自包含模板+逻辑+样式,`<script setup>` 统一风格 |
| **渐进增强** | 在 Element Plus 基础上叠加品牌样式,不 fork 组件库 |
| **安全默认** | JWT 存储 localStorage、401 自动登出、路由级鉴权 |
### 1.3 层级架构
```
┌─────────────────────────────────────────────┐
│ Views (页面层) │
│ LicenseList CustomersView ContractsView │
├─────────────────────────────────────────────┤
│ Components (组件层) │
│ StatsCard SearchBar DataTable ... │
├─────────────────────────────────────────────┤
│ Layout System (布局系统) │
│ MainLayout (Header+Sidebar+Breadcrumb) │
├──────────────┬──────────────┬───────────────┤
│ Router │ Store │ API │
│ (鉴权/懒加载) │ (Pinia JWT) │ (axios封装) │
├──────────────┴──────────────┴───────────────┤
│ Element Plus + Vue 3 │
├─────────────────────────────────────────────┤
│ Theme System (Token层) │
│ theme.css · CSS Variables │
└─────────────────────────────────────────────┘
```
---
## 2. 技术栈
| 层 | 选型 | 版本 | 说明 |
|----|------|:--:|------|
| 框架 | Vue 3 | ^3.5 | Composition API + `<script setup>` |
| 构建 | Vite | ^6.0 | 极速 HMR + 开箱即用 |
| UI 库 | Element Plus | ^2.9 | 组件库本体,CSS 变量覆盖换肤 |
| 路由 | vue-router | ^4.5 | 懒加载 + meta 鉴权 |
| 状态 | Pinia | ^2.3 | 轻量状态管理 |
| HTTP | axios | ^1.7 | 拦截器 + JWT 注入 |
| 语言 | JavaScript (ESM) | — | 暂用 JS,必要时渐进引入 TS |
### 2.1 不引入的依赖
| 库 | 原因 |
|----|------|
| Tailwind CSS | Element Plus 已覆盖组件样式,避免双体系 |
| Vuex | Pinia 更轻量,官方推荐 |
| Lodash | 项目规模不需要工具函数库 |
| Moment.js | 日期格式化用原生 `Intl` 或字符串裁剪 |
---
## 3. 工程结构
```
web/delivery-platform-ui/
├── public/
│ ├── design-demo.html # 设计 Demo(独立 HTML,无依赖)
│ └── design-system.html # 设计体系可视化
├── src/
│ ├── main.js # 入口:挂载 Vue + Pinia + Router + 401拦截器
│ ├── App.vue # 根组件:纯 <router-view>
│ ├── theme.css # ★ 全局主题 TokenCSS 变量)
│ │
│ ├── layout/
│ │ └── MainLayout.vue # ★ 主布局:Header + Sidebar + Breadcrumb
│ │
│ ├── router/
│ │ └── index.js # ★ 路由表 + 鉴权守卫
│ │
│ ├── stores/
│ │ └── auth.js # ★ 认证状态:JWT + login/logout/roles
│ │
│ ├── api/
│ │ └── platform.js # ★ API 封装:按模块导出 axios 请求函数
│ │
│ ├── utils/
│ │ ├── apiErrorMessage.js # 统一错误消息提取
│ │ └── redactPayload.js # Callback payload 脱敏
│ │
│ └── views/ # ★ 页面组件(按模块命名)
│ ├── HomeView.vue # 工作台/Dashboard
│ ├── LoginView.vue # 登录页
│ ├── ForbiddenView.vue # 403
│ ├── NotFoundView.vue # 404
│ ├── CustomersView.vue # 客户管理
│ ├── ProjectsView.vue # 项目管理
│ ├── ContractsView.vue # 合同列表
│ ├── ContractWizardView.vue # 合同创建向导
│ ├── ContractDetailView.vue # 合同详情
│ ├── DeliveriesView.vue # 交付列表
│ ├── DeliveryBatchWizardView.vue
│ ├── DeliveryBatchDetailView.vue
│ ├── LicenseSnListView.vue # 许可SN列表
│ ├── LicenseSnWizardView.vue
│ ├── LicenseSnDetailView.vue
│ ├── LicenseList.vue # ★ 许可证管理(自研SDK核心页)
│ ├── CallbackInboxView.vue # Callback 收件箱
│ ├── CallbackInboxDetailView.vue
│ ├── IntegrationEnvironmentsView.vue
│ ├── IntegrationProductLinesView.vue
│ └── LayoutCompareView.vue # 设计审核工具(内部)
├── package.json
├── vite.config.js
├── Dockerfile
└── README.md
```
### 3.1 文件命名规范
| 类型 | 规范 | 示例 |
|------|------|------|
| 页面视图 | `PascalCaseView.vue` | `CustomersView.vue` |
| 布局组件 | `PascalCase.vue` | `MainLayout.vue` |
| 路由/Store/API | `kebab-case.js` | `platform.js` |
| 工具函数 | `camelCase.js` | `apiErrorMessage.js` |
| 样式文件 | `kebab-case.css` | `theme.css` |
---
## 4. 核心模块
### 4.1 主题系统 (`theme.css`)
所有视觉 Token 集中在一个 CSS 文件中,通过覆盖 Element Plus 变量实现换肤。
```css
/* 核心 3 行即可改变全站主色调 */
:root {
--el-color-primary: #2C3E6B;
--el-bg-color-page: #EAEFFA;
--el-border-radius-base: 6px;
}
/* 完整 Token 见 docs/superpowers/specs/2026-05-19-design-system.md */
```
**设计意图**
- 不修改 Element Plus 源码,只覆盖 CSS 变量
- 更换品牌色只需改 `--el-color-primary` 一个值
- 新页面自动继承主题,无需额外引入
### 4.2 路由系统 (`router/index.js`)
**路由表结构**
```js
const routes = [
// 公开路由(无鉴权)
{ path: "/login", component: LoginView },
// 受保护路由(需要登录)
{
path: "/",
component: MainLayout, // 统一布局壳
meta: { requiresAuth: true },
children: [
{
path: "licenses", // 子路由路径
name: "licenses", // 命名路由(用于编程跳转)
component: () => import("..."), // 懒加载
meta: { roles: ["SYS_ADMIN", "DEVELOPER"] } // 角色鉴权
},
// ... 更多子路由
]
},
// 兜底路由
{ path: "/403", component: ForbiddenView },
{ path: "/:pathMatch(.*)*", component: NotFoundView },
]
```
**鉴权守卫逻辑**
```
用户访问页面
├─ meta.requiresAuth && 无 token → 重定向 /login?redirect=原路径
├─ meta.roles && 用户角色不匹配 → 重定向 /403
└─ 通过 → 正常渲染
```
**新增页面只需**
1.`children` 数组中加一条路由
2. 设置 `meta.roles` 控制可见角色
3. `component: () => import(...)` 懒加载
### 4.3 状态管理 (`stores/auth.js`)
```js
export const useAuthStore = defineStore("auth", {
state: () => ({
token: localStorage.getItem("craftlabs_platform_token") || "",
displayName: "",
roles: [],
}),
actions: {
async login(username, password) {
const { data } = await axios.post("/api/v1/auth/login", { username, password })
this.token = data.token
this.roles = data.roles
localStorage.setItem("craftlabs_platform_token", this.token)
axios.defaults.headers.common.Authorization = `Bearer ${this.token}`
},
logout() {
this.token = ""
localStorage.removeItem("craftlabs_platform_token")
delete axios.defaults.headers.common.Authorization
},
},
})
```
**使用方式**
```js
import { useAuthStore } from "../stores/auth"
const auth = useAuthStore()
// 模板中
auth.hasAnyRole(["SYS_ADMIN", "DEVELOPER"]) // 角色判断
auth.displayName // 用户名
```
### 4.4 API 层 (`api/platform.js`)
```js
// 每个后端端点导出为一个具名函数
export function listCustomers(params) {
return axios.get("/api/v1/customers", { params })
}
export function createCustomer(body) {
return axios.post("/api/v1/customers", body)
}
export function updateCustomer(id, body) {
return axios.put(`/api/v1/customers/${id}`, body)
}
export function deleteCustomer(id) {
return axios.delete(`/api/v1/customers/${id}`)
}
```
**约定**
- 函数名 = HTTP 动词 + 资源名:`listXxx` / `createXxx` / `updateXxx` / `deleteXxx` / `getXxx`
- 状态变更用语义化函数名:`patchContractStatus()``addLine()`
- 所有函数返回 axios Promise,由调用方 `await` + try/catch
- 查询参数用 `{ params }` 传递(axios 自动序列化)
### 4.5 全局错误处理 (`main.js`)
```js
// 401 自动登出拦截器
axios.interceptors.response.use(
(r) => r,
(err) => {
if (err.response?.status === 401) {
const auth = useAuthStore()
auth.logout()
router.push({ name: "login", query: { redirect: router.currentRoute.value.fullPath } })
}
return Promise.reject(err)
}
)
```
---
## 5. 布局系统
### 5.1 MainLayout 结构
```
┌──────────────────────────────────────────────────────────┐
│ <header> 60px │
│ Logo │ 导航菜单 │ 全局搜索 │ 🔔 │ ⚙️ │ 👤用户名 │
├────────┬─────────────────────────────────────────────────┤
│<aside> │ <div class="breadcrumb"> 46px │
│ 232px │ 授权运营 › 当前页面 │
│ ├─────────────────────────────────────────────────┤
│ 菜单 │ │
│ 分组 │ <router-view /> │
│ │ (各页面通过 slot 注入到此区域) │
│ 11项 │ │
│ │ │
└────────┴─────────────────────────────────────────────────┘
```
### 5.2 布局尺寸契约
| 元素 | 尺寸 | CSS |
|------|:--:|------|
| Header | 60px | `height: 60px; flex-shrink: 0` |
| Sidebar | 232px | `width: 232px; flex-shrink: 0` |
| Breadcrumb | 46px | `height: 46px; flex-shrink: 0` |
| Content | 自适应 | `flex: 1; padding: 16px 20px` |
### 5.3 布局变体
| 变体 | 触发条件 | 说明 |
|------|---------|------|
| 标准布局 | 默认 | Header + Sidebar + Breadcrumb + 全宽 Content |
| Tree 布局 | `activeModule === 'licenses'` | 额外 280px Tree 面板在 Content 左侧 |
| 全屏布局 | 无 Header/Sidebar | 登录页、403/404 页 |
---
## 6. 组件体系
### 6.1 组件分类
```
组件层级:
Element Plus 基础组件 (el-button, el-table, el-dialog, ...)
├── 直接使用(无需封装)
│ el-button el-input el-select el-table el-pagination
│ el-tag el-badge el-tree el-dialog el-card
├── 组合组件(页面内组合)
│ 搜索栏 + 表格 + 分页 → 列表页模板
│ 统计卡片行 → Dashboard 模板
└── 业务组件(跨页面复用,按需抽取)
StatsCard SearchFilterBar StatusTag ConfirmDialog
```
### 6.2 组件开发原则
- **优先直接用 Element Plus**90% 场景不需要封装
- **抽取时机**:同一模式在 3+ 个页面出现时才抽取为独立组件
- **Props 优先于全局状态**:组件通过 props 接收数据,不直接读 store
- **Events 向上**:组件通过 `$emit` 通知父组件,不直接改父组件状态
### 6.3 CSS 约定
```css
/* ✅ 推荐:scoped + CSS 变量 */
<style scoped>
.my-page { background: var(--el-bg-color-page); }
.card { border-radius: var(--el-border-radius-base); }
.btn-primary { background: var(--el-color-primary); }
</style>
/* ❌ 避免:硬编码色值 */
<style scoped>
.card { background: #EAEFFA; } /* 应使用 CSS 变量 */
</style>
```
---
## 7. 页面模板
### 7.1 标准列表页
适用于:客户管理、合同管理、交付管理、许可SN
```
结构:
<统计卡片行> (可选)
<筛选栏>
[搜索框] [状态下拉] [查询按钮] [+ 新建按钮]
</筛选栏>
<数据表格>
<el-table> + <el-pagination>
</数据表格>
<新建/编辑 Dialog>
```
**样板代码(约 200 行)**
```vue
<script setup>
import { ref, onMounted } from "vue"
import { ElMessage, ElMessageBox } from "element-plus"
import { useAuthStore } from "../stores/auth"
import { listXxx, createXxx, updateXxx, deleteXxx } from "../api/platform"
import { apiErrorMessage } from "../utils/apiErrorMessage"
const auth = useAuthStore()
const loading = ref(false), saving = ref(false)
const rows = ref([]), total = ref(0), page = ref(1), pageSize = ref(10)
const keyword = ref(""), filterStatus = ref("")
const dialogVisible = ref(false), editingId = ref(null)
const formRef = ref(null)
const form = reactive({ name: "", ... })
onMounted(() => { auth.restoreAxiosAuth(); load() })
async function load() {
loading.value = true
try {
const { data } = await listXxx({ page: page.value-1, size: pageSize.value, keyword: keyword.value?.trim() })
rows.value = data.content ?? []
total.value = data.totalElements ?? 0
} catch(e) {
ElMessage.error(apiErrorMessage(e))
} finally { loading.value = false }
}
async function submit() {
await formRef.value.validate()
saving.value = true
try {
const payload = { name: form.name.trim(), ... }
editingId.value ? await updateXxx(editingId.value, payload) : await createXxx(payload)
dialogVisible.value = false
await load()
} catch(e) {
ElMessage.error(apiErrorMessage(e))
} finally { saving.value = false }
}
async function onDelete(row) {
await ElMessageBox.confirm(`确定删除「${row.name}」?`, "提示", { type: "warning" })
await deleteXxx(row.id)
await load()
}
</script>
```
### 7.2 Tree + 列表页
适用于:许可证管理
```
结构:
<div class="license-page"> ← display: flex
<Tree Panel 280px> ← 左侧固定宽度
<Main Panel> ← flex: 1
<统计卡片行>
<筛选栏 + 签发按钮>
<数据表格>
</Main Panel>
</div>
```
### 7.3 工作台 Dashboard
适用于:首页
```
结构:
<统计卡片行 4列>
<趋势图 + 待办列表 2列>
<快捷入口>
```
### 7.4 详情页
适用于:合同详情、交付详情、SN详情
```
结构:
<返回按钮 + 页面标题>
<详情卡片>
<el-descriptions> 或 自定义 KV 布局
</详情卡片>
<关联数据卡片> (可选:审计事件、Callback关联)
<操作按钮行>
```
---
## 8. 数据流
### 8.1 请求生命周期
```
用户操作(点击查询/新建/保存)
loading.value = true ← 显示加载态
await apiFunction(payload) ← axios 自动注入 JWT Header
├─ 200: data → 更新响应式状态
│ ElMessage.success("操作成功")
├─ 4xx: err → ElMessage.error(apiErrorMessage(e))
│ 401 → 自动登出
│ 403 → 提示无权限
└─ 5xx / Network Error → ElMessage.error("服务器错误")
loading.value = false ← 恢复交互
```
### 8.2 状态分类
| 状态类型 | 存储位置 | 示例 |
|---------|---------|------|
| 页面级临时状态 | 组件内 `ref()` / `reactive()` | 表格数据、表单数据、loading |
| 跨页面持久状态 | Pinia `auth.js` | token、用户名、角色 |
| UI 状态 | 组件内 | dialogVisible、activeTab |
| URL 状态 | `route.query` / `route.params` | 分页页码、详情页 ID |
---
## 9. 开发规范
### 9.1 新增页面检查清单
```
□ 文件命名:PascalCaseView.vue
□ 使用 <script setup> + Composition API
□ 所有 API 调用有 try/catch + apiErrorMessage
□ 列表有 v-loading / 提交按钮有 :loading
□ 路由 meta 包含 roles 鉴权
□ 表格表头使用 #F2F5FC 背景(继承 theme.css
□ 状态标签使用 .tag-active / .tag-revoked 等语义类
□ CTA 按钮使用 .btn-cta 带阴影
□ 色值使用 CSS 变量,不硬编码
□ 弹框使用 <el-dialog>(自动继承 8px 圆角)
```
### 9.2 禁止事项
| 禁止 | 原因 |
|------|------|
| 硬编码色值 (`#EAEFFA`) | 应使用 CSS 变量,便于换肤 |
| 直接操作 DOM (`document.querySelector`) | 使用 Vue 响应式 + ref |
| 在 `setup` 外定义响应式数据 | 非 `.vue` 文件需使用 `defineStore` |
| `v-if` + `v-for` 同时使用 | Vue 性能警告 |
| 忽略 try/catch | 未捕获异常导致白屏 |
| 提交 `console.log` | 使用 `ElMessage` 或移除 |
### 9.3 Git 提交规范
```
feat(web): 新增许可证管理页面
fix(web): 修复合同列表分页重置bug
refactor(web): 抽取 StatsCard 为独立组件
style(web): 统一表格表头样式
docs: 更新前端框架设计文档
```
---
## 10. 构建与部署
### 10.1 开发环境
```bash
cd web/delivery-platform-ui
npm install
npm run dev # → http://localhost:5173
# /api → proxy → http://127.0.0.1:8080
```
### 10.2 生产构建
```bash
# 指定后端地址
VITE_API_BASE=https://api.craftlabs.cn npm run build
# 产物:dist/
# 部署:Nginx 静态托管 + 反代 /api 到后端
```
### 10.3 CI/CD
```yaml
# .github/workflows/ci-platform.yml (前端部分)
- name: Build Frontend
run: |
cd web/delivery-platform-ui
npm ci
npm run build
```
### 10.4 Nginx 配置模板
```nginx
server {
listen 80;
server_name platform.craftlabs.cn;
root /var/www/delivery-platform-ui/dist;
index index.html;
location /api/ {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
location / {
try_files $uri $uri/ /index.html; # SPA fallback
}
}
```
---
## 附录 A:文件职责速查
| 文件 | 职责 | 谁需要修改 |
|------|------|-----------|
| `theme.css` | 全局色彩/圆角/阴影 Token | 设计师/UED |
| `MainLayout.vue` | 页面壳:Header/Sidebar/Breadcrumb | 架构师 |
| `router/index.js` | 路由表 + 鉴权守卫 | 全栈/前端 |
| `stores/auth.js` | JWT 认证状态 | 全栈/前端 |
| `api/platform.js` | 后端 API 封装 | 前端(新增接口时) |
| `utils/*.js` | 通用工具函数 | 前端 |
| `views/*View.vue` | 业务页面 | 前端 |
## 附录 B:快速上手
**创建一个新页面(以「审计日志」为例)**
1. 创建 `src/views/AuditLogView.vue`,使用标准列表页模板
2.`api/platform.js` 添加 `listAuditEvents()` 函数
3.`router/index.js` 添加路由,设置 `meta.roles`
4.`MainLayout.vue``menuItems` 数组添加菜单项
5. `npm run dev` 验证
---
*修订记录:2026-05-19 初版,基于设计体系 v1.0*