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

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 更新审批流程

  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 配置

# 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秒 全局loadingElLoading.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、无黄色WarningVue警告也算不通过
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 接口定义。

规则-3QueryPanel 内表单控件必须有最小宽度约束

// ❌ 错误el-form inline 模式下 el-select 无宽度约束
// 导致下拉框收缩到最小宽度,选项文字被截断

// ✅ 正确QueryPanel 组件内部统一设置
:deep(.el-select) { min-width: 160px; }
:deep(.el-input) { min-width: 160px; }

[强制] QueryPanel 组件必须在 scoped style 中通过 :deep() 为 el-select / el-input / el-date-picker 设置 min-width。

规则-4DataTable 至少有一列使用自适应宽度

<!--  错误所有列都用固定 width宽屏下右侧大片空白 -->
<el-table-column prop="name" width="180" />
<el-table-column prop="status" width="80" />
<!-- 总宽仅260px1920屏下剩余1660px空白 -->

<!--  正确关键列使用 min-width 而非 width自动填充剩余空间 -->
<el-table-column prop="name" min-width="180" />  <!-- 自适应 -->
<el-table-column prop="status" width="80" />     <!-- 固定 -->
<!-- name列会自动扩展填充剩余空间 -->

[建议] 列表中"主字段"(如名称/标题)使用 min-width 而非固定 width;操作列、状态列等短内容使用固定 width

规则-5组件内使用的图标必须显式导入或确认全局注册

// ❌ 错误:<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. 创建 RealAdapterapi/real/adapters/{module}.ts
3. 修改环境变量将该模块切换为real
4. 验证TypeScript类型与接口响应格式一致
5. 逐个接口联调:
   a. 对比Mock数据结构 vs 接口响应结构
   b. 修正字段映射差异
   c. 验证分页/排序/筛选参数
   d. 验证错误码处理
6. 联调通过后删除该模块Mock数据可选保留用于回归测试

10.3 数据格式对齐要求

对齐项 要求
响应结构 遵循 05-接口规范.md 统一响应格式 { code, message, data, timestamp }
分页结构 { list, pagination: { page, pageSize, total, totalPages } }
错误码 遵循 05-接口规范.md 错误码规范
日期格式 ISO 86012026-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文档对照
  • 推荐:联调初期用手动维护(灵活),稳定后切换到自动生成(防漂移)

本文档为前端界面开发强制规范,所有开发人员必须严格遵守。如有疑问或需要调整,需经技术负责人审批。