9.2 KiB
9.2 KiB
前端开发指南
技术栈
| 技术 | 版本 | 用途 |
|---|---|---|
| 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> 语法
<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 配置
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函数定义
// 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}`)
}
状态管理规范
// 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
}
})
路由规范
// 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
样式规范
<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>
错误处理规范
// 使用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)
}
}
}
页面模板
列表页面模板
<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>
开发命令
# 安装依赖
npm install
# 启动开发服务器
npm run dev
# 构建生产版本
npm run build
# 预览生产构建
npm run preview
常见问题
跨域问题
开发环境通过Vite代理解决:
// vite.config.js
export default {
server: {
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true
}
}
}
}
Token刷新
当前实现为Token过期后跳转登录页,后续可考虑实现Token自动刷新机制。