first commit
This commit is contained in:
415
docs/frontend.md
Normal file
415
docs/frontend.md
Normal file
@@ -0,0 +1,415 @@
|
||||
# 前端开发指南
|
||||
|
||||
## 技术栈
|
||||
|
||||
| 技术 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| 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自动刷新机制。
|
||||
Reference in New Issue
Block a user