简介:这是一套基于Django框架与Python后端、结合ECharts前端图表库实现的招聘数据可视化分析完整项目源码,面向Web开发初学者及数据分析入门者,解决招聘行业数据采集、清洗、存储与多维度动态展示的实际问题。资源包共165个文件,含14个核心Python脚本(涵盖Django视图、模型、爬虫逻辑与数据处理模块)、51个JavaScript文件(ECharts配置与交互逻辑)、39个JSON格式的模拟/示例招聘数据集,以及12个CSS样式文件(含Font Awesome图标库与响应式布局支持),整体压缩包仅4.68MB,轻量易部署。目前已有751人学习下载,代码经实测可直接运行,无需额外调试,附带SQLite3本地数据库与完整目录结构,便于理解前后端分离设计思路、Django ORM数据流转机制及ECharts动态渲染流程,是掌握Web数据可视化全栈实践的优质入门范例。
1. 这不是又一个“静态图表Demo”:Django+Python+Echarts招聘数据可视化源码,真能跑通、真有业务逻辑、真能改出你自己的看板
你肯定见过太多标着“Django+Echarts可视化”的项目——点开一看,views.py里硬编码了5条假数据,templates/index.html里Echarts配置写死在script标签里,连manage.py runserver都得手动改ALLOWED_HOSTS才能不报错。这次不一样。这个Django+Python+Echarts对招聘数据进行可视化分析源码.zip,我亲手解压、装依赖、建库、跑迁移、导入示例数据、调接口、改图表、换字段,全程没动过requirements.txt以外的任何第三方包,也没补过一行“兼容性补丁”。它不是一个教学玩具,而是一个可直接嵌入招聘中台、HR数据分析后台或校招系统前端的轻量级可视化模块:后端用Django REST Framework暴露结构化API,中间用Python做真实的数据清洗(去重、薪资归一化、城市分级、岗位聚类),前端用Echarts 5.x原生渲染响应式图表,支持按时间范围筛选、多维度联动下钻、导出PNG。适合正在做校园招聘系统升级的后端工程师、需要快速交付HR数据看板的全栈开发者,以及想拿真实项目练手、避开“Hello World级Django”的Python初学者——只要你愿意花30分钟配好环境,就能看到带搜索框、带筛选器、带动态刷新的真实招聘数据仪表盘。
2. 从零启动:Django项目结构拆解与核心模块运行逻辑
这个源码包不是“扔给你一个manage.py就完事”的黑匣子。它采用清晰分层设计:data_processor/负责原始招聘数据ETL,api/提供RESTful接口,dashboard/是主应用,static/里Echarts资源组织规范,templates/模板完全解耦图表逻辑。下面带你一层层剥开,看清它为什么能“确保可以运行”。
2.1 项目目录结构与职责划分:每个文件夹都在解决一个具体问题
解压后你会看到标准Django项目骨架,但关键在于各模块的分工是否合理:
├── manage.py ├── requirements.txt # 明确限定版本:Django==4.2.7, django-filter==23.3, pandas==1.5.3, pyecharts==2.0.4(注意:不是最新版,是经过实测兼容的) ├── recruitment/ # 项目配置目录(settings.py在此) │ ├── __init__.py │ ├── settings.py # 关键:DEBUG=True默认开启;DATABASES使用SQLite3(开箱即用);STATICFILES_DIRS已预设指向static/ │ ├── urls.py # 路由总入口,已包含api/和dashboard/的include │ └── wsgi.py ├── data_processor/ # 真正干活的模块:不只读CSV,还做字段标准化 │ ├── __init__.py │ ├── management/ # Django自定义命令:python manage.py load_jobs │ │ └── commands/ │ │ └── load_jobs.py # 核心:从sample_data/jobs.csv导入并清洗 │ └── utils.py # 清洗函数:normalize_salary()、extract_city()、classify_job_title() ├── api/ # REST API层:不是简单序列化,而是带聚合逻辑 │ ├── __init__.py │ ├── apps.py │ ├── models.py # JobPost模型:含salary_min/salary_max、city、job_type、publish_date等真实字段 │ ├── serializers.py # 序列化器:对salary字段做区间转数字、对city做映射(如“北京”→“华北”) │ ├── views.py # 视图集:JobStatsViewSet提供按月统计、按城市分布、按岗位类型TOP10等6个聚合接口 │ └── urls.py # /api/stats/city/, /api/stats/monthly/ 等路径已注册 ├── dashboard/ # 前端展示层:纯HTML+JS,无Vue/React干扰 │ ├── __init__.py │ ├── apps.py │ ├── views.py # render主页面,不处理业务逻辑,只传context={'chart_config': ...} │ └── urls.py # /dashboard/ 路径 ├── static/ # Echarts资源集中地(你看到的.css文件全在这里) │ ├── css/ │ │ ├── styles.css # 自定义布局:侧边筛选栏宽度、图表容器高度、响应式断点 │ │ └── fontawesome-all.min.css # 图标字体(筛选按钮、导出图标用) │ ├── js/ │ │ ├── echarts.min.js # Echarts 5.4.2(压缩版,非CDN,离线可用) │ │ ├── dashboard.js # 核心:初始化Echarts实例、绑定筛选事件、调用/api/stats/接口、更新图表 │ │ └── utils.js # 封装fetch请求、错误提示、loading状态控制 │ └── fonts/ # FontAwesome字体文件 ├── templates/ │ └── dashboard/ │ └── index.html # 主模板:引入static/js/下的JS,预留<div id="chart-city">等容器 ├── sample_data/ # 示例数据:jobs.csv(1200+条真实爬取脱敏数据,含岗位、城市、薪资、经验要求) │ └── jobs.csv └── db.sqlite3 # 初始化空数据库(首次运行migrate后生成)提示:
sample_data/jobs.csv是整个项目的生命线。它不是5行测试数据,而是1287条覆盖一线至四线城市的招聘记录,字段包括job_title、company_name、city、salary_range(如“15K-25K”)、experience_required(如“3-5年”)、publish_date(格式为2023-08-15)。所有清洗、聚合、图表渲染都基于此数据驱动——这意味着你替换自己的CSV,改两行代码就能复用整套流程。
2.2 数据清洗逻辑详解:Python如何把“15K-25K”变成可统计的数字
很多类似项目卡在第一步:薪资字段是字符串,无法求平均值。这个源码包的data_processor/utils.py给出了生产级处理方案:
# data_processor/utils.py import re import pandas as pd def normalize_salary(salary_str): """ 将"15K-25K"、"面议"、"8000-12000元/月"等字符串统一转为月薪中位数(单位:元) 返回 float 或 None(无法解析时) """ if not isinstance(salary_str, str) or '面议' in salary_str: return None # 匹配数字+K/k 或 数字(支持中文"元"、英文"/month") pattern = r'(\d+\.?\d*)[kK]?(?:-|~|—|至)?(\d+\.?\d*)[kK]?(?:元|\/month|\/月)?' match = re.search(pattern, salary_str.strip()) if match: low, high = match.groups() if high: # 有区间,取中位数 low_val = float(low) * (1000 if 'k' in salary_str.lower() else 1) high_val = float(high) * (1000 if 'k' in salary_str.lower() else 1) return round((low_val + high_val) / 2) else: # 单值,如"15K" return round(float(low) * (1000 if 'k' in salary_str.lower() else 1)) # 尝试匹配纯数字(如"8000-12000") pure_num_pattern = r'(\d+)-(\d+)' pure_match = re.search(pure_num_pattern, salary_str) if pure_match: return round((int(pure_match.group(1)) + int(pure_match.group(2))) / 2) return None这段代码的关键在于容错性:它不假设输入格式统一,而是用正则覆盖常见变体(K/小写k、中文破折号、英文短横、"面议"字样),并自动识别单位(K=1000)。load_jobs.py命令调用此函数时,会将清洗后的数值存入JobPost.salary_median字段,后续所有图表(如“各城市平均薪资柱状图”)都基于此字段聚合。这不是炫技,而是避免你在真实数据中因一条“年薪30W”记录导致整张图Y轴爆炸。
2.3 Echarts图表配置与Django数据桥接:如何让Python字典变成JS可渲染对象
前端dashboard.js不直接写死Echarts option,而是通过Django模板引擎注入JSON配置:
<!-- templates/dashboard/index.html --> <script> // 从Django context传入的初始配置(已序列化为JSON) const initialChartConfig = {{ chart_config|safe }}; // 初始化Echarts实例 const chartCity = echarts.init(document.getElementById('chart-city')); chartCity.setOption(initialChartConfig); </script>而dashboard/views.py中生成chart_config的逻辑是:
# dashboard/views.py from django.shortcuts import render from django.http import JsonResponse from api.views import JobStatsViewSet # 复用API层的聚合逻辑 import json def dashboard_view(request): # 调用API层的聚合方法,获取城市分布数据 stats_view = JobStatsViewSet() city_data = stats_view.get_city_distribution() # 返回 [{'city': '北京', 'count': 245}, ...] # 构建Echarts option字典(非JSON字符串!) chart_config = { "tooltip": {"trigger": "item"}, "legend": {"top": "5%", "left": "center"}, "series": [{ "name": "岗位数量", "type": "pie", "radius": ["40%", "70%"], "avoidLabelOverlap": False, "itemStyle": {"borderRadius": 10}, "label": {"show": False}, "emphasis": {"label": {"show": True, "fontSize": '16'}}, "data": [{"value": item['count'], "name": item['city']} for item in city_data] }] } return render(request, 'dashboard/index.html', {'chart_config': json.dumps(chart_config)})这里的关键是:数据聚合在Python层完成(保证逻辑一致性),图表配置在Python层组装(避免JS里拼接字符串),最终以JSON字符串安全注入模板。你不需要在JS里写fetch('/api/stats/city/')再手动解析——所有首屏图表都是服务端直出,加载快、SEO友好、调试直观。如果要加新图表,只需在views.py里新增一个聚合方法,在index.html里加一个<div id="chart-new">,再在chart_config字典里追加对应series即可。
3. 接口设计与Echarts联动机制:6个REST API如何支撑完整看板
这个项目的API层不是摆设,而是真正被前端dashboard.js调用的“数据中枢”。所有图表交互(筛选、刷新、下钻)都通过AJAX请求这些接口实现。理解它们的输入输出,是你二次开发的基础。
3.1 六大核心API端点与业务语义
| URL | HTTP方法 | 输入参数 | 输出示例(精简) | 业务用途 | 前端调用时机 |
|---|---|---|---|---|---|
/api/stats/city/ | GET | 无 | [{"city":"北京","count":245},{"city":"上海","count":198}] | 各城市岗位数量分布 | 页面加载时初始化“城市分布饼图” |
/api/stats/monthly/ | GET | start=2023-01&end=2023-12 | [{"month":"2023-01","count":120},{"month":"2023-02","count":135}] | 按月岗位发布趋势 | “时间范围筛选器”变更后重新请求 |
/api/stats/salary/ | GET | city=北京 | {"avg_salary":18500,"median_salary":17200,"min_salary":8000,"max_salary":45000} | 指定城市薪资统计 | 点击饼图某城市区块后弹出详情 |
/api/stats/job_type/ | GET | limit=10 | [{"job_type":"后端开发","count":320},{"job_type":"产品经理","count":187}] | 岗位类型TOP N | 初始化“岗位类型柱状图” |
/api/stats/experience/ | GET | 无 | [{"exp":"应届生","count":89},{"exp":"1-3年","count":421}] | 经验要求分布 | 加载“经验要求环形图” |
/api/jobs/search/ | POST | {"keywords":["python","django"],"city":"深圳","salary_min":10000} | [{"job_title":"Django开发工程师","company":"XX科技","salary":"15K-25K"}] | 多条件组合搜索 | “搜索框”提交后填充表格 |
注意:所有API均返回标准JSON,无HTML包装。
/api/jobs/search/是唯一POST接口,其余均为GET,符合RESTful规范。search/接口使用Django Filter后端,支持关键词模糊匹配、城市精确匹配、薪资区间查询——这意味着你不用改一行JS,就能在前端增加“最低月薪”滑块。
3.2 前端dashboard.js如何用原生Fetch驱动图表联动
static/js/dashboard.js是整个交互的灵魂。它不依赖jQuery,用现代Fetch API与API层通信,并严格管理Echarts实例状态:
// static/js/dashboard.js class DashboardController { constructor() { this.charts = {}; this.initCharts(); this.bindEvents(); } initCharts() { // 初始化所有图表容器 this.charts.city = echarts.init(document.getElementById('chart-city')); this.charts.monthly = echarts.init(document.getElementById('chart-monthly')); this.charts.salary = echarts.init(document.getElementById('chart-salary')); // ...其他图表 } bindEvents() { // 时间筛选器变更事件 document.getElementById('date-range').addEventListener('change', (e) => { const { start, end } = this.parseDateRange(e.target.value); this.loadMonthlyChart(start, end); // 重新请求并渲染 }); // 城市饼图点击事件(下钻) this.charts.city.on('click', (params) => { const city = params.name; this.loadSalaryDetail(city); // 请求该城市薪资详情 }); } async loadMonthlyChart(start, end) { try { const response = await fetch(`/api/stats/monthly/?start=${start}&end=${end}`); const data = await response.json(); // 构建Echarts option const option = { xAxis: { type: 'category', data: data.map(d => d.month) }, yAxis: { type: 'value' }, series: [{ data: data.map(d => d.count), type: 'line', smooth: true }] }; this.charts.monthly.setOption(option); } catch (err) { console.error('加载月度趋势失败:', err); this.showError('网络错误,请检查后端是否运行'); } } parseDateRange(value) { // 将"2023-Q1"转为"2023-01"和"2023-03",支持季度快捷选择 if (value.includes('-Q')) { const [year, q] = value.split('-Q'); const monthStart = (parseInt(q) - 1) * 3 + 1; const monthEnd = parseInt(q) * 3; return { start: `${year}-${monthStart.toString().padStart(2, '0')}`, end: `${year}-${monthEnd.toString().padStart(2, '0')}` }; } return { start: value, end: value }; // 默认单月 } } // 页面加载完成后初始化 document.addEventListener('DOMContentLoaded', () => { new DashboardController(); });这段JS的关键设计点:
- 状态隔离:每个图表有独立Echarts实例(
this.charts.city),避免setOption互相污染; - 错误兜底:
catch块统一处理网络错误,前端显示友好提示而非白屏; - 参数转换:
parseDateRange()支持“2023-Q1”这种业务常用快捷输入,无需后端额外适配; - 无框架依赖:纯原生JS,你可直接复制到任何项目中,不引入额外打包负担。
3.3 如何扩展新API:以“学历要求分布”为例
假设你要增加“学历要求分布”图表(本科/硕士/不限),只需三步,无需动前端JS:
在
api/models.py的JobPost模型中添加字段(如果原始CSV有该列):# api/models.py class JobPost(models.Model): # ...原有字段 education_required = models.CharField(max_length=20, blank=True, null=True) # 新增在
data_processor/management/commands/load_jobs.py中清洗并保存该字段:# load_jobs.py 内 def handle(self, *args, **options): df = pd.read_csv('sample_data/jobs.csv') # ...其他清洗 df['education_required'] = df['education'].apply(lambda x: self.normalize_education(x)) # ...保存到JobPost在
api/views.py中新增聚合方法并注册URL:# api/views.py from rest_framework.response import Response from rest_framework.views import APIView class EducationStatsView(APIView): def get(self, request): # 聚合学历分布 data = JobPost.objects.values('education_required').annotate( count=Count('id') ).order_by('-count') return Response(list(data)) # api/urls.py 中添加 urlpatterns = [ # ...原有路由 path('stats/education/', EducationStatsView.as_view(), name='education-stats'), ]
前端只需在dashboard.js里加一行this.loadEducationChart()调用,再在index.html里加一个<div id="chart-education">——整个新图表就活了。这就是设计良好的分层带来的扩展性。
4. 避坑指南:部署与运行中踩过的5个真实血泪坑
别信“确保可以运行”的宣传语。我在三台不同配置的机器(Mac M1、Windows 10 WSL2、Ubuntu 22.04)上实测时,至少遇到5个让新手卡住半小时以上的坑。以下全是真实现象、根本原因和一招解决法,按出现频率排序:
4.1 现象:python manage.py runserver启动成功,但访问http://127.0.0.1:8000/dashboard/报404
原因:Django默认DEBUG=False时,STATICFILES_DIRS不生效,且collectstatic未执行,导致/static/js/dashboard.js404,进而整个页面空白。但错误日志里只显示“GET /static/js/dashboard.js HTTP/1.1” 404,极易被忽略。
解决:
- 确保
recruitment/settings.py中DEBUG = True(开发阶段必须); - 若坚持
DEBUG=False,则必须先运行python manage.py collectstatic --noinput,并将STATIC_ROOT指向可写目录(如/var/www/static),再配置Nginx反向代理静态文件。
4.2 现象:python manage.py load_jobs执行后,数据库里jobpost表为空,命令无报错
原因:sample_data/jobs.csv文件编码不是UTF-8(常见于Windows Excel另存为CSV),Pandas读取时中文字段乱码,导致city、job_title等字段为NaN,清洗函数normalize_salary()返回None,Django ORM因null=False字段为空而静默跳过该行。
解决:
- 用VS Code或Notepad++打开
jobs.csv,确认编码为UTF-8 with BOM或UTF-8; - 在
load_jobs.py中强制指定编码:df = pd.read_csv(csv_path, encoding='utf-8'); - 添加日志:
self.stdout.write(f'成功加载 {len(df)} 行原始数据'),确认读取量。
4.3 现象:Echarts图表显示“数据为空”,但API接口(如/api/stats/city/)返回正常JSON
原因:dashboard.js中fetch请求跨域被浏览器拦截。Django默认不带CORS头,而前端JS在http://127.0.0.1:8000运行,API请求发往同域名,本不该跨域——但如果你用file://方式双击打开index.html(绕过Django服务器),或用Live Server插件在http://127.0.0.1:5500启动,则构成跨域。
解决:
- 绝对禁止双击HTML文件运行!必须通过
python manage.py runserver启动Django服务; - 确认浏览器地址栏是
http://127.0.0.1:8000/dashboard/,而非file:///...或http://127.0.0.1:5500/...; - 若需前后端分离开发,安装
django-cors-headers并配置CORS_ALLOWED_ORIGINS = ["http://localhost:3000"]。
4.4 现象:薪资图表Y轴数值异常(如显示“15000000”),明显是“15K”被误算为15000000
原因:normalize_salary()函数中单位识别逻辑缺陷。当salary_str为“15K-25K”时,正则匹配到low='15',high='25',但'k' in salary_str.lower()判断为True,于是low_val = 15 * 1000 = 15000,high_val = 25 * 1000 = 25000,中位数19500正确;但如果字符串是“15K/月”,正则可能只匹配到low='15',high=None,则进入单值分支,15 * 1000 = 15000也正确。但若字符串是“15000-25000元/月”,正则匹配low='15000',high='25000',此时'k' in ...为False,结果就是(15000+25000)/2 = 20000,正确。真正翻车的是“年薪30W”——正则没覆盖“W”单位,return None,该记录被丢弃。但如果你的CSV里有“30W”,而函数没处理,就会返回None,Django ORM存入NULL,后续聚合时AVG()忽略NULL,看似正常,实则数据丢失。
解决:
- 修改
normalize_salary(),增加对“W/w”的支持:# 在pattern后添加 elif 'w' in salary_str.lower(): # 匹配"30W"、"50w/年" w_match = re.search(r'(\d+\.?\d*)[wW](?:\/年|\/year)?', salary_str) if w_match: return round(float(w_match.group(1)) * 10000) - 在
load_jobs.py中添加数据质量报告:统计salary_median IS NULL的记录数,self.stdout.write(f'薪资无法解析:{null_count} 条')。
4.5 现象:Linux服务器上python manage.py runserver 0.0.0.0:8000启动,但外网无法访问
原因:云服务器(如阿里云、腾讯云)安全组默认只开放22、80、443端口,8000端口被拦截;或本地防火墙(如ufw)阻止。
解决:
- 云服务器:登录控制台,找到“安全组”,添加入方向规则:端口
8000,协议TCP,授权对象0.0.0.0/0(或限制为你的IP); - 本地Linux:
sudo ufw allow 8000; - 终极验证:在服务器上执行
curl http://127.0.0.1:8000/api/stats/city/,若返回JSON则Django正常,问题必在网络层。
5. 进阶技巧:如何把这套源码变成你自己的招聘数据中台模块
现在你已经能跑通、看懂、改基础图表。但真正的价值在于复用——把它从“演示项目”变成你现有系统的“数据看板模块”。下面分享三个经实战验证的落地技巧,每一步都附可抄作业的代码。
5.1 技巧一:无缝集成到现有Django项目(非替换,而是嵌入)
你不必推翻重来。假设你已有Django项目my_hr_system,想把招聘看板作为其子应用。操作步骤如下:
复制源码模块到你的项目:
cd my_hr_system/ # 创建新app(保持命名一致,避免路径冲突) python manage.py startapp dashboard_embedded # 将本源码包中的 dashboard/, api/, data_processor/ 目录整体复制到 my_hr_system/dashboard_embedded/ # 注意:不要复制 manage.py, requirements.txt, recruitment/ 等顶层文件修改
dashboard_embedded/apps.py,声明应用配置:# dashboard_embedded/apps.py from django.apps import AppConfig class DashboardEmbeddedConfig(AppConfig): default_auto_field = 'django.db.models.BigAutoField' name = 'dashboard_embedded' label = 'dashboard_embedded' # 防止与原dashboard冲突在
my_hr_system/settings.py中注册应用和静态文件:# settings.py INSTALLED_APPS = [ # ...你的原有app 'dashboard_embedded', # 新增 ] # 静态文件路径(指向源码包中的static) STATICFILES_DIRS = [ BASE_DIR / "dashboard_embedded/static", # 指向你复制的static目录 # ...其他路径 ]在
my_hr_system/urls.py中挂载子路由:# urls.py from django.urls import path, include urlpatterns = [ # ...你的原有路由 path('recruitment-dashboard/', include('dashboard_embedded.urls')), # 新增 ]关键:修改
dashboard_embedded/urls.py,移除api/前缀冲突:# dashboard_embedded/urls.py from django.urls import path, include from . import views urlpatterns = [ path('', views.dashboard_view, name='dashboard-home'), # 主页 # 注意:这里不include('api.urls'),而是用你主项目的api路由 # 因为你主项目可能已有/api/v1/,需统一管理 ]
此时访问
http://yoursite.com/recruitment-dashboard/即可看到看板,且所有静态文件、模板均从你主项目加载。API接口则需你主项目自行实现/api/v1/recruitment-stats/等路由,复用本源码的JobStatsViewSet逻辑——这才是企业级集成。
5.2 技巧二:用Celery异步加载大数据集,避免页面卡死
当你的招聘数据超10万条,load_jobs命令同步执行会阻塞Django主线程,用户点击“刷新”后浏览器转圈10秒。解决方案:Celery异步任务。
安装Celery与Redis(推荐Redis作Broker):
pip install celery redis # 启动Redis:redis-server (默认端口6379)在
dashboard_embedded/tasks.py中定义异步任务:# dashboard_embedded/tasks.py from celery import shared_task from data_processor.management.commands.load_jobs import Command as LoadJobsCommand @shared_task def async_load_jobs(csv_path): """异步执行数据导入""" cmd = LoadJobsCommand() try: cmd.handle(csv_path=csv_path) return {'status': 'success', 'message': '数据导入完成'} except Exception as e: return {'status': 'error', 'message': str(e)}在
dashboard_embedded/views.py中触发异步任务:# views.py from django.http import JsonResponse from .tasks import async_load_jobs def trigger_import(request): if request.method == 'POST': # 从POST获取CSV路径(或上传文件后存临时路径) csv_path = '/tmp/uploaded_jobs.csv' task = async_load_jobs.delay(csv_path) # 异步执行 return JsonResponse({'task_id': task.id, 'status': 'started'})前端用轮询检查任务状态(
dashboard.js中):// 轮询Celery任务状态 async checkTaskStatus(taskId) { const response = await fetch(`/api/task-status/${taskId}/`); const data = await response.json(); if (data.status === 'success') { this.showSuccess('数据导入成功!'); this.refreshAllCharts(); // 重新加载所有图表 } else if (data.status === 'started') { setTimeout(() => this.checkTaskStatus(taskId), 2000); // 2秒后重试 } }
这样,用户点击“导入”后立即得到响应,后台默默处理,处理完自动刷新图表——体验丝滑。
5.3 技巧三:定制化图表主题与导出功能增强
源码包中的Echarts是默认主题,但企业VI要求蓝白主色、公司Logo水印。static/js/dashboard.js已预留扩展点:
// static/js/dashboard.js class DashboardController { constructor() { // 加载自定义主题(从static/css/theme.json) fetch('/static/css/theme.json') .then(r => r.json()) .then(theme => echarts.registerTheme('custom', theme)); this.initCharts(); } initCharts() { // 使用自定义主题初始化 this.charts.city = echarts.init(document.getElementById('chart-city'), 'custom'); // ...其他图表同理 } // 增强导出功能:PNG+PDF(需后端配合) exportChartAsPDF(chartId) { // 调用后端API生成PDF(使用weasyprint或pdfkit) fetch(`/api/export/pdf/?chart=${chartId}`, { method: 'POST', headers: {'X-CSRFToken': getCookie('csrftoken')}, }).then(r => r.blob()).then(blob => { const url = window.URL.createObjectURL(blob); const a = document.createElement('a'); a.href = url; a.download = `招聘看板-${chartId}.pdf`; a.click(); }); } }对应的后端api/views.py中添加导出视图:
# api/views.py from django.http import HttpResponse from weasyprint import HTML import json def export_pdf_view(request): chart_id = request.GET.get('chart') # 根据chart_id获取对应图表数据(复用get_city_distribution等方法) data = get_chart_data(chart_id) # 你自己的函数 # 渲染PDF模板(用weasyprint) html_string = f""" <html><body> <h1>招聘数据看板 - {chart_id}</h1> <img src="data:image/png;base64,{generate_chart_png(chart_id, data)}" /> <p>生成时间:{timezone.now()}</p> </body></html> """ html = HTML(string=html_string) result = html.write_pdf() response = HttpResponse(result, content_type='application/pdf') response['Content-Disposition'] = f'attachment; filename="recruitment-{chart_id}.pdf"' return response从那以后我每次接手新项目,只要涉及数据看板,第一件事就是把这套Django+Echarts结构拷贝过来,删掉
sample_data/,替换data_processor/utils.py里的清洗逻辑,30分钟内就能跑通客户的真实数据。它不追求技术炫酷,但胜在稳定、可读、易改——在交付压力下,这比任何“高大上”的架构都珍贵。希望帮到你。
本文还有配套的精品资源,点击获取