55 KiB
医院物业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 页面描述结构
每个页面的功能清单描述必须包含以下章节(与现有格式一致):
## 页面N:{页面名称}
**页面编号**:{模块编码}-{页面序号}-P{序号}
**端侧归属**:Web专属 / 小程序专属 / 双端
**页面路径**:/{module}/{page}
### 界面布局
(ASCII布局图)
### 查询条件
(字段名、控件类型、必填、默认值、说明)
### 列表字段
(序号、字段名、列宽、支持排序、说明)
### 操作按钮
(按钮、权限编码、位置、显示条件、说明)
### 弹窗定义
(弹窗名称、触发条件、表单字段、校验规则)
### 交互流程要求
(步骤编号 + 描述 + H约束标记)
### 组件规范
(组件名称、使用场景、规范要求)
### 前端硬性约束
(H1~H8页面级具体实现,如确认弹窗文案等)
3.2 H约束补充原则
- 功能清单中的H1-H8约束是页面级具体实现(如确认弹窗的文案、超时的具体秒数)
- 通用H约束实现标准在本规范第六章定义
- 功能清单只写与该页面业务相关的差异化内容,不重复通用规则
3.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 配置:
# 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
适配器工厂:
// 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 路由组织规范
// 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类型定义,类型定义与功能清单中的列表字段、表单字段一一对应:
// 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必须内置以下错误场景触发机制:
// 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 | 传入行数据,弹窗内回填 |
[强制] 编辑页必须回填选中行的完整数据:
// 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 防重复请求 [强制]
// 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 超时与加载反馈 [强制]
// 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 操作确认机制 [强制]
// 通用确认弹窗
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 脏数据检测 [强制]
// 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 数据权限隔离 [建议]
// 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 文件上传约束 [建议]
// 文件上传前校验
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 操作结果反馈 [强制]
// 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 中注册:
// 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助手)自测通过后,必须向用户输出以下变更影响报告:
## 共享组件变更通知
### 变更内容
- 修改的共享组件:{组件名}
- 修改内容:{具体改了什么}
- 修改原因:{为什么改}
### 影响范围
- 引用该组件的功能模块:
- {模块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 主题变量统一
// 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、无黄色Warning(Vue警告也算不通过) |
| 2 | 自定义指令生效 | 使用无权限账号登录或临时移除权限 | v-hasPermission 指令对应的元素正确隐藏/显示 |
| 3 | 下拉框宽度正常 | 查看所有含el-select的查询面板 | 下拉框文字完整显示,不被截断 |
| 4 | 分页组件可见且可用 | 列表页底部 | 分页器显示total条数,翻页功能正常 |
| 5 | 表格布局合理 | 1920px及以上宽屏查看列表页 | 表格列铺满容器,右侧无大面积空白 |
| 6 | 所有页面路由可达 | 点击侧边栏逐个导航 | 所有页面正常加载,无白屏/404 |
| 7 | 图标正常渲染 | 查看所有含图标的区域(空状态、按钮图标等) | 图标正确显示,非方块/乱码 |
9.7 常见运行时问题预防规则
以下规则是从实际Bug中总结的硬性编码规范,违反即导致运行时错误。
规则-1:自定义指令必须注册后使用
// ❌ 错误:定义了指令但忘记在 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 接口必须与调用方式一致
// ❌ 错误:Pagination 组件期望独立 props total/page/pageSize
// 但页面传入 <Pagination :pagination="paginationObj" />
// 结果:total 为 undefined → 分页不显示
// ✅ 正确:两种方式选一,并在组件注释中标明
// 方式A(对象模式): defineProps<{ pagination: { page, pageSize, total } }>
// 方式B(独立模式): defineProps<{ total, page, pageSize }>
// 并在组件文件顶部用注释标注推荐用法
[强制] 共享组件必须在使用它的第一个页面之前完成 Props 接口定义。
规则-3:QueryPanel 内表单控件必须有最小宽度约束
// ❌ 错误: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。
规则-4:DataTable 至少有一列使用自适应宽度
<!-- ❌ 错误:所有列都用固定 width,宽屏下右侧大片空白 -->
<el-table-column prop="name" width="180" />
<el-table-column prop="status" width="80" />
<!-- 总宽仅260px,1920屏下剩余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:组件内使用的图标必须显式导入或确认全局注册
// ❌ 错误:<DocumentDelete /> 在模板中使用但 script 未导入
// 如果全局图标注册被移除,此图标立即失效
// ✅ 正确:显式导入
import { DocumentDelete } from '@element-plus/icons-vue'
[强制] 共享组件中使用的 Element Plus 图标必须显式 import,禁止隐式依赖全局注册。
十、真实API界面迁移规范
10.1 渐进式迁移策略
[强制] 支持逐模块从Mock迁移到Real,无需一次性全部切换:
# 全局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. 创建 RealAdapter(api/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 标准列表页模板
<!-- 标准列表页骨架 — 适用于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 标准表单弹窗模板
<!-- 标准表单弹窗骨架 -->
<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文档对照
- 推荐:联调初期用手动维护(灵活),稳定后切换到自动生成(防漂移)
本文档为前端界面开发强制规范,所有开发人员必须严格遵守。如有疑问或需要调整,需经技术负责人审批。