- 新增 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
14 KiB
14 KiB
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 组件命名
<!-- ✅ 正确 - 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文件结构
// 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 标准页面模板
<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 弹窗组件模板
<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)
// 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
七、路由配置规范
// 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
<style scoped>
.app-container {
padding: 20px;
}
</style>
8.2 使用 Element Plus 变量
: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 路由懒加载
component: () => import('@/views/registration/index.vue')
10.2 组件按需导入
import { ElButton, ElTable } from 'element-plus'
10.3 大列表优化
- 超过100行使用虚拟滚动
- 列表接口必须支持分页
- 图片使用懒加载
v-lazy
10.4 内存泄漏防护
onMounted中注册的事件在onUnmounted中移除- 定时器在组件销毁时清除
- 避免在
watch中创建新对象
十一、测试规范
11.1 单元测试 (Vitest)
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)
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态、骨架屏、分页 |
| 尼尔森十大原则 | 全面可用性 | 操作反馈、防错、容错 |
| 泰斯勒定律 | 复杂性守恒 | 智能默认值、常用模板 |
| 峰终定律 | 关键时刻做好 | 成功动画、错误优雅处理 |
| 冯·雷斯托夫 | 不同的更容易记住 | 危急值红色脉冲、徽标通知 |
设计文档必备
每个新页面/模块的设计文档必须包含:
- 页面UI布局描述(组件位置、栅格、比例)
- 交互效果清单(每个操作→效果→反馈)
- 前后端调用流程(操作→API→处理链→渲染)
- 状态流转图
- 异常/边界处理方案