- 新增 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
555 lines
14 KiB
Markdown
555 lines
14 KiB
Markdown
# 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. 异常/边界处理方案
|