Files
hospital_performance/docs/frontend.md
2026-02-28 15:02:08 +08:00

416 lines
9.2 KiB
Markdown
Raw Permalink 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.

# 前端开发指南
## 技术栈
| 技术 | 版本 | 用途 |
|------|------|------|
| Vue | 3.4+ | 前端框架 |
| Vue Router | 4.2+ | 路由管理 |
| Pinia | 2.1+ | 状态管理 |
| Element Plus | 2.5+ | UI组件库 |
| Axios | 1.6+ | HTTP请求 |
| ECharts | 5.4+ | 图表库 |
| Day.js | 1.11+ | 日期处理 |
| Vite | 5.0+ | 构建工具 |
| Sass | 1.70+ | CSS预处理 |
## 项目结构
```
frontend/src/
├── api/ # API请求模块
│ ├── request.js # Axios实例配置
│ ├── auth.js # 认证API
│ ├── staff.js # 员工API
│ ├── department.js # 科室API
│ ├── indicator.js # 指标API
│ ├── assessment.js # 考核API
│ ├── salary.js # 工资API
│ └── stats.js # 统计API
├── stores/ # Pinia状态管理
│ ├── index.js # Store入口
│ ├── user.js # 用户状态
│ └── app.js # 应用状态
├── router/ # 路由配置
│ └── index.js
├── views/ # 页面组件
│ ├── Login.vue # 登录页
│ ├── Layout.vue # 布局框架
│ ├── Dashboard.vue # 工作台
│ ├── basic/ # 基础数据管理
│ ├── assessment/ # 考核管理
│ ├── salary/ # 工资核算
│ └── reports/ # 统计报表
├── components/ # 通用组件
├── assets/ # 静态资源
├── App.vue # 根组件
└── main.js # 入口文件
```
## 开发规范
### 组件规范
#### 使用 `<script setup>` 语法
```vue
<script setup>
import { ref, reactive, onMounted } from 'vue'
import { ElMessage } from 'element-plus'
// 响应式状态
const loading = ref(false)
const tableData = ref([])
const form = reactive({
name: '',
status: 'active'
})
// 生命周期
onMounted(() => {
loadData()
})
// 方法
async function loadData() {
loading.value = true
try {
const res = await getStaffList()
tableData.value = res.data || []
} finally {
loading.value = false
}
}
</script>
```
#### 命名规范
- **组件文件**: `PascalCase.vue` (如 `StaffList.vue`)
- **变量**: `camelCase` (如 `tableData`, `searchForm`)
- **常量**: `UPPER_SNAKE_CASE` (如 `API_BASE_URL`)
- **模板ref**: `xxxRef` (如 `formRef`, `tableRef`)
### API层规范
#### request.js 配置
```javascript
import axios from 'axios'
import { ElMessage } from 'element-plus'
import router from '@/router'
const request = axios.create({
baseURL: '/api/v1',
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(
config => {
const token = localStorage.getItem('token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
error => Promise.reject(error)
)
// 响应拦截器
request.interceptors.response.use(
response => response.data,
error => {
if (error.response?.status === 401) {
localStorage.removeItem('token')
router.push('/login')
}
ElMessage.error(error.response?.data?.detail || '请求失败')
return Promise.reject(error)
}
)
export default request
```
#### API函数定义
```javascript
// api/staff.js
import request from './request'
export function getStaffList(params) {
return request.get('/staff', { params })
}
export function getStaffById(id) {
return request.get(`/staff/${id}`)
}
export function createStaff(data) {
return request.post('/staff', data)
}
export function updateStaff(id, data) {
return request.put(`/staff/${id}`, data)
}
export function deleteStaff(id) {
return request.delete(`/staff/${id}`)
}
```
### 状态管理规范
```javascript
// stores/user.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login, getCurrentUser } from '@/api/auth'
export const useUserStore = defineStore('user', () => {
// 状态
const token = ref(localStorage.getItem('token') || '')
const userInfo = ref(null)
// 计算属性
const isLoggedIn = computed(() => !!token.value)
// 方法
async function loginAction(username, password) {
const res = await login({ username, password })
token.value = res.access_token
localStorage.setItem('token', res.access_token)
await fetchUserInfo()
}
async function fetchUserInfo() {
const res = await getCurrentUser()
userInfo.value = res
}
function logout() {
token.value = ''
userInfo.value = null
localStorage.removeItem('token')
}
return {
token,
userInfo,
isLoggedIn,
loginAction,
fetchUserInfo,
logout
}
})
```
### 路由规范
```javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { title: '登录' }
},
{
path: '/',
component: () => import('@/views/Layout.vue'),
redirect: '/dashboard',
children: [
{
path: 'dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { title: '工作台', icon: 'HomeFilled' }
}
]
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
document.title = `${to.meta.title || '首页'} - 医院绩效考核系统`
const token = localStorage.getItem('token')
if (to.path !== '/login' && !token) {
next('/login')
} else {
next()
}
})
export default router
```
### 样式规范
```vue
<style scoped lang="scss">
// 使用scoped避免样式污染
// 使用SCSS嵌套提高可读性
.page-container {
padding: 20px;
.search-bar {
display: flex;
gap: 12px;
margin-bottom: 20px;
.el-input {
width: 200px;
}
.el-select {
width: 150px;
}
}
.table-container {
background: #fff;
border-radius: 4px;
}
.pagination {
margin-top: 20px;
display: flex;
justify-content: flex-end;
}
}
</style>
```
### 错误处理规范
```javascript
// 使用try-catch-finally处理异步操作
async function handleSubmit() {
submitting.value = true
try {
await createStaff(form)
ElMessage.success('创建成功')
dialogVisible.value = false
loadData() // 刷新列表
} catch (error) {
// 错误已在request.js中统一处理
console.error('创建失败:', error)
} finally {
submitting.value = false
}
}
// 删除确认
async function handleDelete(row) {
try {
await ElMessageBox.confirm('确定要删除该记录吗?', '提示', {
type: 'warning'
})
await deleteStaff(row.id)
ElMessage.success('删除成功')
loadData()
} catch (error) {
if (error !== 'cancel') {
console.error('删除失败:', error)
}
}
}
```
## 页面模板
### 列表页面模板
```vue
<template>
<div class="page-container">
<!-- 搜索栏 -->
<div class="search-bar">
<el-input v-model="searchForm.name" placeholder="请输入姓名" clearable />
<el-select v-model="searchForm.status" placeholder="状态" clearable>
<el-option label="在职" value="active" />
<el-option label="休假" value="leave" />
</el-select>
<el-button type="primary" @click="handleSearch">搜索</el-button>
<el-button @click="handleReset">重置</el-button>
<el-button type="primary" @click="handleAdd">新增</el-button>
</div>
<!-- 表格 -->
<el-table :data="tableData" v-loading="loading" border>
<el-table-column prop="name" label="姓名" />
<el-table-column prop="department_name" label="科室" />
<el-table-column prop="status" label="状态">
<template #default="{ row }">
<el-tag :type="row.status === 'active' ? 'success' : 'info'">
{{ statusMap[row.status] }}
</el-tag>
</template>
</el-table-column>
<el-table-column label="操作" width="180">
<template #default="{ row }">
<el-button link type="primary" @click="handleEdit(row)">编辑</el-button>
<el-button link type="danger" @click="handleDelete(row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<!-- 分页 -->
<div class="pagination">
<el-pagination
v-model:current-page="pagination.page"
v-model:page-size="pagination.pageSize"
:total="pagination.total"
:page-sizes="[10, 20, 50, 100]"
layout="total, sizes, prev, pager, next"
@change="loadData"
/>
</div>
</div>
</template>
```
## 开发命令
```bash
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
# 预览生产构建
npm run preview
```
## 常见问题
### 跨域问题
开发环境通过Vite代理解决
```javascript
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
}
```
### Token刷新
当前实现为Token过期后跳转登录页后续可考虑实现Token自动刷新机制。