Files
his/MD/specs/FRONTEND_DEVELOPMENT_STANDARD.md
华佗 db5fb88627 docs(specs): 添加UI设计铁律法则 - 十大设计法则+医疗HIS专项规范
- 新增 MD/specs/UI_DESIGN_IRON_RULES.md (404行)
  - 十大UI设计铁律法则: 希克/费茨/米勒/雅各布/格式塔/多赫蒂/尼尔森/泰斯勒/峰终/冯雷斯托夫
  - HIS医疗系统专项UI规范: 色彩体系/间距系统/字体/表格/表单/弹窗/交互反馈
  - 医疗特殊交互: 危急值/医嘱/处方/费用/电子签名/打印
  - 设计文档必备模板: UI布局+交互清单+调用流程+状态流转+异常处理
  - 违反检查清单

- 更新铁律体系
  - RULES.md: 新增铁律14 - 设计文档必须包含UI设计和调用流程
  - MD/specs/IRON_RULES.md: 新增铁律#9详细说明
  - MD/specs/FRONTEND_DEVELOPMENT_STANDARD.md: 新增UI设计法则速查表

- 同步7个AI工具配置: AGENTS.md/.cursorrules/.copilot/.windsurf/.cline/.qwen/.aider
2026-06-06 11:12:02 +08:00

555 lines
14 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# HealthLink-HIS 前端开发规范
> **文档类型**: 技术规范
> **适用范围**: 前端 Vue3 开发
> **版本**: v1.0
> **编制日期**: 2026-06-06
> **最后更新**: 2026-06-06
---
## 一、技术栈
| 组件 | 版本 | 说明 |
|------|------|------|
| Vue | 3.x | 前端框架 |
| Vite | 5.x | 构建工具 |
| Element Plus | 2.x | UI组件库 |
| Pinia | 2.x | 状态管理 |
| Vue Router | 4.x | 路由管理 |
| Axios | 1.x | HTTP客户端 |
| RuoYi-Vue3 | 3.9.2+ | 基础框架 |
---
## 二、项目结构
```
healthlink-his-ui/
├── src/
│ ├── api/ # API接口定义
│ │ ├── module_name/ # 按模块分组
│ │ │ ├── index.js # 接口入口
│ │ │ └── *.js # 各接口文件
│ │ └── system/ # 系统管理接口
│ ├── views/ # 页面视图
│ │ └── module_name/ # 按模块分组
│ │ └── index.vue # 页面组件
│ ├── components/ # 公共组件
│ ├── store/ # Pinia状态管理
│ │ ├── modules/ # 模块store
│ │ └── store.js # store入口
│ ├── router/ # 路由配置
│ ├── utils/ # 工具函数
│ ├── directive/ # 自定义指令
│ ├── plugins/ # 插件
│ ├── layout/ # 布局组件
│ └── assets/ # 静态资源
├── vite.config.js # Vite配置
├── package.json # 依赖配置
└── .env.dev # 开发环境变量
```
---
## 三、命名规范
### 3.1 文件命名
| 类型 | 命名规则 | 示例 |
|------|---------|------|
| 页面组件 | `index.vue` | `views/registration/index.vue` |
| 弹窗组件 | `XxxDialog.vue` | `PatientDialog.vue` |
| 子组件 | `XxxDetail.vue` | `RegistrationDetail.vue` |
| API文件 | `index.js``xxx.js` | `api/registration/index.js` |
| Store模块 | `xxx.js` | `store/modules/user.js` |
| 工具函数 | `xxx.js` | `utils/validate.js` |
### 3.2 组件命名
```vue
<!-- 正确 - PascalCase -->
<template>
<PatientDialog ref="dialogRef" @success="getList" />
</template>
<!-- 错误 -->
<template>
<patient-dialog ref="dialogRef" />
</template>
```
### 3.3 变量命名
| 类型 | 命名规则 | 示例 |
|------|---------|------|
| 响应式变量 | `camelCase` | `const patientList = ref([])` |
| 常量 | `UPPER_SNAKE_CASE` | `const MAX_RETRY = 3` |
| 事件处理函数 | `handle` 前缀 | `const handleClick = () => {}` |
| 获取数据函数 | `getList` / `getData` | `const getList = async () => {}` |
| 表单引用 | `xxxForm` / `ruleForm` | `const ruleForm = ref(null)` |
| 表格引用 | `xxxTable` / `tableRef` | `const tableRef = ref(null)` |
---
## 四、API 接口规范
### 4.1 API文件结构
```javascript
// api/registration/index.js
import request from '@/utils/request'
// 查询挂号列表
export function listRegistration(query) {
return request({
url: '/healthlink-his/api/v1/registration/list',
method: 'get',
params: query
})
}
// 查询挂号详情
export function getRegistration(id) {
return request({
url: '/healthlink-his/api/v1/registration/' + id,
method: 'get'
})
}
// 新增挂号
export function addRegistration(data) {
return request({
url: '/healthlink-his/api/v1/registration',
method: 'post',
data: data
})
}
// 修改挂号
export function updateRegistration(data) {
return request({
url: '/healthlink-his/api/v1/registration',
method: 'put',
data: data
})
}
// 删除挂号
export function delRegistration(ids) {
return request({
url: '/healthlink-his/api/v1/registration/' + ids,
method: 'delete'
})
}
```
### 4.2 API 路径规范
- 统一前缀:`/healthlink-his/api/v1/`
- 使用 kebab-case`/patient-allergy` 而非 `/patientAllergy`
- 列表接口:`/list`
- 详情接口:`/{id}`
- 新增:`POST /`
- 修改:`PUT /`
- 删除:`DELETE /{id}`
- 批量删除:`DELETE /{ids}`(逗号分隔)
---
## 五、页面组件规范
### 5.1 标准页面模板
```vue
<template>
<div class="app-container">
<!-- 搜索区域 -->
<el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch">
<el-form-item label="患者姓名" prop="patientName">
<el-input v-model="queryParams.patientName" placeholder="请输入患者姓名" clearable />
</el-form-item>
<el-form-item>
<el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
<el-button icon="Refresh" @click="resetQuery">重置</el-button>
</el-form-item>
</el-form>
<!-- 操作按钮 -->
<el-row :gutter="10" class="mb8">
<el-col :span="1.5">
<el-button type="primary" plain icon="Plus" @click="handleAdd" v-hasPermi="['registration:add']">新增</el-button>
</el-col>
<right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
</el-row>
<!-- 数据表格 -->
<el-table v-loading="loading" :data="dataList" @selection-change="handleSelectionChange">
<el-table-column type="selection" width="55" align="center" />
<el-table-column label="患者姓名" prop="patientName" />
<el-table-column label="操作" width="180" align="center">
<template #default="scope">
<el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['registration:edit']">修改</el-button>
<el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['registration:remove']">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<pagination v-show="total > 0" :total="total" v-model:page="queryParams.pageNum" v-model:limit="queryParams.pageSize" @pagination="getList" />
<!-- 新增/修改弹窗 -->
<XxxDialog ref="dialogRef" @success="getList" />
</div>
</template>
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { listXxx, delXxx } from '@/api/xxx'
import XxxDialog from './XxxDialog.vue'
const { proxy } = getCurrentInstance()
const dataList = ref([])
const loading = ref(true)
const showSearch = ref(true)
const total = ref(0)
const ids = ref([])
const queryParams = reactive({
pageNum: 1,
pageSize: 10,
patientName: undefined
})
const dialogRef = ref(null)
/** 查询列表 */
const getList = async () => {
loading.value = true
const res = await listXxx(queryParams)
dataList.value = res.rows
total.value = res.total
loading.value = false
}
/** 搜索 */
const handleQuery = () => {
queryParams.pageNum = 1
getList()
}
/** 重置 */
const resetQuery = () => {
proxy.resetForm('queryForm')
handleQuery()
}
/** 多选 */
const handleSelectionChange = (selection) => {
ids.value = selection.map(item => item.id)
}
/** 新增 */
const handleAdd = () => {
dialogRef.value.open()
}
/** 修改 */
const handleUpdate = (row) => {
dialogRef.value.open(row.id)
}
/** 删除 */
const handleDelete = async (row) => {
await proxy.$modal.confirm('确认删除该记录?')
await delXxx(row.id)
proxy.$modal.msgSuccess('删除成功')
getList()
}
onMounted(() => {
getList()
})
</script>
```
### 5.2 弹窗组件模板
```vue
<template>
<el-dialog :title="title" v-model="open" width="600px" append-to-body>
<el-form ref="formRef" :model="form" :rules="rules" label-width="100px">
<el-form-item label="患者姓名" prop="patientName">
<el-input v-model="form.patientName" placeholder="请输入患者姓名" />
</el-form-item>
</el-form>
<template #footer>
<el-button @click="cancel"> </el-button>
<el-button type="primary" @click="submitForm"> </el-button>
</template>
</el-dialog>
</template>
<script setup>
import { ref, reactive } from 'vue'
import { getXxx, addXxx, updateXxx } from '@/api/xxx'
const { proxy } = getCurrentInstance()
const title = ref('')
const open = ref(false)
const formRef = ref(null)
const form = reactive({ id: undefined, patientName: '' })
const rules = {
patientName: [{ required: true, message: '患者姓名不能为空', trigger: 'blur' }]
}
/** 打开弹窗 */
const openDialog = async (id) => {
reset()
if (id) {
const res = await getXxx(id)
Object.assign(form, res.data)
title.value = '修改'
} else {
title.value = '新增'
}
open.value = true
}
/** 提交 */
const submitForm = async () => {
await proxy.$refs.formRef.validate()
if (form.id) {
await updateXxx(form)
proxy.$modal.msgSuccess('修改成功')
} else {
await addXxx(form)
proxy.$modal.msgSuccess('新增成功')
}
open.value = false
emit('success')
}
/** 取消 */
const cancel = () => {
open.value = false
reset()
}
const reset = () => {
form.id = undefined
form.patientName = ''
}
const emit = defineEmits(['success'])
defineExpose({ open: openDialog })
</script>
```
---
## 六、状态管理规范 (Pinia)
```javascript
// store/modules/user.js
import { defineStore } from 'pinia'
import { login, logout, getInfo } from '@/api/login'
const useUserStore = defineStore('user', {
state: () => ({
token: getToken(),
name: '',
roles: [],
permissions: []
}),
actions: {
async loginAction(userInfo) {
const res = await login(userInfo)
setToken(res.token)
this.token = res.token
},
async getInfoAction() {
const res = await getInfo()
this.name = res.user.nickName
this.roles = res.roles
this.permissions = res.permissions
},
logoutAction() {
this.token = ''
this.name = ''
this.roles = []
removeToken()
}
}
})
export default useUserStore
```
---
## 七、路由配置规范
```javascript
// router/index.js
const routes = [
{
path: '/registration',
component: Layout,
children: [
{
path: '',
name: 'Registration',
component: () => import('@/views/registration/index.vue'),
meta: { title: '挂号管理', icon: 'ticket' }
}
]
}
]
```
### 路由命名规则
- 路径使用 kebab-case`/patient-allergy`
- name 使用 PascalCase`PatientAllergy`
- meta.title 使用中文:`患者过敏史`
---
## 八、样式规范
### 8.1 使用 scoped
```vue
<style scoped>
.app-container {
padding: 20px;
}
</style>
```
### 8.2 使用 Element Plus 变量
```css
:deep(.el-button--primary) {
--el-button-bg-color: #1890ff;
}
```
### 8.3 禁止事项
- ❌ 使用内联样式(除动态绑定外)
- ❌ 使用 `!important`
- ❌ 全局样式污染其他组件
---
## 九、安全规范
### 9.1 XSS 防护
- 用户输入使用 `v-text` 而非 `v-html`
- 必须使用 `v-html` 时需做转义处理
### 9.2 敏感信息
- 不在前端硬编码密码、密钥
- API请求通过 `request.js` 统一拦截添加Token
- Token 存储在 `localStorage`,设置过期时间
### 9.3 权限控制
- 使用 `v-hasPermi` 指令控制按钮权限
- 使用路由 `meta.roles` 控制页面权限
- 接口请求在 `request.js` 中统一处理 401/403
---
## 十、性能优化
### 10.1 路由懒加载
```javascript
component: () => import('@/views/registration/index.vue')
```
### 10.2 组件按需导入
```javascript
import { ElButton, ElTable } from 'element-plus'
```
### 10.3 大列表优化
- 超过100行使用虚拟滚动
- 列表接口必须支持分页
- 图片使用懒加载 `v-lazy`
### 10.4 内存泄漏防护
- `onMounted` 中注册的事件在 `onUnmounted` 中移除
- 定时器在组件销毁时清除
- 避免在 `watch` 中创建新对象
---
## 十一、测试规范
### 11.1 单元测试 (Vitest)
```javascript
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import PatientDialog from './PatientDialog.vue'
describe('PatientDialog', () => {
it('renders correctly', () => {
const wrapper = mount(PatientDialog)
expect(wrapper.find('.el-dialog').exists()).toBe(true)
})
})
```
### 11.2 E2E测试 (Playwright)
```javascript
import { test, expect } from '@playwright/test'
test('registration flow', async ({ page }) => {
await page.goto('/login')
await page.fill('#username', 'admin')
await page.fill('#password', 'admin123')
await page.click('.login-button')
await expect(page).toHaveURL('/')
await page.goto('/registration')
await expect(page.locator('.el-table')).toBeVisible()
})
```
---
## 十二、Git提交规范
同后端规范(`MD/specs/IRON_RULES.md`),额外要求:
- 提交前执行 `npm run lint` 确保无报错
- 提交前执行 `npm run build:dev` 确保构建成功
---
> **文档版本**: v1.0
> **最后更新**: 2026-06-06
---
## 七、UI设计铁律法则
> 所有前端页面设计和开发必须遵守以下法则,详见 `MD/specs/UI_DESIGN_IRON_RULES.md`
### 核心设计法则速查
| 法则 | 核心思想 | HIS应用 |
|------|---------|---------|
| 希克定律 | 选项越少决策越快 | 菜单≤7项表单≤12字段 |
| 费茨定律 | 目标大且近操作快 | 按钮≥44px危险操作远离安全操作 |
| 米勒定律 | 记忆负荷≤7±2 | 信息分组Tab≤6个 |
| 雅各布定律 | 遵循用户已有习惯 | 若依标准布局模式 |
| 格式塔原则 | 视觉分组要清晰 | 间距系统、颜色体系 |
| 多赫蒂阈值 | 响应<400ms | loading态骨架屏分页 |
| 尼尔森十大原则 | 全面可用性 | 操作反馈防错容错 |
| 泰斯勒定律 | 复杂性守恒 | 智能默认值常用模板 |
| 峰终定律 | 关键时刻做好 | 成功动画错误优雅处理 |
| ·雷斯托夫 | 不同的更容易记住 | 危急值红色脉冲徽标通知 |
### 设计文档必备
每个新页面/模块的设计文档必须包含
1. 页面UI布局描述组件位置栅格比例
2. 交互效果清单每个操作效果反馈
3. 前后端调用流程操作API处理链渲染
4. 状态流转图
5. 异常/边界处理方案