|
|
# 医院物业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、无黄色Warning(Vue警告也算不通过) |
|
|
|
| 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 接口定义。
|
|
|
|
|
|
#### 规则-3:QueryPanel 内表单控件必须有最小宽度约束
|
|
|
|
|
|
```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。
|
|
|
|
|
|
#### 规则-4:DataTable 至少有一列使用自适应宽度
|
|
|
|
|
|
```vue
|
|
|
<!-- ❌ 错误:所有列都用固定 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:组件内使用的图标必须显式导入或确认全局注册
|
|
|
|
|
|
```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. 创建 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 标准列表页模板
|
|
|
|
|
|
```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文档对照
|
|
|
- **推荐**:联调初期用手动维护(灵活),稳定后切换到自动生成(防漂移)
|
|
|
|
|
|
---
|
|
|
|
|
|
> **本文档为前端界面开发强制规范,所有开发人员必须严格遵守。如有疑问或需要调整,需经技术负责人审批。**
|