- 新增 MD/specs/IRON_RULES.md — 执行铁律汇总(v2.0, 8条铁律) - 新增 MD/specs/BACKEND_DEVELOPMENT_STANDARD.md — 后端开发规范 - 新增 MD/specs/FRONTEND_DEVELOPMENT_STANDARD.md — 前端开发规范 - 新增 healthlink-his-ui/AGENTS.md — 前端铁律引用 - 更新 healthlink-his-server/AGENTS.md — 同步规范文档引用 - 修复10个文档缺失的元数据(文档类型标签) - 全部30个文档通过命名规范和元数据检查
13 KiB
13 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