# 医院物业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 getOrderDetail(id: string): Promise createOrder(data: CreateRepairOrderRequest): Promise<{ id: string }> updateOrder(id: string, data: UpdateRepairOrderRequest): Promise deleteOrder(id: string): Promise } ``` #### 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 { 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 { 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() /** 防重复提交装饰器 */ export function useDebounceSubmit() { const submitLoading = ref(false) async function debounceSubmit( key: string, fn: () => Promise, options?: { cooldown?: number } ): Promise { 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 { 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>( formData: Ref, router: Router ) { const initialSnapshot = ref('') 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 | `` | | 全局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 | ` ``` ### 11.2 标准表单弹窗模板 ```vue ``` --- ## 十二、接口文档对齐机制 ### 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文档对照 - **推荐**:联调初期用手动维护(灵活),稳定后切换到自动生成(防漂移) --- > **本文档为前端界面开发强制规范,所有开发人员必须严格遵守。如有疑问或需要调整,需经技术负责人审批。**