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

9.2 KiB
Raw Permalink Blame History

前端开发指南

技术栈

技术 版本 用途
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自动刷新机制。