You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
houqin-java/docs/07-前端界面开发规范.md

1647 lines
55 KiB
Markdown

This file contains ambiguous Unicode characters!

This file contains ambiguous Unicode characters that may be confused with others in your current locale. If your use case is intentional and legitimate, you can safely ignore this warning. Use the Escape button to highlight these characters.

# 医院物业SaaS管理后台 — 前端界面开发规范
> 版本v1.0
> 定位前端界面开发强制规范所有开发工作100%遵循
> 日期2026-04-17
> 级别:**强制** — 除非经技术负责人审批,否则不得偏离
---
## 一、总则
### 1.1 适用范围
本规范适用于以下四套前端界面开发:
| 端侧 | 技术栈 | UI组件库 |
|------|--------|----------|
| 超级管理员后台Web | Vue 3 + TypeScript + Vite | Element Plus |
| 物业公司后台Web | Vue 3 + TypeScript + Vite | Element Plus |
| 医院后台Web | Vue 3 + TypeScript + Vite | Element Plus |
| 微信小程序端 | uni-app 3.x | uni-ui |
### 1.2 文档体系关系
```
01-模块划分.md → 模块定义、角色体系
02-功能清单-*/*.md → 页面布局、字段、按钮、弹窗、业务逻辑(页面特有)
03-业务流转逻辑-*.md → 状态机、审批流
04-开发与测试规范.md → 代码规范命名、分层、Git、测试
05-接口规范.md → API定义、IModulePlugin、权限矩阵
06-项目技术要求.md → 技术栈、架构、安全、性能
07-前端界面开发规范.md → ★ 界面开发流程、交付标准、组件规范(本文档)
```
**分层原则**
- 本文档定义**通用强制规范**开发流程、组件标准、H约束实现、自测标准
- 功能清单文件定义**页面特有内容**(字段、按钮、弹窗、业务交互)
- 本文档不重复功能清单中已有的H1-H8约束描述但定义其**落地实现标准**
### 1.3 强制级别
| 标记 | 含义 | 违反后果 |
|------|------|----------|
| **[强制]** | 必须遵循,无例外 | 代码审查打回 |
| **[建议]** | 推荐遵循,可申明理由偏离 | 记录偏离原因 |
| **[禁止]** | 绝对不允许 | 代码审查打回 + 问题追踪 |
---
## 二、开发流程规范
### 2.1 五步开发流程
```
用户描述功能需求
┌─────────────────┐
│ ① 确认修改计划 │ 输出:修改计划文档(含影响范围)
└────────┬────────┘
┌─────────────────┐
│ ② 更新功能清单 │ 输出02-功能清单-*/*.md 已更新
└────────┬────────┘
┌─────────────────┐
│ ③ 静态界面开发 │ 输出Mock数据驱动的完整界面
└────────┬────────┘
┌─────────────────┐
│ ④ 全面自测 │ 输出自测通过报告见第九章Checklist
└────────┬────────┘
┌────┴────┐
▼ 不通过 ▼ 通过
回到③ ┌─────────────────┐
│ ⑤ 真实API界面 │ 输出API联调完成的界面
└─────────────────┘
```
### 2.2 每步详解
#### ① 确认修改计划
| 项目 | 要求 |
|------|------|
| 输入 | 用户的功能需求描述 |
| 输出 | 修改计划文档,包含:涉及的功能清单文件、新增/修改的页面、共享组件影响分析 |
| 责任人 | 开发者 |
| 通过标准 | 用户确认修改计划,无遗漏 |
**[强制]** 修改计划必须包含**共享组件影响分析**:列出本次修改涉及的共享组件,以及这些组件在哪些功能模块中被引用。详见第七章。
#### ② 更新功能清单
| 项目 | 要求 |
|------|------|
| 输入 | 已确认的修改计划 |
| 输出 | `02-功能清单-*/*.md` 中对应页面描述已更新 |
| 责任人 | 开发者 |
| 通过标准 | 功能清单与修改计划一致,用户确认 |
**[强制]** 功能清单更新后方可开始编码。功能清单是界面开发的唯一权威依据。
#### ③ 静态界面开发
| 项目 | 要求 |
|------|------|
| 输入 | 已更新的功能清单 |
| 输出 | 使用Mock数据、无API调用的完整界面 |
| 责任人 | 开发者 |
| 通过标准 | 界面布局、交互逻辑、页面跳转全部可操作 |
**[强制]** 静态界面必须满足:
- 所有列表页有至少20条逼真模拟数据支持分页
- 所有表单页可填写提交数据写入本地Mock状态
- 所有页面跳转和路由正常工作
- 编辑页回填选中行的数据
- H1-H8硬性约束全部实现
- 错误场景(空列表/403/超时/500/校验失败)可演示
#### ④ 全面自测
| 项目 | 要求 |
|------|------|
| 输入 | 静态界面代码 |
| 输出 | 自测通过报告按第九章Checklist逐项验证 |
| 责任人 | 开发者 |
| 通过标准 | Checklist全部通过 |
**[强制]** 自测未通过不得进入下一步。自测范围包括共享组件引用方的回归验证。
#### ⑤ 真实API界面开发
| 项目 | 要求 |
|------|------|
| 输入 | 自测通过的静态界面 |
| 输出 | API联调完成的界面 |
| 责任人 | 开发者 + 后端联调 |
| 通过标准 | 所有接口调用正确,数据展示与接口响应一致 |
**[强制]** API迁移支持逐模块渐进式切换见第十章无需一次性全部切换。
### 2.3 [禁止] 跳过流程
以下行为严格禁止:
- **[禁止]** 不更新功能清单直接编码
- **[禁止]** 跳过自测直接进入API联调
- **[禁止]** 静态界面未通过自测就切换到真实API
- **[禁止]** 修改共享组件不执行影响分析和覆盖测试
---
## 三、功能清单更新规范
### 3.1 页面描述结构
每个页面的功能清单描述必须包含以下章节(与现有格式一致):
```markdown
## 页面N{页面名称}
**页面编号**{模块编码}-{页面序号}-P{序号}
**端侧归属**Web专属 / 小程序专属 / 双端
**页面路径**/{module}/{page}
### 界面布局
ASCII布局图
### 查询条件
(字段名、控件类型、必填、默认值、说明)
### 列表字段
(序号、字段名、列宽、支持排序、说明)
### 操作按钮
(按钮、权限编码、位置、显示条件、说明)
### 弹窗定义
(弹窗名称、触发条件、表单字段、校验规则)
### 交互流程要求
(步骤编号 + 描述 + H约束标记
### 组件规范
(组件名称、使用场景、规范要求)
### 前端硬性约束
H1~H8页面级具体实现如确认弹窗文案等
```
### 3.2 H约束补充原则
- 功能清单中的H1-H8约束是**页面级具体实现**(如确认弹窗的文案、超时的具体秒数)
- 通用H约束实现标准在本规范第六章定义
- 功能清单只写**与该页面业务相关的差异化内容**,不重复通用规则
### 3.3 更新审批流程
1. 开发者根据修改计划更新功能清单
2. 用户确认功能清单更新内容
3. 确认后方可开始编码
---
## 四、前端项目架构规范
### 4.1 整体目录结构
```
src/
├── api/ # API适配器层
│ ├── index.ts # 适配器工厂(环境变量切换)
│ ├── types/ # API类型定义
│ │ ├── repair.ts # 报修模块 Request/Response 类型
│ │ ├── inspection.ts
│ │ └── ...
│ ├── mock/ # Mock适配器实现
│ │ ├── index.ts # Mock注册入口
│ │ ├── data/ # Mock数据文件
│ │ │ ├── repair.ts # 报修模块模拟数据≥20条
│ │ │ ├── inspection.ts
│ │ │ └── ...
│ │ └── adapters/
│ │ ├── repair.ts # 报修模块MockAdapter
│ │ ├── inspection.ts
│ │ └── ...
│ └── real/ # 真实API适配器实现
│ ├── index.ts # Axios实例 + 拦截器
│ └── adapters/
│ ├── repair.ts # 报修模块ApiAdapter
│ ├── inspection.ts
│ └── ...
├── components/ # 组件
│ ├── shared/ # [共享组件] — 修改须遵守第七章规范
│ │ ├── registry.ts # 共享组件注册表
│ │ ├── Breadcrumb/
│ │ ├── QueryPanel/
│ │ ├── ActionBar/
│ │ ├── DataTable/
│ │ ├── Pagination/
│ │ ├── FormDialog/
│ │ ├── DetailDrawer/
│ │ └── ...
│ └── business/ # 业务组件(模块独有)
│ ├── repair/
│ ├── inspection/
│ └── ...
├── layouts/ # 布局组件
│ ├── AdminLayout.vue # 管理后台统一布局
│ ├── sidebar/ # 侧边栏
│ ├── header/ # 顶栏
│ └── tags/ # 标签页
├── views/ # 页面视图(按后台+模块划分)
│ ├── super-admin/ # 超级管理员后台
│ │ ├── account/
│ │ ├── permission/
│ │ ├── system/
│ │ └── log/
│ ├── property/ # 物业公司后台
│ │ ├── repair/
│ │ ├── inspection/
│ │ ├── cleaning/
│ │ ├── org/
│ │ ├── attendance/
│ │ ├── evaluation/
│ │ ├── stats/
│ │ ├── log/
│ │ └── system/
│ ├── hospital/ # 医院后台
│ │ ├── contract/
│ │ ├── bidding/
│ │ ├── supervise/
│ │ ├── evaluation/
│ │ └── stats/
│ └── miniprogram/ # 小程序(独立项目,此处仅做参考)
├── router/ # 路由
│ ├── index.ts # 路由入口
│ ├── guards.ts # 路由守卫
│ └── modules/ # 按后台拆分路由模块
│ ├── super-admin.ts
│ ├── property.ts
│ └── hospital.ts
├── stores/ # Pinia状态管理
│ ├── global/ # 全局状态
│ │ ├── useUserStore.ts # 用户信息+权限
│ │ ├── useDictStore.ts # 字典数据
│ │ └── useAppStore.ts # 应用配置
│ └── modules/ # 模块状态
│ ├── useRepairStore.ts
│ └── ...
├── styles/ # 样式
│ ├── variables.scss # Element Plus主题变量覆盖
│ ├── mixins.scss # 公共mixin
│ └── reset.scss # 样式重置
├── utils/ # 工具函数
│ ├── request.ts # Axios封装拦截器、Token、错误处理
│ ├── permission.ts # 权限校验工具
│ ├── dirty-check.ts # H4脏数据检测工具
│ ├── debounce-submit.ts # H1防重复请求工具
│ ├── result-feedback.ts # H8操作结果反馈工具
│ └── shared-impact.ts # 共享组件影响分析工具
├── hooks/ # 组合式函数
│ ├── useListPage.ts # 列表页通用逻辑
│ ├── useFormPage.ts # 表单页通用逻辑
│ └── usePermission.ts # 权限控制Hook
└── types/ # 全局类型
├── enums/ # 业务枚举
├── api.d.ts # API通用类型
└── global.d.ts # 全局类型声明
```
### 4.2 多后台差异化规范
**[强制]** 三个Web后台遵循以下目录划分原则
| 规则 | 说明 |
|------|------|
| 独立路由入口 | 每个后台有独立路由模块(`router/modules/` |
| 独立页面目录 | 页面视图按后台分目录(`views/super-admin/`、`views/property/`、`views/hospital/` |
| 共享组件目录 | 跨后台复用的组件放在 `components/shared/` |
| 业务组件目录 | 模块独有组件放在 `components/business/{module}/` |
| **[禁止]跨后台复制粘贴** | 同一功能在不同后台必须复用共享组件,不得复制代码 |
### 4.3 环境变量与API模式切换
**`.env` 配置**
```bash
# API模式mock | real | 渐进式
VITE_API_MODE=mock
# 渐进式迁移示例报修用真实API其余用Mock
# VITE_API_MODE=mock:repair=real,inspection=mock,cleaning=mock
# API基础路径
VITE_API_BASE_URL=/api/v1
# 超时配置与H2约束对齐
VITE_API_TIMEOUT_GET=15000
VITE_API_TIMEOUT_POST=30000
VITE_API_TIMEOUT_UPLOAD=60000
VITE_API_TIMEOUT_STATS=30000
```
**适配器工厂**
```typescript
// api/index.ts
import type { IRepairApi } from './types/repair'
// ... 其他模块类型
type ApiMode = 'mock' | 'real'
function getModuleMode(moduleCode: string): ApiMode {
const mode = import.meta.env.VITE_API_MODE
if (mode === 'mock' || mode === 'real') return mode
// 渐进式解析mock:repair=real,inspection=mock
const overrides = mode.split(':').slice(1)[0]?.split(',') || []
const override = overrides.find(o => o.startsWith(`${moduleCode}=`))
return override ? override.split('=')[1] as ApiMode : mode.split(':')[0] as ApiMode
}
// 懒加载适配器
export function getRepairApi(): IRepairApi {
const mode = getModuleMode('repair')
return mode === 'real'
? import('./real/adapters/repair').then(m => new m.RepairApiAdapter())
: import('./mock/adapters/repair').then(m => new m.RepairApiAdapter())
}
```
### 4.4 路由组织规范
```typescript
// router/modules/property.ts
export default [
{
path: '/repair',
component: AdminLayout,
meta: { platform: 'property' },
children: [
{
path: 'orders',
name: 'RepairOrderList',
component: () => import('@/views/property/repair/OrderList.vue'),
meta: {
title: '工单列表',
permissions: ['repair:list:view'],
keepAlive: true
}
},
{
path: 'orders/:id',
name: 'RepairOrderDetail',
component: () => import('@/views/property/repair/OrderDetail.vue'),
meta: {
title: '工单详情',
permissions: ['repair:detail:view']
}
}
]
}
]
```
**[强制]** 路由meta必须包含
- `title`:页面标题(面包屑/标签页使用)
- `permissions`:权限编码数组(路由守卫判断)
- `keepAlive`列表页设为true缓存查询状态
### 4.5 布局框架规范
所有Web后台使用统一 `AdminLayout`,结构如下:
```
┌──────────────────────────────────────────────┐
│ Header顶栏Logo/用户信息/退出) │
├────────┬─────────────────────────────────────┤
│ │ TagsView标签页导航
│ Side ├─────────────────────────────────────┤
│ bar │ │
│ (侧 │ Main Content页面内容区
│ 边 │ │
│ 栏) │ │
│ │ │
├────────┴─────────────────────────────────────┤
│ Footer可选版权信息
└──────────────────────────────────────────────┘
```
---
## 五、静态界面开发规范
### 5.1 Mock数据规范
#### 5.1.1 类型定义
**[强制]** 每个模块的Mock数据必须有对应的TypeScript类型定义类型定义与功能清单中的列表字段、表单字段一一对应
```typescript
// api/types/repair.ts
/** 工单列表项 — 对应功能清单"列表字段"表格 */
export interface RepairOrderItem {
id: string
orderNo: string // 工单号
repairType: string // 报修类型
urgency: 'URGENT' | 'NORMAL' | 'LOW' // 紧急程度
status: RepairOrderStatus // 状态
reporterName: string // 报修人
teamName: string // 负责班组
handlerName: string // 维修人员
submitTime: string // 提交时间
appointmentTime: string // 预约时间
isSupplement: boolean // 补录标记
}
/** 工单列表查询参数 — 对应功能清单"查询条件"表格 */
export interface RepairOrderQuery {
orderNo?: string
status?: RepairOrderStatus[]
repairType?: string
urgency?: string
submitDateRange?: [string, string]
teamId?: string
areaId?: string
page: number
pageSize: number
}
/** 工单列表响应 */
export interface RepairOrderListResponse {
list: RepairOrderItem[]
pagination: {
page: number
pageSize: number
total: number
totalPages: number
}
}
/** API接口定义 — Mock和Real共同实现 */
export interface IRepairApi {
getOrderList(query: RepairOrderQuery): Promise<RepairOrderListResponse>
getOrderDetail(id: string): Promise<RepairOrderDetail>
createOrder(data: CreateRepairOrderRequest): Promise<{ id: string }>
updateOrder(id: string, data: UpdateRepairOrderRequest): Promise<void>
deleteOrder(id: string): Promise<void>
}
```
#### 5.1.2 数据量要求
| 场景 | 最少数据量 | 说明 |
|------|-----------|------|
| 列表页 | 20条 | 支撑分页演示默认每页20条至少2页 |
| 下拉选项 | 5-10个 | 班组/区域/类型等 |
| 树形结构 | 3级×5个 | 区域/组织等层级 |
| 级联选择 | 3级×5个 | 项目→区域→楼栋→楼层 |
#### 5.1.3 数据逼真度要求
**[强制]** Mock数据必须符合真实业务场景
| 字段类型 | 逼真度要求 | 示例 |
|----------|-----------|------|
| 姓名 | 中文常见姓名 | 张伟、李明、王芳 |
| 地址 | 医院建筑格式 | 1号楼3层301室 |
| 工单号 | 带前缀+日期+序号 | WX202604170001 |
| 时间 | 合理日期范围 | 近3个月内 |
| 状态 | 符合业务状态分布 | 待分配20%、处理中40%、已完成30%、已关闭10% |
| 金额 | 合理范围 | 合同金额50万-500万 |
| 手机号 | 脱敏格式 | 138****1234 |
#### 5.1.4 边界值覆盖
**[强制]** Mock数据必须包含以下边界场景
| 场景 | 数据要求 |
|------|----------|
| 空列表 | 返回 `list: [], pagination: { total: 0 }` |
| 长文本 | 备注/描述字段包含50+字的文本 |
| 特殊字符 | 工单描述包含换行、引号、尖括号 |
| 补录数据 | 至少2条 `isSupplement: true` 的记录 |
| 未分配 | 至少3条无负责人/班组的记录 |
| 紧急工单 | 至少2条 `urgency: 'URGENT'` 的记录 |
#### 5.1.5 错误场景Mock
**[强制]** MockAdapter必须内置以下错误场景触发机制
```typescript
// api/mock/adapters/repair.ts
export class RepairApiAdapter implements IRepairApi {
async getOrderList(query: RepairOrderQuery): Promise<RepairOrderListResponse> {
await this.delay()
// 错误场景触发通过特殊query参数
if (query._mockError === 'empty') {
return { list: [], pagination: { page: 1, pageSize: 20, total: 0, totalPages: 0 } }
}
if (query._mockError === '403') {
throw new ApiError(40300, '无数据权限(越权访问)')
}
if (query._mockError === 'timeout') {
await this.delay(16000) // 超过GET 15s阈值
return normalData
}
if (query._mockError === '500') {
throw new ApiError(50000, '服务器内部错误')
}
// 正常数据返回
return this.filterAndPaginate(mockRepairOrders, query)
}
/** 模拟网络延时 200-800ms */
private delay(ms?: number): Promise<void> {
const duration = ms ?? Math.floor(Math.random() * 600) + 200
return new Promise(resolve => setTimeout(resolve, duration))
}
}
```
**错误场景测试入口**:在开发环境的查询面板中提供"模拟错误"下拉框,可切换空列表/403/超时/500场景。
### 5.2 组件使用标准
**[强制]** 界面组件严格按功能清单"组件规范"表格选用Element Plus组件
| 功能清单组件规范 | Element Plus组件 | 说明 |
|----------------|-----------------|------|
| 文本输入 | `el-input` | — |
| 下拉单选 | `el-select` | — |
| 下拉多选 | `el-select multiple` | — |
| 日期选择 | `el-date-picker` | — |
| 日期范围 | `el-date-picker type="daterange"` | — |
| 级联选择 | `el-cascader` | 区域/组织树 |
| 表格 | `el-table` | 列表数据展示 |
| 分页 | `el-pagination` | — |
| 对话框 | `el-dialog` | 弹窗表单 |
| 抽屉 | `el-drawer` | 详情侧滑 |
| 确认弹窗 | `ElMessageBox.confirm` | H3操作确认 |
| 消息提示 | `ElMessage.success/error` | H8结果反馈 |
| 加载 | `ElLoading.service` | H2加载反馈 |
| 标签 | `el-tag` | 状态/类型标记 |
| 树控件 | `el-tree` | 组织架构/区域树 |
| 文件上传 | `el-upload` | H7文件约束 |
### 5.3 交互实现标准
#### 5.3.1 列表页交互
| 交互 | 实现要求 |
|------|----------|
| 查询 | 点击查询按钮 → 触发查询 → 列表刷新到第1页 |
| 重置 | 点击重置 → 清空所有筛选条件 → 重新查询 |
| 分页 | 切换页码/每页条数 → 刷新列表 → 保留筛选条件 |
| 排序 | 点击列头排序 → 触发查询 → 保留筛选条件 |
| 新增 | 点击新增 → 打开弹窗/跳转新增页 → 提交后刷新列表 |
| 编辑 | 点击编辑/行内编辑 → 打开弹窗回填数据 → 提交后刷新列表 |
| 删除 | 点击删除 → H3确认弹窗 → 确认后删除 → H8结果反馈 → 刷新列表 |
| 批量操作 | 勾选行 → 按钮启用 → 点击 → H3确认 → 执行 → H8反馈 |
| 导出 | 点击导出 → H3确认 → 调用导出API → 下载文件 → H8反馈 |
#### 5.3.2 表单页交互
| 交互 | 实现要求 |
|------|----------|
| 表单校验 | 提交前 `el-form.validate()` — 全部通过才提交 |
| 取消/返回 | H4脏数据检测 — 有修改弹确认框,无修改直接返回 |
| 保存 | H1防重复 → 提交 → H8反馈 → 成功后跳转/关闭 |
| 字段联动 | 上级字段变更 → 清空下级字段 → 重新加载选项 |
#### 5.3.3 页面跳转与数据传递
**[强制]** 页面间数据传递遵循以下规则:
| 场景 | 传递方式 | 实现要求 |
|------|----------|----------|
| 列表→详情 | 路由参数 `/:id` | 详情页通过ID重新查询完整数据 |
| 列表→编辑 | 路由参数 `/:id/edit` | 编辑页通过ID查询数据并回填 |
| 新增→列表 | 路由跳转 | 新增成功后跳转列表并刷新 |
| 弹窗编辑 | 组件Props | 传入行数据,弹窗内回填 |
**[强制]** 编辑页必须回填选中行的完整数据:
```typescript
// views/property/repair/OrderEdit.vue
const route = useRoute()
const router = useRouter()
onMounted(async () => {
const id = route.params.id as string
if (id) {
// 通过ID查询完整数据回填表单
const detail = await repairApi.getOrderDetail(id)
Object.assign(formData, detail)
// H4: 保存初始快照
initialSnapshot = deepClone(formData)
}
})
```
---
## 六、H1-H8硬性约束实现标准
### 6.1 H1 防重复请求 [强制]
```typescript
// utils/debounce-submit.ts
interface PendingRequest {
key: string
timestamp: number
}
const pendingRequests = new Map<string, PendingRequest>()
/** 防重复提交装饰器 */
export function useDebounceSubmit() {
const submitLoading = ref(false)
async function debounceSubmit<T>(
key: string,
fn: () => Promise<T>,
options?: { cooldown?: number }
): Promise<T | null> {
if (submitLoading.value) return null
const pending = pendingRequests.get(key)
const cooldown = options?.cooldown ?? 1000
if (pending && Date.now() - pending.timestamp < cooldown) {
return null
}
submitLoading.value = true
pendingRequests.set(key, { key, timestamp: Date.now() })
try {
return await fn()
} finally {
submitLoading.value = false
pendingRequests.delete(key)
}
}
return { submitLoading, debounceSubmit }
}
```
**实现要求**
| 要求 | 实现 |
|------|------|
| pending去重 | Map结构记录进行中的请求key |
| 按钮disabled | `submitLoading` 绑定按钮 `:disabled` |
| loading态 | `submitLoading` 绑定按钮 `:loading` |
| abort重发 | 列表查询使用 `AbortController`新查询abort上一个 |
### 6.2 H2 超时与加载反馈 [强制]
```typescript
// utils/request.ts 中的超时配置
const service = axios.create({
timeout: 15000, // 默认GET 15s
})
// 按请求类型动态超时
service.interceptors.request.use(config => {
if (config.method === 'post' || config.method === 'put') {
config.timeout = 30000 // POST/PUT 30s
}
if (config.url?.includes('/upload')) {
config.timeout = 60000 // 上传 60s
}
if (config.url?.includes('/statistics') || config.url?.includes('/report')) {
config.timeout = 30000 // 统计报表 30s
}
return config
})
```
**实现要求**
| 要求 | 实现 |
|------|------|
| GET 15s | Axios默认timeout |
| POST 30s | 请求拦截器动态设置 |
| 上传导出 60s | URL匹配设置 |
| 统计 30s | URL匹配设置 |
| 超时提示 | 响应拦截器捕获ECONNABORTED |
| 加载>2秒 | 全局loading`ElLoading.service` |
### 6.3 H3 操作确认机制 [强制]
```typescript
// 通用确认弹窗
import { ElMessageBox } from 'element-plus'
/** 操作确认 — 具体文案由功能清单页面级H3定义 */
export async function confirmAction(message: string, title = '操作确认'): Promise<boolean> {
try {
await ElMessageBox.confirm(message, title, {
confirmButtonText: '确认',
cancelButtonText: '取消',
type: 'warning',
})
return true
} catch {
return false
}
}
// 使用示例(具体文案来自功能清单)
async function handleDelete(row: RepairOrderItem) {
const confirmed = await confirmAction(
`确认删除工单 ${row.orderNo}?删除后不可恢复。`
)
if (!confirmed) return
// ... 执行删除
}
```
**实现要求**
- 不可逆操作(删除、关闭、终止、批量操作)必须二次确认
- 确认弹窗文案在功能清单页面级H3中定义
- 小程序端使用 `wx.showModal` 替代 `ElMessageBox.confirm`
### 6.4 H4 脏数据检测 [强制]
```typescript
// utils/dirty-check.ts
import { ref, watch } from 'vue'
import { ElMessageBox } from 'element-plus'
import type { Router } from 'vue-router'
/** 脏数据检测 */
export function useDirtyCheck<T extends Record<string, any>>(
formData: Ref<T>,
router: Router
) {
const initialSnapshot = ref<string>('')
const isDirty = ref(false)
// 保存初始快照
function saveSnapshot() {
initialSnapshot.value = JSON.stringify(formData.value)
}
// 监听变化
watch(formData, () => {
isDirty.value = JSON.stringify(formData.value) !== initialSnapshot.value
}, { deep: true })
// 路由离开拦截
router.beforeEach(async (to, from, next) => {
if (isDirty.value && from.name?.toString().includes('Edit')) {
try {
await ElMessageBox.confirm('当前页面有未保存的修改,确认离开?', '提示', {
confirmButtonText: '离开',
cancelButtonText: '留在此页',
type: 'warning',
})
next()
} catch {
next(false)
}
} else {
next()
}
})
// 浏览器关闭拦截
onMounted(() => {
window.addEventListener('beforeunload', handleBeforeUnload)
})
onUnmounted(() => {
window.removeEventListener('beforeunload', handleBeforeUnload)
})
function handleBeforeUnload(e: BeforeUnloadEvent) {
if (isDirty.value) {
e.preventDefault()
e.returnValue = ''
}
}
return { isDirty, saveSnapshot }
}
```
**实现要求**
| 要求 | 实现 |
|------|------|
| deep clone快照 | `JSON.stringify` 对比 |
| isDirty检测 | `watch` deep监听 |
| 取消拦截 | 弹确认框 |
| 离开拦截 | `router.beforeEach` + `beforeunload` |
| 仅编辑型表单 | 新增页不检测 |
### 6.5 H5 数据权限隔离 [建议]
```typescript
// 403错误处理 — 在Axios响应拦截器中
service.interceptors.response.use(
response => response,
error => {
if (error.response?.data?.code === 40300) {
ElMessage.error('您没有权限访问该数据')
} else if (error.response?.data?.code === 40301) {
// 数据权限越权 — 显示"暂无数据"而非"无权限"
// 由页面组件根据上下文判断显示"暂无数据"
}
return Promise.reject(error)
}
)
```
### 6.6 H6 批量操作限制 [建议]
| 操作 | 上限 | 超限提示 |
|------|------|----------|
| 批量删除 | 50条 | "单次最多删除50条请分批操作" |
| 批量导出 | 500条 | "导出数据量较大,请缩小查询范围" |
| 批量审批 | 100条 | "单次最多审批100条" |
### 6.7 H7 文件上传约束 [建议]
```typescript
// 文件上传前校验
export function validateFileUpload(file: File): string | null {
const MAX_SIZE = 10 * 1024 * 1024 // 10MB
const MAX_COUNT = 9
const ALLOWED_TYPES = [
'image/jpeg', 'image/png', 'image/gif', 'image/webp',
'application/pdf',
'application/msword', 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',
'application/vnd.ms-excel', 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
]
if (file.size > MAX_SIZE) return '文件大小不能超过10MB'
if (!ALLOWED_TYPES.includes(file.type)) return '不支持该文件类型'
return null
}
```
### 6.8 H8 操作结果反馈 [强制]
```typescript
// utils/result-feedback.ts
import { ElMessage } from 'element-plus'
/** 操作成功反馈 */
export function showSuccess(message = '操作成功', duration = 2000) {
ElMessage.success({ message, duration })
}
/** 操作失败反馈 */
export function showError(message = '操作失败', duration = 0) {
ElMessage.error({ message, duration }) // duration=0 需手动关闭
}
/** 网络异常反馈 */
export function showNetworkError(retryCallback?: () => void) {
ElMessage.error({
message: '网络异常,请检查网络后重试',
duration: 0,
showClose: true,
})
}
```
**实现要求**
| 场景 | 反馈方式 | 持续时间 |
|------|----------|----------|
| 操作成功 | `ElMessage.success` + silent刷新列表 | 2秒自动关闭 |
| 操作失败 | `ElMessage.error` | 0手动关闭 |
| 网络异常 | `ElMessage.error` + 重试按钮 | 0手动关闭 |
| 小程序成功 | `wx.showToast({ icon: 'success' })` | 2秒 |
| 小程序失败 | `wx.showToast({ icon: 'none' })` | 0手动关闭 |
---
## 七、共享组件管理规范
### 7.1 共享组件定义
**共享组件**被2个及以上功能模块或2个及以上后台引用的组件。
### 7.2 共享组件标记 [强制]
**[强制]** 所有共享组件必须在文件头部标记 `@shared` 注释,并在 `components/shared/registry.ts` 中注册:
```typescript
// components/shared/registry.ts
/**
* 共享组件注册表
* 修改任何共享组件前,必须查阅此注册表确认影响范围
* 修改后必须执行100%覆盖测试,并向用户发出变更通知
*/
export const SharedComponentRegistry = {
Breadcrumb: {
path: 'components/shared/Breadcrumb/index.vue',
description: '面包屑导航',
referencedBy: ['所有页面'] as string[],
},
QueryPanel: {
path: 'components/shared/QueryPanel/index.vue',
description: '查询条件面板(折叠/展开)',
referencedBy: ['repair:OrderList', 'inspection:TaskList', 'cleaning:AreaList', 'attendance:RecordList'] as string[],
},
ActionBar: {
path: 'components/shared/ActionBar/index.vue',
description: '操作按钮栏(权限控制+批量操作)',
referencedBy: ['repair:OrderList', 'inspection:TaskList', 'org:StaffList', 'contract:ContractList'] as string[],
},
DataTable: {
path: 'components/shared/DataTable/index.vue',
description: '数据表格(排序/选择/行操作)',
referencedBy: ['所有列表页'] as string[],
},
Pagination: {
path: 'components/shared/Pagination/index.vue',
description: '分页组件',
referencedBy: ['所有列表页'] as string[],
},
FormDialog: {
path: 'components/shared/FormDialog/index.vue',
description: '表单弹窗(新增/编辑)',
referencedBy: ['repair:OrderList', 'inspection:PlanList', 'cleaning:AreaList', 'org:TeamList'] as string[],
},
DetailDrawer: {
path: 'components/shared/DetailDrawer/index.vue',
description: '详情侧滑抽屉',
referencedBy: ['repair:OrderList', 'inspection:TaskList', 'contract:ContractList'] as string[],
},
StatusTag: {
path: 'components/shared/StatusTag/index.vue',
description: '状态标签(彩色区分)',
referencedBy: ['repair:OrderList', 'inspection:TaskList', 'cleaning:TaskBoard', 'contract:ContractList'] as string[],
},
} as const
/** 获取组件被哪些模块引用 */
export function getComponentReferences(componentName: string): string[] {
return SharedComponentRegistry[componentName]?.referencedBy ?? []
}
```
### 7.3 共享组件修改流程 [强制]
**[强制]** 修改共享组件必须执行以下流程:
```
识别修改涉及共享组件
查询 registry.ts 获取引用模块列表
制定修改方案(必须向后兼容或同步更新所有引用方)
执行修改
执行100%覆盖测试(所有引用模块)
向用户发出变更通知
```
### 7.4 共享组件100%覆盖测试 [强制]
**[强制]** 修改共享组件后,必须对所有引用模块执行回归测试:
| 测试项 | 验证方法 |
|--------|----------|
| 功能正常 | 每个引用模块的核心功能正常运行 |
| 样式一致 | 每个引用模块的组件显示效果与修改前一致 |
| 交互完整 | 每个引用模块的组件交互行为正常 |
| 无副作用 | 未直接引用该组件的模块不受影响 |
| 类型安全 | TypeScript类型检查通过 |
### 7.5 共享组件变更通知 [强制]
**[强制]** 当修改涉及共享组件时开发者含AI助手自测通过后必须向用户输出以下**变更影响报告**
```markdown
## 共享组件变更通知
### 变更内容
- 修改的共享组件:{组件名}
- 修改内容:{具体改了什么}
- 修改原因:{为什么改}
### 影响范围
- 引用该组件的功能模块:
- {模块1}{页面1}、{页面2}
- {模块2}{页面3}
- 受影响的后台:{超级管理员/物业公司/医院}
### 自测结果
- ✅ {模块1}-{页面1}:功能正常
- ✅ {模块1}-{页面2}:功能正常
- ✅ {模块2}-{页面3}:功能正常
### 需要人工测试
请重点验证以上引用模块的显示效果和交互行为。
```
### 7.6 共享组件修改原则 [强制]
| 原则 | 说明 |
|------|------|
| **[强制] 向后兼容优先** | 新增Props/Slot不删除已有Props/Slot |
| **[强制] 废弃标记** | 如需废弃Props先用`@deprecated`标记至少保留1个版本周期 |
| **[强制] 禁止破坏性修改** | 不得修改已有Props的默认值、类型、语义 |
| **[建议] 版本化** | 重大变更时创建V2组件如`FormDialogV2`),并行存在 |
---
## 八、跨后台风格统一规范
### 8.1 主题变量统一
```scss
// styles/variables.scss — Element Plus主题变量覆盖
// 品牌色
$--color-primary: #409EFF;
$--color-success: #67C23A;
$--color-warning: #E6A23C;
$--color-danger: #F56C6C;
$--color-info: #909399;
// 布局
$--layout-sidebar-width: 220px;
$--layout-sidebar-collapsed-width: 64px;
$--layout-header-height: 56px;
$--layout-tags-height: 36px;
$--layout-footer-height: 48px;
// 字体
$--font-size-base: 14px;
$--font-size-small: 12px;
$--font-size-large: 16px;
// 间距
$--spacing-page: 20px;
$--spacing-section: 16px;
$--spacing-item: 12px;
```
### 8.2 布局结构统一
**[强制]** 三个Web后台使用完全一致的布局框架
| 区域 | 规范 | 实现 |
|------|------|------|
| 侧边栏 | 固定宽度220px可折叠至64px | `AdminLayout` + `el-menu` |
| 顶栏 | 固定高度56px含Logo/面包屑/用户信息 | `LayoutHeader` |
| 标签页 | 固定高度36px支持右键关闭 | `TagsView` |
| 内容区 | 自适应宽度内边距20px | `<router-view>` |
| 全局Loading | 固定在内容区中央 | `ElLoading.service` |
### 8.3 公共组件统一
**[强制]** 以下公共组件跨后台必须使用共享组件,不得各后台自行实现:
| 组件 | 用途 | 规范 |
|------|------|------|
| `Breadcrumb` | 面包屑导航 | 自动从路由meta生成 |
| `QueryPanel` | 查询条件面板 | 支持折叠/展开默认收起超过3行 |
| `ActionBar` | 操作按钮栏 | 权限控制 + 批量操作启用/禁用 |
| `DataTable` | 数据表格 | 统一空数据文案、行高、斑马纹 |
| `Pagination` | 分页 | 统一布局:总数 + 每页条数 + 页码 |
| `FormDialog` | 表单弹窗 | 统一宽度/确认取消按钮/H4脏数据检测 |
| `DetailDrawer` | 详情侧滑 | 统一宽度/关闭确认 |
| `StatusTag` | 状态标签 | 统一颜色方案:待处理=warning/进行中=primary/完成=success/关闭=info |
---
## 九、自测检查清单
### 9.1 功能清单符合度
| # | 检查项 | 验证方法 | 通过标准 |
|---|--------|----------|----------|
| 1 | 界面布局与功能清单ASCII图一致 | 人工对比 | 布局区域、排列顺序一致 |
| 2 | 查询条件字段完整 | 逐一核对 | 字段名、控件类型、必填性一致 |
| 3 | 列表字段完整 | 逐一核对 | 列名、列宽、排序、说明一致 |
| 4 | 操作按钮完整 | 逐一核对 | 按钮名、权限编码、显示条件一致 |
| 5 | 弹窗定义完整 | 逐一核对 | 弹窗名称、字段、校验规则一致 |
| 6 | 页面路径正确 | 浏览器访问 | 路由可正常跳转 |
| 7 | 权限编码正确 | 模拟不同权限 | 按钮显示/隐藏符合预期 |
### 9.2 交互完整性
| # | 检查项 | 验证方法 | 通过标准 |
|---|--------|----------|----------|
| 1 | 查询/重置功能 | 输入条件→查询→重置 | 列表数据正确筛选和重置 |
| 2 | 分页功能 | 翻页→改每页条数 | 数据正确切换 |
| 3 | 排序功能 | 点击列头 | 排序方向和数据正确 |
| 4 | 新增功能 | 点击新增→填写→提交 | 数据添加成功,列表刷新 |
| 5 | 编辑功能 | 点击编辑→修改→提交 | 数据更新成功,列表刷新 |
| 6 | 删除功能 | 点击删除→确认 | 数据删除成功H3确认弹窗出现 |
| 7 | 批量操作 | 勾选→批量操作 | 操作成功,未勾选时按钮禁用 |
| 8 | 页面跳转 | 列表→详情→返回 | 路由正常,数据正确 |
| 9 | 编辑回填 | 列表→编辑 | 选中行数据完整回填到表单 |
| 10 | 表单校验 | 提交空表单 | 必填项校验提示正确 |
### 9.3 H约束合规性
| # | 检查项 | 验证方法 | 通过标准 |
|---|--------|----------|----------|
| 1 | H1防重复 | 快速双击提交按钮 | 第二次点击无效按钮loading态 |
| 2 | H2超时 | 触发超时Mock | 超时提示出现,按钮恢复可点击 |
| 3 | H2加载反馈 | 查询大量数据 | 加载>2秒时全局loading显示 |
| 4 | H3操作确认 | 点击删除/批量操作 | 确认弹窗出现,文案与功能清单一致 |
| 5 | H4脏数据 | 编辑表单→不保存→返回 | 确认弹窗出现"有未保存修改" |
| 6 | H4浏览器关闭 | 编辑表单→关闭浏览器 | 浏览器提示"有未保存修改" |
| 7 | H5权限隔离 | 无权限账号访问 | 显示"暂无数据"而非报错 |
| 8 | H6批量限制 | 勾选超过限制数量 | 提示"单次最多N条" |
| 9 | H7上传约束 | 上传超限文件 | 提示大小/类型/数量限制 |
| 10 | H8成功反馈 | 提交成功 | 成功提示2秒自动消失列表刷新 |
| 11 | H8失败反馈 | 模拟500错误 | 错误提示需手动关闭 |
| 12 | H8网络异常 | 断开网络操作 | 网络异常提示 + 重试 |
### 9.4 组件规范合规性
| # | 检查项 | 验证方法 | 通过标准 |
|---|--------|----------|----------|
| 1 | 使用Element Plus指定组件 | 代码审查 | 无自行实现的表格/分页/弹窗等 |
| 2 | `<style scoped>` | 代码审查 | 所有组件使用scoped样式 |
| 3 | TypeScript类型完整 | `vue-tsc --noEmit` | 无类型错误 |
| 4 | 共享组件标记 | 代码审查 | 所有共享组件有`@shared`注释 |
| 5 | 组件粒度 | 代码审查 | 单组件≤300行 |
### 9.5 Mock数据合规性
| # | 检查项 | 验证方法 | 通过标准 |
|---|--------|----------|----------|
| 1 | 数据量≥20条 | 查看Mock数据文件 | 列表页分页正常 |
| 2 | 数据逼真 | 人工审查 | 姓名/地址/时间/状态合理 |
| 3 | 边界值覆盖 | 切换Mock场景 | 空列表/长文本/特殊字符可展示 |
| 4 | 错误场景 | 触发错误Mock | 403/超时/500有正确反馈 |
### 9.6 浏览器渲染验证(**[强制]**
> **新增原因**`vite build` 编译通过 ≠ 浏览器运行无报错。以下问题编译器无法检测自定义指令未注册、组件Props接口不匹配、运行时样式异常等。必须启动 dev server 进行可视化验证。
| # | 检查项 | 验证方法 | 通过标准 |
|---|--------|----------|----------|
| 1 | **控制台零报错** | 打开浏览器F12 → Console面板 | 无红色Error、无黄色WarningVue警告也算不通过 |
| 2 | **自定义指令生效** | 使用无权限账号登录或临时移除权限 | `v-hasPermission` 指令对应的元素正确隐藏/显示 |
| 3 | **下拉框宽度正常** | 查看所有含el-select的查询面板 | 下拉框文字完整显示,不被截断 |
| 4 | **分页组件可见且可用** | 列表页底部 | 分页器显示total条数翻页功能正常 |
| 5 | **表格布局合理** | 1920px及以上宽屏查看列表页 | 表格列铺满容器,右侧无大面积空白 |
| 6 | **所有页面路由可达** | 点击侧边栏逐个导航 | 所有页面正常加载,无白屏/404 |
| 7 | **图标正常渲染** | 查看所有含图标的区域(空状态、按钮图标等) | 图标正确显示,非方块/乱码 |
### 9.7 常见运行时问题预防规则
> 以下规则是从实际Bug中总结的**硬性编码规范**,违反即导致运行时错误。
#### 规则-1自定义指令必须注册后使用
```typescript
// ❌ 错误:定义了指令但忘记在 main.ts 注册
// utils/permission.ts 中只有函数 export无 directive 定义
// ✅ 正确:
// 步骤1: 在 directives/hasPermission.ts 中定义指令
// 步骤2: 在 main.ts 中 app.directive('hasPermission', { ... })
// 步骤3: 页面中 v-hasPermission="'xxx'" 即可使用
```
**[强制]** 禁止在模板中使用未在 main.ts 全局注册的自定义指令。
#### 规则-2共享组件 Props 接口必须与调用方式一致
```typescript
// ❌ 错误Pagination 组件期望独立 props total/page/pageSize
// 但页面传入 <Pagination :pagination="paginationObj" />
// 结果total 为 undefined → 分页不显示
// ✅ 正确:两种方式选一,并在组件注释中标明
// 方式A(对象模式): defineProps<{ pagination: { page, pageSize, total } }>
// 方式B(独立模式): defineProps<{ total, page, pageSize }>
// 并在组件文件顶部用注释标注推荐用法
```
**[强制]** 共享组件必须在使用它的第一个页面之前完成 Props 接口定义。
#### 规则-3QueryPanel 内表单控件必须有最小宽度约束
```scss
// ❌ 错误el-form inline 模式下 el-select 无宽度约束
// 导致下拉框收缩到最小宽度,选项文字被截断
// ✅ 正确QueryPanel 组件内部统一设置
:deep(.el-select) { min-width: 160px; }
:deep(.el-input) { min-width: 160px; }
```
**[强制]** QueryPanel 组件必须在 scoped style 中通过 `:deep()` 为 el-select / el-input / el-date-picker 设置 min-width。
#### 规则-4DataTable 至少有一列使用自适应宽度
```vue
<!-- 错误所有列都用固定 width宽屏下右侧大片空白 -->
<el-table-column prop="name" width="180" />
<el-table-column prop="status" width="80" />
<!-- 总宽仅260px1920屏下剩余1660px空白 -->
<!-- 正确关键列使用 min-width 而非 width自动填充剩余空间 -->
<el-table-column prop="name" min-width="180" /> <!-- 自适应 -->
<el-table-column prop="status" width="80" /> <!-- 固定 -->
<!-- name列会自动扩展填充剩余空间 -->
```
**[建议]** 列表中"主字段"(如名称/标题)使用 `min-width` 而非固定 `width`;操作列、状态列等短内容使用固定 `width`
#### 规则-5组件内使用的图标必须显式导入或确认全局注册
```typescript
// ❌ 错误:<DocumentDelete /> 在模板中使用但 script 未导入
// 如果全局图标注册被移除,此图标立即失效
// ✅ 正确:显式导入
import { DocumentDelete } from '@element-plus/icons-vue'
```
**[强制]** 共享组件中使用的 Element Plus 图标必须显式 import禁止隐式依赖全局注册。
---
## 十、真实API界面迁移规范
### 10.1 渐进式迁移策略
**[强制]** 支持逐模块从Mock迁移到Real无需一次性全部切换
```bash
# 全局Mock
VITE_API_MODE=mock
# 全局Real
VITE_API_MODE=real
# 渐进式报修用Real其余Mock
VITE_API_MODE=mock:repair=real
# 渐进式报修和巡检用Real其余Mock
VITE_API_MODE=mock:repair=real,inspection=real
# 渐进式仅保洁用Mock其余Real收尾阶段
VITE_API_MODE=real:cleaning=mock
```
### 10.2 Mock→Real切换步骤
```
1. 确认后端接口已开发完成并可访问
2. 创建 RealAdapterapi/real/adapters/{module}.ts
3. 修改环境变量将该模块切换为real
4. 验证TypeScript类型与接口响应格式一致
5. 逐个接口联调:
a. 对比Mock数据结构 vs 接口响应结构
b. 修正字段映射差异
c. 验证分页/排序/筛选参数
d. 验证错误码处理
6. 联调通过后删除该模块Mock数据可选保留用于回归测试
```
### 10.3 数据格式对齐要求
| 对齐项 | 要求 |
|--------|------|
| 响应结构 | 遵循 `05-接口规范.md` 统一响应格式 `{ code, message, data, timestamp }` |
| 分页结构 | `{ list, pagination: { page, pageSize, total, totalPages } }` |
| 错误码 | 遵循 `05-接口规范.md` 错误码规范 |
| 日期格式 | ISO 8601`2026-04-17T10:30:00` |
| 空值处理 | 后端返回 `null` → 前端显示 `—` |
### 10.4 联调问题处理
| 问题类型 | 处理方式 |
|----------|----------|
| 字段名不一致 | RealAdapter中做映射记录差异清单 |
| 数据类型不一致 | RealAdapter中做类型转换 |
| 接口缺失 | 临时回退Mock记录接口待开发清单 |
| 错误码新增 | 更新前端错误处理逻辑 |
| 分页参数不同 | RealAdapter中做参数转换 |
---
## 十一、页面模板脚手架
### 11.1 标准列表页模板
```vue
<!-- 标准列表页骨架 适用于90%的列表页 -->
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { useDebounceSubmit } from '@/utils/debounce-submit'
import { useDirtyCheck } from '@/utils/dirty-check'
import { confirmAction, showSuccess, showError } from '@/utils/result-feedback'
import QueryPanel from '@/components/shared/QueryPanel/index.vue'
import ActionBar from '@/components/shared/ActionBar/index.vue'
import DataTable from '@/components/shared/DataTable/index.vue'
import Pagination from '@/components/shared/Pagination/index.vue'
// ====== 类型定义(根据功能清单替换) ======
interface QueryParams {
keyword?: string
status?: string[]
page: number
pageSize: number
}
interface TableItem {
id: string
// ... 根据功能清单"列表字段"补充
}
// ====== API适配器模式 ======
// import { getXxxApi } from '@/api'
// const xxxApi = await getXxxApi()
// ====== 查询条件 ======
const queryParams = reactive<QueryParams>({
page: 1,
pageSize: 20,
})
const queryLoading = ref(false)
const showAdvancedQuery = ref(false)
// ====== 列表数据 ======
const tableData = ref<TableItem[]>([])
const total = ref(0)
const selectedRows = ref<TableItem[]>([])
// ====== H1防重复 ======
const { submitLoading, debounceSubmit } = useDebounceSubmit()
// ====== 查询方法 ======
async function fetchList() {
queryLoading.value = true
try {
// const res = await xxxApi.getList(queryParams)
// tableData.value = res.list
// total.value = res.pagination.total
} catch (error) {
showError('查询失败')
} finally {
queryLoading.value = false
}
}
function handleQuery() {
queryParams.page = 1
fetchList()
}
function handleReset() {
Object.assign(queryParams, { keyword: undefined, status: undefined, page: 1, pageSize: 20 })
fetchList()
}
// ====== 分页 ======
function handlePageChange(page: number) {
queryParams.page = page
fetchList()
}
function handleSizeChange(size: number) {
queryParams.pageSize = size
queryParams.page = 1
fetchList()
}
// ====== 操作 ======
async function handleCreate() {
// 新增逻辑
}
async function handleEdit(row: TableItem) {
router.push({ name: 'XxxEdit', params: { id: row.id } })
}
async function handleDelete(row: TableItem) {
const confirmed = await confirmAction(`确认删除该条数据?删除后不可恢复。`)
if (!confirmed) return
await debounceSubmit('delete', async () => {
try {
// await xxxApi.delete(row.id)
showSuccess('删除成功')
fetchList()
} catch (error) {
showError('删除失败')
}
})
}
// ====== 初始化 ======
onMounted(() => {
fetchList()
})
</script>
<template>
<div class="page-container">
<!-- 查询条件 -->
<QueryPanel v-model:advanced="showAdvancedQuery" @query="handleQuery" @reset="handleReset">
<!-- 根据功能清单"查询条件"表格添加表单项 -->
</QueryPanel>
<!-- 操作栏 -->
<ActionBar>
<!-- 根据功能清单"操作按钮"表格添加按钮 -->
</ActionBar>
<!-- 数据表格 -->
<DataTable
v-model:selected="selectedRows"
:data="tableData"
:loading="queryLoading"
>
<!-- 根据功能清单"列表字段"表格添加列 -->
</DataTable>
<!-- 分页 -->
<Pagination
:total="total"
v-model:page="queryParams.page"
v-model:page-size="queryParams.pageSize"
@change="fetchList"
/>
</div>
</template>
<style scoped>
.page-container {
padding: var(--spacing-page);
}
</style>
```
### 11.2 标准表单弹窗模板
```vue
<!-- 标准表单弹窗骨架 -->
<script setup lang="ts">
import { ref, reactive, watch } from 'vue'
import type { FormInstance, FormRules } from 'element-plus'
import { useDebounceSubmit } from '@/utils/debounce-submit'
import { showSuccess, showError } from '@/utils/result-feedback'
import FormDialog from '@/components/shared/FormDialog/index.vue'
const props = defineProps<{
visible: boolean
editData?: Record<string, any>
}>()
const emit = defineEmits<{
'update:visible': [value: boolean]
success: []
}>()
// ====== 表单数据 ======
const formRef = ref<FormInstance>()
const formData = reactive({
// ... 根据功能清单"弹窗定义"补充字段
})
const formRules: FormRules = {
// ... 根据功能清单"校验规则"补充
}
// ====== H4脏数据检测 ======
let initialSnapshot = ''
const isDirty = ref(false)
watch(() => props.visible, (val) => {
if (val) {
if (props.editData) {
Object.assign(formData, props.editData)
} else {
// 重置表单
}
initialSnapshot = JSON.stringify(formData)
isDirty.value = false
}
})
watch(formData, () => {
isDirty.value = JSON.stringify(formData) !== initialSnapshot
}, { deep: true })
// ====== H1防重复 ======
const { submitLoading, debounceSubmit } = useDebounceSubmit()
// ====== 提交 ======
async function handleSubmit() {
const valid = await formRef.value?.validate().catch(() => false)
if (!valid) return
await debounceSubmit('form-submit', async () => {
try {
if (props.editData) {
// await xxxApi.update(props.editData.id, formData)
showSuccess('修改成功')
} else {
// await xxxApi.create(formData)
showSuccess('新增成功')
}
emit('success')
handleClose()
} catch (error) {
showError('操作失败')
}
})
}
function handleClose() {
emit('update:visible', false)
}
</script>
<template>
<FormDialog
:visible="visible"
:title="editData ? '编辑' : '新增'"
:loading="submitLoading"
:dirty="isDirty"
@close="handleClose"
@submit="handleSubmit"
>
<el-form ref="formRef" :model="formData" :rules="formRules" label-width="100px">
<!-- 根据功能清单"弹窗定义"补充表单项 -->
</el-form>
</FormDialog>
</template>
```
---
## 十二、接口文档对齐机制
### 12.1 前后端类型对齐流程
```
功能清单确定字段
前端定义TypeScript类型api/types/
├──────────────────────┐
▼ ▼
前端MockAdapter 后端定义DTO/VO
基于TS类型生成Mock数据 基于Swagger生成接口文档
│ │
▼ ▼
静态界面开发 后端接口开发
│ │
└──────────┬───────────┘
联调阶段
对比TS类型 vs Swagger Schema
┌──────┴──────┐
▼ 一致 ▼ 不一致
直接联调 协商修改 → 更新类型 → 重新联调
```
### 12.2 对齐要求
| 对齐项 | 要求 |
|--------|------|
| 字段名 | 前端TS类型的字段名必须与后端DTO/VO的JSON字段名一致 |
| 数据类型 | TS类型与后端Java类型对应String↔string, Long↔number, List↔Array, Enum↔union type |
| 分页结构 | 统一使用 `{ list, pagination }` 格式 |
| 枚举值 | 前端枚举值必须与后端枚举的name()一致 |
| 必填性 | TS类型可选字段与后端@Nullable对应 |
### 12.3 类型同步工具(建议)
- **方式一**后端Swagger → `openapi-typescript` 自动生成前端TS类型
- **方式二**前端TS类型 → 手动维护联调时与Swagger文档对照
- **推荐**:联调初期用手动维护(灵活),稳定后切换到自动生成(防漂移)
---
> **本文档为前端界面开发强制规范,所有开发人员必须严格遵守。如有疑问或需要调整,需经技术负责人审批。**