Files
his/MD/specs/FRONTEND_DEVELOPMENT_STANDARD.md
华佗 3578a24254 docs(specs): 汇总铁律和前后端开发规范文档到MD目录
- 新增 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个文档通过命名规范和元数据检查
2026-06-06 09:33:20 +08:00

524 lines
13 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