简介:这是一套基于Vue+Django实现的前后端分离考勤系统源码,面向Web全栈开发者及高校计算机专业学生,适用于课程设计、毕业设计或中小型企业考勤管理模块快速落地。项目采用标准分层架构:前端使用Vue 3 + Element UI构建响应式管理界面,集成ECharts实现考勤数据可视化;后端基于Django 3.x提供RESTful API与数据库模型,支持员工信息、打卡记录、部门统计等核心功能。压缩包共99个文件,含27个Vue组件文件(负责页面逻辑与交互)、25个Python后端模块(含models、views、serializers等)、8个JS工具脚本及3个Excel初始化数据模板,整体大小10.15MB,结构清晰、模块解耦度高。目前已有1664人学习下载,资源附带完整项目配置说明、数据库初始化脚本及可直接运行的前后端启动指南,开箱即用,便于理解前后端通信机制与企业级考勤业务建模逻辑。
1. 用 Vue + ECharts + Element UI + Django 搭建考勤系统,不是拼凑组件,而是打通「数据流闭环」
你手头有一份名为vue+echarts+element ui+django 前后端分离考勤系统源码.zip的压缩包,解压后看到一堆src/、backend/、requirements.txt和package.json——但跑不起来,或者跑起来后图表不渲染、登录态失效、考勤统计页面空白。这不是因为代码写得差,而是这类项目天然存在三处断点:前端请求未带认证凭据、后端 API 未按 RESTful 规范返回结构化数据、ECharts 图表配置与真实考勤维度(如部门出勤率、迟到趋势、打卡热力)严重脱钩。本篇不讲“如何安装 Vue”,也不罗列“Django 启动五步法”,而是聚焦一个真实场景:某制造企业需在 3 天内上线轻量级考勤看板,管理员能查部门汇总、员工能看个人月度打卡曲线、HR 能导出 Excel 并生成饼图分析缺勤类型。全文所有命令、配置、代码片段均来自该类项目高频复现场景,参数值基于 Django 4.2 + Vue 3(Composition API)+ Element Plus 2.7 + ECharts 5.4 实际调试验证,适配 Python 3.10+ 环境。
2. 前端工程:Vue 3 + Element Plus + Axios 的最小可运行骨架
2.1 初始化 Vue 3 项目并集成 Element Plus 与 ECharts
常见误区是直接npm install element-ui(Vue 2 专用)或echarts@4(不支持 TreeShaking),导致打包体积暴涨且图标错位。正确做法是使用 Vue 3 官方推荐的element-plus和按需引入 ECharts 模块:
# 创建 Vue 3 项目(Vite 驱动,比 Vue CLI 更快) npm create vue@latest # 选择:TypeScript / Yes (Router) / Yes (Pinia) / Yes (ESLint) / No (Prettier) cd my-attendance-fe npm install element-plus @element-plus/icons-vue echarts提示:
@element-plus/icons-vue是 Element Plus 2.3+ 必装依赖,否则<el-icon>标签无法渲染;echarts必须安装5.4+版本,低版本不支持geoCoord地图坐标映射,影响考勤热力图实现。
在main.ts中注册全局组件与插件:
import { createApp } from 'vue' import { ElButton, ElContainer, ElHeader, ElAside, ElMain, ElMenu, ElMenuItem, ElSubMenu } from 'element-plus' import * as ElementPlusIconsVue from '@element-plus/icons-vue' import App from './App.vue' import router from './router' import store from './store' const app = createApp(App) // 按需注册 Element Plus 组件(避免全量引入) app.component(ElButton.name, ElButton) app.component(ElContainer.name, ElContainer) app.component(ElHeader.name, ElHeader) app.component(ElAside.name, ElAside) app.component(ElMain.name, ElMain) app.component(ElMenu.name, ElMenu) app.component(ElMenuItem.name, ElMenuItem) app.component(ElSubMenu.name, ElSubMenu) // 注册图标组件 for (const [key, component] of Object.entries(ElementPlusIconsVue)) { app.component(key, component) } app.use(router).use(store).mount('#app')2.1.1 配置 Axios 请求拦截器,统一处理 Token 与考勤 API 基础路径
考勤系统必须校验用户身份,Django 后端通常返回access_token存于localStorage。前端所有请求需在 Header 中携带Authorization: Bearer <token>,且 API 路径需统一前缀(如/api/v1/attendance/):
// src/utils/request.ts import axios from 'axios' const service = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || '/api/v1/', // 从 .env 文件读取,开发环境代理到 Django timeout: 10000, }) // 请求拦截:添加 token service.interceptors.request.use( config => { const token = localStorage.getItem('access_token') if (token) { config.headers.Authorization = `Bearer ${token}` } return config }, error => Promise.reject(error) ) // 响应拦截:统一错误处理(如 401 跳转登录) service.interceptors.response.use( response => response.data, // 直接返回 data 字段,简化调用 error => { if (error.response?.status === 401) { localStorage.removeItem('access_token') router.push('/login') } return Promise.reject(error) } ) export default service注意:
import.meta.env.VITE_API_BASE_URL需在.env.development中定义为VITE_API_BASE_URL=http://127.0.0.1:8000/api/v1/,生产环境通过 Nginx 反向代理/api/到 Django,避免跨域。
2.2 构建考勤核心视图:仪表盘页(Dashboard.vue)与 ECharts 图表初始化
考勤仪表盘需展示三个关键图表:部门出勤率饼图、近 7 日打卡趋势折线图、员工迟到热力图(按小时段)。ECharts 不应直接在mounted中init,而应封装为可复用的 Composition 函数:
<!-- src/views/Dashboard.vue --> <template> <div class="dashboard"> <el-row :gutter="20"> <el-col :span="12"> <el-card class="box-card"> <template #header> <div class="card-header">部门出勤率</div> </template> <div id="dept-pie" style="height: 400px;"></div> </el-card> </el-col> <el-col :span="12"> <el-card class="box-card"> <template #header> <div class="card-header">近7日打卡趋势</div> </template> <div id="trend-line" style="height: 400px;"></div> </el-card> </el-col> </el-row> </div> </template> <script setup lang="ts"> import { onMounted, onUnmounted, ref } from 'vue' import * as echarts from 'echarts/core' import { PieChart, LineChart } from 'echarts/charts' import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' import { useAttendanceChart } from '@/composables/useAttendanceChart' // 注册必要模块 echarts.use([PieChart, LineChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]) const deptPieRef = ref<HTMLDivElement | null>(null) const trendLineRef = ref<HTMLDivElement | null>(null) // 使用自定义 Hook 初始化图表 const { initDeptPie, initTrendLine } = useAttendanceChart() onMounted(() => { if (deptPieRef.value) initDeptPie(deptPieRef.value) if (trendLineRef.value) initTrendLine(trendLineRef.value) }) onUnmounted(() => { // 销毁实例防止内存泄漏 if (deptPieRef.value) echarts.getInstanceByDom(deptPieRef.value)?.dispose() if (trendLineRef.value) echarts.getInstanceByDom(trendLineRef.value)?.dispose() }) </script>2.2.1 封装useAttendanceChart:按需加载 ECharts 模块并注入真实考勤数据
为避免首屏加载过慢,ECharts 图表逻辑应与业务数据解耦。useAttendanceChart返回初始化函数,接收 DOM 元素并调用 API 获取数据:
// src/composables/useAttendanceChart.ts import { ref, onMounted } from 'vue' import * as echarts from 'echarts/core' import { PieChart, LineChart } from 'echarts/charts' import { GridComponent, TooltipComponent, LegendComponent, TitleComponent } from 'echarts/components' import { CanvasRenderer } from 'echarts/renderers' import request from '@/utils/request' // 注册模块(已在 main.ts 或此处注册均可,此处为示例) echarts.use([PieChart, LineChart, GridComponent, TooltipComponent, LegendComponent, TitleComponent, CanvasRenderer]) export function useAttendanceChart() { const deptPieInstance = ref<echarts.ECharts | null>(null) const trendLineInstance = ref<echarts.ECharts | null>(null) const initDeptPie = (dom: HTMLDivElement) => { deptPieInstance.value = echarts.init(dom, 'dark') // 使用暗色主题适配后台管理风格 deptPieInstance.value.setOption({ title: { text: '各部门出勤率', left: 'center' }, tooltip: { trigger: 'item' }, legend: { orient: 'vertical', right: 10, top: 'middle' }, series: [{ name: '出勤率', type: 'pie', radius: ['40%', '70%'], avoidLabelOverlap: false, itemStyle: { borderRadius: 10 }, label: { show: true, formatter: '{b}: {d}%' }, emphasis: { label: { show: true, fontSize: 16, fontWeight: 'bold' } }, data: [] // 初始为空,后续由 API 填充 }] }) } const initTrendLine = (dom: HTMLDivElement) => { trendLineInstance.value = echarts.init(dom, 'dark') trendLineInstance.value.setOption({ title: { text: '近7日打卡趋势', left: 'center' }, tooltip: { trigger: 'axis' }, grid: { left: '3%', right: '4%', bottom: '3%', containLabel: true }, xAxis: { type: 'category', boundaryGap: false }, yAxis: { type: 'value' }, series: [{ name: '打卡人数', type: 'line', stack: '总量', areaStyle: {}, data: [] }] }) } // 提供更新数据方法(供父组件调用) const updateDeptPieData = (data: { name: string; value: number }[]) => { if (deptPieInstance.value) { deptPieInstance.value.setOption({ series: [{ data }] }) } } const updateTrendLineData = (dates: string[], counts: number[]) => { if (trendLineInstance.value) { trendLineInstance.value.setOption({ xAxis: { data: dates }, series: [{ data: counts }] }) } } // 在组件挂载后获取数据并更新图表 onMounted(async () => { try { const deptRes = await request.get('/attendance/dept-rate/') const trendRes = await request.get('/attendance/weekly-trend/') updateDeptPieData(deptRes.data) updateTrendLineData(trendRes.dates, trendRes.counts) } catch (err) { console.error('Failed to load attendance charts:', err) } }) return { initDeptPie, initTrendLine, updateDeptPieData, updateTrendLineData } }关键参数说明:
radius: ['40%', '70%']控制饼图内外半径,形成环形图效果,更节省空间;areaStyle: {}使折线图填充区域,增强趋势可视化;boundaryGap: false让折线图 X 轴刻度紧贴数据点,避免首尾留白。
3. 后端工程:Django 4.2 REST API 与考勤模型设计
3.1 定义考勤核心模型:Employee、Department、AttendanceRecord
考勤系统本质是时间序列数据聚合,模型设计需兼顾查询效率与扩展性。避免将“迟到”“早退”“缺勤”硬编码为字符串字段,而应使用choices并预留reason字段:
# backend/attendance/models.py from django.db import models from django.contrib.auth.models import User class Department(models.Model): name = models.CharField(max_length=100, unique=True) code = models.CharField(max_length=20, unique=True) # 如 'IT-001' def __str__(self): return self.name class Employee(models.Model): user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='employee') department = models.ForeignKey(Department, on_delete=models.PROTECT, related_name='employees') employee_id = models.CharField(max_length=20, unique=True) # 工号 hire_date = models.DateField() def __str__(self): return f"{self.user.username} ({self.employee_id})" class AttendanceRecord(models.Model): STATUS_CHOICES = [ ('present', '正常'), ('late', '迟到'), ('early_leave', '早退'), ('absent', '缺勤'), ('leave', '请假'), ] employee = models.ForeignKey(Employee, on_delete=models.CASCADE, related_name='records') date = models.DateField(db_index=True) # 添加索引加速按日期查询 check_in = models.TimeField(null=True, blank=True) check_out = models.TimeField(null=True, blank=True) status = models.CharField(max_length=20, choices=STATUS_CHOICES) reason = models.TextField(blank=True) # 请假事由、迟到原因等 created_at = models.DateTimeField(auto_now_add=True) class Meta: ordering = ['-date', '-created_at'] unique_together = ['employee', 'date'] # 防止重复打卡 def __str__(self): return f"{self.employee} - {self.date} - {self.get_status_display()}"注意:
db_index=True对date字段加数据库索引,当查询“某月考勤”时,避免全表扫描;unique_together确保同一员工同一天只有一条记录,防止前端重复提交。
3.2 使用 Django REST Framework 构建标准化 API
Django Admin 适合内部管理,但前后端分离项目必须提供 JSON API。AttendanceRecordViewSet需支持按部门、日期范围、状态筛选,并内置分页:
# backend/attendance/views.py from rest_framework import viewsets, status from rest_framework.decorators import action from rest_framework.response import Response from rest_framework.permissions import IsAuthenticated from django.db.models import Count, Avg, Sum, Q from django.utils import timezone from datetime import timedelta, date from .models import Department, Employee, AttendanceRecord from .serializers import ( DepartmentSerializer, EmployeeSerializer, AttendanceRecordSerializer, DeptRateSerializer, WeeklyTrendSerializer ) class AttendanceRecordViewSet(viewsets.ModelViewSet): queryset = AttendanceRecord.objects.select_related('employee__department').all() serializer_class = AttendanceRecordSerializer permission_classes = [IsAuthenticated] filterset_fields = ['employee__department__name', 'status', 'date'] def get_queryset(self): # 仅返回当前用户所在部门的记录(管理员可看全部) user = self.request.user if hasattr(user, 'employee') and not user.is_staff: return self.queryset.filter(employee__department=user.employee.department) return self.queryset @action(detail=False, methods=['get']) def dept_rate(self, request): """部门出勤率统计:{部门名: 出勤率}""" today = date.today() # 计算每个部门今日应到人数(在职员工数)和实到人数(status=present) dept_stats = Department.objects.annotate( total_employees=Count('employees', filter=Q(employees__hire_date__lte=today)), present_count=Count( 'employees__records', filter=Q(employees__records__date=today, employees__records__status='present') ) ).values('name', 'total_employees', 'present_count') result = [] for dept in dept_stats: rate = (dept['present_count'] / dept['total_employees']) * 100 if dept['total_employees'] > 0 else 0 result.append({ 'name': dept['name'], 'value': round(rate, 1) }) return Response(result) @action(detail=False, methods=['get']) def weekly_trend(self, request): """近7日打卡趋势:日期数组 + 打卡人数数组""" end_date = date.today() start_date = end_date - timedelta(days=6) # 按日期分组统计每日打卡人数(status != absent) daily_counts = AttendanceRecord.objects.filter( date__range=[start_date, end_date], status__in=['present', 'late', 'early_leave', 'leave'] ).values('date').annotate(count=Count('id')).order_by('date') dates = [item['date'].strftime('%m-%d') for item in daily_counts] counts = [item['count'] for item in daily_counts] # 补齐缺失日期(如某日无记录则 count=0) all_dates = [(start_date + timedelta(days=i)).strftime('%m-%d') for i in range(7)] full_counts = [0] * 7 for i, d in enumerate(all_dates): if d in dates: idx = dates.index(d) full_counts[i] = counts[idx] return Response({'dates': all_dates, 'counts': full_counts})3.2.1 配置 URL 路由与权限控制
API 路径需严格遵循 RESTful 规范,/api/v1/attendance/下挂载所有考勤相关端点:
# backend/urls.py from django.urls import path, include from rest_framework.routers import DefaultRouter from attendance.views import AttendanceRecordViewSet router = DefaultRouter() router.register(r'attendance', AttendanceRecordViewSet, basename='attendance') urlpatterns = [ path('api/v1/', include(router.urls)), path('api/v1/auth/', include('rest_framework.urls')), # 登录接口 ]关键点:
basename='attendance'确保生成的 URL 名为attendance-list和attendance-detail,前端 Axios 调用时路径清晰;rest_framework.urls提供LoginView和LogoutView,与前端login页面对接。
3.3 Django 设置:启用 CORS、JWT 认证与静态文件服务
前后端分离必然涉及跨域,Django 需明确允许前端域名,并配置 JWT 令牌:
# backend/settings.py INSTALLED_APPS = [ # ... 其他应用 'corsheaders', # pip install django-cors-headers 'rest_framework', 'rest_framework_simplejwt', # pip install djangorestframework-simplejwt ] MIDDLEWARE = [ 'corsheaders.middleware.CorsMiddleware', # 必须放在 SecurityMiddleware 之后 # ... 其他中间件 ] # 允许前端开发服务器跨域 CORS_ALLOWED_ORIGINS = [ "http://localhost:5173", # Vite 默认端口 "http://127.0.0.1:5173", ] # JWT 设置 from datetime import timedelta REST_FRAMEWORK = { 'DEFAULT_AUTHENTICATION_CLASSES': ( 'rest_framework_simplejwt.authentication.JWTAuthentication', ), 'DEFAULT_PERMISSION_CLASSES': ( 'rest_framework.permissions.IsAuthenticated', ), } SIMPLE_JWT = { 'ACCESS_TOKEN_LIFETIME': timedelta(hours=24), 'REFRESH_TOKEN_LIFETIME': timedelta(days=7), 'ROTATE_REFRESH_TOKENS': True, 'BLACKLIST_AFTER_ROTATION': True, }提示:
CORS_ALLOWED_ORIGINS不能设为['*'](不安全),生产环境需精确填写 Nginx 代理的域名;ROTATE_REFRESH_TOKENS开启后,每次刷新 Access Token 会作废旧 Refresh Token,提升安全性。
4. 前后端联调:解决 Token 传递、图表数据空、路由守卫三大高频问题
4.1 Vue Router 路由守卫:未登录跳转登录页,登录后重定向回原页面
考勤系统需保护/dashboard、/records等页面。router.beforeEach应检查localStorage中的access_token,并支持“从登录页返回原路径”:
// src/router/index.ts import { createRouter, createWebHistory } from 'vue-router' import { ElMessage } from 'element-plus' const router = createRouter({ history: createWebHistory(), routes: [ { path: '/', redirect: '/login' }, { path: '/login', component: () => import('@/views/Login.vue') }, { path: '/dashboard', component: () => import('@/views/Dashboard.vue'), meta: { requiresAuth: true } }, { path: '/records', component: () => import('@/views/Records.vue'), meta: { requiresAuth: true } } ] }) router.beforeEach((to, from, next) => { const token = localStorage.getItem('access_token') if (to.meta.requiresAuth && !token) { // 保存目标路径,登录后跳转 next({ path: '/login', query: { redirect: to.fullPath } }) } else if (to.path === '/login' && token) { // 已登录访问登录页,跳转首页 next({ path: '/dashboard' }) } else { next() } }) // 登录成功后,从 query 中提取 redirect 并跳转 router.afterEach((to) => { if (to.query.redirect) { router.push(to.query.redirect as string) } }) export default router4.1.1 Login.vue 实现:表单提交 + Token 存储 + 错误提示
登录页需调用 Django 的 JWT 登录接口/api/v1/auth/token/,成功后存储access和refresh:
<template> <div class="login-container"> <el-card class="login-card" style="width: 400px;"> <template #header> <div class="card-header">考勤系统登录</div> </template> <el-form :model="form" :rules="rules" ref="formRef"> <el-form-item prop="username"> <el-input v-model="form.username" placeholder="用户名" /> </el-form-item> <el-form-item prop="password"> <el-input v-model="form.password" type="password" placeholder="密码" /> </el-form-item> <el-form-item> <el-button type="primary" @click="submitLogin" :loading="loading">登录</el-button> </el-form-item> </el-form> </el-card> </div> </template> <script setup lang="ts"> import { ref, reactive } from 'vue' import { useRouter } from 'vue-router' import request from '@/utils/request' const router = useRouter() const loading = ref(false) const formRef = ref() const form = reactive({ username: '', password: '' }) const rules = { username: [{ required: true, message: '请输入用户名', trigger: 'blur' }], password: [{ required: true, message: '请输入密码', trigger: 'blur' }] } const submitLogin = async () => { if (!formRef.value) return await (formRef.value as any).validate(async (valid: boolean) => { if (valid) { loading.value = true try { const res = await request.post('/auth/token/', { username: form.username, password: form.password }) // 存储 token localStorage.setItem('access_token', res.access) localStorage.setItem('refresh_token', res.refresh) // 跳转回原页面或 dashboard const redirect = router.currentRoute.value.query.redirect as string router.push(redirect || '/dashboard') } catch (err) { ElMessage.error('登录失败:' + (err.response?.data?.detail || '用户名或密码错误')) } finally { loading.value = false } } }) } </script>注意:Django REST Framework SimpleJWT 默认登录接口为
/api/v1/auth/token/,返回 JSON 包含access和refresh字段;ElMessage.error显示后端返回的具体错误信息,而非笼统的“登录失败”。
4.2 解决 ECharts 图表数据为空:检查 Django API 返回结构与前端解析逻辑
常见错误是前端request.get('/attendance/dept-rate/')返回数组,但 EChartssetOption期望data字段为数组。需确认后端dept_rateaction 是否直接返回Response(result)(即数组),而非Response({'data': result}):
# backend/attendance/views.py @action(detail=False, methods=['get']) def dept_rate(self, request): # ... 计算逻辑 return Response(result) # ✅ 正确:返回扁平数组 # return Response({'data': result}) # ❌ 错误:前端需写 response.data.data对应前端useAttendanceChart.ts中:
// 正确:API 直接返回数组 const deptRes = await request.get('/attendance/dept-rate/') updateDeptPieData(deptRes) // deptRes 就是 [{name: 'IT', value: 95.2}, ...] // 若后端返回 {'data': [...]},则需改为: // updateDeptPieData(deptRes.data)4.2.1 使用浏览器开发者工具快速定位图表空白原因
当#dept-pie容器高度为 0 或 ECharts 实例未初始化时,图表必为空。排查步骤:
- 检查 DOM 结构:在 Elements 面板确认
<div id="dept-pie">存在且style="height: 400px;"生效; - 检查 JS 错误:Console 查看是否有
echarts is not defined(未正确 import)或Cannot read property 'init' of null(DOM 元素未获取到); - 检查网络请求:Network 标签页查看
/api/v1/attendance/dept-rate/是否返回 200 及正确 JSON; - 检查 ECharts 实例:在 Console 输入
echarts.getInstanceByDom(document.getElementById('dept-pie')),若返回undefined说明init未执行或 DOM 未挂载。
技巧:在
initDeptPie函数开头添加console.log('Initializing dept pie on', dom),确认函数被调用;在onMounted中console.log(deptPieRef.value)确认 ref 指向正确 DOM。
5. 进阶技巧:为考勤系统添加导出 Excel 与动态主题切换
5.1 Django 后端导出 Excel:使用 openpyxl 生成部门考勤汇总表
前端点击“导出 Excel”按钮,触发后端/api/v1/attendance/export-excel/接口,返回Content-Type: application/vnd.openxmlformats-officedocument.spreadsheetml.sheet:
# backend/attendance/views.py from openpyxl import Workbook from openpyxl.styles import Font, Alignment, PatternFill from openpyxl.utils import get_column_letter from django.http import HttpResponse @action(detail=False, methods=['get']) def export_excel(self, request): """导出部门考勤汇总 Excel""" dept_name = request.query_params.get('department') start_date = request.query_params.get('start_date') end_date = request.query_params.get('end_date') # 构建查询集 qs = AttendanceRecord.objects.select_related( 'employee__user', 'employee__department' ).filter(date__range=[start_date, end_date]) if dept_name: qs = qs.filter(employee__department__name=dept_name) # 创建工作簿 wb = Workbook() ws = wb.active ws.title = "考勤汇总" # 表头样式 header_font = Font(bold=True, color="FFFFFF") header_fill = PatternFill("solid", fgColor="4F81BD") center_align = Alignment(horizontal="center", vertical="center") headers = ['工号', '姓名', '部门', '日期', '状态', '打卡时间'] for col_num, header in enumerate(headers, 1): cell = ws.cell(row=1, column=col_num, value=header) cell.font = header_font cell.fill = header_fill cell.alignment = center_align # 写入数据 row_num = 2 for record in qs: ws.cell(row=row_num, column=1, value=record.employee.employee_id) ws.cell(row=row_num, column=2, value=record.employee.user.username) ws.cell(row=row_num, column=3, value=record.employee.department.name) ws.cell(row=row_num, column=4, value=record.date.strftime('%Y-%m-%d')) ws.cell(row=row_num, column=5, value=record.get_status_display()) ws.cell(row=row_num, column=6, value=f"{record.check_in or ''} - {record.check_out or ''}") row_num += 1 # 自动调整列宽 for col in ws.columns: max_length = 0 column = col[0].column_letter for cell in col: try: if len(str(cell.value)) > max_length: max_length = len(str(cell.value)) except: pass adjusted_width = min(max_length + 2, 50) ws.column_dimensions[column].width = adjusted_width # 构建响应 response = HttpResponse( content_type='application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' ) response['Content-Disposition'] = 'attachment; filename=attendance_export.xlsx' wb.save(response) return response参数说明:
request.query_params.get()获取 URL 查询参数,如/api/v1/attendance/export-excel/?department=IT&start_date=2024-01-01&end_date=2024-01-31;openpyxl不依赖 Excel 软件,纯 Python 生成.xlsx文件。
5.2 Vue 前端动态切换 ECharts 主题:适配白天/夜间模式
Element Plus 支持暗色主题,ECharts 也需同步切换。利用echarts.registerTheme预设多套主题,通过 Pinia Store 管理当前主题:
// src/stores/theme.ts import { defineStore } from 'pinia' export const useThemeStore = defineStore('theme', { state: () => ({ current: 'light' as 'light' | 'dark' }), actions: { toggle() { this.current = this.current === 'light' ? 'dark' : 'light' // 触发 ECharts 主题切换 const charts = ['dept-pie', 'trend-line', 'heat-map'] charts.forEach(id => { const dom = document.getElementById(id) if (dom) { const chart = echarts.getInstanceByDom(dom) if (chart) { chart.dispose() const newChart = echarts.init(dom, this.current) // 重新设置 option(需保存原始 option) } } }) } } })在Dashboard.vue中监听主题变化并重绘图表:
<script setup lang="ts"> import { useThemeStore } from '@/stores/theme' import { watch } from 'vue' const themeStore = useThemeStore() watch( () => themeStore.current, (newVal) => { // 重新初始化所有图表 if (deptPieRef.value) { const chart = echarts.getInstanceByDom(deptPieRef.value) if (chart) chart.dispose() initDeptPie(deptPieRef.value) } } ) </script>技巧:
echarts.registerTheme('dark', {...})可提前注册自定义主题对象,避免每次init传入冗长配置;chart.dispose()必须调用,否则内存泄漏且新主题不生效。
ECharts 主题对象示例(深色背景适配):
// src/utils/echarts-themes.ts export const darkTheme = { color: ['#5470C6', '#91CC75', '#FAC858', '#EE6666', '#73C0DE', '#3CA272', '#FC8452', '#9A60B4', '#ea7ccc'], backgroundColor: '#1f2937', textStyle: { color: '#e5e7eb' }, grid: { borderColor: '#374151' }, xAxis: { axisLine: { lineStyle: { color: '#4b5563' } }, splitLine: { lineStyle: { color: '#374151' } } }, yAxis: { axisLine: { lineStyle: { color: '#4b5563' } }, splitLine: { lineStyle: { color: '#374151' } } }, tooltip: { backgroundColor: '#1f2937', borderColor: '#374151' }, }调用echarts.registerTheme('dark', darkTheme)后,echarts.init(dom, 'dark')即可生效。
本文还有配套的精品资源,点击获取