416 lines
9.2 KiB
Markdown
416 lines
9.2 KiB
Markdown
# 前端开发指南
|
||
|
||
## 技术栈
|
||
|
||
| 技术 | 版本 | 用途 |
|
||
|------|------|------|
|
||
| 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自动刷新机制。
|