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

14 KiB
Raw Blame History

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.jsxxx.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 使用 PascalCasePatientAllergy
  • 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态、骨架屏、分页
尼尔森十大原则 全面可用性 操作反馈、防错、容错
泰斯勒定律 复杂性守恒 智能默认值、常用模板
峰终定律 关键时刻做好 成功动画、错误优雅处理
冯·雷斯托夫 不同的更容易记住 危急值红色脉冲、徽标通知

设计文档必备

每个新页面/模块的设计文档必须包含:

  1. 页面UI布局描述组件位置、栅格、比例
  2. 交互效果清单(每个操作→效果→反馈)
  3. 前后端调用流程操作→API→处理链→渲染
  4. 状态流转图
  5. 异常/边界处理方案