219 Commits

Author SHA1 Message Date
aaa3994305 feat: 态势感知看板全量 mock 适配(page1+page2+page3 共 48 接口)
- 新增 src/mock/{page1,page2,page3,registry}.ts,按 URL 路由 13+10+25 个接口
- request.ts 在 proxyService 顶部加 mock 短路,命中直接返回本地假数据
- .env.development 新增 VITE_USE_MOCK='true',生产环境不设即默认关闭
- 业务代码(API、视图)零改动
2026-06-29 20:27:15 +08:00
c1e9a4be83 chore: sync local changes and add documentation
- Update yarn.lock
- Add project implementation docs in docs/
- Add personal internship experience notes in 实习讲解/
2026-06-29 19:47:30 +08:00
2cfe01694a chore: ignore .obsidian and .claudian local tool directories 2026-06-29 19:41:09 +08:00
fmk1023
98aeef9af8 态势感知平台-登录跳转到态势感知数据中台 2026-03-17 14:53:23 +08:00
d266377
1250268701 态势感知感知平台-武汉市开源生态建设-统计概览2样式修改 2026-02-04 15:03:39 +08:00
d266377
bb55997e01 态势感知平台-开源生态与贡献价值全景-全球开源企业贡献榜排名样式修改 2026-02-04 14:37:17 +08:00
d266377
5fd0e37e71 态势感知平台-武汉市开源生态建设-全国重点城市开源镜像站接口对接 2026-02-04 14:30:55 +08:00
d266377
2c49f9a92a 态势感知平台-武汉市开源生态建设-开放原子校园行接口对接 2026-02-04 14:24:54 +08:00
d266377
efd9493cb0 态势感知平台-武汉市开源生态建设-重点城市高校开源社团数量接口对接 2026-02-04 14:16:10 +08:00
d266377
c5fc4a301b 态势感知平台-武汉市开源生态建设-武汉知名开源社区接口对接 2026-02-04 14:08:33 +08:00
d266377
9ce6bbe07f 态势感知平台-武汉市开源生态建设-城市社区数量排行接口对接 2026-02-04 11:37:29 +08:00
d266377
0901ddd469 态势感知平台-武汉市开源生态建设-武汉明星开发者贡献榜接口对接 2026-02-04 11:11:24 +08:00
d266377
a5c3b03eb8 态势感知平台-武汉市开源生态建设-城市开源人才数量对比接口对接 2026-02-04 10:38:51 +08:00
d266377
46eb668c31 态势感知平台-武汉市开源生态建设-2025武汉优秀开源项目接口对接 2026-02-04 09:51:45 +08:00
d266377
1aaefae7e8 态势感知平台-武汉市开源生态建设-2024武汉优秀开源项目接口对接 2026-02-03 17:34:46 +08:00
d266377
00652dab3b 态势感知平台-武汉市开源生态建设-中国城市开源项目数量对比接口对接 2026-02-03 16:08:25 +08:00
d266377
7553c7ebd0 态势感知平台-武汉市开源生态建设-武汉优质开源项目榜接口对接 2026-02-03 15:17:55 +08:00
d266377
4f58ace30c 态势感知平台-武汉市开源生态建设-湖北高校鸿蒙贡献者接口对接 2026-02-03 14:43:04 +08:00
d266377
40be92afc3 态势感知平台-武汉市开源生态建设-中国高校鸿蒙贡献分布接口对接 2026-02-03 14:24:52 +08:00
d266377
693fb32bb3 态势感知平台-武汉市开源生态建设-武汉企业贡献榜接口对接 2026-02-02 16:14:39 +08:00
d266377
4a6d0137a1 态势感知平台-武汉市开源生态建设-开源企业贡献榜接口对接 2026-02-02 15:57:50 +08:00
d266377
bbad6ce7e2 态势感知平台-武汉市开源生态建设-开源企业分布接口对接 2026-02-02 15:45:10 +08:00
d266377
d280d32326 态势感知平台-开源生态与贡献价值全景卡片样式修改 2026-02-02 15:02:45 +08:00
d266377
42d4f24c4b 态势感知平台-全球开源企业贡献榜和语言与技术竞争样式卡片样式修改 2026-02-02 14:49:49 +08:00
d266377
ce325e8518 态势感知平台-开源生态与贡献价值全景-明星开发者聚焦样式修改 2026-02-02 14:49:00 +08:00
d266377
157048054f 态势感知平台-开源生态与贡献价值全景-社区治理健康度对比样式修改 2026-02-02 14:48:12 +08:00
d266377
1fd3c46b42 态势感知平台-全球开源风险态势感知监控中心-行业分布样式修改 2026-02-02 14:14:04 +08:00
d266377
4b695b932a 态势感知平台-开源生态与贡献价值全景-基础设施覆盖率注释样式修改 2026-02-02 14:08:56 +08:00
d266377
483c6d5c6b 态势感知平台-首页概览的数据格式修改 2026-02-02 09:34:00 +08:00
d266377
3fc27a684b 态势感知平台-武汉市开源生态建设接口字段修改 2026-01-30 16:13:57 +08:00
d266377
b2b04763d3 态势感知平台-武汉市开源生态建设-武汉开发者贡献榜接口对接 2026-01-30 16:13:32 +08:00
d266377
3d487c8d6c 态势感知平台-武汉市开源生态建设-开发者贡献榜接口对接 2026-01-30 15:56:21 +08:00
d266377
613b801da8 态势感知平台-武汉市开源生态建设-开发者分布接口对接 2026-01-30 15:03:12 +08:00
d266377
2affb8296e 态势感知平台-开源生态与贡献价值全景-高校开源社团/俱乐部数量卡片样式修改 2026-01-30 14:51:52 +08:00
d266377
88612f25ef 态势感知平台-开源生态与贡献价值全景-社区治理健康度对比样式修改 2026-01-30 14:48:15 +08:00
d266377
520b030fab 态势感知平台-武汉市开源生态建设-明星开发者聚焦样式修改 2026-01-30 14:34:07 +08:00
d266377
ecc78c82b3 态势感知平台-全球开源风险态势感知监控中心-行业分布样式修改 2026-01-30 14:33:06 +08:00
d266377
82005a43a3 态势感知平台-开源生态与贡献价值全景-基础设施覆盖率标注样式修改 2026-01-30 12:45:00 +08:00
d266377
7b59d3ac9c 态势感知平台-开源生态与贡献价值全景接口字段修改 2026-01-30 11:08:30 +08:00
d266377
15b04e6788 态势感知平台-武汉市开源生态建设接口字段修改 2026-01-30 11:07:58 +08:00
d266377
4ddc7aa90f Merge branch 'SAP' of http://222.20.126.217:3000/JYHlab_ZLZXB/Situation-Awareness-Platform-Frontend into SAP 2026-01-30 10:23:05 +08:00
d266377
c09541afb7 态势感知平台-全球开源风险态势感知监控中心接口字段修改 2026-01-30 10:22:54 +08:00
fmk1023
5c0d46904b Merge remote-tracking branch 'origin/SAP' into SAP
# Conflicts:
#	src/views/Jyh/Community/index.vue
2026-01-30 10:08:44 +08:00
fmk1023
0fcbf2ba73 态势感知平台-自动化服务器部署配置 2026-01-29 18:14:50 +08:00
fmk1023
a832c64d41 态势感知平台-自动化服务器部署配置 2026-01-29 17:58:36 +08:00
d266377
f640eca25c 态势感知平台-武汉市开源生态建设- 武汉鸿蒙项目接口对接 2026-01-29 17:47:25 +08:00
d266377
97ab1a1e79 态势感知平台-武汉市开源生态建设-鸿蒙开源项目接口对接 2026-01-29 17:34:40 +08:00
d266377
33a617ef91 态势感知平台-武汉市开源生态建设-鸿蒙项目城市分布接口对接 2026-01-29 17:28:43 +08:00
d266377
9caa5322ba 态势感知平台-武汉市开源生态建设-统计概览2 接口对接 2026-01-29 17:15:56 +08:00
d266377
3eef874bb8 态势感知平台-武汉市开源生态建设-统计概览1 接口对接 2026-01-29 16:55:18 +08:00
d266377
e51e9a2176 态势感知平台-武汉市开源生态建设接口定义 2026-01-29 14:48:57 +08:00
d266377
e67c386cb7 态势感知平台-开源生态与贡献价值全景-语言与技术竞争接口对接 2026-01-29 11:42:57 +08:00
d266377
bd9decfed8 态势感知平台-开源生态与贡献价值全景接口字段修改 2026-01-29 11:40:58 +08:00
d266377
bf25fcfc92 态势感知平台-开源生态与贡献价值全景-全球开源企业贡献榜top30接口对接 2026-01-29 11:20:01 +08:00
d266377
05cce185e6 态势感知平台-开源生态与贡献价值全景-开源项目热力图接口对接 2026-01-29 10:56:13 +08:00
d266377
2876d23502 态势感知平台-开源生态与贡献价值全景接口字段修改 2026-01-29 10:54:13 +08:00
d266377
5ce6380dea 态势感知平台-开源生态与贡献价值全景-中国开源人才密度地图接口对接 2026-01-29 10:12:33 +08:00
d266377
424a0a1de6 态势感知平台-开源生态与贡献价值全景-开发者活力波形图接口对接 2026-01-29 09:52:43 +08:00
d266377
231d8bd14e 态势感知平台-开源生态与贡献价值全景-明星开发者聚焦接口对接 2026-01-28 17:44:33 +08:00
d266377
6f79cf1f9c 态势感知平台-开源生态与贡献价值全景-社区治理健康度对比接口对接 2026-01-28 17:20:44 +08:00
d266377
89b5a01446 态势感知平台-开源生态与贡献价值全景-高校开源社区/俱乐部数量接口对接 2026-01-28 17:14:33 +08:00
d266377
6c2a33996f 态势感知平台-开源生态与贡献价值全景-中国开源人才密度地图组件抽离 2026-01-28 16:42:01 +08:00
d266377
119b2f0ec5 态势感知平台-开源生态与贡献价值全景-基础设施覆盖率接口对接 2026-01-28 15:41:19 +08:00
d266377
1853de9c3a 态势感知平台-开源生态与贡献价值全景接口定义 2026-01-28 15:28:49 +08:00
d266377
bcab3014ac 态势感知平台-开源生态与贡献价值全景统计概览对接接口 2026-01-28 15:09:53 +08:00
d266377
e66ca622f5 态势感知平台-开源生态与贡献价值全景-明星开发者聚焦组件抽离 2026-01-28 14:54:02 +08:00
d266377
d2603e1a38 态势感知平台-开源生态与贡献价值全景-武汉4项开源数据统计组件抽离 2026-01-28 11:33:12 +08:00
d266377
03a099ea10 态势感知平台-开源生态与贡献价值全景-开发者活力波形图组件抽离 2026-01-28 11:20:44 +08:00
d266377
3ffe0716e6 态势感知平台-开源生态与贡献价值全景-社区治理健康度对比组件抽离 2026-01-28 10:12:57 +08:00
d266377
3ac1ef0d4d 态势感知平台-开源生态与贡献价值全景高校开源社区/俱乐部数量组件抽离 2026-01-28 09:46:59 +08:00
d266377
fa07a197f0 态势感知平台-开源生态与贡献价值全景基础设施覆盖率组件抽离 2026-01-27 17:50:35 +08:00
d266377
f36602bab3 态势感知平台-全球开源风险态势感知监控中心CVE月度趋势接口对接 2026-01-27 16:49:57 +08:00
d266377
e9d8e48514 态势感知平台-全球开源风险态势感知监控中心编程语言安全态势接口对接 2026-01-27 16:40:15 +08:00
d266377
e495e3ec9f 态势感知平台-全球开源风险态势感知监控中心漏洞等级分布对接后端 2026-01-27 16:21:48 +08:00
d266377
3dba3a2189 态势感知中心-全球开源风险态势感知监控中心漏洞语言分布对接接口 2026-01-27 15:49:05 +08:00
d266377
aae51d0b6a 态势感知中心-全球开源风险态势感知监控中心情报流对接接口 2026-01-27 15:27:16 +08:00
d266377
5c8f9273c4 态势感知中心-全球开源风险态势感知中心编程语言安全态势组件抽离 2026-01-27 14:42:50 +08:00
d266377
d0be493658 态势感知平台-全球开源风险态势感知监控中心高危漏洞图组件抽离 2026-01-27 14:40:47 +08:00
d266377
75085051ff 态势感知平台-全球开源风险态势感知监控中心漏洞语言分布图组件抽离 2026-01-27 14:18:30 +08:00
d266377
a8207b44ae 态势感知平台-全球开源风险态势感知监控中心CVE月度趋势图组件抽离 2026-01-27 11:24:49 +08:00
d266377
08ee73c225 Merge branch 'SAP' of http://222.20.126.217:3000/JYHlab_ZLZXB/Situation-Awareness-Platform-Frontend into SAP 2026-01-27 10:50:57 +08:00
d266377
1643ea6e63 态势感知平台-全球开源风险态势感知监控中心高危组件top10接口对接 2026-01-27 10:50:43 +08:00
fmk1023
9fd93c460c 态势感知平台-生产环境接口上下文修改 2026-01-26 18:24:09 +08:00
fmk1023
d62710e910 态势感知平台-生产环境接口上下文修改 2026-01-26 18:11:43 +08:00
fmk1023
f68ced16f8 Merge remote-tracking branch 'origin/SAP' into SAP 2026-01-26 18:03:55 +08:00
fmk1023
5d74295706 态势感知平台-武汉市开源生态建设开源鸿蒙态势分析模块调整 2026-01-26 18:03:44 +08:00
d266377
a70db39a08 态势感知平台-全球开源风险态势感知监控中心-全球威胁情报分析与检测4个接口对接 2026-01-26 17:34:30 +08:00
d266377
d9d6fbdba6 态势感知平台-全球开源风险态势感知监控中心情报流组件补充 2026-01-26 17:32:15 +08:00
d266377
43ca3c13a1 态势感知平台-全球开源风险态势感知监控中心抽离情报流组件 2026-01-26 17:30:14 +08:00
d266377
1085a8f8c8 态势感知中心-全球开源风险态势感知监控中心数据概览优化 2026-01-26 17:25:16 +08:00
d266377
21f068bc9d 态势感知平台-全球开源风险态势感知监控中心行业分布接口对接 2026-01-26 17:23:49 +08:00
fmk1023
a8d8773c63 Merge remote-tracking branch 'origin/SAP' into SAP
# Conflicts:
#	src/views/Jyh/security/index.vue
2026-01-26 15:47:32 +08:00
fmk1023
25f2c6d50a 态势感知平台-全球开源风险态势感知监控中心数据概览接口对接 2026-01-26 15:46:01 +08:00
d266377
181caeb231 Merge branch 'SAP' of http://222.20.126.217:3000/JYHlab_ZLZXB/Situation-Awareness-Platform-Frontend into SAP 2026-01-26 15:40:55 +08:00
d266377
a9e688a4b0 态势感知平台-全球开源风险态势感知监控中心抽离两个组件 2026-01-26 15:40:52 +08:00
fmk1023
666ca8c5a7 态势感知平台-增加武汉鸿蒙项目榜单、武汉开发者贡献榜、武汉企业贡献榜 2026-01-26 10:48:34 +08:00
fmk1023
9f41da5df8 态势感知平台-增加武汉鸿蒙项目榜单、武汉开发者贡献榜、武汉企业贡献榜 2026-01-23 19:08:28 +08:00
fmk1023
4d9f1034d7 Merge remote-tracking branch 'origin/SAP' into SAP
# Conflicts:
#	src/views/Jyh/Community/components/HarmonyEcoCharts.vue
2026-01-23 17:27:21 +08:00
fmk1023
e0281f3b7c 态势感知平台-增加武汉鸿蒙项目榜单 2026-01-23 17:25:10 +08:00
d266377
0aa47a7827 Merge branch 'SAP' of http://222.20.126.217:3000/JYHlab_ZLZXB/Situation-Awareness-Platform-Frontend into SAP 2026-01-23 17:18:14 +08:00
d266377
f113ea5582 态势感知平台-武汉开源城市生态建设页面增加两个榜单 2026-01-23 17:15:31 +08:00
fmk1023
b7f0ce70ae 态势感知平台-数据显示更新 2026-01-23 15:58:36 +08:00
fmk1023
1aef4f7d55 Merge remote-tracking branch 'origin/SAP' into SAP 2026-01-22 17:07:58 +08:00
fmk1023
336b4c2a47 态势感知平台-态势感知接口api定义文件 2026-01-22 17:07:40 +08:00
d266377
c8262ea17d 态势感知平台-态势感知接口api定义文件 2026-01-22 14:54:35 +08:00
fmk1023
944fb16bf4 态势感知平台-态势感知接口api定义文件 2026-01-22 11:37:20 +08:00
fmk1023
b2a702324a 态势感知平台-态势感知接口api定义文件 2026-01-22 11:32:13 +08:00
fmk1023
7e39d3164b Merge branch 'master' into SAP
# Conflicts:
#	src/views/Jyh/Community/index.vue
2026-01-22 10:15:44 +08:00
fmk1023
7b0e00bed5 态势感知平台-增加武汉市开源建设生态页面展示 2026-01-12 17:44:58 +08:00
fmk1023
5f22b7d30e 态势感知平台-武汉市开源建设生态页面数据搜集与修改 2026-01-12 09:20:10 +08:00
fmk1023
d63b9f32bd 态势感知平台-华科开放原子俱乐部Logo增加 2026-01-08 14:55:02 +08:00
fmk1023
cc1514d3c4 态势感知平台-武汉市开源生态建设页面修改,增加武汉开源项目列表、中国城市开源项目数量对比、开源创新联盟优秀项目、城市开源人才数量对比、城市社区数量对比、重点城市高校开源社团数量、各省市开源相关政策发布数量、开放原子校园行·城市活动场次、全国重点城市开源镜像站/节点数量对比等数据可视化展示 2026-01-07 14:05:28 +08:00
fmk1023
907a26cc94 态势感知平台-账号密码登录及验证码接口对接治理平台登录接口 2026-01-05 09:58:49 +08:00
fmk1023
4d9c8ed52c 态势感知平台-增加武汉市开源建设生态页面展示 2026-01-04 15:29:42 +08:00
fmk1023
31da7bdea9 态势感知平台-增加武汉市开源建设生态页面展示 2026-01-04 15:16:50 +08:00
fmk1023
e90006368a 态势感知平台-账号密码登录及验证码接口对接治理平台登录接口 2026-01-04 15:15:51 +08:00
fmk1023
f3b63fdfd9 使用Esbuild压缩、移除Legacy插件、增加optimizeDeps、取消chunk大小计算、提高超大静态资源临界值优化打包速度 2025-12-25 16:11:27 +08:00
fmk1023
29fa1a2199 SAP打包配置修改 2025-12-25 14:54:23 +08:00
fmk1023
b80ff647c9 态势感知平台-项目初始化搭建及菜单修改、增加tabs滚动吸顶效果 2025-12-25 14:36:22 +08:00
d0d9adfde9 Merge branch 'dev' of gitee.com:fumk/open-repo_-portal into dev 2025-12-23 16:14:05 +08:00
81e4f3693b Tcode运营平台改造升级—技术优势部分图片叠加 2025-12-23 16:13:51 +08:00
fmk1023
6fe0681152 Tcode运营平台改造升级-路由跳转增加自动滚动顶部功 2025-12-23 15:35:30 +08:00
e18b165979 Tcode运营平台改造升级—部分图片替换和优化 2025-12-23 10:24:51 +08:00
fmk1023
4edc5527b0 Tcode运营平台改造升级-部分图标替换 2025-12-19 13:48:38 +08:00
fmk1023
b54e84f62e Tcode运营平台改造升级-产品图片优化 2025-12-19 11:28:08 +08:00
fmk1023
8a021d0adc Tcode运营平台改造升级-中国地图配置 2025-12-18 21:14:36 +08:00
fmk1023
5a1b50ba7f Tcode运营平台改造升级-首页样式优化 2025-12-18 20:41:06 +08:00
fmk1023
dc1f4d30ad Tcode运营平台改造升级-态势感知页面样式优化 2025-12-18 20:23:34 +08:00
fmk1023
594e982469 Tcode运营平台改造升级-态势感知页面样式优化 2025-12-18 19:50:16 +08:00
fmk1023
75a2b30776 Tcode运营平台改造升级-态势感知页面样式优化 2025-12-18 19:47:06 +08:00
fmk1023
5e9c7686e5 Tcode运营平台改造升级-全球开源风险态势感知监控中心世界地图修改 2025-12-18 19:34:43 +08:00
fmk1023
abaaeff09e Tcode运营平台改造升级-智能终端应用安全监测平台图片替换 2025-12-18 18:57:56 +08:00
gcw_yTjNWok9
365ebe9964 Merge branch 'dev' of https://gitee.com/fumk/open-repo_-portal into dev 2025-12-18 17:47:31 +08:00
gcw_yTjNWok9
562eae4342 修改图片 2025-12-18 17:47:14 +08:00
djgj
ed608275ed qq 2025-12-18 17:43:34 +08:00
djgj
bd1c1902b0 链路平台 2025-12-18 17:39:12 +08:00
fmk1023
7c87be069f Merge remote-tracking branch 'origin/dev' into dev 2025-12-18 17:30:36 +08:00
fmk1023
785d783226 Tcode运营平台改造升级-Header组件改造 2025-12-18 17:30:10 +08:00
gcw_yTjNWok9
dda0aecfa7 Merge branch 'dev' of https://gitee.com/fumk/open-repo_-portal into dev 2025-12-18 17:15:57 +08:00
gcw_yTjNWok9
38f8b376af feat: 更新金银湖代码安全大模型架构图片和优化显示效果
- 添加新的大模型架构示意图 (1.png)
- 更新 product2.vue 中的图片引用
- 移除图片显示的3秒延迟动画,优化用户体验
- 添加相关的图片资源文件
2025-12-18 17:15:44 +08:00
0f0cbb9998 Tcode运营平台改造升级-文本样式修改 2025-12-18 17:13:53 +08:00
fmk1023
a7d802a40f Tcode运营平台改造升级-定价页面根据UI样式进行美化 2025-12-18 17:08:11 +08:00
798f98e194 Merge branch 'dev' of gitee.com:fumk/open-repo_-portal into dev 2025-12-18 17:05:18 +08:00
adc4a67221 Tcode平台改造升级-图片替换-可信开源态势感知平台 2025-12-18 17:05:02 +08:00
fmk1023
1714534d3b Tcode运营平台改造升级-产品服务页面根据UI样式进行美化 2025-12-18 17:02:53 +08:00
fmk1023
5c793a2759 Tcode运营平台改造升级-文字修改 2025-12-18 16:17:42 +08:00
fmk1023
d9baf96cce Tcode运营平台改造升级-Header组件改造 2025-12-18 16:05:12 +08:00
djgj
e698c4f166 完成ecosystem页面 2025-12-18 14:09:34 +08:00
gcw_yTjNWok9
e3ee78b9f4 更改文件路径 2025-12-18 14:02:38 +08:00
gcw_yTjNWok9
12cf936916 地图部分修改 2025-12-18 13:46:53 +08:00
gcw_yTjNWok9
42aa1eda2b 解决合并冲突,使用远程版本 2025-12-18 13:39:13 +08:00
gcw_yTjNWok9
7e6321a7b9 删除第一张风险监控地图(SecurityRiskMap)及相关代码 2025-12-18 13:37:44 +08:00
8cab400008 Tcode平台改造升级-编程语言安全态势图表 2025-12-18 11:31:18 +08:00
e79572cd34 Tcode平台改造升级-漏洞等级分布图表 2025-12-18 11:22:26 +08:00
678dd3842a Tcode平台改造升级-漏洞语言分布 2025-12-18 11:06:07 +08:00
be36557c0c Tcode平台改造升级-情报流图表 2025-12-18 10:51:53 +08:00
634726b75e Tcode平台改造升级-CVE 月度趋势图表 2025-12-18 10:36:08 +08:00
b51d981d83 Tcode平台改造升级-行业分布图表 2025-12-18 10:12:13 +08:00
db4ad5744c Tcode平台改造升级-高危组件 Top 10 图表 2025-12-18 10:06:41 +08:00
fmk1023
bb17c69ba9 Tcode平台改造升级-产品图片样式优化 2025-12-18 09:30:53 +08:00
fmk1023
58579b7ad3 Tcode平台改造升级-开源生态与贡献价值全景页面路由修改 2025-12-18 09:30:34 +08:00
fmk1023
8885af40c2 Tcode平台改造升级-全球开源风险态势感知监控中心页面路由修改 2025-12-18 09:22:15 +08:00
djgj
2906b515e4 更新了四个产品平台的内容和样式 2025-12-17 16:44:38 +08:00
ffab7ede4a Tcode文稿修改-产品服务详情页部分 2025-12-17 15:38:52 +08:00
fmk1023
43ab6ec1c3 Tcode平台改造升级-header组件修改 2025-12-16 14:14:38 +08:00
fmk1023
dc09f426b4 Merge remote-tracking branch 'origin/dev' into dev
# Conflicts:
#	src/components/Header/index.vue
2025-12-16 14:14:22 +08:00
fmk1023
8f208d4b73 Tcode平台改造升级-header组件修改 2025-12-16 14:10:28 +08:00
djgj
a812a96d82 简化导航栏文字,合并某些服务详情 2025-12-16 10:20:00 +08:00
djgj
59d5988e2a Merge branch 'master' into dev
t merge master
2025-12-15 17:18:54 +08:00
djgj
168a885a7d 更新产品四(智能终端安全监测平台)的服务描述:极速扫描、深度分析、持续免疫、威胁情报四大服务 2025-12-15 17:12:33 +08:00
fmk1023
d8a8629c4b Merge remote-tracking branch 'origin/dev' into dev 2025-12-15 16:58:27 +08:00
fmk1023
ca7eee0fc2 Tcode平台改造升级-首页页轮播图固定一个图展示 2025-12-15 16:58:17 +08:00
722a4c2a30 Tcode文稿修改-智能终端应用安全监测平台(源端) 2025-12-15 16:46:40 +08:00
ea34772999 Tcode文稿修改-供应链安全检测系统(源链) 2025-12-15 16:41:00 +08:00
5114f2cfee Tcode文稿修改-金银湖代码安全大模型(源鉴) 2025-12-15 16:30:15 +08:00
a8b8192ed4 Tcode文稿修改-可信开源态势感知平台(源感) 2025-12-15 16:12:59 +08:00
fmk1023
68ef282e65 Tcode平台改造升级-首页菜单修改 2025-12-15 11:14:37 +08:00
fmk1023
1ee6a6c330 Tcode平台改造升级-登录跳转相关修改 2025-12-12 17:56:36 +08:00
fmk1023
ea2bd0aa5a Tcode平台改造升级-AI助手增加工具菜单 2025-12-12 15:14:50 +08:00
fmk1023
97dbec995f Tcode平台改造升级-AI助手增加工具菜单 2025-12-12 09:22:31 +08:00
fmk1023
51078acc1d Tcode平台改造升级-产品服务页面修改开发 2025-12-11 18:27:57 +08:00
fmk1023
d6b5456e43 Tcode平台改造升级-武汉市开源建设生态页面开发 2025-12-11 17:32:06 +08:00
fmk1023
0f84f23c63 Tcode平台改造升级-首屏样式修改开发 2025-12-11 15:28:46 +08:00
fmk1023
f13c2d06b3 Tcode平台改造升级-首屏样式修改开发 2025-12-11 15:22:27 +08:00
fmk1023
6bd1db5ed7 Tcode平台改造升级-态势感知指挥中心页面优化,使用滚动模式会使页面短暂白屏 2025-12-11 11:23:03 +08:00
fmk1023
6c477d923b Tcode平台改造升级-LinePage页面完成 2025-12-11 10:08:18 +08:00
fmk1023
a61dbfd821 Tcode平台改造升级-LinePage页面完成 2025-12-10 18:28:55 +08:00
fmk1023
7924714d0e Tcode平台改造升级-设置相关配置完成 2025-12-10 16:11:05 +08:00
fmk1023
a630f40b76 Tcode平台改造升级-删除多余备注 2025-12-10 15:46:18 +08:00
fmk1023
61f8e017de Tcode平台改造升级-用户工作台试用页面及支付页面开发 2025-12-10 15:25:35 +08:00
fmk1023
356810f316 Tcode平台改造升级-用户工作台页面样式优化及邀请你的团队功能开发 2025-12-10 14:34:34 +08:00
fmk1023
fd4f9bc73d Tcode平台改造升级-用户工作台页面样式优化及邀请你的团队功能开发 2025-12-10 14:34:29 +08:00
fmk1023
44467bc22a Tcode平台改造升级-产品服务详情页面开发 2025-12-10 10:27:52 +08:00
fmk1023
e8eabfab9f Tcode平台改造升级-产品服务详情页面开发 2025-12-10 10:26:48 +08:00
fmk1023
c249b9d70f Tcode平台改造升级-态势感知中心页面开发及菜单栏展示修改 2025-12-09 18:12:33 +08:00
fmk1023
d72546c81b Tcode平台改造升级-态势感知中心页面开发 2025-12-09 14:56:29 +08:00
fmk1023
87c2777923 Tcode平台改造升级-控制台登录组件开发及用户工作台页面开发 2025-12-09 14:31:11 +08:00
fmk1023
af7578ec2a Tcode平台改造升级-项目名称openRepoPortalV2改为tcode 2025-12-05 17:48:00 +08:00
fmk1023
1e2d8f775d Tcode平台改造升级-安全事件中心页面开发 2025-12-05 16:54:27 +08:00
fmk1023
e7150457c4 Tcode平台改造升级-漏洞数据库页面开发 2025-12-05 16:15:39 +08:00
fmk1023
5550b0fb7f Tcode平台改造升级-漏洞数据库页面开发 2025-12-05 16:08:59 +08:00
fmk1023
28ea0cc27f Tcode平台改造升级-开源服务专区菜单组件完成 2025-12-05 11:36:11 +08:00
fmk1023
18319e66c5 Tcode平台改造升级-关于我们页面开发 2025-12-05 10:54:13 +08:00
fmk1023
b032e2b074 Tcode平台改造升级-定价页面设计与前端开发 2025-12-04 17:29:35 +08:00
fmk1023
63d4751838 Tcode平台改造升级-解决方案页面开发 2025-12-04 14:58:14 +08:00
fmk1023
c7de332275 Tcode平台改造升级-底部页脚信息模块开发 2025-12-04 13:58:20 +08:00
fmk1023
fcb247395b Tcode平台改造升级-产品服务菜单修改 2025-12-04 11:22:28 +08:00
fmk1023
9e86d1e6bc Tcode平台改造升级-系统菜单框架完成、首页页面开发 2025-12-04 10:14:46 +08:00
fmk1023
e76110239d Tcode平台改造升级-header头组件开发完成 2025-12-03 11:21:49 +08:00
fmk1023
86d26f161a Tcode平台改造升级-AI助手菜单显示功能修改 2025-12-03 09:53:16 +08:00
fmk1023
e01a050f80 Tcode平台改造升级-页面字段展示优化及定价页面路由创建 2025-12-01 10:20:14 +08:00
fmk1023
4440333736 Tcode平台改造升级-页面字段展示优化 2025-11-28 17:56:28 +08:00
fmk1023
d7dfa9bf8d openRepoPortalV2打包命名修改 2025-11-28 14:57:04 +08:00
fmk1023
0ab4649909 安全检测样式优化 2025-11-28 14:48:13 +08:00
fmk1023
ae00eb3d6f 首页样式优化 2025-11-28 14:32:07 +08:00
fmk1023
cf25a6feb1 TODO:安全态势地址修改 2025-11-28 09:05:28 +08:00
fmk1023
0d266e8a03 可信代码库V2版-首页展示页面原型开发 2025-11-19 17:39:35 +08:00
fmk1023
f1f05c11f6 可信代码库V2版-首页展示页面原型开发 2025-11-11 14:48:43 +08:00
fmk1023
f5b59fadd9 可信代码库V2版-安全智库页面原型开发、安全检测中心页面原型开发、关于我们页面原型开发 2025-11-06 10:54:58 +08:00
58 changed files with 15749 additions and 2987 deletions

2
.env
View File

@@ -1,4 +1,4 @@
VITE_APP_TITLE = '可信开源代码库' VITE_APP_TITLE = 'Tcode'
VITE_TEST_REPOID = 34 VITE_TEST_REPOID = 34
VITE_SECRET_KEY = 'SA!nUNPZ5o!OSV&B' VITE_SECRET_KEY = 'SA!nUNPZ5o!OSV&B'

View File

@@ -5,10 +5,10 @@ VITE_ENV = 'development'
VITE_HOST = 'https://test.gitcode.net' VITE_HOST = 'https://test.gitcode.net'
# 接口地址 # 接口地址
VITE_API_HOST = '/gateway/official' VITE_API_HOST = 'http://222.20.126.217:8100/'
# devPress接口地址 # devPress接口地址
VITE_DEV_API_HOST = '/gateway/official' VITE_DEV_API_HOST = 'http://222.20.126.217:8100/'
# devPress地址 # devPress地址
VITE_DEV_HOST = 'https://test.gitcode.net' VITE_DEV_HOST = 'https://test.gitcode.net'
@@ -41,3 +41,6 @@ VITE_BASE_URL=/
VITE_AI_API_HOST = 'http://222.20.126.208:8087' VITE_AI_API_HOST = 'http://222.20.126.208:8087'
# 治理平台地址 # 治理平台地址
VITE_GOVERNANCE_HOST = 'http://222.20.126.217:10010/governancePortal' VITE_GOVERNANCE_HOST = 'http://222.20.126.217:10010/governancePortal'
# 启用 mock 模式(内网不可达时使用本地假数据)
VITE_USE_MOCK = 'true'

View File

@@ -39,7 +39,7 @@ VITE_USER_CENTER_HOST = 'https://local.gitcode.net:8002'
# ai-copilot 子应用 # ai-copilot 子应用
VITE_COPILOT_HOST = 'https://local.gitcode.net:8003/child/aichat-app' VITE_COPILOT_HOST = 'https://local.gitcode.net:8003/child/aichat-app'
VITE_BASE_URL=/openRepoPortal/ VITE_BASE_URL=/SAP/
# AI大模型接口地址 # AI大模型接口地址
VITE_AI_API_HOST = 'http://222.20.126.208:8089' VITE_AI_API_HOST = 'http://222.20.126.208:8089'

View File

@@ -37,7 +37,7 @@ VITE_USER_CENTER_HOST = 'https://pre-usercenter-app.gitcode.com'
# ai-copilot 子应用 # ai-copilot 子应用
VITE_COPILOT_HOST = 'https://pre-copilot-app.gitcode.com' VITE_COPILOT_HOST = 'https://pre-copilot-app.gitcode.com'
VITE_BASE_URL=/openRepoPortal/ VITE_BASE_URL=/SAP/
# AI大模型接口地址 # AI大模型接口地址
VITE_AI_API_HOST = 'http://222.20.126.208:8089' VITE_AI_API_HOST = 'http://222.20.126.208:8089'

View File

@@ -2,13 +2,13 @@
VITE_ENV = 'production' VITE_ENV = 'production'
# 主域名 # 主域名
VITE_HOST = 'http://222.20.126.217:10010/openRepoPortal' VITE_HOST = 'http://222.20.126.217:10010/SAP'
# 接口地址 # 接口地址
VITE_API_HOST = '/gateway/official' VITE_API_HOST = 'http://222.20.126.217:8100/'
# devPress接口地址 # devPress接口地址
VITE_DEV_API_HOST = '/gateway/official' VITE_DEV_API_HOST = 'http://222.20.126.217:8100/'
# devPress地址 # devPress地址
VITE_DEV_HOST = 'https://gitcode.com' VITE_DEV_HOST = 'https://gitcode.com'
@@ -26,7 +26,7 @@ VITE_CSDN_URL = 'https://web-api.gitcode.com/oauth/login/csdn'
VITE_ORG_COMMUNITY = 'https://gitcode.com' VITE_ORG_COMMUNITY = 'https://gitcode.com'
# 静态资源域名 # 静态资源域名
VITE_STATIC_HOST = 'http://222.20.126.217:10010/openRepoPortal' VITE_STATIC_HOST = 'http://222.20.126.217:10010/tcode'
# 主页搜索子应用 # 主页搜索子应用
VITE_CHILD_HOMEWEB_HOST = 'https://homepage-app.gitcode.com' VITE_CHILD_HOMEWEB_HOST = 'https://homepage-app.gitcode.com'
@@ -37,7 +37,7 @@ VITE_USER_CENTER_HOST = 'https://usercenter-app.gitcode.com'
# ai-copilot 子应用 # ai-copilot 子应用
VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com' VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com'
VITE_BASE_URL=/openRepoPortal/ VITE_BASE_URL=/SAP/
# AI大模型接口地址 # AI大模型接口地址
VITE_AI_API_HOST = 'http://222.20.126.208:8087' VITE_AI_API_HOST = 'http://222.20.126.208:8087'

8
.gitignore vendored
View File

@@ -12,7 +12,7 @@ lerna-debug.log*
node_modules node_modules
.DS_Store .DS_Store
dist dist
openRepoPortal SAP
nginx nginx
dist-ssr dist-ssr
coverage coverage
@@ -39,3 +39,9 @@ components.d.ts
# 组件汇总文件,仅限开发本地浏览 # 组件汇总文件,仅限开发本地浏览
component.md component.md
./deploy.js
# 本地工具配置目录
.obsidian/
.claudian/

View File

@@ -1,4 +1,4 @@
# 可信开源代码库 # Tcode
架构:基于[create-vue](https://github.com/vuejs/create-vue)构建vite+vue3+pinia+scss 架构:基于[create-vue](https://github.com/vuejs/create-vue)构建vite+vue3+pinia+scss
组件库:[Vue DevUI](https://vue-devui.github.io/) 组件库:[Vue DevUI](https://vue-devui.github.io/)

46
china.js Normal file

File diff suppressed because one or more lines are too long

75
deploy.js Normal file
View File

@@ -0,0 +1,75 @@
import { Client } from 'ssh2';
import fs from 'fs';
import path from 'path';
import { fileURLToPath } from 'url';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const serverConfig = {
host: '222.20.126.217',
port: 22,
username: 'admini',
password: 'zlzxb@1888',
remotePath: '/home/admini/ossrpp/base/nginx/dist/SAP/'
};
async function uploadDirectory(sftp, localDir, remoteDir) {
const items = fs.readdirSync(localDir);
// 确保远程目录存在
try {
await new Promise((res) => sftp.mkdir(remoteDir, { mode: '0755' }, () => res()));
} catch (e) {}
for (const item of items) {
const localPath = path.join(localDir, item);
const remotePath = path.join(remoteDir, item).replace(/\\/g, '/'); // 转换为Linux路径
if (fs.statSync(localPath).isDirectory()) {
await uploadDirectory(sftp, localPath, remotePath);
} else {
await new Promise((resolve, reject) => {
console.log(`🚀 上传中: ${item}`);
sftp.fastPut(localPath, remotePath, (err) => {
if (err) reject(err);
else resolve();
});
});
}
}
}
async function startDeploy() {
const conn = new Client();
console.log('✨ 开始部署流程...');
conn.on('ready', () => {
console.log('✅ SSH 连接成功!');
conn.sftp(async (err, sftp) => {
if (err) throw err;
try {
const localDist = path.join(__dirname, 'SAP');
await uploadDirectory(sftp, localDist, serverConfig.remotePath);
console.log('🎉 所有文件上传成功!');
conn.end();
} catch (uploadErr) {
console.error('❌ 上传失败:', uploadErr);
conn.end();
}
});
}).on('error', (err) => {
console.error('❌ 连接出错 (检查IP或密码):', err.message);
}).on('close', () => {
console.log('🔌 连接已关闭');
process.exit(0);
}).connect({
host: serverConfig.host,
port: serverConfig.port,
username: serverConfig.username,
password: serverConfig.password,
readyTimeout: 10000 // 10秒超时
});
}
startDeploy();

View File

@@ -0,0 +1,364 @@
# AI 智能助手功能 — 面试版
> 简历原话:**"集成 @matechat/core 实现 AI 智能助手功能,支持多轮对话、思考过程展示、组件化消息渲染(软件详情/预警列表/智能推荐)"**
>
> 这篇文档帮你理解这句话背后到底做了什么,以及面试时怎么讲。
---
## 一、先搞清楚:这句话到底是什么意思?
拆成四部分理解:
| 关键词 | 含义 | 项目中对应 |
|--------|------|------------|
| **集成 @matechat/core** | 使用华为 MateChat 组件库提供聊天 UI | McBubble气泡、McInput输入框、McMarkdownCardMarkdown 渲染)等组件 |
| **多轮对话** | 用户可以连续提问AI 记住上下文 | 前端创建 conversationId后端维护对话历史每次请求带上 conversationId |
| **思考过程展示** | AI 回答前展示它"想"了什么步骤 | 后端返回 states 对象,前端解析为"意图分析→SQL判断→SQL执行"步骤展示 |
| **组件化消息渲染** | 不同类型的 AI 回答用不同组件展示 | 5 种消息类型:普通文本、开源软件列表、预警列表、软件详情、数据表格 |
**一句话概括**:我用 MateChat 组件库搭建了 AI 聊天界面实现了多轮对话9 个 API、思考过程可视化、以及 5 种消息类型的组件化渲染。
---
## 二、整体架构
```
┌─────────────────────────────────────────────────┐
│ AI 助手页面(/ai
│ │
│ ┌──────────┐ ┌──────────────────────────────┐ │
│ │ 左侧边栏 │ │ 对话区域 │ │
│ │ │ │ ┌──────────────────────────┐ │ │
│ │ 历史会话列表│ │ │ McBubble用户消息 │ │ │
│ │ 收藏会话 │ │ └──────────────────────────┘ │ │
│ │ 搜索/删除 │ │ ┌──────────────────────────┐ │ │
│ │ 批量管理 │ │ │ McBubbleAI回复 │ │ │
│ │ │ │ │ ├── 思考过程(可折叠) │ │ │
│ │ │ │ │ ├── OssList软件列表 │ │ │
│ │ │ │ │ ├── WarningList预警列表 │ │ │
│ │ │ │ │ ├── OssDetail软件详情 │ │ │
│ │ │ │ │ ├── AIList数据表格 │ │ │
│ │ │ │ │ └── OtherMarkdown文本 │ │ │
│ │ │ │ └──────────────────────────┘ │ │
│ │ │ │ ┌──────────────────────────┐ │ │
│ │ │ │ │ 猜你想问(推荐问题) │ │ │
│ │ │ │ └──────────────────────────┘ │ │
│ │ │ │ ┌──────────────────────────┐ │ │
│ │ │ │ │ McInput输入框 │ │ │
│ │ │ │ └──────────────────────────┘ │ │
│ └──────────┘ └──────────────────────────────┘ │
└─────────────────────────────────────────────────┘
```
---
## 三、@matechat/core 用在了哪里?
MateChat 是华为开源的 AI 聊天 UI 组件库。项目中用了这些组件:
| 组件 | 用在哪 | 干什么 |
|------|--------|--------|
| `McBubble` | 对话区域 | 消息气泡(区分用户/AI支持 loading 状态) |
| `McInput` | 底部输入框 | 文本输入,支持字数限制、回车发送、清空 |
| `McMarkdownCard` | AI 文本回复 | 渲染 Markdown 格式的 AI 回答 |
| `McLayout` / `McLayoutContent` / `McLayoutSender` | 弹窗版 AI | KnowledgeHub 的 AI 弹窗布局 |
全局注册后直接使用:
```typescript
// main.ts
import MateChat from '@matechat/core';
app.use(MateChat);
```
```vue
<!-- 直接用不需要 import -->
<McBubble :content="msg.content" :align="'right'" /> <!-- 用户消息靠右 -->
<McBubble :loading="true"> <!-- AI 回复靠左 + loading -->
<Other :content="msg.content" />
</McBubble>
<McInput @submit="onSubmit" /> <!-- 底部输入框 -->
```
---
## 四、怎么实现的?(面试核心)
### 4.1 多轮对话
**问题**用户连续问多个问题AI 要记住之前的对话内容。
**解决方案**:用 conversationId 串联整个对话,后端维护历史。
```
用户发第一条消息
前端没有 conversationId → 调用 createConversation API → 获得 conversationId
前端发送消息:{ userId, conversationId, question }
后端根据 conversationId 找到历史消息 → AI 结合历史回答
用户发第二条消息 → 带同一个 conversationId → 后端知道上下文
```
```typescript
// 1. 首次对话:创建会话
const CONV_ID = ref('');
const createConversation = async () => {
const { data } = await fetchCreateConversation({ userId: username });
CONV_ID.value = data.data.conversationId; // 保存会话 ID
};
// 2. 发送消息:带上 conversationId
const getAIAnswer = async (question) => {
const { data } = await fetchChatUseSql({
userId: username,
conversationId: CONV_ID.value, // ← 后端靠这个找到对话历史
question: question,
});
// 处理 AI 回复...
};
// 3. 切换历史会话:加载旧对话
const handleSelectConv = (convId) => {
CONV_ID.value = convId;
getConversationDetail(convId); // 从后端加载完整对话记录
};
```
**会话管理功能**(左侧边栏):
- 查看历史/收藏会话列表
- 搜索会话
- 重命名会话
- 收藏/取消收藏
- 删除会话
- 批量管理
---
### 4.2 思考过程展示
**问题**AI 回答时用户看不到它在干什么,体验像"黑盒"。
**解决方案**:后端返回 `states` 对象,前端解析为步骤列表展示。
```typescript
// 后端返回的数据包含 states
const { text, type, states } = row;
// 解析思考步骤
const processThinkStates = (states) => {
let thinkProcess = []; // 主步骤
let steps = []; // 子步骤
Object.keys(states).forEach(key => {
if (key === 'steps') {
// 子步骤
Object.keys(states[key]).forEach(subKey => {
steps.push({ name: subKey, status: states[key][subKey] });
});
} else {
// 主步骤意图分析、SQL有效性判断、SQL执行
thinkProcess.push({ name: key, status: states[key] });
}
});
return { thinkProcess, steps };
};
```
展示效果:
```
▼ 思考过程:
✓ 意图分析 成功
✓ SQL有效性判断 成功
✓ SQL执行 成功
✓ 数据查询 成功
✓ 结果格式化 成功
```
用户可以点击折叠/展开思考过程。每个步骤显示绿色"成功"或红色"失败"。
---
### 4.3 组件化消息渲染5 种消息类型)
**问题**AI 的回答不只是纯文本,还可能是表格、软件详情等不同格式。
**解决方案**:根据后端返回的 `type` 字段,用不同组件渲染。
```vue
<template v-for="(msg, idx) in messages" :key="idx">
<!-- 用户消息纯文本气泡 -->
<McBubble v-if="msg.role === 'user'" :content="msg.content" />
<!-- AI 消息根据 type 选择不同组件 -->
<McBubble v-else :loading="msg.loading">
<!-- 思考过程可折叠 -->
<div class="think-toggle-btn" @click="toggleThink(idx)">...</div>
<!-- 根据 type 渲染不同内容 -->
<OssList v-if="msg.type === 1" /> <!-- 开源软件列表 -->
<WarningList v-else-if="msg.type === 2" /> <!-- 预警列表 -->
<OssDetail v-else-if="msg.type === 'critical_software_info'" :id="msg.id" /> <!-- 软件详情 -->
<AIList v-else-if="msg.type === 'df'" :content="msg.content" /> <!-- 数据表格 -->
<Other v-else :content="msg.content" /> <!-- Markdown 文本 -->
</McBubble>
</template>
```
**5 种消息类型详解**
| type 值 | 渲染组件 | 用户看到什么 |
|---------|----------|------------|
| `1` | OssList | 开源软件列表表格(有分页、导出功能) |
| `2` | WarningList | 预警列表表格(有严重程度标签:高危/中危/低危) |
| `'critical_software_info'` | OssDetail | 软件详情页面(嵌入完整的软件详情组件) |
| `'df'` | AIList | 通用数据表格动态列名、JSON 解析) |
| `'text'` | Other | Markdown 格式的文本回答 |
| `'sql_error'` | Other | 错误信息(带错误图标) |
**type 判断逻辑**(后端返回 `df` 类型时还要二次判断):
```typescript
if (type === 'df') {
// 默认当普通表格
msg.type = 'df';
msg.content = row[type];
// 但如果表名是 critical_software_info 且只有一条数据 → 当软件详情
if (table_name.length === 1 && table_name[0] === 'critical_software_info') {
let arr = JSON.parse(row[type]);
if (arr.length === 1) {
msg.type = 'critical_software_info'; // 升级为软件详情
msg.id = arr[0].id;
msg.software_name = arr[0].software_name;
}
}
}
```
---
### 4.4 猜你想问(推荐问题)
AI 回答后,自动推荐相关后续问题:
```typescript
// AI 回答后,调用推荐问题 API
const fetchFollowupQuestions = async (question, df_id) => {
const { data } = await FetchFollowupQuestions({ question, df_id });
if (data.code === 200) {
followupQuestions.value = data.data; // 更新推荐问题列表
}
};
// 用户点击推荐问题 → 直接发送
<a v-for="q in followupQuestions" @click="onSubmit(q)">
<span>{{ q.question }}</span>
</a>
```
---
### 4.5 AI API 接口总览
项目对接了 9 个 AI 后端接口:
| 接口 | 干什么 | 调用时机 |
|------|--------|----------|
| `createConversation` | 创建新会话 | 用户发第一条消息时 |
| `chat_use_sql` | 发送消息/获取 AI 回复 | 每次用户发送消息 |
| `conversationList` | 获取会话列表 | 打开侧边栏时 |
| `conversationDetail` | 获取会话详情 | 点击历史会话时 |
| `provide_followup_questions` | 获取推荐问题 | AI 回答后 |
| `changeConversationTitle` | 重命名会话 | 用户编辑会话名 |
| `toggleConversationCollect` | 收藏/取消收藏 | 用户点击收藏 |
| `deleteConversation` | 删除会话 | 用户删除 |
| `update_recommend_questions` | 刷新推荐问题 | 用户点击"换一批" |
---
## 五、整体架构总结
```
@matechat/core 提供McBubble气泡+ McInput输入框+ McMarkdownCardMarkdown渲染
搭建聊天界面骨架
在 McBubble 内部渲染自定义内容组件:
├── OssList软件列表 ← type=1
├── WarningList预警列表 ← type=2
├── OssDetail软件详情 ← type='critical_software_info'
├── AIList数据表格 ← type='df'
└── OtherMarkdown文本 ← 默认
9 个 API 接口对接后端
conversationId 串联多轮对话
states 对象解析为思考过程
```
---
## 六、面试问答准备
### Q1"集成 @matechat/core 实现 AI 智能助手"具体做了什么?
> 我用华为开源的 MateChat 组件库搭建了 AI 聊天界面,核心组件是 McBubble消息气泡和 McInput输入框。实现了三个关键功能第一多轮对话通过 conversationId 串联整个对话,后端维护历史上下文,前端对接了 9 个 API 接口管理会话;第二,思考过程展示,后端返回 states 对象,前端解析为"意图分析→SQL判断→SQL执行"的步骤列表,支持折叠展开;第三,组件化消息渲染,根据后端返回的 type 字段,用 5 种不同组件渲染 AI 回复——开源软件列表、预警列表、软件详情、数据表格、Markdown 文本。
### Q2多轮对话怎么实现的
> 前后端配合。用户发第一条消息时,前端调 createConversation API 获取 conversationId。之后每条消息都带上这个 conversationId后端根据它找到整个对话历史AI 结合上下文回答。切换历史会话时,调 conversationDetail API 加载完整对话记录。前端还有完整的会话管理:历史列表、收藏、搜索、重命名、删除、批量管理。
### Q3思考过程展示怎么做的
> 后端 AI 接口返回的数据里有一个 states 对象包含主步骤意图分析、SQL有效性判断、SQL执行和子步骤。我写了一个 processThinkStates 函数解析这个对象,转换为 `{name, status}` 数组,存储在消息对象的 thinkProcess 和 steps 字段里。前端用可折叠的 UI 展示,每个步骤显示成功(绿色)或失败(红色)。这个功能是自定义实现的,不是 MateChat 组件库自带的。
### Q4组件化消息渲染是什么意思
> AI 的回答不只是纯文本,还可能是软件列表、预警表格、软件详情等。我根据后端返回的 type 字段判断消息类型,用 5 种不同组件渲染type=1 用 OssList 展示开源软件列表带分页和导出type=2 用 WarningList 展示预警列表带严重程度标签type='critical_software_info' 用 OssDetail 展示软件详情type='df' 用 AIList 展示通用数据表格,其他情况用 Other 组件渲染 Markdown 文本。所有这些组件都嵌在 MateChat 的 McBubble 气泡里。
### Q5为什么选 MateChat 而不是自己写聊天 UI
> 两个原因第一MateChat 提供了开箱即用的聊天气泡、输入框、Markdown 渲染等组件不用从零实现开发效率高第二MateChat 是华为开源的,和我们平台的技术方向一致,组件风格也和 DevUI 统一。不过思考过程展示、组件化消息渲染这些核心功能是自定义实现的MateChat 主要提供了 UI 骨架。
### Q6AI 后端是怎么连接的SSE 还是 WebSocket
> 用的是标准的 HTTP POST 请求,不是 SSE 也不是 WebSocket。每次用户发消息前端发一个 POST 请求,后端处理完返回完整响应。代码里有模拟流式输出的注释代码(用 setTimeout 逐字显示),但生产环境用的是同步请求-响应模式。
---
## 七、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| MateChat 组件库版本 | 1.4.0 |
| 使用的 MateChat 组件 | 5 种McBubble、McInput、McMarkdownCard、McLayout 系列) |
| 自定义消息组件 | 5 种OssList、WarningList、OssDetail、AIList、Other |
| AI API 接口数量 | 9 个 |
| 消息类型 | 6 种text、df、critical_software_info、1、2、sql_error |
| 思考过程步骤 | 3 个主步骤 + N 个子步骤 |
| 会话管理功能 | 7 个(列表/搜索/重命名/收藏/删除/批量管理/历史限制) |
---
## 八、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| AI 助手主页面(生产版) | `src/views/Jyh/AI/Home/index.vue` |
| 软件列表组件 | `src/views/Jyh/AI/Home/components/OssList.vue` |
| 预警列表组件 | `src/views/Jyh/AI/Home/components/WarningList.vue` |
| 软件详情组件 | `src/views/Jyh/AI/Home/components/OssDetail.vue` |
| 数据表格组件 | `src/views/Jyh/AI/Home/components/AIList.vue` |
| Markdown 文本组件 | `src/views/Jyh/AI/Home/components/Other.vue` |
| 猜你想问组件 | `src/views/Jyh/AI/Home/GuessYouWantToAsk.vue` |
| 功能轮播组件 | `src/views/Jyh/AI/Home/SwiperComponent.vue` |
| 会话历史侧边栏 | `src/views/Jyh/AI/Home/components/ViewHistoryAside/ViewHistoryAsideNew.vue` |
| 批量管理弹窗 | `src/views/Jyh/AI/Home/components/ViewHistoryAside/BatchManageModal.vue` |
| AI 布局 | `src/layouts/AILayout/index.vue` |
| AI 头部导航 | `src/components/Header/AIHeader.vue` |
| AI API 接口定义 | `src/api/jyh/index.ts`380-460 行) |
| KnowledgeHub AI 弹窗 | `src/views/Jyh/KnowledgeHub/Components/AIModal.vue` |
| AI 修复建议页面 | `src/views/Jyh/AIRepair/index.vue` |

View File

@@ -0,0 +1,768 @@
# Composition API Hooks 逻辑复用 — 面试版
> 简历原话:**"封装 Composition API Hooks 实现逻辑复用"**
>
> 这篇文档帮你理解项目里到底封装了哪些 Hooks、怎么复用的以及面试时怎么讲。
---
## 一、先搞清楚:什么是 Hooks为什么要封装
### 问题场景
```
没有 Hooks 的时候:
页面A需要响应式布局 → 写一堆 width 判断逻辑
页面B也需要响应式布局 → 再写一遍 width 判断逻辑
页面C也需要响应式布局 → 又写一遍...
页面D需要获取仓库权限 → 调 API、存 ref、写 computed
页面E也需要获取仓库权限 → 同样的代码再写一遍...
```
**痛点**:相同的逻辑散落在多个组件里,改了 A 忘改 B代码臃肿、维护困难。
### 有了 Hooks 之后
```
封装一个 usePageResize() Hook
页面A → const { widthType } = usePageResize()
页面B → const { widthType } = usePageResize()
页面C → const { widthType } = usePageResize()
26 个文件复用同一套逻辑,改 Hook 一处,全局生效
```
**一句话概括**:把可复用的响应式逻辑封装成独立的 `useXxx()` 函数,哪个组件需要就直接调用,避免重复代码。这是 Vue3 Composition API 最核心的设计思想。
### 项目中 Hooks 的定位
```
┌────────────────────────────────────────────────────────┐
│ Vue 组件层 │
│ Security/index.vue Ecosystem/index.vue AI/index.vue │
└────────┬───────────┬───────────┬──────────────────────┘
│ │ │ 调用 useXxx()
▼ ▼ ▼
┌────────────────────────────────────────────────────────┐
│ Hooks 逻辑层30+ 个) │
│ usePageResize useRepoId usePagination useLogin ... │
└────────┬───────────┬───────────┬──────────────────────┘
│ │ │
▼ ▼ ▼
┌────────────────────────────────────────────────────────┐
│ Pinia Store / API / Router │
│ (底层数据源和基础能力) │
└────────────────────────────────────────────────────────┘
```
---
## 二、项目里有 30+ 个 Hooks分六大类
| 分类 | Hooks | 数量 | 核心思想 |
|------|-------|------|----------|
| **通用交互** | usePageResize、useShowMore、useModel、usePagination、useLazyImport | 5+ | 跨组件复用的 UI 交互逻辑 |
| **表单与认证** | useFormInteraction、useLogin、useAccount、useAccModal、useNotice、useLoginCheck | 6+ | 登录/注册/表单校验的完整流程 |
| **数据获取** | useReq/useAsync、useRepoInit、useWikiHome/create/detail/history、useUserDashboard、useRepoList | 9+ | 异步请求 + 状态管理的一站式封装 |
| **路由/参数** | useRepoId、useOrgId、useBranchName、useTitle、useNav、useRepoPathValidate | 6+ | URL 参数解析、导航控制 |
| **权限控制** | useUserAccessLevel、usePagePermission、useIsPrivate | 3 | 权限判断 + computed 封装 |
| **其他辅助** | useFile、usePopup、useReport、useTimeFormat、useStarFollow、useUserInfo、useDiscussGetUserInfo 等 | 7+ | 文件图标、弹窗管理、埋点上报等 |
---
## 三、每个分类怎么实现的?(面试核心)
### 3.1 通用交互 Hooks最常用
#### usePageResize — 响应式断点判断26 个文件复用)
```typescript
import { computed } from 'vue';
import { useWindowSize } from '@vueuse/core';
const { width } = useWindowSize();
export const usePageResize = () => {
const widthConfig = { xxl: 1536, xl: 1280, md: 1024, lg: 768 };
const widthType = computed(() => {
if (width.value > 1536) return 'xxl';
if (width.value > 1280) return 'xl';
if (width.value > 1024) return 'md';
if (width.value > 768) return 'lg';
return 'sm';
});
const isMobile = computed(() => width.value <= 1024);
return { widthType, width, isMobile };
};
```
**怎么用**
```vue
<script setup>
const { widthType, isMobile } = usePageResize();
// 根据不同断点渲染不同布局
</script>
```
**复用在**15 个布局文件 + 4 个态势感知图表组件 + 导航栏、底部栏等,共 26 个文件。
**面试话术**
> "封装了 usePageResize基于 vueuse 的 useWindowSize 响应式监听窗口宽度,定义了 5 个断点xxl/xl/md/lg/sm返回 computed 类型的 widthType 和 isMobile。26 个布局和组件文件都在用,页面布局根据断点自动切换,不需要每个文件自己写一遍判断。"
---
#### useModel — v-model 双向绑定封装18 个文件复用)
```typescript
// 把 v-model 的 prop + emit 封装成统一的写法
export function useModel(props, emits) {
const vModels = computed({
get() { return props.modelValue; },
set(val) { emits('update:modelValue', val); }
});
return { vModels };
}
```
**解决什么**Vue3 自定义组件里使用 v-model 需要写 `props.modelValue` + `emits('update:modelValue')`,每个弹窗/输入组件都写一遍很繁琐。这个 Hook 一行代码搞定。
**怎么用**
```vue
<script setup>
const props = defineProps({ modelValue: Boolean });
const emits = defineEmits(['update:modelValue']);
const { vModels } = useModel(props, emits);
// vModels.value 就是双向绑定的值
</script>
```
**复用在**18 个弹窗/表单组件(登录弹窗、标签选择、分支选择等)。
---
#### usePagination — 分页器封装
```typescript
export const usePagination = ({ storageKey, clientType }) => {
const pager = reactive({
page: 1, pageSize: 10, total: 0,
loading: false, isFirstLoad: true,
showLoading: false, showEmpty: false,
});
// 自动判断显示骨架屏还是 loading
watch(() => pager.loading, (value) => {
if (value) {
if (pager.isFirstLoad || pager.total === 0) {
pager.showLoading = false; // 首次加载用骨架屏
} else {
pager.showLoading = true; // 切换页用 loading
}
} else {
pager.showEmpty = pager.total === 0;
}
});
return { pager, pageOptions };
};
```
**亮点**:自动区分"首次加载"和"翻页加载",首次加载显示骨架屏(结构预览),翻页显示 loading 动画,体验更好。
---
#### useShowMore — 内容溢出检测
```typescript
export const useShowMore = (eleRef) => {
const isOver = ref(false); // 内容是否超出容器
const checkRange = () => {
isOver.value = eleRef.value.scrollHeight > eleRef.value.clientHeight;
};
useResizeObserver(eleRef, checkRange); // 利用 ResizeObserver 自动检测
return { isOver, showMore, onShowMore };
};
```
**解决什么**:很多卡片展示简介时只能显示 3 行,超出部分要显示"查看更多"。这个 Hook 自动检测内容是否溢出。
---
#### useLazyImport — 异步组件懒加载封装
```typescript
export function useLazyImport(loader, options = {}) {
return defineAsyncComponent({
loader,
loadingComponent: showLoading ? LoadingComponents : '',
timeout: 2000, // 2s 超时
delay: 100, // 100ms 后才显示 loading避免闪烁
onError: (_, retry, fail) => {
Message.error('加载失败');
fail();
}
});
}
```
**解决什么**:大组件异步加载时,统一处理 loading 状态、超时、错误重试。项目中有至少 6 个组件通过它注册为异步组件。
---
### 3.2 表单与认证 Hooks
#### useFormInteraction — 表单校验完整流程最复杂332 行)
这是项目中最大的 Hook封装了整个表单交互逻辑
```typescript
export function useFormInteraction(currentForm, flag, extraStatus) {
// 状态管理
const formErrors = reactive({}); // 表单校验错误
const disabled = ref(true); // 提交按钮禁用
const loading = ref(false); // 提交 loading
const FormRef = shallowRef(null); // 表单组件引用
const status = ref(flag); // 协议勾选状态
// 表单字段 change实时校验单个字段
const handleFormChange = ({ key, errors }) => { ... };
// 表单 input判断是否禁用提交
const handleFormInput = (val) => { disabled.value = val; };
// 表单 submit校验全部 → 通过则回调
const handleSubmit = async (callback) => {
const formData = await FormRef.value.ValidateForm();
if (formData.type === 'success') {
await callback(formData.forms); // 执行提交逻辑
} else {
catchFormErrors(formData); // 展示错误
}
};
// 倒计时:发送验证码后 59 秒倒计时
const handleCountDown = async (conf, callback) => { ... };
// 第三方登录OAuth 跳转
const handleAuthLogin = (type) => { ... };
return {
FormRef, formErrors, disabled, loading,
handleFormChange, handleFormInput, handleSubmit,
handleCountDown, handleAuthLogin, ...
};
}
```
**封装了什么**:表单校验、清错、提交、协议勾选、倒计时、第三方 OAuth 登录——所有登录/注册页的通用逻辑,全部封装在一个 Hook 里。
**怎么用**
```vue
<script setup>
const { FormRef, disabled, loading, handleSubmit, handleFormChange } =
useFormInteraction(formList);
</script>
<template>
<Form ref="FormRef" @change="handleFormChange">
<!-- 表单内容 -->
</Form>
<Button :disabled="disabled" :loading="loading" @click="handleSubmit(onLogin)">
登录
</Button>
</template>
```
---
#### useLogin / useAccModal / useNotice — 弹窗类 Hooks
这三个 Hook 都基于 `usePopup` 封装,负责不同类型的弹窗:
```typescript
// useLogin — 登录弹窗
export function useLogin() {
const { mount, unMount, isMounted } = usePopup();
const login = (options) => {
if (isMounted()) return; // 防止重复弹出
mount(LoginModal, {
onLogin: (userInfo) => { unMount(); RecordInfo(userInfo); },
onClose: () => { unMount(); }
});
};
return { login };
}
// useAccModal — 快捷登录弹窗(支持 CSDN/Gitee/GitHub
export function useAccModal() {
const { mount, unMount } = usePopup('g-acc-modal');
const openModal = () => {
if (isMounted()) return;
mount(AccModal, { onConfirm: (type) => { /* OAuth 跳转 */ } });
};
return { openModal };
}
// useNotice — 邮箱修改提醒弹窗
export function useNotification() {
const { mount, unMount } = usePopup('global-notification', document.body);
const notice = () => {
mount(NoticeModal, { onConfirm: () => { router.push('/setting/email'); } });
};
return { notice };
}
```
**设计模式**:三个弹窗 Hook 基于同一个 `usePopup` 底层能力,各自封装不同的弹窗组件和业务逻辑。调用方只需要一行:
```typescript
const { login } = useLogin();
login({ type: 'login' }); // 弹出登录窗口
```
---
#### useAccount — 用户信息增删改查
```typescript
export function useAccount() {
const userInfo = useAccountStore();
const { accountInfo } = storeToRefs(userInfo);
// 记录用户信息(登录时)
const RecordInfo = (source) => {
localStorage.setItem('op_access_token', source.op_access_token);
localStorage.setItem('opUserInfo', JSON.stringify(userInfo));
saveStatus(true);
saveAccountInfo(userInfo);
};
// 清除用户信息(退出时)
const RemoveInfo = () => {
localStorage.removeItem('op_access_token');
// ... 清除 10+ 个 localStorage key
saveStatus(false);
saveAccountInfo();
};
return { RecordInfo, RemoveInfo, accountInfo };
}
```
**封装的逻辑**Token 存 localStorage + Store 同步 + 登录状态更新。一次调用 RecordInfo 完成三件事13 个文件复用。
---
### 3.3 数据获取 Hooks
#### useReq/useAsync — 异步请求封装(通用能力)
```typescript
export const useAsync = (fn, params, precondition, map) => {
const data = ref(null);
const error = ref(null);
const loading = ref(false);
watchEffect(() => {
if (!precondition()) return; // 前提条件不满足,不发请求
loading.value = true;
fn(params).then(res => data.value = map(res))
.catch(e => error.value = e)
.finally(() => loading.value = false);
});
return { data, error, loading, mutate }; // mutate 手动触发重新请求
};
```
**解决什么**:每个页面都要写 `data/loading/error` 三件套 + try/catch。这个 Hook 一行调用搞定,自动处理 loading 状态和错误捕获。
**面试话术**
> "封装了 useAsync 和 useReq 两个通用请求 Hook。useAsync 接收请求函数、参数、前置条件和结果映射函数,自动管理 data/loading/error 三种状态。前置条件不满足时不会发请求(比如用户未登录时不发),通过 watchEffect 自动追踪依赖变化。还提供了 mutate 方法手动触发重新请求。"
---
#### useRepoInit — 仓库首页全部数据一站式初始化243 行)
```typescript
export const useRepoInit = () => {
const { repoId } = useRepoId();
const repoInfo = reactive({...}); // 仓库信息
const loadingStatus = reactive({ // 5 个模块的加载状态
profileLoading: true,
readmeLoading: true,
eventsLoading: true,
contributorLoading: true,
releasesLoading: true,
});
const initRepoHeader = async () => { // 顶部初始化(快)
initRepoData(); await initNotice();
};
const initRepoDashboard = () => { // 首页初始化(完整)
initRepoData(); initEvents(); initReadme();
initContributors(); initRelease();
};
return {
repoInfo, loadingStatus, readmeText, timeData,
contributorList, releasesList,
initRepoHeader, initRepoDashboard, ...
};
};
```
**为什么这样设计**仓库首页需要加载仓库信息、README、动态、贡献者、Release 等 5+ 个模块的数据。每个模块都有独立的 loading 状态。Hook 封装了它们的初始化时机header 先加载dashboard 完整加载),页面只管调用 `initRepoHeader()``initRepoDashboard()`
---
#### useWikiHome / useWikiCreate / useWikiDetail / useWikiHistory — Wiki 全流程 Hooks
四个 Hook 覆盖 Wiki 的完整生命周期(首页 → 创建 → 详情 → 历史版本),每个 Hook 封装对应的数据获取 + 状态管理:
```
useWikiHome() → 首页:获取 Wiki 列表 + Home.md 内容
useWikiCreate() → 创建/编辑页:表单数据 + 提交 + 删除
useWikiDetail() → 详情页:内容 + 侧边栏 + 页脚
useWikiHistory() → 历史版本:版本列表 + 分页
```
**设计思想**:每个页面一个 Hook页面组件只负责渲染数据逻辑全部在 Hook 里。
---
#### useUserDashboard — 用户首页一站式数据初始化272 行)
```typescript
export const useUserDashboard = () => {
// 6 个模块的数据
const userRepoList = ref([]); // 我的仓库
const userEventsList = ref({}); // 最近动态
const recommandRepoList = ref([]); // 推荐仓库
const recommandOrgList = ref([]); // 推荐组织
const userActivityList = ref([]); // 关注动态
const operationData = ref({}); // 运营信息
// 6 个模块的加载状态
const loadingStatus = reactive({
boardLoading: true, myRepoLoading: true,
eventLoading: true, repoRecmLoading: true,
orgRecmLoading: true, activityLoading: true,
});
const pageInit = async () => { // 一键初始化
loadingStatus.boardLoading = true;
await getUserInfo();
await getUserRepos();
loadingStatus.boardLoading = false;
getRecentEvents(); // 不阻塞首屏
getOperationData(); // 不阻塞首屏
recommandRepos();
recommandOrgs();
getUserActivity();
};
return { userInfo, userRepoList, loadingStatus, pageInit, ... };
};
```
**面试话术**
> "比如 useUserDashboard 这个 Hook封装了用户首页 6 个模块的数据获取——仓库列表、最近动态、推荐仓库、推荐组织、关注动态、运营信息,每个模块有独立的 loading 状态。pageInit 方法分层加载:先加载用户信息和仓库列表(阻塞首屏),然后并行加载其他 5 个模块(不阻塞首屏)。页面上只需要 `const { pageInit } = useUserDashboard(); onMounted(pageInit);` 两行代码。"
---
#### useRepoList — 用户仓库列表(含 Star 操作)
```typescript
export const useRepoList = (params) => {
const repoList = ref([]);
const allRepoList = ref([]);
const createdRepoList = ref([]);
const starredRepoList = ref([]);
const loading = ref(false);
// 四类仓库列表 + Star/Unstar 操作
const toggleRepoStar = ({ id, isStar }) => {
if (!userInfo.username) {
emitEvent('login', { triggerType: 'Star' }); // 未登录 → 弹出登录框
return;
}
isStar ? unstarRepo({ repoId: id }) : starRepo({ repoId: id });
init(); // 操作后重新拉取列表
};
return { allRepoList, createdRepoList, starredRepoList, toggleRepoStar, init };
};
```
**封装的逻辑**:四种仓库列表 + Star/Unstar 操作 + 未登录时自动弹出登录框。8 个文件复用。
---
### 3.4 路由/参数 Hooks
#### useRepoId — 从 URL 提取仓库 ID30 个文件复用,使用最多)
```typescript
export const useRepoId = (connector = '%2F', namespace = '') => {
const route = useRoute();
const repoId = computed(() => {
const [, ns, repo, ...rest] = route.path.split('/');
if (namespace) {
return [ns, repo, ...rest].join(connector); // 拼接完整路径
}
return [ns, repo].join(connector); // namespace/repo
});
return { repoId };
};
```
**解决什么**:项目中大量页面需要从 URL 中提取仓库的 namespace/repoName`gitcode/MyProject`。30 个文件都在用这个 Hook不需要每个文件自己解析 URL。
**类似的还有**`useOrgId`(提取组织 ID`useBranchName`(提取分支名+编码)。
---
#### useTitle — 页面标题设置
```typescript
export const usePageTitle = (title, name) => {
if (title && name) {
pageTitle.value = `${title}`;
} else {
pageTitle.value = title || (name ? `${name} - GitCode` : 'Tcode');
}
};
```
路由守卫中每个页面切换时调用,自动更新浏览器标签页标题。
---
### 3.5 权限控制 Hooks
#### useUserAccessLevel — 权限查询 + 角色判断
```typescript
export function useUserAccessLevel({ type = 'org', targetId = '' } = {}) {
const access_level = ref(0);
async function getPermission(id) {
if (type === 'org') {
const res = await getOrgPermission({ group_id: id });
access_level.value = res.data.data.my_role?.access_level || 0;
} else {
const res = await getRepoPermission({ repo_id: id });
access_level.value = res.data.data.access_level || 0;
}
}
// 三种角色自动 computed
const isAdmin = computed(() => access_level.value >= admin); // 50
const isDeveloper = computed(() => access_level.value >= developer); // 30
const isVisitor = computed(() => access_level.value >= visitor); // 10
return { access_level, getPermission, isAdmin, isDeveloper, isVisitor };
}
```
**解决什么**:查询用户在当前组织/仓库的权限后,自动算出三个布尔角色值。配合 Store 里的权限 computed页面只需 `if (isAdmin)` 判断是否显示管理按钮。
#### usePagePermission — 页面级权限控制
```typescript
export function usePagePermission(access_level, isPrivate) {
// 根据权限等级和是否私有,返回哪些菜单/操作可用
}
```
---
### 3.6 其他辅助 Hooks
#### useFile — 文件类型识别(图标 + 语言高亮)
```typescript
export function useFile() {
// 根据文件名后缀返回对应图标名
const getFileIcon = (filename) => {
const format = filename?.split('.').pop();
if (/^(zip|rar|7z)$/i.test(format)) return 'gt-file-zip-c';
if (/^(png|jpg|jpeg|gif)$/i.test(format)) return 'gt-picture-c';
if (/^(js|ts|vue|py)$/i.test(format)) return 'gt-file-code-c';
return 'gt-file-c';
};
// 根据文件名返回代码高亮语言
const getFileLanguage = (filename) => {
const map = { js: 'javascript', ts: 'typescript', vue: 'html', py: 'python' };
return map[format] || format;
};
return { getIcon, getFileLanguage, getFileFormat };
}
```
#### useStarFollow — 关注/粉丝操作
```typescript
export const useStarFollow = (params, auto) => {
const toggleStar = (username, followedUsername, follow) => {
if (!username) {
emitEvent('login', { triggerType: '关注用户' }); // 未登录→弹出登录
return;
}
follow ? followUser(...) : unfollowUser(...);
microApp.setData('user-center', { type: 'user_hasFollowed_update', ... });
};
return { fanCount, followCount, hasFollowed, toggleStar };
};
```
**封装了什么**:关注/取消关注的 API 调用 + 状态更新 + 微前端数据同步 + 未登录自动弹登录框。
---
#### useReport — 埋点上报
```typescript
export const useReport = (eventID, eventParams, headers) => {
// 统一埋点上报逻辑
};
```
#### usePopup — 弹窗管理器(底层能力)
被 useLogin、useAccModal、useNotice 三个 Hook 依赖的底层弹窗管理:
```typescript
export function usePopup(className?, rootElement?) {
const mount = (component, props) => { /* 挂载弹窗到 DOM */ };
const unMount = () => { /* 卸载弹窗 */ };
const isMounted = () => { /* 是否已挂载 */ };
return { mount, unMount, isMounted };
}
```
---
## 四、Hooks 之间的层次关系
```
底层能力(被其他 Hook 依赖)
├── usePopup → 弹窗挂载/卸载
├── useReq/useAsync → 通用异步请求封装
├── useRepoId → URL 参数解析
└── useModel → v-model 封装
中间能力(组合底层能力)
├── useLogin → 基于 usePopup
├── useAccModal → 基于 usePopup
├── useNotice → 基于 usePopup
├── useFormInteraction → 基于 useAccount + useRouter
├── useUserAccessLevel → 基于 useUserInfo
业务能力(组合中间能力)
├── useRepoInit → 基于 useRepoId
├── useWikiHome → 基于 useRepoId + useRouter
├── useRepoList → 基于 API 调用
├── useUserDashboard → 基于 6 个 API 调用
├── useStarFollow → 基于 API + eventBus + microApp
└── usePageResize → 基于 useWindowSizevueuse
```
**面试话术**
> "我设计的 Hooks 是有层次的。底层 Hooks 提供通用能力(如 usePopup 管理弹窗生命周期、useAsync 封装异步请求);中间层 Hooks 基于底层组合出业务场景(如 useLogin 基于 usePopup 封装登录弹窗流程);最上层是业务 Hooks组件里一行代码调用就能获得完整的数据和操作方法。"
---
## 五、整体架构总结
```
30+ 个 Hooks 在项目中的定位:
可复用逻辑提取为 Hooks
┌───────────────┐
│ 通用交互 (5+) │ usePageResize(26文件) useModel(18文件) usePagination useShowMore useLazyImport
├───────────────┤
│ 表单认证 (6+) │ useFormInteraction(8文件) useLogin useAccount(13文件) useAccModal useNotice
├───────────────┤
│ 数据获取 (9+) │ useAsync/useReq useRepoInit(7文件) useWikiHome/create/detail/history
│ │ useUserDashboard useRepoList(8文件) useDiscussGetUserInfo
├───────────────┤
│ 路由参数 (6+) │ useRepoId(30文件) useOrgId useBranchName useTitle useNav useRepoPathValidate
├───────────────┤
│ 权限控制 (3) │ useUserAccessLevel usePagePermission useIsPrivate
├───────────────┤
│ 其他辅助 (7+) │ useFile usePopup useReport useTimeFormat useStarFollow(4文件) useUserInfo
└───────────────┘
Vue 组件层40+ 组件调用)
```
---
## 六、面试问答准备
### Q1"封装 Composition API Hooks 实现逻辑复用"具体做了什么?
> 项目里封装了 30 多个 Hooks分为六大类。最核心的几个usePageResize 把响应式断点判断封装成 Hook26 个布局文件复用useRepoId 从 URL 提取仓库 ID30 个文件复用useFormInteraction 封装了完整的表单校验流程332 行8 个登录/注册页共享同一套校验逻辑useUserDashboard 封装了用户首页 6 个模块的数据初始化页面只需两行代码完成全部数据加载。Hooks 之间还有层次关系——底层 HooksusePopup、useAsync提供通用能力中间层组合底层能力业务层直接给组件用。
### Q2Hooks 和之前写的工具函数有什么区别?
> 工具函数是无状态的纯函数输入→输出Hooks 是有状态的(包含 ref、computed、watch。比如 useFormInteraction 内部管理了 formErrors、disabled、loading 等 6 个状态,还监听了表单 change 事件自动校验。如果写成工具函数这些状态管理就要在每个页面里重复写。Hooks 把"响应式状态 + 操作逻辑"封装在一起,是 Vue3 Composition API 的核心优势。
### Q3Hooks 怎么保证类型安全?
> 所有 Hooks 都用 TypeScript 写,返回值和参数都有类型定义。比如 usePagination 接收 `{storageKey, clientType}` 接口类型useRepoList 接收包含 profile/all/created/starred 四种子参数的类型。组件调用时 VSCode 有完整的智能提示。
### Q4有没有用过 VueUse 的 Hooks
> 用过useWindowSize、useResizeObserver、useTitle 等都是从 @vueuse/core 引入的。但项目不只是用第三方 Hooks更关键的是基于 VueUse 做二次封装——比如 usePageResize 基于 useWindowSize 封装了断点判断逻辑useShowMore 基于 useResizeObserver 封装了内容溢出检测。既用了社区的最佳实践,又做了业务定制。
### Q530 多个 Hooks 怎么组织的?会不会管理混乱?
> 按职责分目录:通用的放在 `src/utils/hooks/`30 个),特定业务域放在对应的 views 目录下(如 `src/views/User/hooks/` 放用户相关的 7 个 Hook布局相关的放在 `src/layouts/hooks/`。调用时能直观看出 Hook 的作用域。
### Q6Hooks 和 Pinia Store 怎么分工?
> Hooks 和 Store 的分工很明确。Store 存跨组件共享的持久状态用户信息、仓库信息Hooks 封装的是"行为逻辑"——怎么获取数据、怎么校验表单、怎么处理交互。Hooks 经常调用 Store 来读写状态,但它们不替代 Store。比如 useAccount Hook 既调用 Store 的 saveAccountInfo又操作 localStorage把"用户信息存储到 Store + 持久化到 localStorage + 更新登录状态"三步封装成一步 RecordInfo。
---
## 七、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| Hooks 总数 | 30+ 个 |
| 使用最多的 Hook | useRepoId30 个文件、61 处引用) |
| 第二常用 | usePageResize26 个文件、51 处引用) |
| 第三常用 | useModel18 个文件、35 处引用) |
| 最大的 Hook | useFormInteraction332 行) |
| 业务最全的 Hook | useUserDashboard6 个模块数据初始化) |
| 弹窗类 Hooks | 3 个useLogin/useAccModal/useNotice共用 usePopup |
| Wiki 全家桶 | 4 个Home/Create/Detail/History |
| 权限类 Hooks | 2 个useUserAccessLevel/usePagePermission |
| 基于 @vueuse/core 封装的 | 3 个useWindowSize/useResizeObserver/useTitle |
---
## 八、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 通用交互 Hooks5 个) | `src/utils/hooks/usePageResize.ts``useShowMore.ts``useModel.ts``usePagination.ts``useLazy.ts` |
| 表单与认证 Hooks6 个) | `src/utils/hooks/useForm.ts``useLogin.ts``useAccount.ts``useAccModal.ts``useNotice.ts``useLoginCheck.ts` |
| 数据获取 Hooks9 个) | `src/utils/hooks/useReq.ts``useRepoInit.ts``useWikiInit.ts``useIssueTemplate.ts``src/api/discussion/hook.ts` |
| 用户域 Hooks7 个) | `src/views/User/hooks/useUserDashboard.ts``useRepoList.ts``useStarFollow.ts``useContributes.ts``useTimelineActivities.ts``useIsPrivate.ts``useUserLang.ts` |
| 路由/参数 Hooks6 个) | `src/utils/hooks/useRepoId.ts``useOrgId.ts``usebranchName.ts``useTitle.ts``useNav.ts``useRepoPathValidate.ts` |
| 权限控制 Hooks3 个) | `src/utils/hooks/useUserAccessLevel.ts``src/layouts/hooks/usePagePermission.ts``src/views/User/hooks/useIsPrivate.ts` |
| 其他辅助 Hooks7 个) | `src/utils/hooks/useFile.ts``usePopup.ts``useReport.ts``useTimeFormat.ts``useUserInfo.ts``useRepoHeaderInit.ts``useBranchOptions.ts` |
| 组织域 Hooks | `src/views/Org/hooks/orgInfo.ts` |
| 弹窗底层能力 | `src/utils/hooks/usePopup.ts`(被 useLogin/useAccModal/useNotice 依赖) |

View File

@@ -0,0 +1,278 @@
# ECharts 多维度数据分析与展示 — 面试版
> 简历原话:**"集成 ECharts 实现多维度数据分析和展示"**
>
> 这篇文档帮你理解这句话背后到底做了什么,以及面试时怎么讲。
---
## 一、先搞清楚:这句话到底是什么意思?
拆成三部分理解:
| 关键词 | 含义 | 项目中对应 |
|--------|------|------------|
| **集成 ECharts** | 把 ECharts 图表库引入项目,让它能画图 | 安装 echarts 6.x在 Vue3 组件中调用 `echarts.init()` 创建图表 |
| **多维度** | 不是只画一种图、看一组数据,而是从多个角度分析数据 | 3 大业务模块、10+ 种图表类型、覆盖时间/空间/语言/行业等多个角度 |
| **数据分析和展示** | 不是简单的数据罗列,而是通过图表帮助用户发现规律、做出判断 | 四象限风险定位、趋势预测、热力分布等,让数据"说话" |
**一句话概括**:我在平台的 3 个核心模块中,用 ECharts 画了 20 多个图表,从安全漏洞、开源生态、社区发展等多个角度把后端数据可视化出来。
---
## 二、画了哪些图?按模块讲
### 模块 1安全态势最核心面试重点
这个模块帮用户**看懂开源项目的安全风险**
```
安全态势 = 哪里有危险 × 什么漏洞 × 哪些组件有风险
```
| 看什么 | 用什么图 | 怎么理解 |
|--------|----------|----------|
| 全球哪里有攻击 | 世界地图 + 涟漪动画 | 地图上每个国家颜色深浅表示威胁程度,关键位置有闪烁的光点 |
| 中国各省安全情况 | 中国地图热力图 | 像天气预报的温度图,颜色越深=威胁越大 |
| CVE 漏洞数量变化 | 柱状图 + 折线图 | 柱子=每月漏洞数量,折线=增长还是下降,还能切换"全球/国内"视角 |
| 漏洞严重程度 | 堆叠柱状图 | 每根柱子分红(高危)黄(中危)绿(低危)三段,一眼看出哪个季度最严重 |
| 哪种编程语言漏洞多 | 四象限气泡图 | X轴=活跃度Y轴=漏洞密度,气泡大小=漏洞总数,颜色=风险等级 |
| 哪些开源组件最危险 | 水平柱状图 | Log4j2、Spring 等组件的危险程度排名 |
### 模块 2开源生态
帮用户了解**开源世界的整体发展状况**
| 看什么 | 用什么图 | 怎么理解 |
|--------|----------|----------|
| 各省有多少开源人才 | 中国地图散点 | 地图上标出各省人才密度,旁边显示 Top5 排名 |
| 开发者数量变化 | 面积折线图 | 像山丘一样的增长曲线,能看出增长趋势 |
| 基础设施覆盖情况 | 多层环形图 | 三层圆环,分别代表镜像站、代码托管、构建平台的覆盖率 |
| GitHub vs GitCode 对比 | 分组柱状图 | 6 个指标PR合并率、响应速度等两边对比 |
| 编程语言谁强谁弱 | 南丁格尔玫瑰图 | 扇形面积越大=市场份额越大 |
### 模块 3社区分析
帮用户了解**开源社区在各城市的发展**
| 看什么 | 用什么图 |
|--------|----------|
| 各城市项目/人才/社区数量 | 柱状图(多个) |
| 高校俱乐部增长趋势 | 折线图 |
| 各省政策发布数量 | 柱状图 |
| 鸿蒙生态分布 | 多图表组合 |
---
## 三、怎么实现的?(面试核心)
### 3.1 一个图表从无到有的完整流程
用最简单的语言描述:
```
第1步后端给数据API请求
第2步拿到数据存到 Vue 的 ref 里(响应式状态)
第3步等页面渲染好nextTick用 echarts.init() 创建图表
第4步把数据塞进 ECharts 的配置项setOption
第5步页面关闭时销毁图表dispose防止内存泄漏
```
用代码简单表示:
```typescript
// 1. 请求数据
const response = await cveDataGlobalPage1();
// 2. 存到 ref
cveData.value = response.data;
// 3. 等 DOM 好了再画图
nextTick(() => {
const chart = echarts.init(document.getElementById('chart'));
// 4. 配置图表
chart.setOption({
xAxis: { data: ['1月', '2月', '3月'...] },
series: [{ type: 'bar', data: cveData.value }]
});
});
// 5. 组件销毁时清理
onUnmounted(() => chart.dispose());
```
### 3.2 面试必讲的 5 个技术点
#### 技术点 1图表生命周期管理防内存泄漏
**问题**ECharts 每次 `init()` 都会创建一个实例,如果不销毁,页面会越来越卡。
**解决方案**:每次画新图之前,先把旧的 `dispose()` 掉。
```typescript
// 存储图表实例
const chartInstances = ref({});
// 画图前:先销毁旧的,再创建新的
const initChart = () => {
if (chartInstances.value.myChart) {
chartInstances.value.myChart.dispose(); // 销毁旧的
}
const chart = echarts.init(el); // 创建新的
chartInstances.value.myChart = chart;
};
// 组件卸载时也要销毁
onUnmounted(() => {
chartInstances.value.myChart?.dispose();
});
```
**面试话术**
> "我在每个图表组件中都用了 ref 存储 ECharts 实例,每次重绘前先 dispose 旧实例再 init 新实例,组件卸载时也会销毁,避免内存泄漏。"
---
#### 技术点 2地图三级容错加载
**问题**ECharts 画地图需要先注册 GeoJSON 数据。如果数据加载失败,地图就是白的。
**解决方案**:准备三套数据源,依次尝试:
```
本地文件(最快) → CDN远程加载备用 → 手写简化版(兜底)
```
**面试话术**
> "地图数据加载我做了三级容错:先加载本地预编译的地图文件,失败了从阿里 DataV CDN 拉取,再失败用手写的简化 GeoJSON 兜底,保证地图永远不会白屏。"
---
#### 技术点 3一个图表达 4 个维度(气泡散点图)
**问题**:普通的柱状图只能表达 2 个维度X 轴 + Y 轴),怎么在一个图里塞更多信息?
**解决方案**用气泡散点图4 种方式编码 4 个维度:
| 编码方式 | 代表的维度 | 例子 |
|----------|-----------|------|
| X 轴位置 | 代码活跃度 | JavaScript 在右边活跃Rust 在左边 |
| Y 轴位置 | 漏洞密度 | Java 在上面漏洞多Go 在下面 |
| 气泡大小 | 漏洞总数 | JavaScript 气泡最大 |
| 气泡颜色 | 风险等级 | 红色=高风险,蓝色=健康 |
**面试话术**
> "编程语言安全态势图我用了四象限气泡散点图X 轴是代码活跃度Y 轴是漏洞密度,气泡大小代表漏洞总数,颜色代表风险象限。一个图表同时展示了 4 个数据维度,用户可以直观看出哪些语言'又活跃又危险'。"
---
#### 技术点 4兜底数据策略
**问题**:如果后端挂了,页面上 20 多个图表全部空白,看起来像 bug。
**解决方案**:每个图表都准备一套硬编码的"假数据"API 失败时用假数据渲染:
```typescript
try {
const data = await api.getData();
chartData.value = data;
} catch (error) {
// API 失败,用默认数据
chartData.value = [
{ name: 'JavaScript', value: 95 },
{ name: 'Python', value: 90 },
// ... 预设的示例数据
];
}
```
**面试话术**
> "数据展示型产品最怕图表空白所以我给每个图表都加了兜底数据。API 正常就用真实数据,失败了用硬编码的示例数据渲染,保证页面始终有内容。"
---
#### 技术点 5响应式自适应
**问题**:浏览器窗口大小变了,图表不会自动调整。
**解决方案**:监听窗口变化,触发图表重绘:
```typescript
// 监听窗口 resize
window.addEventListener('resize', () => {
chart.resize(); // ECharts 自带的调整大小方法
});
// 更进一步:宽度类型变了(比如从桌面切到平板),重新画图
// 因为小屏可能需要调整字号、隐藏部分标签等
watch(widthType, () => {
initChart(); // 重新初始化,而不是简单的 resize
});
```
**面试话术**
> "我用了自定义的 usePageResize Hook 检测视口变化。简单变化用 ECharts 自带的 resize(),但宽度类型变化时(比如桌面切到移动端)会重新初始化图表,调整 grid、字号、标签等配置。"
---
## 四、整体架构一句话总结
```
后端 API → Axios 请求 → Vue ref 存数据 → ECharts 渲染图表
↓ (失败)
兜底默认数据 → 也能渲染
```
技术栈:**Vue3** 的 Composition API 管理状态和生命周期 + **ECharts 6.x** 负责所有图表渲染 + **自定义工具类**管理地图数据加载。
---
## 五、面试问答准备
### Q1"集成 ECharts 实现多维度数据分析和展示"具体做了什么?
> 我在态势感知平台的安全态势、开源生态、社区分析三大模块中,用 ECharts 实现了 20 多个图表组件。涵盖了 10 种以上的图表类型——柱状图、折线图、饼图、散点气泡图、世界地图、中国地图、堆叠图、环形图、玫瑰图等。从时间、空间、编程语言、行业、组件等多个维度对开源安全和生态数据进行可视化分析。
### Q2遇到的技术难点是什么
> 主要有三个:
> 1. **地图加载**ECharts 画地图需要 GeoJSON 数据加载可能失败。我做了三级容错本地→CDN→兜底保证地图不白屏。
> 2. **性能**20 多个图表如果同时初始化会卡。我用 nextTick 确保 DOM 就绪再画图,组件销毁时及时 dispose 释放内存。
> 3. **数据可靠性**:后端 API 可能失败。我给每个图表加了兜底默认数据,保证页面始终有内容。
### Q3多维度体现在哪里
> 以编程语言安全态势的气泡图为例,一个图表编码了 4 个维度X 轴是代码活跃度Y 轴是漏洞密度,气泡大小是漏洞总数,颜色代表风险等级。从整体项目来看,我们从时间维度(月度/季度趋势)、空间维度(世界地图/中国地图)、语言维度、行业维度、组件维度等多个角度分析数据。
### Q4ECharts 实例的生命周期怎么管理?
> 每个图表组件用 ref 存储 ECharts 实例引用。每次重绘前先 dispose 旧实例再 init 新实例,避免内存泄漏。组件 onUnmounted 时也会销毁。窗口 resize 时调用 ECharts 的 resize() 方法自适应。
### Q5为什么不直接用 vue-echarts 这种封装库?
> 项目中简单图表用了 vue-devui 的 DChart 组件。但复杂图表(比如地图、气泡散点图、混合图表)需要深度自定义配置,直接用 echarts.init() 更灵活,能精确控制每个细节。
---
## 六、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 图表组件数量 | 20+ 个 |
| 图表类型 | 10+ 种 |
| 业务模块 | 3 个(安全/生态/社区) |
| 可视化数据维度 | 时间、空间、语言、行业、组件、人才等 6+ 个 |
| 地图容错级别 | 3 级本地→CDN→兜底 |
| ECharts 版本 | 6.x |
---
## 七、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 地图加载工具 | `src/utils/mapLoader.js` |
| API 接口定义 | `src/api/jyh/situation.ts` |
| 安全模块(最复杂) | `src/views/Jyh/security/` 目录下所有组件 |
| 生态模块 | `src/views/Jyh/ecosystem/` 目录下所有组件 |
| 社区模块 | `src/views/Jyh/Community/` 目录下所有组件 |

View File

@@ -0,0 +1,345 @@
# Pinia 状态管理 — 面试版
> 简历原话:**"使用 Pinia 进行状态管理"**
>
> 这篇文档帮你理解项目中 Pinia 到底管了哪些状态、怎么管的,以及面试时怎么讲。
---
## 一、先搞清楚Pinia 在项目里干什么?
### 什么是状态管理?
```
没有状态管理的问题:
用户登录信息 → 在 A 组件里获取
用户登录信息 → B 组件也要用 → 怎么传?父子组件可以 props兄弟组件呢
用户登录信息 → C 组件也要用 → 再 props 一层?代码变成意大利面...
有了 Pinia
用户登录信息 → 存在 Pinia store 里
A、B、C 组件 → 各自直接从 store 读取,不需要互相传
```
**一句话概括**Pinia 就是"全局数据仓库",存放多个组件都需要共享的数据(登录状态、当前仓库信息、当前组织信息等),避免组件之间层层传递。
### 项目中 Pinia 的定位
```
数据从哪来 → API 请求 / localStorage
存到哪 → Pinia store全局仓库
谁来用 → 32~62 个组件直接从 store 读取
```
---
## 二、项目里有几个 Store各自管什么
项目有 **10 个 Store**,其中 **6 个是真正活跃使用的**
### 核心 Store用得最多
| Store 名 | 管什么 | 被多少文件使用 |
|----------|--------|---------------|
| `useAccountStore` | 当前登录用户的信息ID、头像、昵称、Token | **62 个文件** |
| `orgInfoStore` | 当前组织的信息(名称、头像、权限、成员) | **37 个文件** |
| `repoInfoStore` | 当前仓库的信息名称、Star数、权限、是否私有 | **35 个文件** |
| `useGlobalInfoStore` | 全局 UI 状态(菜单、主题、搜索、语言列表) | **32 个文件** |
### 辅助 Store用得较少
| Store 名 | 管什么 | 被多少文件使用 |
|----------|--------|---------------|
| `otherAccountStore` | 当前正在查看的其他用户的信息 | 10 个文件 |
| `useMrChangeStore` | 代码合并MR的 diff 显示设置 | 6 个文件 |
| `entranceData` | 首页的仓库/组织列表 | 2 个文件 |
### 每个 Store 管理的状态一目了然
```
useAccountStore用户信息
├── isLogin: 是否已登录
└── accountInfo: { id, nickname, avatar, email, token... }
orgInfoStore组织信息
├── orgInfo: { name, avatar, description, visibility... }
├── isFollow: 是否关注了这个组织
├── memList / memCount: 成员列表和数量
└── computed → isAdmin / isDeveloper / isVisitor权限判断
repoInfoStore仓库信息
├── repoInfo: { name, star_count, visibility, archived... }
├── access_level: 权限等级数字
└── computed → isPrivate / isArchived / isAdmin / isDeveloper权限判断
useGlobalInfoStore全局 UI
├── globalMenuInfo: 菜单配置
├── menuType: 当前菜单类型repo/org
├── globalTheme: 主题light/dark
├── headerSearch: 搜索状态
└── languageList: 编程语言列表
```
---
## 三、怎么实现的?(面试核心)
### 3.1 Store 怎么定义的?
项目用的是 Pinia 的 **Setup 语法**(函数式),和 Vue3 Composition API 风格一致:
```typescript
// stores/user.ts — 最核心的用户 Store
import { defineStore } from 'pinia';
import { reactive, ref } from 'vue';
export const useAccountStore = defineStore('accountInfo', () => {
// ========== 状态state==========
const isLogin = ref(Boolean(localStorage.getItem('op_access_token')));
const accountInfo = reactive({
nickname: '',
avatar: '',
email: '',
op_access_token: '',
// ...
});
// ========== 操作actions==========
const saveAccountInfo = (info) => {
if (info) {
Object.assign(accountInfo, info); // 合并用户信息
} else {
Object.keys(accountInfo).forEach(k => accountInfo[k] = ''); // 清空(退出登录)
}
};
const checkIsLogin = async (token, refreshToken) => {
localStorage.setItem('op_access_token', token);
const userRes = await getUserInfo(); // 调 API 验证
if (userRes.data?.code === 200) {
saveAccountInfo(userRes.data.data); // 存到 store
return true;
}
return false;
};
// ========== 返回(暴露给组件用)==========
return { isLogin, accountInfo, saveAccountInfo, checkIsLogin };
});
```
**为什么用 Setup 语法而不是 Options 语法?**
- 和 Vue3 Composition API 风格统一,团队学习成本低
- 可以直接用 `ref``computed``async/await`,更灵活
- TypeScript 类型推导更好
### 3.2 Store 怎么注册的?
```typescript
// main.ts — 一行代码搞定
import { createPinia } from 'pinia';
app.use(createPinia());
```
就这么简单。不需要像 Vuex 那样手动注册每个 module。
### 3.3 组件怎么用 Store
```vue
<script setup>
import { useAccountStore } from '@/stores/user';
import { repoInfoStore } from '@/stores/Repo';
// 获取 store 实例
const accountStore = useAccountStore();
const repoStore = repoInfoStore();
// 直接读状态
console.log(accountStore.isLogin);
console.log(repoStore.repoInfo.name);
// 直接调 action
accountStore.saveAccountInfo({ nickname: '新名字' });
await repoStore.getRepoInfo();
</script>
<template>
<!-- 模板中直接用 -->
<img :src="accountStore.accountInfo.avatar" />
<span v-if="repoStore.isAdmin">管理</span>
</template>
```
**关键点**:不需要 `$store`,不需要 `mapState``mapActions`,直接用就行。
### 3.4 权限判断怎么用 computed 封装?
这是项目中一个很巧妙的设计——把权限判断逻辑封装在 Store 的 computed 里:
```typescript
// stores/Repo/index.ts
const access_level = ref(0);
// 8 个 computed 属性,自动根据 access_level 判断权限
const isAdmin = computed(() => access_level.value >= admin);
const isDeveloper = computed(() => access_level.value >= developer);
const isVisitor = computed(() => access_level.value >= visitor);
const isAdminOperate = computed(() => access_level.value >= admin && !repoInfo.value?.archived);
const isArchived = computed(() => repoInfo.value?.archived);
const isPrivate = computed(() => repoInfo.value?.visibility === 'private');
```
组件中这样用:
```vue
<template>
<!-- 权限按钮只有管理员才能看到 -->
<d-button v-if="repoStore.isAdmin">删除仓库</d-button>
<!-- 归档提示自动根据状态显示 -->
<span v-if="repoStore.isArchived">此仓库已归档</span>
<!-- 私有标识 -->
<d-tag v-if="repoStore.isPrivate">私有</d-tag>
</template>
```
**好处**:权限判断逻辑集中在 Store 里35 个组件共享同一套规则。改规则只改一处。
### 3.5 持久化怎么做的?
项目**没有**用 `pinia-plugin-persistedstate` 插件,而是手动操作 localStorage
```typescript
// 登录时Token 存 localStorage同时存 Store
localStorage.setItem('op_access_token', token);
saveAccountInfo({ op_access_token: token });
// 页面刷新时:从 localStorage 恢复到 Store
const isLogin = ref(Boolean(localStorage.getItem('op_access_token')));
const accountInfo = reactive({
op_access_token: localStorage.getItem('op_access_token') || '',
// ...
});
// 退出时:清 localStorage清 Store
localStorage.removeItem('op_access_token');
saveAccountInfo({}); // 传空 = 清空所有字段
```
**为什么手动而不用插件?** 因为只有用户 Token 需要持久化,其他状态(仓库信息、组织信息)每次进页面都重新从 API 获取。用插件反而多余。
---
## 四、除了 Pinia还有哪些状态管理方式
项目不是"只用 Pinia",而是**三种方式配合使用**
| 方式 | 用在哪 | 为什么 |
|------|--------|--------|
| **Pinia** | 全局共享状态(用户、仓库、组织、菜单) | 多个页面都需要,跨组件共享 |
| **mitt 事件总线** | 跨组件事件通知(登录/登出、错误、微前端通信) | 一次性事件触发,不需要持久化状态 |
| **provide/inject** | 父子组件深层传递移动端的仓库ID、权限标记 | 只在某个组件子树内共享,不需要全局 |
```typescript
// 事件总线示例 — 登录成功后通知全局
import { emitEvent } from '@/utils/eventBus';
emitEvent('login', userData); // 触发
addEventListener('login', handleLogin); // 监听
// provide/inject 示例 — 移动端深层组件获取仓库ID
const repoId = inject('repoId'); // 孙子组件直接拿,不需要层层 props
```
**面试话术**
> "项目中状态管理不是只有 Pinia而是三种方式配合Pinia 管全局持久状态(用户/仓库/组织mitt 事件总线管跨组件事件通知(登录/登出/错误provide/inject 管组件子树内的深层数据传递。根据数据的使用范围和生命周期选择最合适的方式。"
---
## 五、Store 之间的关系
项目中所有 Store **互相独立**,没有任何 Store 引用其他 Store
```
useAccountStore ──┐
orgInfoStore ─────┤ 互不依赖,各自独立
repoInfoStore ────┤
useGlobalInfoStore┘
```
**为什么这样设计?** 因为每个 Store 对应一个业务领域(用户/仓库/组织/全局),它们的数据来源不同(不同的 API使用场景也不同没有必要互相耦合。
---
## 六、整体架构总结
```
Pinia 在项目中的位置:
API 请求 ──→ Pinia Store ──→ Vue 组件32~62 个文件消费)
├── useAccountStore用户62 个文件用)
├── orgInfoStore组织37 个文件用)
├── repoInfoStore仓库35 个文件用)
├── useGlobalInfoStore全局UI32 个文件用)
└── + 6 个辅助 Store
配合使用:
├── mitt 事件总线30+ 个文件用)→ 事件通知
└── provide/inject → 组件子树数据传递
```
---
## 七、面试问答准备
### Q1"使用 Pinia 进行状态管理"具体做了什么?
> 项目用 Pinia 管理了 10 个全局状态 Store其中核心的 4 个分别管理用户登录信息62 个文件使用、仓库信息35 个文件、组织信息37 个文件)、全局 UI 状态32 个文件。比如用户登录后Token 和用户信息存在 Pinia 里,导航栏、个人主页、仓库页面等几十个组件都能直接读取,不需要层层传递 props。
### Q2为什么选 Pinia 而不是 Vuex
> 三个原因第一Pinia 是 Vue3 官方推荐的状态管理方案Vue3 + Composition API 的项目用 Pinia 最自然第二Pinia 的 API 更简洁,不需要 mutationsactions 里可以直接 async/await第三Pinia 的 TypeScript 支持更好,类型推导自动完成,不需要额外写类型声明。
### Q3Pinia 有两种语法,你用的哪种?
> 用的 Setup 语法(函数式),就是 `defineStore('id', () => { ... })` 这种。因为项目用 Vue3 Composition API 开发Setup 语法和 Composition API 风格统一,可以直接用 ref、computed、async/await。整个项目 10 个 Store 有 9 个用 Setup 语法,只有 1 个占位 Store 用了 Options 语法。
### Q4状态持久化怎么做的
> 没有用 pinia-plugin-persistedstate 插件,因为只有用户 Token 需要持久化,其他状态(仓库、组织信息)每次进页面都重新从 API 获取。所以手动在 localStorage 和 Store 之间同步:登录时 Token 存 localStorage 同时存 Store页面刷新时从 localStorage 恢复到 Store退出时两边都清空。
### Q5Store 里的 computed 有什么用?
> 主要用来封装权限判断逻辑。比如 repoInfoStore 里有 8 个 computed 属性isAdmin、isDeveloper、isVisitor 等,根据 access_level 数字自动判断当前用户的角色。35 个组件用到仓库权限的地方,都直接读这些 computed而不是每个组件自己写判断逻辑。这样权限规则改一处就全局生效。
### Q6除了 Pinia 还用了什么状态管理方式?
> 还用了 mitt 事件总线和 provide/inject。三种方式各有分工Pinia 管需要持久化的全局状态mitt 管一次性事件通知(比如登录/登出事件provide/inject 管组件子树内的深层数据传递。不是所有共享数据都适合放 Pinia事件通知用 mitt 更轻量,局部数据用 provide/inject 更合理。
---
## 八、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| Pinia 版本 | 2.1.3 |
| Store 总数 | 10 个 |
| 活跃使用的 Store | 6 个 |
| 核心 Store | 4 个(用户/仓库/组织/全局UI |
| 使用最多的 Store | useAccountStore62 个文件) |
| 语法风格 | Setup 语法9/10 |
| 持久化方式 | 手动 localStorage无插件 |
| 配合的其他方案 | mitt 事件总线30+ 文件、provide/inject |
---
## 九、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| Pinia 注册 | `src/main.ts`app.use(createPinia()) |
| 用户 Store最核心 | `src/stores/user.ts`3 个 Store 共存) |
| 仓库 Store | `src/stores/Repo/index.ts` |
| 组织 Store | `src/stores/Org/index.ts` |
| 全局 UI Store | `src/stores/Global/index.ts` |
| MR diff 设置 Store | `src/stores/merge.ts` |
| 事件总线 | `src/utils/eventBus.ts`mitt |

View File

@@ -0,0 +1,32 @@
# 可信开源态势感知平台 - 项目文档
## 简历原内容
> **可信开源态势感知平台**
>
> **项目背景**一个态势感知平台提供数据监控与分析、可视化展示、态势分析等功能嵌有自研金银湖AI大模型预测安全风险趋势关注开源项目生态如HarmonyOS生态的动态监控
>
> **技术栈**Vue3+JavaScript(ES6+)+SCSS+MicroApps+Echarts+Pinia
>
> **项目内容**
> - 可视化展示集成ECharts实现多维度数据分析和展示采用微前端架构MicroApps支持模块化开发
> - 状态管理使用Pinia构建模块化Store用户/仓库/组织/全局封装Composition API Hooks实现逻辑复用
> - 性能优化采用虚拟滚动和懒加载策略路由守卫优化、API 缓存降级等技术优化页面性能
> - AI对话集成@matechat/core 实现AI智能助手功能支持多轮对话、思考过程展示、组件化消息渲染软件详情/预警列表/智能推荐)
---
## 详细文档
| 模块 | 文档 | 核心内容 |
| ----- | ---------------------------------------------------------------------- | --------------------------------------------------------- |
| 可视化展示 | [ECharts 多维度数据分析与展示实现详解](ECharts多维度数据分析与展示实现详解.md) | 对应简历"集成ECharts实现多维度数据分析和展示"的完整实现解读、面试口述参考 |
| 可视化展示 | [微前端架构 MicroApps 模块化开发实现详解](微前端架构MicroApps模块化开发实现详解.md) | 对应简历"采用微前端架构MicroApps支持模块化开发"的完整实现解读、面试口述参考 |
| 状态管理 | [Pinia 状态管理实现详解](Pinia状态管理实现详解.md) | 对应简历"使用Pinia构建模块化Store"的完整实现解读、面试口述参考 |
| 性能优化 | [性能优化实现详解](性能优化实现详解.md) | 对应简历"采用虚拟滚动和懒加载策略路由守卫优化、API 缓存降级等技术优化页面性能"的完整实现解读、面试口述参考 |
| AI对话 | [AI 智能助手功能实现详解](AI智能助手功能实现详解.md) | 对应简历"集成@matechat/core实现AI智能助手功能"的完整实现解读、面试口述参考 |
| 状态管理 | [Composition API Hooks 逻辑复用实现详解](Composition%20API%20Hooks逻辑复用实现详解.md) | 对应简历"封装Composition API Hooks实现逻辑复用"的完整实现解读、面试口述参考 |
---

View File

@@ -0,0 +1,403 @@
# 微前端架构 MicroApps 模块化开发 — 面试版
> 简历原话:**"采用微前端架构 MicroApps 支持模块化开发"**
>
> 这篇文档帮你理解这句话背后到底做了什么,以及面试时怎么讲。
---
## 一、先搞清楚:这句话到底是什么意思?
拆成三部分理解:
| 关键词 | 含义 | 项目中对应 |
|--------|------|------------|
| **微前端** | 把一个大前端应用拆成多个小应用,每个小应用独立开发、独立部署 | 主应用(态势感知平台)+ 3 个子应用首页、用户中心、AI Copilot |
| **MicroApps** | 具体使用的微前端框架名,全称 `@micro-zoe/micro-app`(京东开源) | package.json 中声明的依赖,版本 1.0.0-rc.3 |
| **模块化开发** | 每个子应用由不同团队/人员独立开发,互不影响 | 3 个子应用各有独立的代码仓库、独立的域名、独立的部署地址 |
**一句话概括**:我用京东开源的 micro-app 框架,把平台拆成主应用 + 3 个子应用,实现了各模块独立开发、独立部署,同时通过消息通信保证它们协同工作。
---
## 二、为什么需要微前端?(面试必答)
### 没有微前端的问题
想象一下没有微前端的情况:
```
一个巨大的 Vue 项目,包含:
├── 首页/搜索功能(首页团队负责)
├── 用户中心/个人主页(用户团队负责)
├── AI Copilot 对话AI 团队负责)
└── 态势感知/安全分析(你负责)
问题:
1. 所有人改同一个代码仓库,合并冲突不断
2. 一个模块出 bug整个平台挂掉
3. 改一个小功能,要重新部署整个项目
4. 各团队技术栈想用不同的,但被绑死在一个项目里
```
### 用了微前端之后
```
主应用(壳):负责顶部导航栏、侧边栏、路由调度
├── 子应用1首页团队的独立项目负责首页/搜索
├── 子应用2用户团队的独立项目负责用户中心
├── 子应用3AI团队的独立项目负责 AI Copilot
└── 你自己开发的模块:直接写在主应用里(安全态势/生态/社区)
好处:
1. 各团队独立仓库、独立开发,互不干扰
2. 子应用挂了不影响主应用和其他子应用
3. 子应用可以独立部署,不用动主应用
4. 各子应用可以用不同的技术栈(虽然我们都是 Vue3
```
---
## 三、项目里具体怎么用的?
### 3.1 整体架构图
```
┌─────────────────────────────────────────────────────┐
│ 主应用(你的项目) │
│ │
│ ┌──────────┐ ┌──────────┐ ┌───────────────────┐ │
│ │ 顶部导航栏 │ │ 侧边菜单栏 │ │ 内容区域(路由) │ │
│ └──────────┘ └──────────┘ │ │ │
│ │ /explore → 子应用1 │ │
│ │ /user → 子应用2 │ │
│ │ /ai → 子应用3 │ │
│ │ /security → 你开发的 │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────┘
│ │ │
▼ ▼ ▼
┌─────────────┐ ┌──────────┐ ┌──────────────┐
│ 子应用1 │ │ 子应用2 │ │ 子应用3 │
│ 首页/搜索 │ │ 用户中心 │ │ AI Copilot │
│ (独立域名) │ │ (独立域名) │ │ (独立域名) │
└─────────────┘ └──────────┘ └──────────────┘
```
### 3.2 3 个子应用分别是什么
| 子应用名 | 干什么用 | 独立域名(生产环境) |
|----------|----------|---------------------|
| `micoro-app-homeweb-app` | 首页、搜索、G-Star 开源榜 | `homepage-app.gitcode.com` |
| `user-center` | 用户个人主页、仓库、收藏、设置 | `usercenter-app.gitcode.com` |
| `copilot-app` | AI 智能助手对话 | `copilot-app.gitcode.com` |
每个子应用都有**4 套环境地址**(开发/测试/预发/生产),通过 `.env` 文件配置:
```bash
# .env.development开发环境
VITE_CHILD_HOMEWEB_HOST = 'https://test.gitcode.net/child/homeweb-app/'
VITE_USER_CENTER_HOST = 'https://test.gitcode.net/child/user-center/'
VITE_COPILOT_HOST = 'https://test.gitcode.net/child/aichat-app/'
# .env.production生产环境
VITE_CHILD_HOMEWEB_HOST = 'https://homepage-app.gitcode.com'
VITE_USER_CENTER_HOST = 'https://usercenter-app.gitcode.com'
VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com'
```
---
## 四、怎么实现的?(面试核心)
### 4.1 怎么把子应用"嵌入"主应用
有两种方式,项目里都用了:
#### 方式一:声明式 — `<micro-app>` 标签(用于用户中心)
就像写 HTML 标签一样简单:
```vue
<!-- proxy.vue 用户中心的代理组件 -->
<template>
<micro-app name="user-center" :url="url" iframe baseroute="/"></micro-app>
</template>
<script setup>
const url = import.meta.env.VITE_USER_CENTER_HOST; // 从环境变量取子应用地址
</script>
```
**意思是**:在页面上放一个 `<micro-app>` 标签,告诉它子应用叫 `user-center`,地址是 `url`,用 iframe 模式加载。就这么简单。
#### 方式二:命令式 — `microApp.renderApp()`(用于首页和 AI
用 JavaScript 手动调用渲染:
```typescript
// proxy-homeweb.vue — 首页子应用的代理组件
onMounted(() => {
microApp.renderApp({
name: 'micoro-app-homeweb-app', // 子应用名
url: origin, // 子应用地址
container: '#homeweb-container', // 渲染到哪个 DOM 元素
data: { emitter, $router: router }, // 传给子应用的数据
iframe: true, // 用 iframe 隔离
baseroute: '/', // 路由前缀
});
});
onBeforeUnmount(() => {
microApp.unmountApp('micoro-app-homeweb-app'); // 页面离开时卸载子应用
});
```
**为什么两种方式都要用**:声明式更简单,但命令式更灵活,可以在渲染时传入更多数据(如事件总线、路由实例)。
### 4.2 子应用怎么和主应用"对话"
微前端最难的部分不是加载子应用,而是**主应用和子应用之间的通信**。项目里用了 3 种通信方式:
#### 方式 1`setData` — 主应用主动发消息给子应用
```typescript
// 主应用告诉用户中心"路由变了"
microApp.setData('user-center', {
type: 'application-route',
name: route.name,
params: route.params,
query: route.query
});
// 主应用告诉首页子应用"搜索关键词变了"
microApp.setData('micoro-app-homeweb-app', {
type: 'keyword-change',
keyword: searchKeyword
});
```
#### 方式 2`addDataListener` — 主应用监听子应用的消息
```typescript
// 主应用监听用户中心发来的各种消息
microApp.addDataListener('user-center', (config) => {
// 子应用说"用户关注状态变了"
if (config.type === 'user_hasFollowed_update') {
otherStore.saveFollowed(config.data.status);
}
// 子应用说"用户资料更新了"
if (config.type === 'user_profile_update') {
saveAccountInfo(config.data);
}
// 子应用说"要跳转路由"
if (config.type === 'route-change') {
router.push({ name: config.name, params: config.params });
}
});
```
#### 方式 3共享事件总线 — 通过 mitt 事件库
```typescript
// 主应用创建事件总线,传给子应用
microApp.renderApp({
name: 'micoro-app-homeweb-app',
data: {
emitter, // ← mitt 事件总线实例
$router: router // ← 路由实例
}
});
// 子应用通过事件总线通知主应用"路由变了"
// 主应用监听这个事件,同步到自己的路由
addEventListener('microRouterChange', (to) => {
router.push(to.fullPath);
});
```
**通信方式总结**
```
主应用 → 子应用microApp.setData('子应用名', 数据)
子应用 → 主应用microApp.addDataListener('子应用名', 回调)
双向通信:共享 emitter 事件总线
```
### 4.3 路由怎么同步?
主应用和子应用各有各的路由,需要保持同步:
```typescript
// microAppConfig.ts — 路由同步工具
// 1. 子应用路由变了 → 同步到主应用
addEventListener('microRouterChange', (to) => {
router.push(to.fullPath); // 主应用跟着跳
});
// 2. 主应用路由变了 → 同步到子应用
router.beforeEach((to, from) => {
if (to.meta.micorApp) {
// 告诉子应用:你要跳到这个路径
to.meta.micorApp.forEach(appName => {
microApp.router.replace({ name: appName, path: to.fullPath });
});
}
});
```
**通俗理解**:就像两个人各开一辆车,要保持同一路线。主应用的路由变了,通知子应用跟上;子应用的路由变了,也通知主应用跟上。
### 4.4 页面加载优化
为了让子应用加载更快,做了两个优化:
#### 优化 1DNS 预解析
```html
<!-- index.html — 提前解析子应用域名的 DNS -->
<link rel="preconnect" href="//homepage-app.gitcode.com" crossorigin />
<link rel="dns-prefetch" href="//homepage-app.gitcode.com">
<link rel="dns-prefetch" href="//usercenter-app.gitcode.com">
```
**通俗理解**:用户还没点到子应用的页面,浏览器就已经提前"打听"子应用域名的地址了,等真正加载时就更快。
#### 优化 2开发环境跨域处理Vite proxy
子应用和主应用不在同一个域名下,但 micro-app 的 iframe 模式下,主子应用通信靠 `postMessage`(原生 API天然支持跨域不受 CORS 限制),所以**不需要额外配置 CORS**。
真正需要处理跨域的是**主应用调用后端 API**的场景。开发环境中,前端跑在 `localhost:443`,后端在 `http://222.20.126.217:8100`域名不同会触发跨域。Vite 用 proxy 代理解决:
```typescript
// vite.config.ts
server: {
proxy: {
'/api': {
target: 'http://222.20.126.217:8100', // 后端地址
changeOrigin: true, // 修改 Origin 头,绕过同源策略
}
}
}
```
**原理**浏览器→Vite 开发服务器同源不跨域→Vite 帮你转发到后端(服务器之间没有跨域限制)。生产环境则由 nginx 做反向代理,原理一样。
---
## 五、Vue 构建层面的适配
微前端框架需要在 Vue 构建工具中做一些特殊配置:
```typescript
// vite.config.ts
vue({
template: {
compilerOptions: {
isCustomElement: tag => /^micro-app/.test(tag)
// ← 告诉 Vue遇到 <micro-app> 标签不要报错,它是自定义元素
}
}
})
```
**通俗理解**Vue 默认不认识 `<micro-app>` 这个标签会报警告。加上这个配置后Vue 就知道"这是个自定义元素,不用管它"。
---
## 六、生命周期管理(防内存泄漏)
每个子应用的代理组件都遵循统一的生命周期:
```typescript
// 挂载时:加载子应用
onMounted(() => {
microApp.renderApp({ name: 'xxx', url: origin, container: '#xxx' });
});
// 卸载时:销毁子应用
onBeforeUnmount(() => {
microApp.unmountApp('xxx'); // 卸载子应用
microApp.clearDataListener(); // 清理数据监听器(避免内存泄漏)
});
```
**为什么重要**:如果不卸载,子应用的 DOM 事件监听器、定时器、网络请求都还在运行,会造成内存泄漏。
---
## 七、整体架构一句话总结
```
主应用(壳)
├── 顶部导航 + 侧边栏 + 路由调度(主应用负责)
├── 态势感知/生态/社区模块(直接写在主应用里)
└── 3 个子应用(通过 micro-app 框架嵌入)
├── 首页/搜索(独立仓库,独立域名)
├── 用户中心(独立仓库,独立域名)
└── AI Copilot独立仓库独立域名
通信方式setData主→子+ addDataListener子→主+ 事件总线(双向)
隔离方式iframe 模式(样式/JS 完全隔离)
路由同步:双向监听,互相通知
```
---
## 八、面试问答准备
### Q1"采用微前端架构 MicroApps 支持模块化开发"具体做了什么?
> 我们平台使用京东开源的 micro-app 框架,把整个应用拆成了主应用 + 3 个子应用。主应用负责导航栏、侧边栏和路由调度3 个子应用首页搜索、用户中心、AI Copilot各自独立开发、独立部署。主应用通过 micro-app 框架将子应用嵌入到自己的页面中,使用 iframe 模式实现样式和 JS 的完全隔离,同时通过 setData、addDataListener 和事件总线三种方式实现主子应用之间的数据通信。
### Q2为什么选择 micro-app 而不是 qiankun
> micro-app 是京东开源的下一代微前端框架,相比 qiankun 有几个优势:第一,接入成本低,只需要一个 `<micro-app>` 标签就能加载子应用,不需要像 qiankun 那样改造子应用的入口文件和打包配置;第二,支持 iframe 模式,能实现更好的样式和 JS 隔离;第三,提供了 setData/addDataListener API 让主子应用通信更方便。
### Q3主应用和子应用之间怎么通信
> 我们用了三种方式:第一,主应用通过 `microApp.setData()` 主动推送数据给子应用,比如路由变化、搜索关键词;第二,主应用通过 `microApp.addDataListener()` 监听子应用发来的消息,比如用户关注状态更新、路由跳转请求;第三,通过共享的 mitt 事件总线实现双向通信,主应用创建事件总线实例后通过 renderApp 的 data 参数传给子应用。路由同步方面,主应用和子应用互相监听路由变化,保持双向同步。
### Q4微前端的隔离和跨域怎么处理的
> 我们用的是 iframe 模式(`iframe: true`),每个子应用运行在独立的 iframe 中。iframe 天然提供了 JS 作用域隔离和 CSS 样式隔离子应用的全局变量、样式都不会污染主应用。跨域方面iframe 模式下主子应用通信靠 `postMessage`,这是浏览器原生 API天然支持跨域不需要额外配置 CORS。开发环境主应用调后端 API 的跨域问题,则通过 Vite 的 proxy 代理解决。
### Q5子应用加载会不会很慢
> 我们做了两个优化:第一,在 index.html 中通过 DNS prefetch 提前解析子应用域名,用户还没点到子应用页面时浏览器就已经在解析域名了;第二,子应用只在用户真正访问对应路由时才加载,不是一进主应用就把所有子应用都加载出来,是按需加载的。
### Q6开发环境和生产环境的子应用地址不一样怎么办
> 通过 Vite 的环境变量机制管理。每个环境(开发/测试/预发/生产)都有独立的 `.env` 文件,里面配置了对应环境的子应用 URL。代码中通过 `import.meta.env.VITE_CHILD_HOMEWEB_HOST` 读取,构建时自动替换为对应环境的地址,不需要改代码。
---
## 九、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 微前端框架 | micro-app 1.0.0-rc.3(京东开源) |
| 子应用数量 | 3 个 |
| 通信方式 | 3 种setData / addDataListener / 事件总线) |
| 隔离方式 | iframe 模式(完全隔离) |
| 环境配置 | 4 套(开发/测试/预发/生产) |
| 代理组件 | 3 个(每个子应用对应一个) |
---
## 十、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 框架依赖声明 | `package.json`@micro-zoe/micro-app |
| 构建配置(自定义元素+CORS | `vite.config.ts` |
| DNS 预解析 | `index.html` |
| 环境变量(子应用地址) | `.env.development` / `.env.production` 等 |
| 首页子应用代理 | `src/views/micro-page/proxy-homeweb.vue` |
| 用户中心子应用代理 | `src/views/UserCenter/proxy.vue` |
| AI Copilot 子应用代理 | `src/views/micro-page/proxy-copilot.vue` |
| 路由同步工具 | `src/utils/microAppConfig.ts` |
| 事件总线 | `src/utils/eventBus.ts` |
| 路由配置(带子应用标记) | `src/router/config/home.ts` / `user.ts` / `setting.ts` |
| 布局中的通信监听 | `src/layouts/v2/DefaultLayout/index.vue` |

View File

@@ -0,0 +1,568 @@
# 性能优化 — 面试版
> 简历原话:**"采用虚拟滚动和懒加载策略路由守卫优化、API 缓存降级等技术优化页面性能"**
>
> ⚠️ **重要提醒**:代码中**没有**虚拟滚动的实现(没有相关库或代码)。"懒加载"是真实存在的路由懒加载、IntersectionObserver 等),"路由守卫优化"是真实存在的interceptor.ts 248 行),"API 缓存降级"也是真实存在的360 行自研代码)。面试时建议把重点放在"路由守卫优化 + 懒加载 + API 缓存降级 + 防抖节流 + shallowRef"这五个真实存在的优化上。
>
> 这篇文档基于代码实际情况,帮你理解项目中做了哪些性能优化,以及面试时怎么讲。
---
## 一、先搞清楚:项目里到底做了哪些优化?
实际代码中有 **7 大类性能优化**,都是真实存在的:
| 优化手段 | 做了什么 | 数量/规模 | |
| ------------------------ | ------------------------------------------------------------------------- | ----------------- | --- |
| **路由守卫优化** | beforeEach 缓存 ID 避免重复 API、Promise.all 并行请求、条件性请求、客户端预校验、声明式 meta 配置、白名单短路 | 248 行核心代码7 种优化策略 | |
| **路由懒加载** | 所有页面组件用动态 import按需加载 | 80+ 个路由 | |
| **防抖节流** | 用户输入、滚动、resize 等高频操作加防抖/节流 | 40+ 处 | |
| **API 缓存降级** | 请求结果缓存到 IndexedDB失败时读缓存兜底 | 自研系统360 行代码 | |
| **shallowRef** | 大数据对象用浅层响应式,避免深度遍历 | 30+ 处 | |
| **IntersectionObserver** | 元素进入视口才加载/上报,不提前渲染 | 6+ 处 | |
| **lodash 按需引入** | 只引入用到的函数,不打包整个 lodash | 全项目 | |
**一句话概括**我从路由守卫、路由加载、用户交互、API 请求、响应式数据、DOM 渲染六个层面做了性能优化,核心思路是"能不加载就不加载,能少算就少算,能缓存就缓存"。
---
## 二、每个优化怎么做的?(面试核心)
### 2.1 路由守卫优化(简历新增,技术含量最高之一)
**问题**80+ 个页面每次导航都要经过 `router.beforeEach` 守卫,如果守卫里每次都调 API 查权限、查组织信息、查仓库信息,页面切换会非常慢。
**解决方案**:在 `src/router/interceptor.ts`248 行)中实现了一个 async beforeEach 守卫,通过 7 种优化策略大幅减少不必要的 API 调用。
#### 守卫的 7 个执行阶段
```
Token 验证 → namespace 类型解析 → ref 追踪 → 组织详情获取 → 仓库详情+权限
→ 页面标题设置 → 登录权限检查
```
#### 优化 1模块级 ID 缓存,避免重复 API 调用
```typescript
// interceptor.ts 第 18-19 行 — 模块级缓存变量
let currentOrgId = ''; // 缓存当前组织 ID
let currentRepoId = ''; // 缓存当前仓库 ID
// 切换组织时:只有 orgId 变了才调 API
if (orgId.value !== currentOrgId) {
const res = await reqCatch(getOrg, { orgId: orgId.value, with_full_path: true });
// ... 请求组织信息
}
currentOrgId = orgId.value; // 更新缓存
// 切换仓库时:只有 repoId 变了才调 API
if (repoId.value !== currentRepoId) {
const [repoRes, roleRes] = await Promise.all([...]);
// ... 请求仓库信息
}
currentRepoId = repoId.value; // 更新缓存
```
**为什么这样设计?** 同一个组织/仓库下有多个子页面如仓库的文件、Issue、Merge Request、Wiki 等 tab用户在这些 tab 之间切换时,仓库信息不需要重新请求。用模块级变量缓存 ID只在真正切换组织/仓库时才发请求。
#### 优化 2Promise.all 并行请求
```typescript
// interceptor.ts 第 128-130 行
// ❌ 串行写法:两个请求先后发,总耗时 = API1 + API2
// const repoRes = await getRepo({ repoId, statistics: true });
// const roleRes = await reqCatch(getRepoRole, { repoId });
// ✅ 并行写法:两个请求同时发,总耗时 = max(API1, API2)
const promises = [getRepo({ repoId: repoId.value, statistics: true }, { customError: true })];
account.isLogin && promises.push(reqCatch(getRepoRole, { repoId: repoId.value }));
const [repoRes, roleRes] = await Promise.all(promises);
```
**效果**:仓库信息 + 角色权限两个请求并行发出,总耗时约等于较慢的那个请求,而不是两者之和。
#### 优化 3条件性请求——不需要的数据就不请求
```typescript
// 只有登录用户才请求角色权限
account.isLogin && promises.push(reqCatch(getRepoRole, { repoId: repoId.value }));
// 登录后才会调用的 notice 接口
// 在 useRepoInit 中if (!isLogin) return;
```
**为什么这样设计?** 未登录用户看不到仓库的管理功能,不需要知道当前用户的角色。跳过这个请求既省时间又省后端资源。同样,组织详情里的关注/通知等接口,未登录时直接跳过。
#### 优化 4客户端预校验——在发请求之前就拦截无效路径
```typescript
// interceptor.ts 第 122-124 行 — 正则预校验
const { repoId } = useRepoId('%2F', to.params.namespace, to.params.repoName);
// 禁止访问以 .wiki、.git、.atom 结尾的仓库路径
if (!repoPathSuffix.test(repoId.value)) {
return { name: '404' }; // 客户端直接返回 404不发任何 API 请求
}
```
**效果**:用户误输入了 `xxx.wiki.git` 这样的路径,在发起任何 API 请求之前就被拦截。节省了一次无意义的网络请求。
#### 优化 5声明式 meta 配置——权限控制不写死在守卫里
```typescript
// 路由定义中声明config 文件)
{ path: '/dashboard', meta: { needLogin: true, loginType: '访问 dashboard' } }
{ path: '/login', meta: { loginHidden: true } }
// 守卫中统一判断interceptor.ts 第 215-232 行)
if (to.meta.needLogin) {
if (account.isLogin) {
return true;
} else {
emitEvent('login', { triggerType: to.meta?.loginType }); // 弹出登录框
return false;
}
} else {
if (account.isLogin && to.meta.loginHidden) {
return { name: 'dashboard' }; // 已登录用户看登录页,重定向到首页
}
}
```
**为什么这样设计?** 新增一个需要登录的页面,只需要在路由定义里加 `meta: { needLogin: true }`,不需要改守卫代码。守卫逻辑和路由配置解耦,可维护性强。
#### 优化 6白名单短路——不该处理的页面直接跳过
```typescript
// interceptor.ts 第 42 行 — 白名单
const fromHomeWeb = ['home', 'search', 'gStar', 'gStarApply', 'explore'];
// ref 追踪:子应用页面不上报 ref
if (!fromHomeWeb.includes(to.name)) {
window.page_ref = ref;
sessionStorage.setItem('ref', BASE_URL + to.fullPath);
}
// 页面量上报:子应用页面不上报 PV
if (fromHomeWeb.includes(to.name)) return;
```
**为什么这样设计?** 微前端子应用homeweb内部有自己的路由和上报逻辑主应用的守卫不应该干涉。白名单让这些页面直接跳过避免冲突和重复调用。
#### 优化 7localStorage 持久化——跨会话记住用户行为
```typescript
// interceptor.ts 第 22-31 行 — 记录已访问的仓库
const setRepoVisited = (id) => {
let visitedRepoIds = localStorage.getItem('visited_repo_list')?.split(',') || [];
visitedRepoIds.push(`${id}`);
visitedRepoIds = Array.from(new Set(visitedRepoIds));
localStorage.setItem(visitedRepoList, visitedRepoIds.join());
};
const isRepoVisited = (id) => {
return localStorage.getItem(visitedRepoList)?.includes(id) || false;
};
// 守卫中的应用:首次进仓库 → 跳转 Dashboard之后进仓库 → 直接进代码 tab
if (!account.isLogin || !repoStore.isVisitor) {
isGotoRepoDashboard = !isRepoVisited(repoId);
}
```
**为什么这样设计?** 用户第一次访问某个仓库时,引导去 Dashboard 了解概况;后续再访问时直接进入代码页面。这个"是否首次访问"的状态持久化在 localStorage 中,跨会话保持,不需要后端 API。
#### 路由守卫优化 — 完整架构图
```
router.beforeEach(async (to, from) => {
├── 1. Token 验证 ──── checkIsLogin() 只在有 token 时才调 API
├── 2. namespace 类型 ── userOrOrg 路由才调 getPathType API
├── 3. ref 追踪 ────── fromHomeWeb 白名单跳过子应用
├── 4. 组织详情 ────── currentOrgId 缓存,切换组织才请求
│ 404/403 → 直接返回错误页,不继续走后续逻辑
├── 5. 仓库详情 ────── currentRepoId 缓存 + Promise.all 并行
│ repoPathSuffix 正则预校验 → 无效路径不发请求
│ module_setting 模块开关 → 禁用模块走 404
│ 首次访问 Dashboard 重定向 → localStorage 持久化
├── 6. 页面标题 ────── 根据 type 自动拼接组织/仓库名
└── 7. 登录检查 ────── meta.needLogin / meta.loginHidden 声明式控制
})
```
**面试话术**
> "路由守卫优化是我在项目中做的一个重要优化。80 多个页面每次导航都经过 beforeEach如果每次都调 API页面切换会很慢。我做了 7 种优化:第一,模块级 ID 缓存,用 currentOrgId 和 currentRepoId 两个变量,只有真正切换组织/仓库时才发 API同一仓库内切换 tab 不发请求;第二, Promise.all 并行请求,仓库信息和角色权限两个请求同时发出;第三,条件性请求,未登录用户不请求角色权限;第四,客户端预校验,用正则过滤无效仓库路径,在发请求之前就返回 404第五声明式 meta 配置权限控制逻辑不在守卫里硬编码第六白名单短路子应用页面跳过主应用守卫逻辑第七localStorage 持久化,记住用户首次访问仓库的状态,跨会话保持。这些优化让页面切换更快、更流畅。"
---
### 2.2 路由懒加载(最重要,必讲)
**问题**:项目有 80+ 个页面,如果打包成一个 JS 文件,首页加载要下载好几 MB。
**解决方案**:每个路由组件用 `() => import(...)` 动态导入,访问哪个页面才下载哪个页面的代码。
```typescript
// router/config/home.ts — 每个路由都是懒加载
const routes = [
{
path: '/explore',
component: () => import('@/views/micro-page/proxy-homeweb.vue'), // ← 动态导入
},
{
path: '/security',
component: () => import('@/views/Jyh/security/index.vue'), // ← 动态导入
},
{
path: '/ecosystem',
component: () => import('@/views/Jyh/ecosystem/index.vue'), // ← 动态导入
},
// ... 80+ 个路由全部这样写
];
```
**效果**
- 首页只加载首页需要的代码(几十 KB
- 用户点到"安全态势"页面,才下载安全态势的代码
- 每个页面变成独立的 JS 文件chunk按需加载
**地图数据也是懒加载的**
```javascript
// utils/mapLoader.js — 地图数据按需加载
export const loadWorldMap = async () => {
await import('../views/Jyh/security/components/world.js'); // 用到世界地图时才加载
};
export const loadChinaMap = async () => {
await import('/china.js'); // 用到中国地图时才加载
};
```
**面试话术**
> "项目有 80 多个页面,全部用路由懒加载。每个页面组件通过 `() => import(...)` 动态导入Vite 构建时自动拆分成独立的 chunk用户访问哪个页面才下载哪个页面的代码。地图数据也是懒加载的世界地图和中国地图的 GeoJSON 文件(几百 KB只在用户进入安全态势页面时才加载。"
---
### 2.3 防抖节流(数量最多)
**问题**:用户快速输入搜索、频繁滚动、拖拽窗口大小时,事件每秒触发几十次,每次都调 API 或重绘图表会很卡。
**解决方案**用防抖debounce和节流throttle控制触发频率。
#### 防抖debounce— 等用户"停下来"才执行
```typescript
import debounce from 'lodash/debounce'; // 按需引入
// 搜索输入:用户停止输入 300ms 后才发请求
const handleSearch = debounce((keyword) => {
fetchSearchResults(keyword);
}, 300);
// 滚动加载:用户停止滚动 300ms 后才检查是否需要加载更多
const handleScroll = debounce(() => {
checkLoadMore();
}, 300);
```
#### 节流throttle— 每隔一段时间才执行一次
```typescript
import throttle from 'lodash/throttle';
// 窗口 resize每 100ms 最多执行一次
const handleResize = throttle(() => {
recalculateLayout();
}, 100);
// 滚动事件:每 200ms 最多执行一次
const handleScroll = throttle(() => {
updateScrollPosition();
}, 200);
```
**项目中的分布**
| 场景 | 使用方式 | 延迟时间 | 出现次数 |
|------|----------|----------|----------|
| 搜索输入 | debounce | 300ms | 15+ 处 |
| 表单提交 | debounce | 300ms | 10+ 处 |
| 列表滚动加载 | debounce | 300ms | 5+ 处 |
| 窗口 resize | throttle | 100ms | 2 处 |
| 滚动事件 | throttle | 200ms | 2 处 |
| 表格选择 | debounce | 10ms | 1 处 |
| 侧边栏展开 | debounce | 600ms | 1 处 |
**为什么 lodash 按需引入?**
```typescript
// ✅ 正确:只引入 debounce 函数
import debounce from 'lodash/debounce';
// ❌ 错误:引入整个 lodash 库(几百 KB
import { debounce } from 'lodash';
```
按需引入让 Vite 的 tree-shaking 生效,最终打包只包含用到的函数,而不是整个 lodash 库。
**面试话术**
> "项目中有 40 多处防抖节流。搜索输入用 300ms 防抖,等用户停下来才发请求;窗口 resize 用 100ms 节流,限制重绘频率;滚动事件用 200ms 节流。所有 lodash 函数都按需引入(`lodash/debounce`),配合 Vite 的 tree-shaking不会打包整个 lodash 库。"
---
### 2.4 API 缓存降级系统(技术含量最高)
**问题**:后端 API 可能超时或报错,用户看到的是空白页面或加载失败。
**解决方案**:自研了一套请求降级拦截器,成功时缓存响应,失败时读缓存兜底。
#### 整体流程
```
请求发出 → 正常返回? → 是 → 缓存到 IndexedDB → 返回数据给页面
→ 否(超时/报错)→ 读 IndexedDB 缓存 → 返回缓存数据给页面
```
#### 核心代码(简化版)
```typescript
// degradeInterceptor.ts — 请求降级拦截器
class DegradeInterceptor {
// 请求成功时:缓存响应到 IndexedDB
onResponseFulfilled(response) {
this.saveToCache(response.config.url, response.data);
return response;
}
// 请求失败时:从 IndexedDB 读缓存兜底
onResponseRejected(error) {
const cachedData = this.readFromCache(error.config.url);
if (cachedData) {
return cachedData; // 返回缓存数据,用户看到的是"旧数据"而不是错误
}
return Promise.reject(error);
}
}
```
#### 缓存配置
```typescript
// storage-apis.ts — 哪些 API 走缓存策略
const storageApis = [
{ url: '/api/v1/issues', timeout: 5000, retry: 2 }, // 5秒超时重试2次
{ url: '/api/v1/merge_requests', timeout: 5000, retry: 2 },
// ...
];
```
#### 401 Token 刷新时的请求去重
```typescript
// request.ts — 防止 token 过期时同时发 10 个刷新请求
let isRefreshing = false;
let pendingQueue = []; // 等待队列
// token 过期时:
if (!isRefreshing) {
isRefreshing = true;
await refreshToken();
isRefreshing = false;
pendingQueue.forEach(cb => cb()); // 把等待的请求全部重发
pendingQueue = [];
} else {
// 已经在刷新了,把当前请求放进队列等着
return new Promise(resolve => pendingQueue.push(resolve));
}
```
**面试话术**
> "我实现了一套 API 缓存降级系统。请求成功时把响应缓存到 IndexedDB后端超时或报错时自动读缓存兜底用户看到的是旧数据而不是错误页面。还有 token 刷新时的请求去重——token 过期时如果有 10 个请求同时发出,只会发一次刷新请求,其他 9 个排队等待,刷新完成后统一重发。"
---
### 2.5 shallowRef 减少响应式开销
**问题**Vue 的 `ref()` 会深度遍历对象的每个属性,给每个属性都加响应式代理。对于大数据对象(几百条列表数据),这个遍历本身就很耗性能。
**解决方案**:用 `shallowRef()` 只监听整个对象的替换,不监听内部属性变化。
```typescript
import { shallowRef } from 'vue';
// ❌ 普通 ref深度响应式遍历每个属性
const tableData = ref(bigDataArray); // 1000 条数据,每条都加代理
// ✅ shallowRef浅层响应式只监听整体替换
const tableData = shallowRef(bigDataArray); // 不遍历,只在 .value 被替换时触发更新
```
**项目中 30+ 处使用**,主要用于:
- 大数据列表(表格数据、搜索结果)
- 恶意软件分析详情(大 JSON 对象)
- 威胁内容列表
- 表单引用(不需要响应式)
- 分页器状态(用 `shallowReactive`
```typescript
// 典型用法:表格数据用 shallowRef
const tableData = shallowRef([]);
const fetchTableData = async () => {
const res = await api.getData();
tableData.value = res.data; // 整体替换,触发更新
};
// 分页器用 shallowReactive
const pager = shallowReactive({ page: 1, size: 20, total: 0 });
```
**面试话术**
> "对于大数据对象,我用 shallowRef 替代 ref。ref 会深度遍历对象每个属性加响应式代理1000 条列表数据的遍历开销很大。shallowRef 只监听整体替换,不监听内部属性变化,数据更新时直接 `tableData.value = newData` 整体替换就行。项目中有 30 多处使用。"
---
### 2.6 IntersectionObserver 懒加载
**问题**:页面下方的内容在用户滚动到之前不需要渲染。
**解决方案**:用 IntersectionObserver 监听元素是否进入视口,进入后才触发加载或上报。
#### 自定义曝光指令
```typescript
// directives/element-exposure.ts — 自定义 Vue 指令
const vExposure = {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
binding.value.trigger('expo'); // 元素进入视口,触发上报
observer.unobserve(el); // 上报一次后取消监听
}
});
}, { threshold: 0 });
observer.observe(el);
}
};
// 使用:<div v-exposure="{ trigger: reportExposure }">...</div>
```
#### 滚动触发动画
```javascript
// product1.vue — 元素滚动到视口时才播放动画
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
startAnimation(); // 进入视口才开始动画
observer.disconnect();
}
});
observer.observe(elementRef.value);
```
**面试话术**
> "我用 IntersectionObserver 实现了元素的懒加载。自定义了一个 Vue 指令,元素进入视口时才触发曝光上报或播放动画,上报后自动取消监听。这样页面下方的内容在用户滚动到之前不会有任何开销。"
---
### 2.7 @vueuse/core 工具库
项目大量使用 vueuse 提供的性能优化工具,避免自己写低效代码:
| 工具 | 用在哪 | 干什么 |
|------|--------|--------|
| `useWindowSize` | 响应式布局 | 监听窗口大小,返回响应式 width/height |
| `useThrottleFn` | resize 事件 | 节流函数100ms 最多执行一次 |
| `useResizeObserver` | 内容溢出检测 | 高效检测元素尺寸变化,不轮询 |
| `useElementSize` | 搜索框/布局 | 获取元素宽高 |
| `useClipboard` | 复制按钮8处 | 剪贴板操作 |
| `useInfiniteScroll` | 历史记录 | 无限滚动加载 |
| `useEventListener` | 文件搜索 | 事件监听,自动清理 |
| `useMutationObserver` | 文本组件 | DOM 变化监听 |
---
## 三、哪些简历上写了但代码里没有?
| 简历描述 | 代码实际情况 | 建议 |
|----------|------------|------|
| **虚拟滚动** | ❌ 没有实现(无相关库/代码,无 vue-virtual-scroller 等依赖) | 面试时别主动提,被问到可以说"评估后发现数据量不需要虚拟滚动,分页已经控制了单页数据量" |
| **懒加载策略** | ✅ 真实存在(路由懒加载 + IntersectionObserver + 地图数据懒加载) | 可以讲 |
| **路由守卫优化** | ✅ 真实存在interceptor.ts 248 行7 种优化策略) | 可以讲,重点讲 ID 缓存 + Promise.all 并行 + 条件性请求 |
| **API 缓存降级** | ✅ 真实存在degradeInterceptor.ts 360 行 + request.ts token 去重) | 可以讲,技术含量最高的优化之一 |
**面试时建议这样讲**
> "性能优化方面我做了路由守卫优化ID 缓存 + Promise.all 并行 + 客户端预校验等 7 种策略、路由懒加载80+ 页面按需加载、防抖节流40+ 处高频事件优化、API 缓存降级IndexedDB 缓存 + 失败兜底 + token 请求去重、shallowRef 减少响应式开销30+ 处大数据对象、IntersectionObserver 元素懒加载。这些优化综合下来,页面切换和首屏加载都有明显改善。"
---
## 四、面试问答准备
### Q1"路由守卫优化"具体做了什么?
> 路由守卫是所有页面切换的入口80+ 个页面每次导航都经过 beforeEach。如果每次都会调 API页面切换会很慢。我做了 7 种优化:第一,模块级 ID 缓存,用 currentOrgId 和 currentRepoId 缓存当前组织/仓库 ID同一仓库内切换 tab 不发请求第二Promise.all 并行请求,仓库信息和角色权限同时发出;第三,条件性请求,未登录用户不请求角色权限;第四,客户端预校验,用正则过滤无效路径,在发请求之前就返回 404第五声明式 meta 配置,用 needLogin/loginHidden 标记控制权限第六白名单短路子应用页面跳过主应用守卫第七localStorage 持久化已访问仓库列表,跨会话记住用户首次访问状态。
### Q2防抖和节流有什么区别你项目里怎么用的
> 防抖是"等用户停下来才执行",比如搜索框输入 300ms 后才发请求,用户一直打字就一直不发。节流是"每隔一段时间执行一次",比如窗口 resize 每 100ms 最多重绘一次。项目中搜索、表单提交用防抖300ms滚动和 resize 用节流100~200ms。总共 40 多处,全部用 lodash 的按需引入(`lodash/debounce`),配合 Vite tree-shaking 不会打包整个 lodash。
### Q3API 缓存降级系统怎么设计的?
> 三层设计:第一层,请求成功时把响应缓存到 IndexedDB配置了最大缓存条数和过期时间第二层请求超时或报错时自动从 IndexedDB 读缓存返回给页面用户看到旧数据而不是错误第三层token 过期时做请求去重,多个并发请求只发一次 token 刷新,其他请求排队等刷新完成后统一重发。
### Q4shallowRef 和 ref 有什么区别?为什么用 shallowRef
> ref 会深度遍历对象的每个属性,给每个属性都加响应式代理。如果数据是 1000 条列表,每条有 10 个字段ref 要给 10000 个属性加代理这个遍历本身就有开销。shallowRef 只监听整个对象的替换(.value 赋值不监听内部属性变化。对于只需要整体替换不需要监听单个字段变化的大数据对象shallowRef 性能更好。项目中表格数据、分析详情等 30 多处都用了 shallowRef。
### Q5虚拟滚动做了吗
> 项目中评估后没有引入虚拟滚动。原因是我们的列表数据量通常在几十到几百条,分页已经控制了单页数据量,虚拟滚动的收益不大,反而增加代码复杂度。如果未来数据量增长到几千条,可以引入 vue-virtual-scroller 做进一步优化。
### Q6还有哪些性能优化手段
> 还有几个lodash 按需引入做 tree-shaking不打包整个库@vueuse/core 的 35+ 个工具函数避免重复造轮子Vite 构建配置了 esbuild 压缩、4KB 以下资源内联为 base64、常用依赖预构建用 rollup-plugin-visualizer 分析打包体积。
### Q7路由守卫里的 reqCatch 是干什么的?
> reqCatch 是一个错误包装函数,包装 API 调用以捕获错误而不触发未捕获异常。在路由守卫里特别重要——比如 getOrg 请求返回 403私有组织无权限如果没有 reqCatch 包装这个错误会中断整个守卫流程导致页面白屏。reqCatch 让守卫能优雅地处理 404/403返回对应的错误页面。
---
## 五、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 路由守卫优化策略 | 7 种ID 缓存、并行请求、条件性请求、预校验、声明式 meta、白名单、localStorage 持久化) |
| 路由守卫代码量 | 248 行 |
| 路由懒加载页面数 | 80+ 个 |
| 防抖/节流使用处 | 40+ 处 |
| shallowRef 使用处 | 30+ 处 |
| @vueuse/core 使用处 | 35+ 处 |
| API 缓存降级系统 | 360 行自研代码 |
| lodash 按需引入 | 全项目统一 |
| IntersectionObserver | 6+ 处 |
---
## 六、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 路由守卫优化(核心) | `src/router/interceptor.ts`248 行7 种优化策略) |
| 路由守卫中的错误包装 | `src/utils/catch.ts`reqCatch 函数) |
| 仓库路径正则预校验 | `src/utils/regex.ts`repoPathSuffix |
| 路由懒加载配置 | `src/router/config/home.ts`40+ 路由)等 |
| API 缓存降级系统 | `src/utils/degradeInterceptor.ts`核心360 行) |
| 缓存存储层 | `src/utils/apiStorage.ts`IndexedDB 封装) |
| 缓存策略配置 | `src/api/storage-apis.ts`(哪些 API 走缓存) |
| 请求去重/Token 刷新 | `src/utils/request.ts`118-238 行) |
| 自定义曝光指令 | `src/directives/element-exposure.ts` |
| 响应式布局 Hook | `src/utils/hooks/usePageResize.ts` |
| 地图懒加载 | `src/utils/mapLoader.js` |
| Vite 构建配置 | `vite.config.ts` |
| 微前端路由同步 | `src/utils/microAppConfig.ts` |

View File

@@ -9,7 +9,7 @@
<link rel="dns-prefetch" href="//usercenter-app.gitcode.com"> <link rel="dns-prefetch" href="//usercenter-app.gitcode.com">
<link rel="dns-prefetch" href="//res.hc-cdn.com"> <link rel="dns-prefetch" href="//res.hc-cdn.com">
<!-- <meta name="referrer" content="no-referrer"> --> <!-- <meta name="referrer" content="no-referrer"> -->
<title>可信开源代码库</title> <title>Tcode</title>
<style type="text/css"> <style type="text/css">
.icon { .icon {
width: 1em; height: 1em; width: 1em; height: 1em;
@@ -23,19 +23,4 @@
<div id="app"></div> <div id="app"></div>
<script type="module" src="/src/main.ts"></script> <script type="module" src="/src/main.ts"></script>
</body> </body>
<!-- <script type="text/javascript" src="https://cdn-static.gitcode.com/js/gitcode-icon.js"></script>-->
<!-- <script type="text/javascript" src="https://cdn-static.gitcode.com/js/event-msg.min.0.0.2.js"></script>-->
<!-- <script type="text/javascript" src="https://cdn-static.gitcode.com/js/lottie.min.js" defer></script>-->
<!-- <script type="text/javascript" src="https://cdn-static.gitcode.com/js/furion.js" defer></script>-->
<!-- baidu统计 -->
<script>
var _hmt = _hmt || [];
_hmt.push(['_setAutoPageview', false]);
(function() {
var hm = document.createElement("script");
hm.src = "https://hm.baidu.com/hm.js?62047c952451105d57bab2c4af9ce85b";
var s = document.getElementsByTagName("script")[0];
s.parentNode.insertBefore(hm, s);
})();
</script>
</html> </html>

733
package-lock.json generated
View File

@@ -1,11 +1,11 @@
{ {
"name": "gitcode-v2", "name": "SAP",
"version": "0.0.0", "version": "0.0.0",
"lockfileVersion": 3, "lockfileVersion": 3,
"requires": true, "requires": true,
"packages": { "packages": {
"": { "": {
"name": "gitcode-v2", "name": "SAP",
"version": "0.0.0", "version": "0.0.0",
"dependencies": { "dependencies": {
"@devui-design/icons": "^1.4.0", "@devui-design/icons": "^1.4.0",
@@ -14,15 +14,18 @@
"@sentry/vue": "^7.101.1", "@sentry/vue": "^7.101.1",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.2.1",
"ant-design-vue": "4.x",
"axios": "^1.6.8", "axios": "^1.6.8",
"canvas-confetti": "^1.9.2", "canvas-confetti": "^1.9.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"devui-theme": "^0.0.7", "devui-theme": "^0.0.7",
"dompurify": "^3.0.5", "dompurify": "^3.0.5",
"echarts": "^6.0.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"highlightjs-line-numbers.js": "^2.9.0", "highlightjs-line-numbers.js": "^2.9.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsencrypt": "^3.5.4",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lottie-web": "^5.12.2", "lottie-web": "^5.12.2",
@@ -37,6 +40,7 @@
"swiper": "^10.3.1", "swiper": "^10.3.1",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-devui": "^1.6.31", "vue-devui": "^1.6.31",
"vue-fast-marquee": "^1.0.6",
"vue-router": "^4.2.2", "vue-router": "^4.2.2",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"yaml": "^2.4.1" "yaml": "^2.4.1"
@@ -61,6 +65,7 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.9.2", "rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.63.6", "sass": "^1.63.6",
"ssh2": "^1.17.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"terser": "^5.39.0", "terser": "^5.39.0",
"typescript": "~5.0.4", "typescript": "~5.0.4",
@@ -103,6 +108,34 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/@ant-design/colors": {
"version": "6.0.0",
"resolved": "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz",
"integrity": "sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==",
"license": "MIT",
"dependencies": {
"@ctrl/tinycolor": "^3.4.0"
}
},
"node_modules/@ant-design/icons-svg": {
"version": "4.4.2",
"resolved": "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz",
"integrity": "sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==",
"license": "MIT"
},
"node_modules/@ant-design/icons-vue": {
"version": "7.0.1",
"resolved": "https://registry.npmmirror.com/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz",
"integrity": "sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-svg": "^4.2.1"
},
"peerDependencies": {
"vue": ">=3.0.3"
}
},
"node_modules/@babel/code-frame": { "node_modules/@babel/code-frame": {
"version": "7.24.2", "version": "7.24.2",
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
@@ -1699,7 +1732,6 @@
"version": "7.24.1", "version": "7.24.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==", "integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
"dev": true,
"dependencies": { "dependencies": {
"regenerator-runtime": "^0.14.0" "regenerator-runtime": "^0.14.0"
}, },
@@ -1710,8 +1742,7 @@
"node_modules/@babel/runtime/node_modules/regenerator-runtime": { "node_modules/@babel/runtime/node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
"dev": true
}, },
"node_modules/@babel/template": { "node_modules/@babel/template": {
"version": "7.24.0", "version": "7.24.0",
@@ -1765,11 +1796,389 @@
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz", "resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
"integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==" "integrity": "sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A=="
}, },
"node_modules/@ctrl/tinycolor": {
"version": "3.6.1",
"resolved": "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz",
"integrity": "sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==",
"license": "MIT",
"engines": {
"node": ">=10"
}
},
"node_modules/@devui-design/icons": { "node_modules/@devui-design/icons": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://mirrors.cloud.tencent.com/npm/@devui-design/icons/-/icons-1.4.0.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/@devui-design/icons/-/icons-1.4.0.tgz",
"integrity": "sha512-taAX1RNW0QHUqQTRPqLTYTB2PZIqUplhWeF4hcmWkSTjpWlDNI40DssG/WRb3sISkfBk/4BMUxxC5XeTL3jo7A==" "integrity": "sha512-taAX1RNW0QHUqQTRPqLTYTB2PZIqUplhWeF4hcmWkSTjpWlDNI40DssG/WRb3sISkfBk/4BMUxxC5XeTL3jo7A=="
}, },
"node_modules/@emotion/hash": {
"version": "0.9.2",
"resolved": "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz",
"integrity": "sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==",
"license": "MIT"
},
"node_modules/@emotion/unitless": {
"version": "0.8.1",
"resolved": "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz",
"integrity": "sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==",
"license": "MIT"
},
"node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild/win32-x64": { "node_modules/@esbuild/win32-x64": {
"version": "0.18.20", "version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz", "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
@@ -2322,6 +2731,16 @@
"vue": "2.x || 3.x" "vue": "2.x || 3.x"
} }
}, },
"node_modules/@simonwep/pickr": {
"version": "1.8.2",
"resolved": "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz",
"integrity": "sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==",
"license": "MIT",
"dependencies": {
"core-js": "^3.15.1",
"nanopop": "^2.1.0"
}
},
"node_modules/@tsconfig/node18": { "node_modules/@tsconfig/node18": {
"version": "2.0.1", "version": "2.0.1",
"resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-2.0.1.tgz", "resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-2.0.1.tgz",
@@ -3196,6 +3615,46 @@
"node": ">=4" "node": ">=4"
} }
}, },
"node_modules/ant-design-vue": {
"version": "4.2.6",
"resolved": "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-4.2.6.tgz",
"integrity": "sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==",
"license": "MIT",
"dependencies": {
"@ant-design/colors": "^6.0.0",
"@ant-design/icons-vue": "^7.0.0",
"@babel/runtime": "^7.10.5",
"@ctrl/tinycolor": "^3.5.0",
"@emotion/hash": "^0.9.0",
"@emotion/unitless": "^0.8.0",
"@simonwep/pickr": "~1.8.0",
"array-tree-filter": "^2.1.0",
"async-validator": "^4.0.0",
"csstype": "^3.1.1",
"dayjs": "^1.10.5",
"dom-align": "^1.12.1",
"dom-scroll-into-view": "^2.0.0",
"lodash": "^4.17.21",
"lodash-es": "^4.17.15",
"resize-observer-polyfill": "^1.5.1",
"scroll-into-view-if-needed": "^2.2.25",
"shallow-equal": "^1.0.0",
"stylis": "^4.1.3",
"throttle-debounce": "^5.0.0",
"vue-types": "^3.0.0",
"warning": "^4.0.0"
},
"engines": {
"node": ">=12.22.0"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/ant-design-vue"
},
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/any-promise": { "node_modules/any-promise": {
"version": "1.3.0", "version": "1.3.0",
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
@@ -3242,6 +3701,12 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/array-tree-filter": {
"version": "2.1.0",
"resolved": "https://registry.npmmirror.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz",
"integrity": "sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==",
"license": "MIT"
},
"node_modules/array-union": { "node_modules/array-union": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
@@ -3273,6 +3738,16 @@
"url": "https://github.com/sponsors/ljharb" "url": "https://github.com/sponsors/ljharb"
} }
}, },
"node_modules/asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"safer-buffer": "~2.1.0"
}
},
"node_modules/async-validator": { "node_modules/async-validator": {
"version": "4.2.5", "version": "4.2.5",
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz", "resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
@@ -3390,6 +3865,16 @@
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
"dev": true "dev": true
}, },
"node_modules/bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"dev": true,
"license": "BSD-3-Clause",
"dependencies": {
"tweetnacl": "^0.14.3"
}
},
"node_modules/binary-extensions": { "node_modules/binary-extensions": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
@@ -3468,6 +3953,16 @@
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true "dev": true
}, },
"node_modules/buildcheck": {
"version": "0.0.7",
"resolved": "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz",
"integrity": "sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==",
"dev": true,
"optional": true,
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/call-bind": { "node_modules/call-bind": {
"version": "1.0.7", "version": "1.0.7",
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
@@ -3686,6 +4181,12 @@
"node": ">=18" "node": ">=18"
} }
}, },
"node_modules/compute-scroll-into-view": {
"version": "1.0.20",
"resolved": "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz",
"integrity": "sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==",
"license": "MIT"
},
"node_modules/computeds": { "node_modules/computeds": {
"version": "0.0.1", "version": "0.0.1",
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz", "resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
@@ -3708,7 +4209,6 @@
"version": "3.36.1", "version": "3.36.1",
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz", "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==", "integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
"dev": true,
"hasInstallScript": true, "hasInstallScript": true,
"funding": { "funding": {
"type": "opencollective", "type": "opencollective",
@@ -3728,6 +4228,21 @@
"url": "https://opencollective.com/core-js" "url": "https://opencollective.com/core-js"
} }
}, },
"node_modules/cpu-features": {
"version": "0.0.10",
"resolved": "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz",
"integrity": "sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==",
"dev": true,
"hasInstallScript": true,
"optional": true,
"dependencies": {
"buildcheck": "~0.0.6",
"nan": "^2.19.0"
},
"engines": {
"node": ">=10.0.0"
}
},
"node_modules/crc-32": { "node_modules/crc-32": {
"version": "1.2.2", "version": "1.2.2",
"resolved": "https://mirrors.cloud.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz",
@@ -4693,6 +5208,18 @@
"node": ">=6.0.0" "node": ">=6.0.0"
} }
}, },
"node_modules/dom-align": {
"version": "1.12.4",
"resolved": "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz",
"integrity": "sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==",
"license": "MIT"
},
"node_modules/dom-scroll-into-view": {
"version": "2.0.1",
"resolved": "https://registry.npmmirror.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz",
"integrity": "sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==",
"license": "MIT"
},
"node_modules/dompurify": { "node_modules/dompurify": {
"version": "3.0.11", "version": "3.0.11",
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz",
@@ -4705,12 +5232,13 @@
"dev": true "dev": true
}, },
"node_modules/echarts": { "node_modules/echarts": {
"version": "5.3.3", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz", "resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
"integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==", "integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
"license": "Apache-2.0",
"dependencies": { "dependencies": {
"tslib": "2.3.0", "tslib": "2.3.0",
"zrender": "5.3.2" "zrender": "6.0.0"
} }
}, },
"node_modules/echarts/node_modules/tslib": { "node_modules/echarts/node_modules/tslib": {
@@ -5517,6 +6045,21 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/fsevents": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz",
"integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": "^8.16.0 || ^10.6.0 || >=11.0.0"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@@ -6144,6 +6687,15 @@
"node": ">=8" "node": ">=8"
} }
}, },
"node_modules/is-plain-object": {
"version": "3.0.1",
"resolved": "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz",
"integrity": "sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/is-regex": { "node_modules/is-regex": {
"version": "1.1.4", "version": "1.1.4",
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz", "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
@@ -6294,8 +6846,7 @@
"node_modules/js-tokens": { "node_modules/js-tokens": {
"version": "4.0.0", "version": "4.0.0",
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
"dev": true
}, },
"node_modules/js-yaml": { "node_modules/js-yaml": {
"version": "4.1.0", "version": "4.1.0",
@@ -6310,6 +6861,12 @@
"js-yaml": "bin/js-yaml.js" "js-yaml": "bin/js-yaml.js"
} }
}, },
"node_modules/jsencrypt": {
"version": "3.5.4",
"resolved": "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.5.4.tgz",
"integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==",
"license": "MIT"
},
"node_modules/jsesc": { "node_modules/jsesc": {
"version": "2.5.2", "version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -6516,6 +7073,18 @@
"dev": true, "dev": true,
"peer": true "peer": true
}, },
"node_modules/loose-envify": {
"version": "1.4.0",
"resolved": "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz",
"integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==",
"license": "MIT",
"dependencies": {
"js-tokens": "^3.0.0 || ^4.0.0"
},
"bin": {
"loose-envify": "cli.js"
}
},
"node_modules/lottie-web": { "node_modules/lottie-web": {
"version": "5.12.2", "version": "5.12.2",
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz", "resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz",
@@ -6725,6 +7294,14 @@
"thenify-all": "^1.0.0" "thenify-all": "^1.0.0"
} }
}, },
"node_modules/nan": {
"version": "2.25.0",
"resolved": "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz",
"integrity": "sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==",
"dev": true,
"license": "MIT",
"optional": true
},
"node_modules/nanoid": { "node_modules/nanoid": {
"version": "3.3.11", "version": "3.3.11",
"resolved": "https://mirrors.cloud.tencent.com/npm/nanoid/-/nanoid-3.3.11.tgz", "resolved": "https://mirrors.cloud.tencent.com/npm/nanoid/-/nanoid-3.3.11.tgz",
@@ -6742,6 +7319,12 @@
"node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1"
} }
}, },
"node_modules/nanopop": {
"version": "2.4.2",
"resolved": "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz",
"integrity": "sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==",
"license": "MIT"
},
"node_modules/natural-compare": { "node_modules/natural-compare": {
"version": "1.4.0", "version": "1.4.0",
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
@@ -7667,6 +8250,12 @@
"node": ">=0.10.0" "node": ">=0.10.0"
} }
}, },
"node_modules/resize-observer-polyfill": {
"version": "1.5.1",
"resolved": "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz",
"integrity": "sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==",
"license": "MIT"
},
"node_modules/resolve": { "node_modules/resolve": {
"version": "1.22.8", "version": "1.22.8",
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
@@ -7866,6 +8455,15 @@
"node": ">=14.0.0" "node": ">=14.0.0"
} }
}, },
"node_modules/scroll-into-view-if-needed": {
"version": "2.2.31",
"resolved": "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz",
"integrity": "sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==",
"license": "MIT",
"dependencies": {
"compute-scroll-into-view": "^1.0.20"
}
},
"node_modules/select": { "node_modules/select": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz", "resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
@@ -7911,6 +8509,12 @@
"node": ">= 0.4" "node": ">= 0.4"
} }
}, },
"node_modules/shallow-equal": {
"version": "1.2.1",
"resolved": "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz",
"integrity": "sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==",
"license": "MIT"
},
"node_modules/shebang-command": { "node_modules/shebang-command": {
"version": "2.0.0", "version": "2.0.0",
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
@@ -8063,6 +8667,24 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/ssh2": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz",
"integrity": "sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==",
"dev": true,
"hasInstallScript": true,
"dependencies": {
"asn1": "^0.2.6",
"bcrypt-pbkdf": "^1.0.2"
},
"engines": {
"node": ">=10.16.0"
},
"optionalDependencies": {
"cpu-features": "~0.0.10",
"nan": "^2.23.0"
}
},
"node_modules/string-width": { "node_modules/string-width": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
@@ -8314,7 +8936,7 @@
}, },
"node_modules/swiper": { "node_modules/swiper": {
"version": "10.3.1", "version": "10.3.1",
"resolved": "https://mirrors.cloud.tencent.com/npm/swiper/-/swiper-10.3.1.tgz", "resolved": "https://registry.npmmirror.com/swiper/-/swiper-10.3.1.tgz",
"integrity": "sha512-24Wk3YUdZHxjc9faID97GTu6xnLNia+adMt6qMTZG/HgdSUt4fS0REsGUXJOgpTED0Amh/j+gRGQxsLayJUlBQ==", "integrity": "sha512-24Wk3YUdZHxjc9faID97GTu6xnLNia+adMt6qMTZG/HgdSUt4fS0REsGUXJOgpTED0Amh/j+gRGQxsLayJUlBQ==",
"funding": [ "funding": [
{ {
@@ -8326,6 +8948,7 @@
"url": "http://opencollective.com/swiper" "url": "http://opencollective.com/swiper"
} }
], ],
"license": "MIT",
"engines": { "engines": {
"node": ">= 4.7.0" "node": ">= 4.7.0"
} }
@@ -8425,6 +9048,15 @@
"node": ">=0.8" "node": ">=0.8"
} }
}, },
"node_modules/throttle-debounce": {
"version": "5.0.2",
"resolved": "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz",
"integrity": "sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==",
"license": "MIT",
"engines": {
"node": ">=12.22"
}
},
"node_modules/tiny-emitter": { "node_modules/tiny-emitter": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz", "resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
@@ -8469,6 +9101,13 @@
"typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta" "typescript": ">=2.8.0 || >= 3.2.0-dev || >= 3.3.0-dev || >= 3.4.0-dev || >= 3.5.0-dev || >= 3.6.0-dev || >= 3.6.0-beta || >= 3.7.0-dev || >= 3.7.0-beta"
} }
}, },
"node_modules/tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==",
"dev": true,
"license": "Unlicense"
},
"node_modules/type-check": { "node_modules/type-check": {
"version": "0.4.0", "version": "0.4.0",
"resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz",
@@ -8930,6 +9569,16 @@
"resolved": "https://registry.npmjs.org/devui-theme/-/devui-theme-0.0.1.tgz", "resolved": "https://registry.npmjs.org/devui-theme/-/devui-theme-0.0.1.tgz",
"integrity": "sha512-5nF6fChlsXKeAtvkaAF4bZ0NMiEAbzwqQ9XZQiNuM0RRFz5lW29nnbhfDCNPkmnw5ZyCVyXqYwVRBIZrwZHXrA==" "integrity": "sha512-5nF6fChlsXKeAtvkaAF4bZ0NMiEAbzwqQ9XZQiNuM0RRFz5lW29nnbhfDCNPkmnw5ZyCVyXqYwVRBIZrwZHXrA=="
}, },
"node_modules/vue-devui/node_modules/echarts": {
"version": "5.3.3",
"resolved": "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz",
"integrity": "sha512-BRw2serInRwO5SIwRviZ6Xgm5Lb7irgz+sLiFMmy/HOaf4SQ+7oYqxKzRHAKp4xHQ05AuHw1xvoQWJjDQq/FGw==",
"license": "Apache-2.0",
"dependencies": {
"tslib": "2.3.0",
"zrender": "5.3.2"
}
},
"node_modules/vue-devui/node_modules/entities": { "node_modules/vue-devui/node_modules/entities": {
"version": "2.1.0", "version": "2.1.0",
"resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz", "resolved": "https://registry.npmjs.org/entities/-/entities-2.1.0.tgz",
@@ -8966,11 +9615,26 @@
"resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz", "resolved": "https://registry.npmjs.org/mdurl/-/mdurl-1.0.1.tgz",
"integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g==" "integrity": "sha512-/sKlQJCBYVY9Ers9hqzKou4H6V5UWc/M59TH2dvkt+84itfnq7uFOMLpOiOS4ujvHP4etln18fmIxA5R5fll0g=="
}, },
"node_modules/vue-devui/node_modules/tslib": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
},
"node_modules/vue-devui/node_modules/uc.micro": { "node_modules/vue-devui/node_modules/uc.micro": {
"version": "1.0.6", "version": "1.0.6",
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz", "resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
"integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA==" "integrity": "sha512-8Y75pvTYkLJW2hWQHXxoqRgV7qb9B+9vFEtidML+7koHUFapnVJAZ6cKs+Qjz5Aw3aZWHMC6u0wJE3At+nSGwA=="
}, },
"node_modules/vue-devui/node_modules/zrender": {
"version": "5.3.2",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.2.tgz",
"integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==",
"license": "BSD-3-Clause",
"dependencies": {
"tslib": "2.3.0"
}
},
"node_modules/vue-eslint-parser": { "node_modules/vue-eslint-parser": {
"version": "9.4.2", "version": "9.4.2",
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz",
@@ -9028,6 +9692,15 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "dev": true
}, },
"node_modules/vue-fast-marquee": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/vue-fast-marquee/-/vue-fast-marquee-1.0.6.tgz",
"integrity": "sha512-VMqaeuBSS3L55Q84EuLJPHZ4Zv4UVBDXmh+2hb1IBTLKt/FmurJnWVXv8BNnbQSupA4kHbPlWDLwp+kvEMOF7A==",
"license": "MIT",
"peerDependencies": {
"vue": ">=3.2.0"
}
},
"node_modules/vue-router": { "node_modules/vue-router": {
"version": "4.3.0", "version": "4.3.0",
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz", "resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz",
@@ -9102,6 +9775,30 @@
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
"dev": true "dev": true
}, },
"node_modules/vue-types": {
"version": "3.0.2",
"resolved": "https://registry.npmmirror.com/vue-types/-/vue-types-3.0.2.tgz",
"integrity": "sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==",
"license": "MIT",
"dependencies": {
"is-plain-object": "3.0.1"
},
"engines": {
"node": ">=10.15.0"
},
"peerDependencies": {
"vue": "^3.0.0"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"license": "MIT",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/which": { "node_modules/which": {
"version": "2.0.2", "version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
@@ -9392,9 +10089,10 @@
} }
}, },
"node_modules/zrender": { "node_modules/zrender": {
"version": "5.3.2", "version": "6.0.0",
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.2.tgz", "resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
"integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==", "integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
"license": "BSD-3-Clause",
"dependencies": { "dependencies": {
"tslib": "2.3.0" "tslib": "2.3.0"
} }
@@ -9402,7 +10100,8 @@
"node_modules/zrender/node_modules/tslib": { "node_modules/zrender/node_modules/tslib": {
"version": "2.3.0", "version": "2.3.0",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==" "integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
"license": "0BSD"
} }
} }
} }

View File

@@ -1,5 +1,5 @@
{ {
"name": "gitcode-v2", "name": "SAP",
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
@@ -16,7 +16,8 @@
"preview": "vite preview", "preview": "vite preview",
"type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false", "type-check": "vue-tsc --noEmit -p tsconfig.app.json --composite false",
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore", "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore",
"format": "prettier --write src/" "format": "prettier --write src/",
"deploy": "node deploy.js"
}, },
"dependencies": { "dependencies": {
"@devui-design/icons": "^1.4.0", "@devui-design/icons": "^1.4.0",
@@ -25,15 +26,18 @@
"@sentry/vue": "^7.101.1", "@sentry/vue": "^7.101.1",
"@types/crypto-js": "^4.1.1", "@types/crypto-js": "^4.1.1",
"@vueuse/core": "^10.2.1", "@vueuse/core": "^10.2.1",
"ant-design-vue": "4.x",
"axios": "^1.6.8", "axios": "^1.6.8",
"canvas-confetti": "^1.9.2", "canvas-confetti": "^1.9.2",
"crypto-js": "^4.2.0", "crypto-js": "^4.2.0",
"dayjs": "^1.11.13", "dayjs": "^1.11.13",
"devui-theme": "^0.0.7", "devui-theme": "^0.0.7",
"dompurify": "^3.0.5", "dompurify": "^3.0.5",
"echarts": "^6.0.0",
"highlight.js": "^11.11.1", "highlight.js": "^11.11.1",
"highlightjs-line-numbers.js": "^2.9.0", "highlightjs-line-numbers.js": "^2.9.0",
"js-cookie": "^3.0.5", "js-cookie": "^3.0.5",
"jsencrypt": "^3.5.4",
"localforage": "^1.10.0", "localforage": "^1.10.0",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lottie-web": "^5.12.2", "lottie-web": "^5.12.2",
@@ -48,6 +52,7 @@
"swiper": "^10.3.1", "swiper": "^10.3.1",
"vue": "^3.3.4", "vue": "^3.3.4",
"vue-devui": "^1.6.31", "vue-devui": "^1.6.31",
"vue-fast-marquee": "^1.0.6",
"vue-router": "^4.2.2", "vue-router": "^4.2.2",
"xlsx": "^0.18.5", "xlsx": "^0.18.5",
"yaml": "^2.4.1" "yaml": "^2.4.1"
@@ -72,6 +77,7 @@
"prettier": "^2.8.8", "prettier": "^2.8.8",
"rollup-plugin-visualizer": "^5.9.2", "rollup-plugin-visualizer": "^5.9.2",
"sass": "^1.63.6", "sass": "^1.63.6",
"ssh2": "^1.17.0",
"tailwindcss": "^3.3.3", "tailwindcss": "^3.3.3",
"terser": "^5.39.0", "terser": "^5.39.0",
"typescript": "~5.0.4", "typescript": "~5.0.4",

453
src/api/jyh/situation.ts Normal file
View File

@@ -0,0 +1,453 @@
import { reqCatchV2, type catchRt } from '@/utils/catch';
import request from '@/utils/request';
import axios from 'axios';
// 详情-获取版本信息
export function releaseInfo(params: { softwareId: string }): Promise<any> {
const { softwareId } = params;
return request({
url: `/api/v1/repo/release/info?softwareId=${softwareId}`,
method: 'get'
});
}
// 详情-搜索结果列表
export function releasePage(data): Promise<any> {
return request({
url: `/api/v1/repo/release/page`,
method: 'post',
data
});
}
// 全球开源风险态势感知监控中心-统计概览
export function overviewDataPage1(data): Promise<any> {
return request({
url: `/api/v1/page1/overviewData`,
method: 'get',
data
});
}
// 全球开源风险态势感知监控中心-地图-漏洞类型分布
export function mapVulnStatsPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/map/vulnStats`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-地图-威胁情报地域排行
export function mapRegionStatsPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/map/regionStats`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-地图-实时情报检测
export function mapVulnTypeStatsPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/map/alerts`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-地图-地图上的数据
export function mapDataPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/map/mapData`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-行业分布饼图
export function industryDistributionPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/industryDistribution`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-高危组件 Top 10 柱状图
export function highRiskComponentPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/highRiskComponent`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-情报流
export function feedListPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/feedList`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-CVE 月度趋势-全球数据
export function cveDataGlobalPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/cveData/global`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-CVE 月度趋势-区域数据
export function cveDataRegionalPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/cveData/regional`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-漏洞语言分布堆叠图
export function languageTrendPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/languageTrend`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-漏洞等级分布
export function vulnerabilitySeverityPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/vulnerabilitySeverity`,
method: 'get',
params
});
}
// 全球开源风险态势感知监控中心-编程语言安全态势气泡图
export function scatterDataPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/scatterData`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-统计概览
export function overviewDataPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/overviewData`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-全球开源企业贡献榜 Top 30 (全部 & 中国 & 美国)
export function rawEnterpriseDataPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/rawEnterpriseData`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-基础设施覆盖率
export function infrastructurePage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/infrastructure`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-高校开源社团/俱乐部数量柱状图
export function universityClubsPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/universityClubs`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-社区治理健康度对比柱状图
export function communityHealthPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/communityHealth`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-语言与技术竞争 (主流语言与新兴语言)
export function languageTechPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/languageTech`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-明星开发者聚焦
export function excellentDeveloperPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/excellentDeveloper`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-开发者活力波形图
export function vitalityPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/vitalityData`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-开源项目热力图
export function projectHeatMapPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/projectHeatmap`,
method: 'get',
params
});
}
// 开源生态与贡献价值全景-中国开源人才密度地图
export function talentMapDataPage2(params?: any): Promise<any> {
return request({
url: `/api/v1/page2/talentMapData`,
method: 'get',
params
});
}
// 武汉市开源生态建设-统计概览1
export function hmStatsBarPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/hmStatsBar`,
method: 'get',
params
});
}
// 武汉市开源生态建设-统计概览2
export function statsBarPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/statsBar`,
method: 'get',
params
});
}
// 武汉市开源生态建设-鸿蒙项目城市分布
export function hmCityDataPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/hmCityData`,
method: 'get',
params
});
}
// 武汉市开源生态建设-鸿蒙开源项目
export function hmProjectsPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/hmProjects`,
method: 'get',
params
});
}
// 武汉市开源生态建设-武汉鸿蒙项目
export function whProjectsPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/whProjects`,
method: 'get',
params
});
}
// 武汉市开源生态建设-开发者贡献榜
export function hmDevelopersPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/hmDevelopers`,
method: 'get',
params
});
}
// 武汉市开源生态建设-开发者分布
export function hmDevCityDataPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/hmDevCityData`,
method: 'get',
params
});
}
// 武汉市开源生态建设-武汉开发者贡献榜
export function whHmDevelopersPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/whHmDevelopers`,
method: 'get',
params
});
}
// 武汉市开源生态建设-开源企业分布
export function hmEnterpriseCityDataPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/hmEnterpriseCityData`,
method: 'get',
params
});
}
// 武汉市开源生态建设-开源企业贡献榜
export function hmEnterprisesPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/hmEnterprises`,
method: 'get',
params
});
}
// 武汉市开源生态建设-武汉企业贡献榜
export function whEnterprisesPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/whEnterprises`,
method: 'get',
params
});
}
// 武汉市开源生态建设-全国高校分布
export function uniCityDataPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/uniCityData`,
method: 'get',
params
});
}
// 武汉市开源生态建设-高校贡献者榜
export function uniContributorsPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/uniContributors`,
method: 'get',
params
});
}
// 武汉市开源生态建设-武汉优质开源项目榜
export function wuhanGithubProjectsPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/wuhanGithubProjects`,
method: 'get',
params
});
}
// 武汉市开源生态建设-2024武汉优秀开源项目
export function chinaExcellentProjectsPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/chinaExcellentProjects`,
method: 'get',
params
});
}
// 武汉市开源生态建设-2025武汉优秀开源项目
export function excellentProjectsPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/excellentProjects`,
method: 'get',
params
});
}
// 武汉市开源生态建设-城市社区数量排行
export function communityRankChartPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/communityRankChart`,
method: 'get',
params
});
}
// 武汉市开源生态建设-武汉知名开源社区
export function wuhanCommunitiesPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/wuhanCommunities`,
method: 'get',
params
});
}
// 武汉市开源生态建设-明星开发者名单
export function developerNamesPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/developerNames`,
method: 'get',
params
});
}
// 武汉市开源生态建设-城市项目分布
export function cityProjectPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/cityProject`,
method: 'get',
params
});
}
// 武汉市开源生态建设-城市开源人才数量对比
export function talentPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/talent`,
method: 'get',
params
});
}
// 武汉市开源生态建设-重点城市高校开源开源社区数量
export function ClubPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/club`,
method: 'get',
params
});
}
// 武汉市开源生态建设-各省相关政策数量
export function policyPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/policy`,
method: 'get',
params
});
}
// 武汉市开源生态建设-校园行城市活动场次
export function campusPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/campus`,
method: 'get',
params
});
}
// 武汉市开源生态建设-城市镜像站/节点数量对比
export function mirrorsPage3(params?: any): Promise<any> {
return request({
url: `/api/v1/page3/mirrors`,
method: 'get',
params
});
}

View File

@@ -81,7 +81,8 @@ const dataSubMenus = [
]; ];
const handleLogin = (type) => { const handleLogin = (type) => {
emitEvent('login', {type, triggerType: '主动注册'}); // emitEvent('login', {type, triggerType: '主动注册'});
window.open('http://222.20.126.217:2133/login', '_blank');
}; };
</script> </script>

203
src/mock/page1.ts Normal file
View File

@@ -0,0 +1,203 @@
/**
* 态势感知看板 - Tab1 (全球开源风险态势感知监控中心) Mock 数据
*
* 数据结构根据 src/views/Jyh/security/components/*.vue 反推所得。
* 返回完整的 axios 响应对象(不加密明文),与 request.ts 的 $Decrypt 逻辑兼容。
*/
import type { AxiosResponse } from 'axios';
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
const randomDelay = () => sleep(200 + Math.floor(Math.random() * 300));
function mockResp<T>(data: T, code = 200, msg = 'success'): AxiosResponse<{ code: number; msg: string; data: T }> {
return {
data: { code, msg, data },
status: 200,
statusText: 'OK',
headers: {},
config: {} as any
};
}
// 1. 顶部统计概览 - 4 个数字
export async function overviewData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp({
enterprise: 12480,
university: 326,
activity: 1879,
project: 56420
});
}
// 2. 实时情报检测 (alerts)
export async function mapAlerts(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ title: '高危漏洞通告', desc: 'Apache Log4j2 远程代码执行漏洞 (CVE-2024-XXXX) 复现' },
{ title: '僵尸网络活动', desc: 'Mirai 变种正在扫描 IoT 设备端口 23/2323' },
{ title: 'APT 组织活动', desc: 'OceanLotus 组织针对金融行业的钓鱼攻击激增' },
{ title: '勒索软件预警', desc: 'LockBit 3.0 新变种正在利用 Confluence 漏洞传播' },
{ title: '供应链风险', desc: 'PyPI 仓库发现伪装成流行库的恶意包' }
]);
}
// 3. 世界地图数据
export async function mapData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'China', value: 100 },
{ name: 'United States', value: 88 },
{ name: 'Russia', value: 72 },
{ name: 'India', value: 65 },
{ name: 'Germany', value: 58 },
{ name: 'United Kingdom', value: 52 },
{ name: 'Brazil', value: 45 },
{ name: 'Japan', value: 42 },
{ name: 'France', value: 38 },
{ name: 'Canada', value: 35 },
{ name: 'Australia', value: 28 },
{ name: 'South Korea', value: 25 }
]);
}
// 4. 漏洞类型分布 Top 5
export async function vulnStats(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'XSS 跨站脚本', value: 2840 },
{ name: 'SQL 注入', value: 2156 },
{ name: '权限绕过', value: 1832 },
{ name: '信息泄露', value: 1521 },
{ name: '远程代码执行', value: 1245 }
]);
}
// 5. 威胁情报地域排行
export async function regionStats(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '中国大陆', score: 98.5 },
{ name: '美国', score: 92.3 },
{ name: '俄罗斯', score: 87.1 },
{ name: '德国', score: 76.4 },
{ name: '英国', score: 71.2 },
{ name: '印度', score: 68.9 },
{ name: '巴西', score: 62.5 },
{ name: '日本', score: 58.7 },
{ name: '韩国', score: 54.3 },
{ name: '法国', score: 49.8 }
]);
}
// 6. 行业分布饼图
export async function industryDistribution(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '互联网/科技', value: 32 },
{ name: '金融', value: 21 },
{ name: '政府/事业单位', value: 15 },
{ name: '教育科研', value: 12 },
{ name: '医疗健康', value: 9 },
{ name: '制造业', value: 7 },
{ name: '其他', value: 4 }
]);
}
// 7. 高危组件 Top 10
export async function highRiskComponent(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'log4j-core', cvss: 10.0, vulnCount: 18, usage: '2.1M+', desc: 'Apache Log4j 核心库,远程代码执行高危' },
{ name: 'spring-core', cvss: 9.8, vulnCount: 24, usage: '4.5M+', desc: 'Spring 框架核心,权限绕过风险' },
{ name: 'openssl', cvss: 9.1, vulnCount: 32, usage: '1.8M+', desc: 'OpenSSL 加密库,心脏滴血类漏洞' },
{ name: 'struts2-core', cvss: 9.8, vulnCount: 15, usage: '380K+', desc: 'Apache Struts2 框架OGNL 注入' },
{ name: 'fastjson', cvss: 9.5, vulnCount: 12, usage: '920K+', desc: '阿里 JSON 库,反序列化漏洞' },
{ name: 'shiro-core', cvss: 9.0, vulnCount: 8, usage: '650K+', desc: 'Apache Shiro 安全框架,认证绕过' },
{ name: 'druid', cvss: 8.5, vulnCount: 6, usage: '1.2M+', desc: 'Druid 数据库连接池,配置不当风险' },
{ name: 'jquery', cvss: 7.5, vulnCount: 22, usage: '8.4M+', desc: 'jQuery 旧版本 XSS 漏洞' },
{ name: 'tomcat-embed-core', cvss: 8.1, vulnCount: 14, usage: '2.7M+', desc: '嵌入式 Tomcat安全默认配置' },
{ name: 'snakeyaml', cvss: 8.5, vulnCount: 5, usage: '1.5M+', desc: 'YAML 解析库,反序列化风险' }
]);
}
// 8. 情报流
export async function feedList(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ type: '漏洞', content: 'CVE-2024-38063: Windows TCP/IP 远程代码执行漏洞披露,影响所有 Windows 11/10 系统' },
{ type: '威胁', content: '新型勒索软件 Akira 针对中型企业发起攻击,已发现 200+ 受害组织' },
{ type: '漏洞', content: 'GitLab 紧急发布安全更新,修复多个高危权限提升漏洞' },
{ type: '预警', content: 'Node.js 18.x 系列将停止官方支持,建议升级到 20.x LTS' },
{ type: '漏洞', content: 'OpenSSH regreSSHion 漏洞 (CVE-2024-6387) 影响广泛,需立即修补' },
{ type: '威胁', content: '国家级 APT 组织 Lazarus 利用 DeFi 协议漏洞窃取加密货币' },
{ type: '事件', content: '某知名 CDN 服务商遭遇大规模 DDoS 攻击,峰值达 5.6 Tbps' }
]);
}
// 9 & 10. CVE 月度趋势 - 全球 / 区域
function genTrend(months = 12) {
const cveCount: number[] = [];
const acceleration: number[] = [];
let base = 1800;
for (let i = 0; i < months; i++) {
base += Math.floor(Math.random() * 200) - 50;
cveCount.push(base);
acceleration.push(Math.floor(Math.random() * 40) - 20);
}
return { cveCount, acceleration };
}
export async function cveDataGlobal(): Promise<AxiosResponse> {
await randomDelay();
return mockResp(genTrend());
}
export async function cveDataRegional(): Promise<AxiosResponse> {
await randomDelay();
return mockResp(genTrend());
}
// 11. 漏洞语言分布堆叠图
export async function languageTrend(): Promise<AxiosResponse> {
await randomDelay();
const years = ['2022', '2023', '2024'];
const langs = ['JavaScript', 'Python', 'Java', 'C++', 'Go', 'PHP'];
const data: { name: string; year: string; data: number }[] = [];
langs.forEach((lang) => {
years.forEach((year) => {
data.push({ name: lang, year, data: Math.floor(Math.random() * 800) + 200 });
});
});
return mockResp(data);
}
// 12. 漏洞等级分布 (按季度)
export async function vulnerabilitySeverity(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ quarter: '2024Q1', highRisk: 1245, mediumRisk: 3560, lowRisk: 8920 },
{ quarter: '2024Q2', highRisk: 1380, mediumRisk: 3920, lowRisk: 9450 },
{ quarter: '2024Q3', highRisk: 1520, mediumRisk: 4180, lowRisk: 10240 },
{ quarter: '2024Q4', highRisk: 1690, mediumRisk: 4520, lowRisk: 11100 }
]);
}
// 13. 编程语言安全态势散点图
export async function scatterData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ x: 85, y: 92, size: 80, name: 'JavaScript' },
{ x: 78, y: 88, size: 65, name: 'Python' },
{ x: 72, y: 85, size: 55, name: 'Java' },
{ x: 65, y: 78, size: 45, name: 'C++' },
{ x: 60, y: 82, size: 35, name: 'Go' },
{ x: 55, y: 70, size: 30, name: 'PHP' },
{ x: 80, y: 75, size: 50, name: 'TypeScript' },
{ x: 50, y: 68, size: 25, name: 'Ruby' },
{ x: 45, y: 72, size: 28, name: 'Rust' },
{ x: 70, y: 65, size: 38, name: 'C#' },
{ x: 40, y: 60, size: 20, name: 'Swift' },
{ x: 35, y: 55, size: 18, name: 'Kotlin' }
]);
}

261
src/mock/page2.ts Normal file
View File

@@ -0,0 +1,261 @@
/**
* 态势感知看板 - Tab2 (开源生态与贡献价值全景) Mock 数据
*
* 数据结构根据 src/views/Jyh/ecosystem/index.vue 与 components/*.vue 反推所得。
*/
import type { AxiosResponse } from 'axios';
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
const randomDelay = () => sleep(200 + Math.floor(Math.random() * 300));
function mockResp<T>(data: T, code = 200, msg = 'success'): AxiosResponse<{ code: number; msg: string; data: T }> {
return {
data: { code, msg, data },
status: 200,
statusText: 'OK',
headers: {},
config: {} as any
};
}
// 1. 顶部统计概览
export async function overviewData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp({
developer: 2000000, // 200 万
project: 3000000, // 300 万
company: 500,
valuation: 280000000000 // 2800 亿
});
}
// 2. 全球开源企业贡献榜 Top 30
export async function rawEnterpriseData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'Microsoft', openrank: 177654.43, repos: 9211, devs: 14954, country: 'USA' },
{ name: 'Huawei', openrank: 108690.66, repos: 11407, devs: 16413, country: 'China' },
{ name: 'Google', openrank: 64156.6, repos: 3426, devs: 64115, country: 'USA' },
{ name: 'Amazon', openrank: 54776.3, repos: 7850, devs: 35802, country: 'USA' },
{ name: 'Meta', openrank: 34811.3, repos: 1410, devs: 31912, country: 'USA' },
{ name: 'RedHat', openrank: 28004.37, repos: 736, devs: 3626, country: 'USA' },
{ name: 'Mozilla', openrank: 27085.14, repos: 1792, devs: 17242, country: 'USA' },
{ name: 'Alibaba', openrank: 24744.15, repos: 5570, devs: 18661, country: 'China' },
{ name: 'IBM', openrank: 21165.28, repos: 626, devs: 5658, country: 'USA' },
{ name: 'Elastic', openrank: 21472.45, repos: 2437, devs: 17735, country: 'USA' },
{ name: 'Nabu Casa', openrank: 18499.45, repos: 101, devs: 27413, country: 'USA' },
{ name: 'Grafana Labs', openrank: 18237.03, repos: 756, devs: 10925, country: 'USA' },
{ name: 'Ant Group', openrank: 17916.45, repos: 1251, devs: 13760, country: 'China' },
{ name: 'DataDog', openrank: 17795.41, repos: 935, devs: 5401, country: 'USA' },
{ name: 'Intel', openrank: 17725.74, repos: 1046, devs: 6142, country: 'USA' },
{ name: 'HashiCorp', openrank: 17229.81, repos: 745, devs: 17068, country: 'USA' },
{ name: 'Hugging Face', openrank: 16909.25, repos: 277, devs: 15021, country: 'USA' },
{ name: 'NVIDIA', openrank: 16082.9, repos: 850, devs: 12362, country: 'USA' },
{ name: 'Baidu', openrank: 15695.3, repos: 408, devs: 9386, country: 'China' },
{ name: 'VMWare', openrank: 14627.44, repos: 634, devs: 11794, country: 'USA' },
{ name: 'Odoo', openrank: 12959.59, repos: 27, devs: 2678, country: 'Belgium' },
{ name: 'Dio Academia', openrank: 11231.13, repos: 169, devs: 24070, country: 'Portugal' },
{ name: 'Oracle', openrank: 10799.13, repos: 528, devs: 37959, country: 'USA' },
{ name: 'Yandex', openrank: 10025.79, repos: 283, devs: 10112, country: 'Germany' },
{ name: 'NextCloud', openrank: 10029.67, repos: 229, devs: 39516, country: 'Russia' },
{ name: 'ByteDance', openrank: 8310.37, repos: 652, devs: 5989, country: 'China' },
{ name: 'WordPress', openrank: 8267.62, repos: 176, devs: 777, country: 'USA' },
{ name: 'Vercel', openrank: 8214.69, repos: 170, devs: 13420, country: 'USA' },
{ name: 'LangChain', openrank: 8108.64, repos: 135, devs: 10099, country: 'USA' },
{ name: 'ConsenSys', openrank: 8071.43, repos: 640, devs: 4686, country: 'USA' }
]);
}
// 3. 基础设施覆盖率
export async function infrastructure(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '镜像站规模', value: 85 },
{ name: '代码托管平台规模', value: 90 },
{ name: '构建平台活跃度', value: 40 }
]);
}
// 4. 高校开源社团
export async function universityClubs(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ university: '上海交通大学', club: 'SJTU-LUG (Linux User Group)' },
{ university: '上海交通大学', club: '开放原子开源俱乐部' },
{ university: '复旦大学', club: 'Fudan LUG' },
{ university: '复旦大学', club: '开放原子开源俱乐部' },
{ university: '浙江大学', club: 'ZJU-LUG' },
{ university: '浙江大学', club: 'AAA (Azure Availability Association)' },
{ university: '浙江大学', club: '开放原子开源俱乐部' },
{ university: '南京大学', club: 'NJU-LUG' },
{ university: '南京大学', club: 'eScience 协会' },
{ university: '南京大学', club: '开放原子开源俱乐部' },
{ university: '中国科学技术大学', club: 'USTC-LUG' },
{ university: '中国科学技术大学', club: 'VLAB' },
{ university: '同济大学', club: 'Tongji LUG' },
{ university: '同济大学', club: '开放原子开源俱乐部' },
{ university: '华东师范大学', club: 'ECNU LUG' },
{ university: '清华大学', club: 'TUNA (清华大学学生网络与开源软件协会)' },
{ university: '清华大学', club: '开放原子开源俱乐部' },
{ university: '北京大学', club: 'PKU-LUG' },
{ university: '北京大学', club: '开源软件协会' },
{ university: '中国人民大学', club: 'RUC 开源社' },
{ university: '北京航空航天大学', club: 'BUAA-LUG' },
{ university: '北京理工大学', club: 'BIT-LUG' },
{ university: '南开大学', club: 'NKU-LUG' },
{ university: '天津大学', club: 'TJU-LUG' },
{ university: '中山大学', club: 'SYSU-LUG' },
{ university: '华南理工大学', club: 'SCUT-LUG' },
{ university: '暨南大学', club: 'JNU-LUG' },
{ university: '深圳大学', club: 'SZU-LUG' },
{ university: '华南师范大学', club: 'SCNU-LUG' },
{ university: '华中科技大学', club: 'HUST-LUG' },
{ university: '华中科技大学', club: '开放原子开源俱乐部' },
{ university: '武汉大学', club: 'WHU-LUG' },
{ university: '武汉大学', club: '珞珈码农社' },
{ university: '中南大学', club: 'CSU-LUG' },
{ university: '山东大学', club: 'SDU-LUG' },
{ university: '厦门大学', club: 'XMU-LUG' }
]);
}
// 5. 社区治理健康度对比
export async function communityHealth(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ indicators: 'PR 合并率', github: 82, gitcode: 68 },
{ indicators: 'Issue 响应时间', github: 88, gitcode: 75 },
{ indicators: 'CI/CD 使用率', github: 78, gitcode: 55 },
{ indicators: '平均代码质量得分', github: 85, gitcode: 72 },
{ indicators: '安全审计频率', github: 75, gitcode: 60 },
{ indicators: '贡献者多样性', github: 80, gitcode: 65 }
]);
}
// 6. 语言与技术竞争 (组件按 name 字段过滤)
export async function languageTech(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'JavaScript', value: 61, growth: 0 },
{ name: 'Python', value: 57, growth: 0 },
{ name: 'HTML/CSS', value: 52, growth: 1.96 },
{ name: 'Java', value: 49, growth: 6.52 },
{ name: 'SQL', value: 47, growth: -2.08 },
{ name: 'Shell', value: 36, growth: 0 },
{ name: 'TypeScript', value: 42, growth: 13.51 },
{ name: 'C++', value: 25, growth: 0 },
{ name: 'C#', value: 21, growth: -4.55 },
{ name: 'Go', value: 20, growth: 11.11 },
{ name: 'Kotlin', value: 18, growth: 28.57 },
{ name: 'C', value: 18, growth: 0 }
]);
}
// 7. 明星开发者 Top 18
export async function excellentDeveloper(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '稚晖', description: 'TOP 1', org: 'Shanghai', followers: 82738, avatar: 'https://github.com/peng-zhihui.png' },
{ name: 'Crypto Michael', description: 'TOP 2', org: 'Beijing, China', followers: 37622, avatar: 'https://github.com/michaelliao.png' },
{ name: '代码家', description: 'TOP 3', org: 'Beijing, China', followers: 24743, avatar: 'https://github.com/daimajia.png' },
{ name: 'Jackson Tian', description: 'TOP 4', org: 'Hangzhou, China', followers: 21267, avatar: 'https://github.com/JacksonTian.png' },
{ name: '云风', description: 'TOP 5', org: 'China', followers: 21192, avatar: 'https://github.com/cloudwu.png' },
{ name: 'Fengda Huang', description: 'TOP 6', org: 'Shanghai / Hangzhou', followers: 20229, avatar: 'https://github.com/phodal.png' },
{ name: '程序员鱼皮', description: 'TOP 7', org: 'Shanghai', followers: 18461, avatar: 'https://github.com/liyupi.png' },
{ name: 'Trinea', description: 'TOP 8', org: 'HangZhou', followers: 17019, avatar: 'https://github.com/Trinea.png' },
{ name: 'lifesinger', description: 'TOP 9', org: 'Hangzhou, China', followers: 15894, avatar: 'https://github.com/lifesinger.png' },
{ name: 'stormzhang', description: 'TOP 10', org: 'Shanghai, China', followers: 15889, avatar: 'https://github.com/stormzhang.png' },
{ name: 'Wenli Zhang', description: 'TOP 11', org: 'Shanghai, China', followers: 15680, avatar: 'https://github.com/Ovilia.png' },
{ name: 'Wei Wang', description: 'TOP 12', org: 'Yokohama / China', followers: 15532, avatar: 'https://github.com/onevcat.png' },
{ name: 'astaxie', description: 'TOP 13', org: 'Shanghai, China', followers: 15022, avatar: 'https://github.com/astaxie.png' },
{ name: '司徒正美', description: 'TOP 14', org: 'China', followers: 14742, avatar: 'https://github.com/RubyLouvre.png' },
{ name: '云谦', description: 'TOP 15', org: 'HangZhou, China', followers: 14466, avatar: 'https://github.com/sorrycc.png' },
{ name: 'cangdu', description: 'TOP 16', org: 'Shanghai, China', followers: 13728, avatar: 'https://github.com/bailicangdu.png' },
{ name: 'Huang Haiguang', description: 'TOP 17', org: 'Qingdao, China', followers: 13422, avatar: 'https://github.com/fengdu78.png' },
{ name: '花裤衩', description: 'TOP 18', org: 'Shanghai, China', followers: 13236, avatar: 'https://github.com/PanJiaChen.png' }
]);
}
// 8. 开发者活力波形图
export async function vitalityData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ timeData: '2014', total: 20 },
{ timeData: '2015', total: 50 },
{ timeData: '2016', total: 100 },
{ timeData: '2017', total: 200 },
{ timeData: '2018', total: 300 },
{ timeData: '2019', total: 400 },
{ timeData: '2020', total: 600 },
{ timeData: '2021', total: 800 },
{ timeData: '2022', total: 1000 },
{ timeData: '2023', total: 1200 },
{ timeData: '2024', total: 1350 }
]);
}
// 9. 开源项目热力图
export async function projectHeatmap(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ openrank: 67538.71, repo: 29433, developer: 1409, project: 'OpenHarmony', org: 'Huawei' },
{ openrank: 55000, repo: 15000, developer: 6000, project: 'Linux', org: 'Linux Foundation' },
{ openrank: 60000, repo: 8000, developer: 7000, project: 'Android', org: 'Google' },
{ openrank: 52000, repo: 20000, developer: 5500, project: 'Chromium', org: 'Google' },
{ openrank: 38000, repo: 16000, developer: 4500, project: 'TensorFlow', org: 'Google' },
{ openrank: 42000, repo: 13000, developer: 5000, project: 'React', org: 'Meta' },
{ openrank: 45417.67, repo: 18000, developer: 8629, project: 'Azure', org: 'Microsoft' },
{ openrank: 35000, repo: 22000, developer: 5000, project: 'Kubernetes', org: 'CNCF' },
{ openrank: 21844.62, repo: 12340, developer: 57, project: 'LLM', org: '发芽工具' },
{ openrank: 25241.13, repo: 8000, developer: 83, project: 'NixOS', org: '-' },
{ openrank: 22961.09, repo: 4500, developer: 9377, project: 'openEuler', org: 'Huawei' },
{ openrank: 17795.41, repo: 2792, developer: 975, project: 'DataDog', org: 'DataDog' },
{ openrank: 25598.21, repo: 2082, developer: 282, project: '.Net', org: '操作系统' },
{ openrank: 7405.46, repo: 9639, developer: 3, project: 'Expensify', org: 'Expensify' },
{ openrank: 8862.2, repo: 4625, developer: 120, project: 'PaddlePaddle', org: 'Baidu' },
{ openrank: 12959.59, repo: 2363, developer: 1, project: 'Odoo', org: 'Odoo' },
{ openrank: 12326.76, repo: 881, developer: 179, project: 'Rust', org: 'Rust Foundation' },
{ openrank: 6778.7, repo: 400, developer: 479, project: 'CloudHarmony', org: '企业应用' }
]);
}
// 10. 中国开源人才密度地图
export async function talentMapData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '北京市', density: 141.15, total: 231.47 },
{ name: '上海市', density: 269.83, total: 169.99 },
{ name: '浙江省', density: 8.94, total: 90.98 },
{ name: '广东省', density: 7.5, total: 149.76 },
{ name: '台湾省', density: 32.75, total: 117.88 },
{ name: '四川省', density: 1.01, total: 48.7 },
{ name: '江苏省', density: 5.25, total: 56.28 },
{ name: '湖北省', density: 1.84, total: 34.09 },
{ name: '福建省', density: 1.39, total: 16.79 },
{ name: '陕西省', density: 1.04, total: 21.38 },
{ name: '重庆市', density: 1.61, total: 13.24 },
{ name: '山东省', density: 1.02, total: 16.02 },
{ name: '湖南省', density: 0.68, total: 14.25 },
{ name: '安徽省', density: 0.93, total: 12.92 },
{ name: '辽宁省', density: 0.77, total: 11.37 },
{ name: '云南省', density: 0.1, total: 3.68 },
{ name: '天津市', density: 7.79, total: 9.34 },
{ name: '河南省', density: 0.55, total: 9.07 },
{ name: '吉林省', density: 0.15, total: 2.7 },
{ name: '江西省', density: 0.25, total: 4.17 },
{ name: '广西壮族自治区', density: 0.14, total: 3.15 },
{ name: '黑龙江省', density: 0.11, total: 5.13 },
{ name: '澳门特别行政区', density: 737.5, total: 2.36 },
{ name: '河北省', density: 0.26, total: 4.74 },
{ name: '山西省', density: 0.25, total: 3.78 },
{ name: '贵州省', density: 0.11, total: 1.93 },
{ name: '内蒙古自治区', density: 0.01, total: 0.96 },
{ name: '香港特别行政区', density: 1797.7, total: 197.75 },
{ name: '新疆维吾尔自治区', density: 0.01, total: 0.77 },
{ name: '甘肃省', density: 0.04, total: 1.46 },
{ name: '海南省', density: 0.36, total: 1.25 },
{ name: '宁夏回族自治区', density: 0, total: 0.5 },
{ name: '西藏自治区', density: 0, total: 0.3 },
{ name: '青海省', density: 0, total: 0.4 }
]);
}

491
src/mock/page3.ts Normal file
View File

@@ -0,0 +1,491 @@
/**
* 态势感知看板 - Tab3 (武汉市开源生态建设) Mock 数据
*
* 数据结构根据 src/views/Jyh/Community/{index.vue, components/*.vue} 反推所得。
* 包含 25 个 page3 接口2 个 statsBar、11 个 HarmonyEco 子图、12 个 index.vue 主图表。
*/
import type { AxiosResponse } from 'axios';
const sleep = (ms: number) => new Promise<void>((r) => setTimeout(r, ms));
const randomDelay = () => sleep(200 + Math.floor(Math.random() * 300));
function mockResp<T>(data: T, code = 200, msg = 'success'): AxiosResponse<{ code: number; msg: string; data: T }> {
return {
data: { code, msg, data },
status: 200,
statusText: 'OK',
headers: {},
config: {} as any
};
}
// ============================================================
// A. StatsBar (顶部统计概览)
// ============================================================
// 1. 武汉开源统计
export async function statsBar(): Promise<AxiosResponse> {
await randomDelay();
return mockResp({
developer: 700000, // 70万
community: 12,
company: 150,
project: 500
});
}
// 2. 鸿蒙统计
export async function hmStatsBar(): Promise<AxiosResponse> {
await randomDelay();
return mockResp({
developer: 100, // 1000万
certificationDeveloper: 600, // 60万
wuhanCertificationDeveloper: 1, // 1万
project: 728,
company: 600
});
}
// ============================================================
// B. HarmonyEcoCharts (鸿蒙生态可视化)
// ============================================================
// 3. 鸿蒙项目城市分布
export async function hmCityData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '广东', value: 185 },
{ name: '北京', value: 95 },
{ name: '上海', value: 88 },
{ name: '江苏', value: 70 },
{ name: '浙江', value: 52 },
{ name: '湖北', value: 38 },
{ name: '四川', value: 32 },
{ name: '福建', value: 30 },
{ name: '陕西', value: 28 },
{ name: '山东', value: 24 }
]);
}
// 4. 鸿蒙开源项目 Top 10
export async function hmProjects(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'OpenHarmony', link: 'https://github.com/openharmony', stars: '14.6k+', company: '开放原子开源基金会' },
{ name: 'awesome-harmony', link: 'https://github.com/Wscats/awesome-harmony', stars: '3.1k+', company: '社区维护' },
{ name: 'ohos-rs', link: 'https://github.com/ohos-rs/ohos-rs', stars: '1.2k+', company: 'Rust鸿蒙生态' },
{ name: 'OpenHarmony (fenwii)', link: 'https://github.com/fenwii/OpenHarmony', stars: '920+', company: '分微科技' },
{ name: 'ohos-rust-demo', link: 'https://github.com/jschwe/ohos-rust-demo', stars: '380+', company: '社区/个人' },
{ name: 'kelivo', link: 'https://github.com/Chevey339/kelivo', stars: '270+', company: '社区/个人' },
{ name: 'openharmony-rs', link: 'https://github.com/openharmony-rs', stars: '220+', company: 'openharmony-rs组织' },
{ name: 'harmonyos-next-samples', link: 'https://github.com/harmonyos-next-samples', stars: '190+', company: 'NEXT生态社区' },
{ name: 'ohos-sdk', link: 'https://github.com/openharmony-rs/ohos-sdk', stars: '150+', company: 'openharmony-rs组织' },
{ name: 'arkui-rs', link: 'https://github.com/openharmony-rs/arkui-rs', stars: '120+', company: 'openharmony-rs组织' }
]);
}
// 5. 武汉鸿蒙项目 Top 10
export async function whProjects(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'docs', desc: 'OpenHarmony官方文档与指南', stars: '498', company: '中科创达' },
{ name: 'xts_acts', desc: '应用兼容性测试套件(ACTS)', stars: '293', company: '润开鸿' },
{ name: '黄鹤开源技术社区应用', desc: '基于OpenHarmony的社区APP', stars: '191', company: '坚果派' },
{ name: 'manifest', desc: '系统版本与仓库清单管理', stars: '93', company: '中科创达' },
{ name: 'communication_bluetooth', desc: '蓝牙通信子系统适配', stars: '55', company: '普华基础软件' },
{ name: 'vendor_osware', desc: '普华厂商适配层组件', stars: '31', company: '普华基础软件' },
{ name: 'device_soc_nxp', desc: 'NXP系列芯片SoC适配', stars: '15', company: '普华基础软件' },
{ name: 'device_hihope', desc: 'HiHope开发板设备适配', stars: '8', company: '润开鸿' },
{ name: 'device_soc_hisilicon', desc: '海思芯片SoC底层适配', stars: '8', company: '润开鸿' },
{ name: 'kernel_linux_patches', desc: 'Linux内核差异化补丁集', stars: '7', company: '润开鸿' }
]);
}
// 6. 鸿蒙开发者贡献榜 Top 20
export async function hmDevelopers(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'ar*******o', employer: '华为', added: 1348180, deleted: 11022, score: 1359202 },
{ name: 'gu****o', employer: '华为', added: 892199, deleted: 466304, score: 1358503 },
{ name: 'wa***i', employer: '深开鸿', added: 1074812, deleted: 216511, score: 1291323 },
{ name: 'ze****1', employer: '华为', added: 718467, deleted: 452733, score: 1171200 },
{ name: 'wa*****2', employer: '华为', added: 831282, deleted: 249505, score: 1080787 },
{ name: 'wu****g', employer: '华为', added: 648545, deleted: 336836, score: 985381 },
{ name: 'zh*****7', employer: '华为', added: 680056, deleted: 151257, score: 831313 },
{ name: 'zh******5', employer: '开鸿智谷', added: 812520, deleted: 9929, score: 822449 },
{ name: 'zh******7', employer: '华为', added: 492068, deleted: 270411, score: 762479 },
{ name: 'ta*****n', employer: '润开鸿', added: 531640, deleted: 136900, score: 668540 },
{ name: 'an*****i', employer: '华为', added: 429272, deleted: 226777, score: 656049 },
{ name: 'do***i', employer: '深开鸿', added: 502956, deleted: 139366, score: 642322 },
{ name: 'zh****7', employer: '华为', added: 479092, deleted: 159437, score: 638529 },
{ name: 'ne****g', employer: '华为', added: 67272, deleted: 569087, score: 636359 },
{ name: 'ta******1', employer: '华为', added: 630294, deleted: 1861, score: 632155 },
{ name: 'wa*****1', employer: '华为', added: 620905, deleted: 11220, score: 632125 },
{ name: 'xi****3', employer: '华为', added: 498998, deleted: 119428, score: 618426 },
{ name: 'ba****g', employer: '深开鸿', added: 498481, deleted: 46544, score: 545025 },
{ name: 'zh******1', employer: '华为', added: 378416, deleted: 144456, score: 522872 },
{ name: 'ch****0', employer: '华为', added: 498437, deleted: 17953, score: 516390 }
]);
}
// 7. 鸿蒙开发者地域分布
export async function hmDevCityData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '其他地区', value: 25.7 },
{ name: '北京', value: 17 },
{ name: '广东', value: 12 },
{ name: '河北', value: 8.3 },
{ name: '上海', value: 7.2 },
{ name: '浙江', value: 5.6 },
{ name: '山东', value: 5.1 },
{ name: '江苏', value: 4.6 },
{ name: '湖南', value: 4.3 },
{ name: '福建', value: 3.5 },
{ name: '河南', value: 3.4 },
{ name: '湖北', value: 3.3 }
]);
}
// 8. 武汉鸿蒙开发者贡献榜 Top 10
export async function whHmDevelopers(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'yang_hongliang', employer: '润开鸿', added: 168982, deleted: 84294, score: 253276 },
{ name: 'tang_lizhen', employer: '润开鸿', added: 137344, deleted: 25345, score: 162689 },
{ name: 'shilei-hihope', employer: '润开鸿', added: 28144, deleted: 1611, score: 29755 },
{ name: 'zhangtao', employer: '润开鸿', added: 22998, deleted: 0, score: 22998 },
{ name: 'zhaoox0502', employer: '普华/中科创达', added: 37126, deleted: 2235, score: 39361 },
{ name: 'lian_zhian', employer: '润开鸿', added: 17382, deleted: 1708, score: 19090 },
{ name: 'XuejianZhang', employer: '普华/中科创达', added: 17604, deleted: 344, score: 17948 },
{ name: 'zhao_haipeng', employer: '润开鸿', added: 14145, deleted: 2576, score: 16721 },
{ name: 'yanghongliang', employer: '润开鸿', added: 10016, deleted: 78, score: 10094 },
{ name: 'wangzx0705', employer: '普华/中科创达', added: 5810, deleted: 1356, score: 7166 }
]);
}
// 9. 开源企业城市分布
export async function hmEnterpriseCityData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '广东', value: 133 },
{ name: '北京', value: 72 },
{ name: '上海', value: 65 },
{ name: '江苏', value: 55 },
{ name: '浙江', value: 42 },
{ name: '湖北', value: 30 },
{ name: '四川', value: 28 },
{ name: '福建', value: 25 },
{ name: '陕西', value: 22 },
{ name: '山东', value: 20 }
]);
}
// 10. 开源企业贡献榜
export async function hmEnterprises(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '华为技术有限公司', city: '深圳', business: '鸿蒙系统核心开发与生态建设', remark: '鸿蒙发起者' },
{ name: '深圳开鸿数字产业发展有限公司', city: '深圳', business: 'OpenHarmony 数字化底座研发', remark: '核心共建单位' },
{ name: '湖南开鸿智谷数字产业发展有限公司', city: '长沙', business: '鸿蒙操作系统南北向全栈研发', remark: '拓维信息子公司' },
{ name: '江苏润开鸿数字科技有限公司', city: '南京', business: '电鸿及各行业 OpenHarmony 物联网终端', remark: '润和软件子公司' },
{ name: '软通动力信息技术(集团)股份有限公司', city: '北京', business: '鸿蒙 IT 服务与软硬一体化解决方案', remark: '核心合作伙伴' },
{ name: '广东九联开鸿科技发展有限公司', city: '惠州', business: '鸿蒙智能硬件与电力行业解决方案', remark: '九联科技子公司' },
{ name: '鼎桥通信技术有限公司', city: '成都', business: '基于鸿蒙系统的行业定制化终端', remark: '安全终端提供商' },
{ name: '中软国际有限公司', city: '北京', business: '鸿蒙应用开发与全栈服务', remark: '重要参与者' },
{ name: '泰凌微电子(上海)股份有限公司', city: '上海', business: '低功耗无线 IoT 芯片鸿蒙适配', remark: '芯片级合作伙伴' },
{ name: '深圳市汇顶科技股份有限公司', city: '深圳', business: '鸿蒙生态传感与控制芯片研发', remark: '硬件生态伙伴' }
]);
}
// 11. 武汉企业贡献榜
export async function whEnterprises(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '中软国际(武汉)', city: '武汉', business: '开源鸿蒙核心代码贡献、商业发行版研发', remark: 'OpenHarmony创始成员、A类捐赠人' },
{ name: '软通动力(武汉)', city: '武汉', business: '开源鸿蒙操作系统研发、全球化适配', remark: '白金捐赠人、生态人才培养核心方' },
{ name: '深开鸿(武汉)', city: '武汉', business: '开源鸿蒙工业互联方案研发、代码贡献', remark: '底层代码核心贡献企业' },
{ name: '华为(武汉研究所)', city: '武汉', business: '鸿蒙底层技术研发、本地生态赋能', remark: '鸿蒙生态核心技术源头' },
{ name: '烽火通信', city: '武汉', business: '鸿蒙智慧城市/光通信设备适配', remark: '武汉本地龙头、标准制定牵头方' },
{ name: '武汉虹信技术服务有限责任公司', city: '武汉', business: '鸿蒙通信设备运维、应急调度平台研发', remark: '通信领域鸿蒙适配核心企业' },
{ name: '润开鸿(武汉研发中心)', city: '武汉', business: '鸿蒙工业物联网方案、轻量化部署研发', remark: '工业互联鸿蒙应用核心方' },
{ name: '中科创达(武汉)', city: '武汉', business: '鸿蒙智能终端适配、测试工具研发', remark: '应用层适配核心企业' },
{ name: '九联科技(武汉分公司)', city: '武汉', business: '鸿蒙智能终端本地化适配、部署服务', remark: '区域终端适配落地方' },
{ name: '普华基础软件', city: '武汉', business: '鸿蒙与Linux系统兼容接口研发', remark: '基础软件兼容适配企业' }
]);
}
// 12. 中国高校鸿蒙贡献分布
export async function uniCityData(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '北邮', value: 52 },
{ name: '浙大', value: 16 },
{ name: '华科', value: 12 },
{ name: '北理工', value: 10 },
{ name: '中科大', value: 10 },
{ name: '上交大', value: 4 },
{ name: '北大', value: 3 },
{ name: '西安建大', value: 2 },
{ name: '南大', value: 2 },
{ name: '哈工大', value: 2 },
{ name: '东南大学', value: 2 },
{ name: '暨大', value: 1 },
{ name: '武理工', value: 1 },
{ name: '中山大学', value: 1 },
{ name: '北航', value: 1 },
{ name: '华南理工', value: 1 },
{ name: '人大', value: 1 },
{ name: '深大', value: 1 },
{ name: '西电', value: 1 },
{ name: '南理工', value: 1 },
{ name: '云大', value: 1 },
{ name: '广工', value: 1 },
{ name: '复旦', value: 1 },
{ name: '中南大学', value: 1 },
{ name: '清华', value: 1 },
{ name: '东北大学', value: 1 }
]);
}
// 13. 湖北高校鸿蒙贡献者 Top 12
export async function uniContributors(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ authorName: 'd00887893', project: 'arkcompiler_ets_frontend', branch: 'master', repo: 'communication_nfc', authorEmail: 're****g@whut.edu.cn', detailMessage: 'Add SDK test cases Issue: I9CRGH', additions: 664, deletions: 97, changeNum: 761, rollbackCount: 0, university: '武汉理工大学', contributionScore: 845.3 },
{ authorName: 'Jiefeng Li', project: 'openharmony', branch: 'master', repo: 'docs', authorEmail: 'ji****i@hust.edu.cn', detailMessage: 'fix typo', additions: 116, deletions: 116, changeNum: 232, rollbackCount: 0, university: '华中科技大学', contributionScore: 197.2 },
{ authorName: '"景涛"', project: 'openharmony', branch: 'master', repo: 'kernel_liteos_a', authorEmail: 'ji****3@hust.edu.cn', detailMessage: 'fix some typo errors', additions: 17, deletions: 17, changeNum: 34, rollbackCount: 0, university: '华中科技大学', contributionScore: 28.9 },
{ authorName: 'Zian', project: 'openharmony', branch: 'master', repo: 'drivers_peripheral', authorEmail: 'u2****5@hust.edu.cn', detailMessage: 'fix NULL pointer problem caused by function HdfRemoteServiceWriteInterfaceToken()', additions: 10, deletions: 9, changeNum: 19, rollbackCount: 0, university: '华中科技大学', contributionScore: 16.5 },
{ authorName: 'fly1ngpengu1ns', project: 'openharmony', branch: 'master', repo: 'drivers_hdf_core', authorEmail: 'pe****n@hust.edu.cn', detailMessage: 'fix: fix the fault handler when ctrl_hdlr is NULL', additions: 6, deletions: 5, changeNum: 11, rollbackCount: 0, university: '华中科技大学', contributionScore: 9.7 },
{ authorName: 'phd', project: 'openharmony', branch: 'master', repo: 'drivers_hdf_core', authorEmail: 'm2****8@hust.edu.cn', detailMessage: 'fix:An exception when queue is NULL', additions: 5, deletions: 5, changeNum: 10, rollbackCount: 0, university: '华中科技大学', contributionScore: 8.5 },
{ authorName: '辛哲麒', project: 'openharmony', branch: 'master', repo: 'communication_nfc', authorEmail: 'xi****i@hust.edu.cn', detailMessage: 'fix:context maybe nullptr', additions: 3, deletions: 3, changeNum: 6, rollbackCount: 0, university: '华中科技大学', contributionScore: 5.1 },
{ authorName: 'Ruibo Lu', project: 'openharmony', branch: 'master', repo: 'update_packaging_tools', authorEmail: 're****u@hust.edu.cn', detailMessage: '[fix]: fix indent in blocks_manager.py', additions: 1, deletions: 1, changeNum: 2, rollbackCount: 0, university: '华中科技大学', contributionScore: 1.7 },
{ authorName: 'Zian', project: 'openharmony', branch: 'master', repo: 'drivers_peripheral', authorEmail: 'u2****5@hust.edu.cn', detailMessage: 'fix:NULL pointer reference caused by variable hwAdapter', additions: 1, deletions: 1, changeNum: 2, rollbackCount: 0, university: '华中科技大学', contributionScore: 1.7 },
{ authorName: 'xy420', project: 'openharmony', branch: 'master', repo: 'communication_dsoftbus', authorEmail: 'ch*****g@hust.edu.cn', detailMessage: 'update tests/sdk/transmission/fuzztest/rawstreamdata_fuzzer/rawstreamdata_fuzzer.cpp', additions: 1, deletions: 1, changeNum: 2, rollbackCount: 0, university: '华中科技大学', contributionScore: 1.7 },
{ authorName: '辛哲麒', project: 'openharmony', branch: 'master', repo: 'distributeddatamgr_datamgr_service', authorEmail: 'xi****i@hust.edu.cn', detailMessage: 'update services/distributeddataservice/service/directory/src/directory_manager.cpp', additions: 1, deletions: 1, changeNum: 2, rollbackCount: 0, university: '华中科技大学', contributionScore: 1.7 }
]);
}
// ============================================================
// C. index.vue (主图表区)
// ============================================================
// 14. 武汉优质 GitHub 开源项目 Top 20
export async function wuhanGithubProjects(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'halo-dev/halo', desc: '强大易用的开源建站工具', stars: 34200 },
{ name: 'whyour/qinglong', desc: '支持 Python3、JavaScript、Shell、Typescript 的定时任务管理平台', stars: 16800 },
{ name: 'ehang-io/nps', desc: '一款轻量级、高性能、功能强大的内网穿透代理服务器', stars: 29500 },
{ name: 'cloudreve/Cloudreve', desc: '支持多家云存储的云盘系统', stars: 18700 },
{ name: 'dromara/Sa-Token', desc: '一个轻量级 Java 权限认证框架', stars: 16400 },
{ name: 'macrozheng/mall-learning', desc: 'mall学习教程架构、业务、技术要点全方位解析', stars: 12400 },
{ name: 'zerotier/ZT', desc: 'A smart Ethernet switch for planet Earth', stars: 8900 },
{ name: 'wgpsec/DBJ', desc: '大宝剑-信息收集工具', stars: 6500 },
{ name: 'dromara/lamp-cloud', desc: '微服务脚手架', stars: 4600 },
{ name: 'kekingcn/kkFileView', desc: '基于 Spring-Boot 的文件在线预览解决方案', stars: 9800 },
{ name: 'OpenTSDB/asynchbase', desc: 'A fully asynchronous HBase client, based on HBase 1.x', stars: 2100 },
{ name: 'opengoofy/hippo4j', desc: '强大的动态线程池框架', stars: 5400 },
{ name: 'axetroy/smzdm_script', desc: '什么值得买每日签到脚本', stars: 1800 },
{ name: 'mikemintang/awesome-github-wechat-weapp', desc: '微信小程序开源项目汇总', stars: 14200 },
{ name: 'Semporia/Semporia.github.io', desc: '博客', stars: 3200 },
{ name: 'JavaGuide', desc: 'Java 学习&面试指南', stars: 153000 },
{ name: 'cpython', desc: 'The Python programming language', stars: 71000 },
{ name: 'core', desc: 'Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.', stars: 52700 },
{ name: 'bevy', desc: 'A refreshingly simple data-driven game engine built in Rust', stars: 43900 },
{ name: 'linux-insides', desc: 'A book-in-progress about the Linux kernel and its insides.', stars: 31700 }
]);
}
// 15. 中国城市开源项目数量
export async function cityProject(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '北京', data: 757259 },
{ name: '上海', data: 678614 },
{ name: '深圳', data: 343417 },
{ name: '杭州', data: 315406 },
{ name: '广州', data: 177003 },
{ name: '成都', data: 168300 },
{ name: '武汉', data: 120827 },
{ name: '南京', data: 111593 },
{ name: '西安', data: 58678 },
{ name: '重庆', data: 56564 }
]);
}
// 16. 城市开源人才数量
export async function talent(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '北京', number: 2115924 },
{ name: '上海', number: 1591988 },
{ name: '广州', number: 1555240 },
{ name: '深圳', number: 1163580 },
{ name: '成都', number: 894108 },
{ name: '杭州', number: 830306 },
{ name: '武汉', number: 791335 },
{ name: '南京', number: 733581 },
{ name: '郑州', number: 553397 },
{ name: '长沙', number: 487971 }
]);
}
// 17. 城市社区数量排行
export async function communityRankChart(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '北京', value: 69 },
{ name: '深圳', value: 35 },
{ name: '上海', value: 31 },
{ name: '杭州', value: 25 },
{ name: '广州', value: 18 },
{ name: '成都', value: 15 },
{ name: '南京', value: 12 },
{ name: '武汉', value: 12 }
]);
}
// 18. 2024 武汉优秀开源项目 Top 10
export async function chinaExcellentProjects(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: 'Deepin 深度操作系统', desc: '国内领先的国产 Linux 发行版,旨在为全球用户提供美观易用、安全可靠的操作系统体验,是国产软件生态的核心。', org: '武汉深之度' },
{ name: 'Jupiter 微服务治理框架', desc: '由斗鱼研发并开源的面向治理的微服务框架,支持多数据源、多注册中心,适用于高并发场景。', org: '武汉斗鱼' },
{ name: 'RustSBI 安全引导程序', desc: '基于 Rust 语言开发的 RISC-V 指令集体系结构安全引导程序,提供更高内存安全性。', org: '华中科技大学' },
{ name: '芯片开源 EDA 研发', desc: '致力于国产电子设计自动化工具开源化,解决芯片设计环节中的关键技术难题。', org: '华中科技大学' },
{ name: 'LuoJiaSET 框架与 LuoJiaSET 样本库', desc: '专注于智能遥感影像解译,提供海量遥感样本数据集和深度学习框架。', org: '武汉大学' },
{ name: '鲲鹏安全库 (Kunpengsecl)', desc: '基于鲲鹏处理器的机密计算和受信任执行环境安全开发库,支撑云计算与隐私计算。', org: '武汉大学' },
{ name: '柚坛工具箱 (Uotan Toolbox)', desc: '针对 Android 发烧友开发的图形化综合工具箱,集成刷机、系统优化等多种功能。', org: '武昌理工学院' },
{ name: '凹语言 (Wa Language)', desc: '国内首个针对 WebAssembly 设计的通用编程语言,兼具 Web 灵活性与原生运行速度。', org: '武汉凹语言' },
{ name: 'OpenHIS 开源医疗系统', desc: '开源的医院信息系统,旨在通过标准化、模块化方式赋能基层医疗机构数字化升级。', org: '武汉新致新源' },
{ name: 'NextDenovo', desc: '高效且精确的长读长序列纠错和组装工具,主要用于基因组测序领域。', org: '武汉希望组生物科技有限公司' }
]);
}
// 19. 2025 武汉优秀开源项目 Top 10
export async function excellentProjects(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '调问DWSurvey', desc: '开源问卷表单系统', org: '极推科技' },
{ name: 'Eova', desc: '低代码开发平台', org: '武汉简码' },
{ name: 'ClickVisual', desc: '可视化日志分析告警平台', org: '武汉初心' },
{ name: 'Meta2d.js', desc: '2D图元可视化引擎', org: '乐吾乐科技' },
{ name: 'Juno', desc: '分布式微服务管理系统', org: '武汉斗鱼' },
{ name: 'Opentalent', desc: '开源人才评价系统', org: '湖北大学' },
{ name: 'Fountain', desc: '仓颉语言版 Spring 框架替代', org: '江汉大学' },
{ name: 'deepin', desc: '深度桌面操作系统', org: '深之度' },
{ name: 'RustSBI', desc: 'RISC-V 安全引导程序', org: '华中科技大学' },
{ name: 'NextDenovo', desc: '长读长序列组装工具', org: '武汉希望组' }
]);
}
// 20. 武汉明星开发者 Top 10
export async function developerNames(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '白纯骢' },
{ name: '陆红旭' },
{ name: '慕冬亮' },
{ name: '张继德' },
{ name: '蒋周奇' },
{ name: '张天雄' },
{ name: '阮文俊' },
{ name: '李奇' },
{ name: '陈鹏轩' },
{ name: '杨帆' }
]);
}
// 21. 武汉知名开源社区
export async function wuhanCommunities(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '黄鹤开源社区', type: '核心生态', slogan: '国家级、城市级开源社区平台,武汉开源生态的核心枢纽' },
{ name: '深度社区 (Deepin)', type: '操作系统', slogan: '全球最大桌面操作系统社区之一,全球用户超过 750 万' },
{ name: '极狐社区 (GitLab China)', type: 'DevOps', slogan: '全球第二大开源托管平台 GitLab 在汉设立的中国发行版及开发者社区' },
{ name: '开源鸿蒙社区 (OpenHarmony)', type: '万物互联', slogan: '国家级生态创新中心,设有全国首个分委会' },
{ name: 'PostgreSQL 中文社区', type: '数据库', slogan: '全球四大开源数据库之一的运营中心设在武汉' },
{ name: '武汉大学开源社区', type: '学术社团', slogan: '老牌社团,涉及 Linux、嵌入式、深度学习等方向' },
{ name: '武汉大学 OpenHarmony 俱乐部', type: '万物互联', slogan: '专注于华为开源鸿蒙生态' },
{ name: '华中科技大学开源社团', type: '学术社团', slogan: '常年组织 LUG (Linux User Group) 及黑客松活动' },
{ name: '武汉理工大学开源联盟', type: '学术社团', slogan: '聚焦计算机科学与软件工程方向' },
{ name: '华中师范大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '中南民族大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '湖北大学', type: '兴趣小组', slogan: '开源技术兴趣小组' }
]);
}
// 22. 重点城市高校开源社团数量
export async function club(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '成都', number: 10 },
{ name: '北京', number: 7 },
{ name: '上海', number: 5 },
{ name: '武汉', number: 4 },
{ name: '重庆', number: 3 },
{ name: '深圳', number: 2 },
{ name: '苏州', number: 2 },
{ name: '杭州', number: 2 },
{ name: '合肥', number: 2 },
{ name: '青岛', number: 2 }
]);
}
// 23. 各省市开源相关政策数量
export async function policy(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '江苏', number: 11 },
{ name: '北京', number: 7 },
{ name: '浙江', number: 3 },
{ name: '广东', number: 3 },
{ name: '上海', number: 3 },
{ name: '湖北', number: 2 },
{ name: '天津', number: 2 },
{ name: '陕西', number: 2 },
{ name: '河南', number: 2 },
{ name: '云南', number: 2 }
]);
}
// 24. 开放原子校园行·城市活动场次
export async function campus(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '南京', number: 20 },
{ name: '武汉', number: 16 },
{ name: '北京', number: 16 },
{ name: '长沙', number: 16 },
{ name: '成都', number: 11 },
{ name: '上海', number: 10 },
{ name: '天津', number: 8 },
{ name: '深圳', number: 8 },
{ name: '济南', number: 7 },
{ name: '广州', number: 6 }
]);
}
// 25. 全国重点城市开源镜像站/节点数量对比
export async function mirrors(): Promise<AxiosResponse> {
await randomDelay();
return mockResp([
{ name: '上海', number: 4 },
{ name: '北京', number: 4 },
{ name: '西安', number: 2 },
{ name: '重庆', number: 2 },
{ name: '济南', number: 2 },
{ name: '南京', number: 2 },
{ name: '武汉', number: 1 },
{ name: '合肥', number: 1 },
{ name: '香港', number: 1 },
{ name: '长春', number: 1 },
{ name: '成都', number: 1 },
{ name: '深圳', number: 1 },
{ name: '兰州', number: 1 },
{ name: '哈尔滨', number: 1 },
{ name: '南阳', number: 1 },
{ name: '杭州', number: 1 }
]);
}

92
src/mock/registry.ts Normal file
View File

@@ -0,0 +1,92 @@
/**
* Mock 路由注册表
*
* 根据请求 URL 匹配对应的 mock handler匹配命中时短路掉 axios 直接返回假数据。
* 当前覆盖 /api/v1/page1/* 13 个 + /api/v1/page2/* 10 个接口。
*/
import type { AxiosRequestConfig, AxiosResponse } from 'axios';
import * as page1 from './page1';
import * as page2 from './page2';
import * as page3 from './page3';
type MockHandler = (params?: any, data?: any) => Promise<AxiosResponse>;
/** 精确匹配优先,模糊匹配兜底(处理带 query 的 URL */
const registry: Record<string, MockHandler> = {
'/api/v1/page1/overviewData': page1.overviewData,
'/api/v1/page1/map/alerts': page1.mapAlerts,
'/api/v1/page1/map/mapData': page1.mapData,
'/api/v1/page1/map/vulnStats': page1.vulnStats,
'/api/v1/page1/map/regionStats': page1.regionStats,
'/api/v1/page1/industryDistribution': page1.industryDistribution,
'/api/v1/page1/highRiskComponent': page1.highRiskComponent,
'/api/v1/page1/feedList': page1.feedList,
'/api/v1/page1/cveData/global': page1.cveDataGlobal,
'/api/v1/page1/cveData/regional': page1.cveDataRegional,
'/api/v1/page1/languageTrend': page1.languageTrend,
'/api/v1/page1/vulnerabilitySeverity': page1.vulnerabilitySeverity,
'/api/v1/page1/scatterData': page1.scatterData,
// page2 - 开源生态与贡献价值全景
'/api/v1/page2/overviewData': page2.overviewData,
'/api/v1/page2/rawEnterpriseData': page2.rawEnterpriseData,
'/api/v1/page2/infrastructure': page2.infrastructure,
'/api/v1/page2/universityClubs': page2.universityClubs,
'/api/v1/page2/communityHealth': page2.communityHealth,
'/api/v1/page2/languageTech': page2.languageTech,
'/api/v1/page2/excellentDeveloper': page2.excellentDeveloper,
'/api/v1/page2/vitalityData': page2.vitalityData,
'/api/v1/page2/projectHeatmap': page2.projectHeatmap,
'/api/v1/page2/talentMapData': page2.talentMapData,
// page3 - 武汉市开源生态建设
'/api/v1/page3/hmStatsBar': page3.hmStatsBar,
'/api/v1/page3/statsBar': page3.statsBar,
'/api/v1/page3/hmCityData': page3.hmCityData,
'/api/v1/page3/hmProjects': page3.hmProjects,
'/api/v1/page3/whProjects': page3.whProjects,
'/api/v1/page3/hmDevelopers': page3.hmDevelopers,
'/api/v1/page3/hmDevCityData': page3.hmDevCityData,
'/api/v1/page3/whHmDevelopers': page3.whHmDevelopers,
'/api/v1/page3/hmEnterpriseCityData': page3.hmEnterpriseCityData,
'/api/v1/page3/hmEnterprises': page3.hmEnterprises,
'/api/v1/page3/whEnterprises': page3.whEnterprises,
'/api/v1/page3/uniCityData': page3.uniCityData,
'/api/v1/page3/uniContributors': page3.uniContributors,
'/api/v1/page3/wuhanGithubProjects': page3.wuhanGithubProjects,
'/api/v1/page3/cityProject': page3.cityProject,
'/api/v1/page3/talent': page3.talent,
'/api/v1/page3/communityRankChart': page3.communityRankChart,
'/api/v1/page3/chinaExcellentProjects': page3.chinaExcellentProjects,
'/api/v1/page3/excellentProjects': page3.excellentProjects,
'/api/v1/page3/developerNames': page3.developerNames,
'/api/v1/page3/wuhanCommunities': page3.wuhanCommunities,
'/api/v1/page3/club': page3.club,
'/api/v1/page3/policy': page3.policy,
'/api/v1/page3/campus': page3.campus,
'/api/v1/page3/mirrors': page3.mirrors
};
/**
* 根据请求 URL 查找 mock handler
* @returns mock 响应或 null未命中
*/
export async function getMockResponse(
url: string,
config: AxiosRequestConfig
): Promise<AxiosResponse | null> {
// 去掉 query string 和前缀 slash
const cleanUrl = url.split('?')[0].replace(/^\/+/, '');
const fullPath = '/' + cleanUrl;
// 精确匹配
const exact = registry[fullPath];
if (exact) return exact(config?.params, config?.data);
// 模糊匹配registry key 是请求 URL 的前缀)
for (const pattern in registry) {
if (fullPath === pattern || fullPath.startsWith(pattern + '/')) {
return registry[pattern](config?.params, config?.data);
}
}
return null;
}

View File

@@ -52,7 +52,7 @@ export const injectRouter = (router) => {
// 判断命名空间类型 // 判断命名空间类型
const globalStore = useGlobalInfoStore(); const globalStore = useGlobalInfoStore();
globalStore.setNamespaceType(-1); // 重置namespaceType防止非组织路由请求组织信息 globalStore.setNamespaceType(-1); // 重置namespaceType防止非组织路由请求组织信息
globalStore.setLanguageList(); // globalStore.setLanguageList();
if (to.meta.pageType === 'userOrOrg') { if (to.meta.pageType === 'userOrOrg') {
const { namespace } = to.params; const { namespace } = to.params;
const params = { const params = {

View File

@@ -8,6 +8,9 @@ import { cutParamsInUrl, getSignUtmSource } from './index';
import { Message } from 'vue-devui/message'; import { Message } from 'vue-devui/message';
import degradeInterceptor from './degradeInterceptor'; import degradeInterceptor from './degradeInterceptor';
import { useAccountStore } from '@/stores/user'; import { useAccountStore } from '@/stores/user';
import { getMockResponse } from '@/mock/registry';
const USE_MOCK = (import.meta as any).env.VITE_USE_MOCK === 'true';
const handleError = dealWarning(); const handleError = dealWarning();
@@ -131,7 +134,14 @@ const refreshToken = async () => axios.request({
withCredentials: true withCredentials: true
}); });
const proxyService = (params: any, customConfigs?: CustomConfigs) => { const proxyService = async (params: any, customConfigs?: CustomConfigs) => {
if (USE_MOCK) {
const url = typeof params === 'string' ? params : (params?.url || '');
const config = (typeof params === 'object' ? params : { url }) as AxiosRequestConfig;
const mockResp = await getMockResponse(url, config);
if (mockResp) return mockResp;
}
const account = useAccountStore(); const account = useAccountStore();
const service = axios.create({ const service = axios.create({
baseURL: params.apiType === 'devApi' ? baseDevURL : baseURL, baseURL: params.apiType === 'devApi' ? baseDevURL : baseURL,

View File

@@ -0,0 +1,137 @@
<template>
<div class="stats-glass-bar">
<div class="stat-node">
<span class="stat-value">150+</span>
<span class="stat-desc">开源企业</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">12</span>
<span class="stat-desc">武汉开源社区数量</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">70W+</span>
<span class="stat-desc">武汉开源开发者数量</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">500+</span>
<span class="stat-desc">武汉开源项目数量</span>
</div>
</div>
</template>
<script>
export default {
name: "StatsBar"
}
</script>
<style lang="scss" scoped>
.stats-glass-bar {
display: flex;
align-items: center;
justify-content: space-between;
/* 布局控制:撑满容器并增加内边距 */
width: 100%;
padding: 2rem 4rem;
//margin: 2rem 0;
/* 深色玻璃拟态背景 */
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
/* 极细微的边框勾勒 */
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.08);
/* 外阴影与内发光,增加悬浮感 */
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3),
inset 0 0 20px rgba(255, 255, 255, 0.02);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
border-color: rgba(56, 189, 248, 0.4);
background: rgba(255, 255, 255, 0.04);
transform: translateY(-2px);
}
}
.stat-node {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
.stat-value {
text-shadow: 0 0 25px rgba(56, 189, 248, 0.8);
}
}
}
.stat-value {
/* 科技感亮蓝渐变 */
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 2.25rem;
font-weight: 800;
background: linear-gradient(180deg, #fff 0%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 基础发光效果 */
filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.3));
letter-spacing: -1px;
}
.stat-desc {
/* 在黑底上使用半透明白色,更有高级感 */
color: rgba(255, 255, 255, 0.6);
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.stat-separator {
/* 渐隐分割线 */
width: 1px;
height: 3rem;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0)
);
margin: 0 1rem;
}
/* 响应式适配 */
@media (max-width: 1024px) {
.stats-glass-bar {
padding: 2rem;
}
.stat-value {
font-size: 1.75rem;
}
}
@media (max-width: 768px) {
.stats-glass-bar {
flex-direction: column;
gap: 2rem;
}
.stat-separator {
width: 50%;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255,255,255,0.2), transparent);
}
}
</style>

View File

@@ -0,0 +1,210 @@
<template>
<div class="stats-glass-bar">
<div class="stat-node">
<span class="stat-value">{{ hmDevelopers }}</span>
<span class="stat-desc">鸿蒙开发者</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ hmCertificationDevelopers }}</span>
<span class="stat-desc">鸿蒙认证开发者</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ whHmCertificationDevelopers }}</span>
<span class="stat-desc">武汉鸿蒙认证开发者</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ hmProjects }}</span>
<span class="stat-desc">鸿蒙开源项目</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ hmCompanies }}</span>
<span class="stat-desc">鸿蒙开源企业</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { hmStatsBarPage3 } from '@/api/jyh/situation';
// 定义响应式数据
const hmDevelopers = ref<string>('加载中...');
const hmCertificationDevelopers = ref<string>('加载中...');
const whHmCertificationDevelopers = ref<string>('加载中...');
const hmProjects = ref<string>('加载中...');
const hmCompanies = ref<string>('加载中...');
// 加载状态
const loading = ref(true);
// 格式化数字的工具函数
const formatNumber = (num: number, unit: string = '', forceUnit?: string): string => {
if (forceUnit === 'wan') {
// 强制以"万"为单位显示
return Math.floor(num) + '万+';
}
if (num >= 100000000) {
return Math.floor(num / 100000000) + '亿+';
} else if (num >= 10000) {
return Math.floor(num / 10000) + '万+';
} else if (num >= 1000) {
return Math.floor(num / 1000) + '千+';
}
return num.toString() + '+';
};
// 获取统计数据
const fetchStats = async () => {
try {
loading.value = true;
const response = await hmStatsBarPage3();
const businessData = response.data;
if (businessData.code === 200 && businessData) {
const { developer, certificationDeveloper, wuhanCertificationDeveloper, project, company } = businessData.data;
// 格式化数据显示
hmDevelopers.value = `${formatNumber(developer, '', 'wan')}`;
hmCertificationDevelopers.value = `${formatNumber(certificationDeveloper, '', 'wan')}`;
whHmCertificationDevelopers.value = `${formatNumber(wuhanCertificationDeveloper, '', 'wan')}`;
hmProjects.value = `${project}`; // 项目数不需要特殊格式化
hmCompanies.value = `${company}+`; // 公司数直接加+号
} else {
console.error('获取鸿蒙统计数据失败:', businessData.msg || '未知错误');
// 设置默认值
hmDevelopers.value = '1000万+';
hmCertificationDevelopers.value = '60万+';
whHmCertificationDevelopers.value = '1万+';
hmProjects.value = '728';
hmCompanies.value = '600+';
}
} catch (error) {
console.error('获取鸿蒙统计数据异常:', error);
// 设置默认值
hmDevelopers.value = '1000万+';
hmCertificationDevelopers.value = '60万+';
whHmCertificationDevelopers.value = '1万+';
hmProjects.value = '728';
hmCompanies.value = '600+';
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchStats();
});
</script>
<style lang="scss" scoped>
.stats-glass-bar {
display: flex;
align-items: center;
justify-content: space-between;
/* 布局控制:撑满容器并增加内边距 */
width: 100%;
padding: 2rem 4rem;
margin: 0 0 1rem;
/* 深色玻璃拟态背景 */
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
/* 极细微的边框勾勒 */
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.08);
/* 外阴影与内发光,增加悬浮感 */
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3),
inset 0 0 20px rgba(255, 255, 255, 0.02);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
border-color: rgba(56, 189, 248, 0.4);
background: rgba(255, 255, 255, 0.04);
transform: translateY(-2px);
}
}
.stat-node {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
.stat-value {
text-shadow: 0 0 25px rgba(56, 189, 248, 0.8);
}
}
}
.stat-value {
/* 科技感亮蓝渐变 */
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 2.25rem;
font-weight: 800;
background: linear-gradient(180deg, #fff 0%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 基础发光效果 */
filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.3));
letter-spacing: -1px;
}
.stat-desc {
/* 在黑底上使用半透明白色,更有高级感 */
color: rgba(255, 255, 255, 0.6);
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.stat-separator {
/* 渐隐分割线 */
width: 1px;
height: 3rem;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0)
);
margin: 0 1rem;
}
/* 响应式适配 */
@media (max-width: 1024px) {
.stats-glass-bar {
padding: 2rem;
}
.stat-value {
font-size: 1.75rem;
}
}
@media (max-width: 768px) {
.stats-glass-bar {
flex-direction: column;
gap: 2rem;
}
.stat-separator {
width: 50%;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255,255,255,0.2), transparent);
}
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,36 +1,90 @@
<template> <template>
<div class="stats-glass-bar"> <div class="stats-glass-bar">
<div class="stat-node"> <div class="stat-node">
<span class="stat-value">1000W+</span> <span class="stat-value">{{ whCompany }}</span>
<span class="stat-desc">鸿蒙开发者</span> <span class="stat-desc">武汉开源企业</span>
</div> </div>
<div class="stat-separator"></div> <div class="stat-separator"></div>
<div class="stat-node"> <div class="stat-node">
<span class="stat-value">150+</span> <span class="stat-value">{{ whCommunity }}</span>
<span class="stat-desc">开源企业</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">12</span>
<span class="stat-desc">武汉开源社区数量</span> <span class="stat-desc">武汉开源社区数量</span>
</div> </div>
<div class="stat-separator"></div> <div class="stat-separator"></div>
<div class="stat-node"> <div class="stat-node">
<span class="stat-value">70W+</span> <span class="stat-value">{{ whDeveloper }}</span>
<span class="stat-desc">武汉开源开发者数量</span> <span class="stat-desc">武汉开源开发者数量</span>
</div> </div>
<div class="stat-separator"></div> <div class="stat-separator"></div>
<div class="stat-node"> <div class="stat-node">
<span class="stat-value">500+</span> <span class="stat-value">{{ whProject }}</span>
<span class="stat-desc">武汉开源项目数量</span> <span class="stat-desc">武汉开源项目数量</span>
</div> </div>
</div> </div>
</template> </template>
<script> <script lang="ts" setup>
export default { import { ref, onMounted } from 'vue';
name: "StatsBar" import { statsBarPage3 } from '@/api/jyh/situation';
}
// 定义响应式数据
const whCompany = ref<string>('加载中...');
const whCommunity = ref<string>('加载中...');
const whDeveloper = ref<string>('加载中...');
const whProject = ref<string>('加载中...');
// 加载状态
const loading = ref(true);
// 格式化数字的工具函数
const formatNumber = (num: number, unit: string = ''): string => {
if (num >= 100000000) {
return (num / 100000000).toFixed(1) + '亿';
} else if (num >= 10000) {
return (num / 10000).toFixed(1) + '万';
} else if (num >= 1000) {
return (num / 1000).toFixed(1) + '千';
}
return num.toString();
};
// 获取统计数据
const fetchStats = async() => {
try {
loading.value = true;
const response = await statsBarPage3();
const businessData = response.data;
if (businessData.code === 200 && businessData) {
const { developer, community, company, project } = businessData.data;
// 格式化数据显示
whDeveloper.value = `${formatNumber(developer)}万+`;
whCommunity.value = formatNumber(community);
whCompany.value = `${formatNumber(company)}+`;
whProject.value = `${formatNumber(project)}+`;
} else {
console.error('获取统计数据失败:', businessData.msg || '未知错误');
// 设置默认值
whDeveloper.value = '70万+';
whCommunity.value = '12';
whCompany.value = '150+';
whProject.value = '500+';
}
} catch (error) {
console.error('获取统计数据异常:', error);
// 设置默认值
whDeveloper.value = '70万+';
whCommunity.value = '12';
whCompany.value = '150+';
whProject.value = '500+';
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchStats();
});
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@@ -42,7 +96,7 @@ name: "StatsBar"
/* 布局控制:撑满容器并增加内边距 */ /* 布局控制:撑满容器并增加内边距 */
width: 100%; width: 100%;
padding: 2rem 4rem; padding: 2rem 4rem;
//margin: 2rem 0; margin: 0 0 1rem;
/* 深色玻璃拟态背景 */ /* 深色玻璃拟态背景 */
background: rgba(255, 255, 255, 0.02); background: rgba(255, 255, 255, 0.02);

View File

@@ -14,7 +14,6 @@
汇聚武汉开源力量共建城市开源生态 汇聚武汉开源力量共建城市开源生态
</p> </p>
</div> </div>
<StatsBar />
</div> </div>
</div> </div>
@@ -30,9 +29,12 @@
class="lucide lucide-activity" aria-hidden="true"> class="lucide lucide-activity" aria-hidden="true">
<path d="M22 12h-4l-3 9L9 3l-3 9H2" /> <path d="M22 12h-4l-3 9L9 3l-3 9H2" />
</svg> </svg>
<span>武汉开源生态数据可视化</span> <span>武汉开源生态建设总览</span>
</h2> </h2>
</div> </div>
<div class="index-module__NV_5cW__sectionHeader">
<StatsBar />
</div>
<div class="index-module__NV_5cW__chartsGrid"> <div class="index-module__NV_5cW__chartsGrid">
@@ -309,181 +311,479 @@ import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize'; import { usePageResize } from '@/utils/hooks/usePageResize';
import StatsBar from './components/StatsBar.vue'; import StatsBar from './components/StatsBar.vue';
import HarmonyEcoCharts from './components/HarmonyEcoCharts.vue'; import HarmonyEcoCharts from './components/HarmonyEcoCharts.vue';
import { wuhanGithubProjectsPage3, cityProjectPage3, chinaExcellentProjectsPage3, excellentProjectsPage3, talentPage3,
developerNamesPage3, communityRankChartPage3, wuhanCommunitiesPage3, ClubPage3, policyPage3, campusPage3, mirrorsPage3 } from '@/api/jyh/situation';
const { widthType } = usePageResize(); const { widthType } = usePageResize();
const githubProjects = ref([ const githubProjects = ref([]);
{
name: 'bootstrap',
desc: 'The most popular HTML, CSS, and JavaScript framework for developing responsive, mobile first projects on the web.',
stars: '174000'
},
{
name: 'JavaGuide',
desc: 'Java 学习&面试指南Go、Python 后端面试通用,计算机基础面试总结)。准备后端技术面试,首选 JavaGuide',
stars: '153000'
},
{
name: 'cpython',
desc: 'The Python programming language',
stars: '71000'
},
{
name: 'core',
desc: '🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.',
stars: '52700'
},
{
name: 'bevy',
desc: 'A refreshingly simple data-driven game engine built in Rust',
stars: '43900'
},
{
name: 'linux-insides',
desc: 'A book-in-progress about the Linux kernel and its insides.',
stars: '31700'
},
{
name: 'podman',
desc: 'Podman: A tool for managing OCI containers and pods.',
stars: '30300'
},
{
name: 'fastai',
desc: 'The fastai deep learning library',
stars: '27800'
},
{
name: 'OI-wiki',
desc: '🌟 Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)',
stars: '25200'
},
{
name: 'bilibili-API-collect',
desc: '哔哩哔哩-API收集整理【不断更新中....】',
stars: '20100'
}
]);
const activeTab = ref(0); const activeTab = ref(0);
const excellentProjects = ref([ const excellentProjects = ref([]);
{ 'name': '调问DWSurvey', 'desc': '开源问卷表单系统', 'org': '极推科技' },
{ 'name': 'Eova', 'desc': '低代码开发平台', 'org': '武汉简码' },
{ 'name': 'ClickVisual', 'desc': '可视化日志分析告警平台', 'org': '武汉初心' },
{ 'name': 'Meta2d.js', 'desc': '2D图元可视化引擎', 'org': '乐吾乐科技' },
{ 'name': 'Juno', 'desc': '分布式微服务管理系统', 'org': '武汉斗鱼' },
{ 'name': 'Opentalent', 'desc': '开源人才评价系统', 'org': '湖北大学' },
{ 'name': 'Fountain', 'desc': '仓颉语言版 Spring 框架替代', 'org': '江汉大学' },
{ 'name': 'deepin', 'desc': '深度桌面操作系统', 'org': '深之度' },
{ 'name': 'RustSBI', 'desc': 'RISC-V 安全引导程序', 'org': '华中科技大学' },
{ 'name': 'NextDenovo', 'desc': '长读长序列组装工具', 'org': '武汉希望组' }
]);
const chinaExcellentProjects = ref([ const chinaExcellentProjects = ref([]);
{
'name': 'Deepin 深度操作系统', const developerNames = ref([]);
'desc': '国内领先的国产 Linux 发行版,旨在为全球用户提供美观易用、安全可靠的操作系统体验,是国产软件生态的核心。',
'org': '武汉深之度' const wuhanCommunities = ref([]);
},
{ // 武汉优质开源项目备用数据
'name': 'Jupiter 微服务治理框架', const setgithubProjectsdata = () => {
'desc': '由斗鱼研发并开源的面向治理的微服务框架,支持多数据源、多注册中心,适用于高并发场景。', githubProjects.value = [
'org': '武汉斗鱼' {
}, name: 'bootstrap',
{ desc: 'The most popular HTML, CSS, and JavaScript framework for developing responsive, mobile first projects on the web.',
'name': 'RustSBI 安全引导程序', stars: '174000'
'desc': '基于 Rust 语言开发的 RISC-V 指令集体系结构安全引导程序,提供更高内存安全性。', },
'org': '华中科技大学' {
}, name: 'JavaGuide',
{ desc: 'Java 学习&面试指南Go、Python 后端面试通用,计算机基础面试总结)。准备后端技术面试,首选 JavaGuide',
'name': '芯片开源 EDA 研发', stars: '153000'
'desc': '致力于国产电子设计自动化工具开源化,解决芯片设计环节中的关键技术难题。', },
'org': '华中科技大学' {
}, name: 'cpython',
{ desc: 'The Python programming language',
'name': 'LuoJiaSET 框架与 LuoJiaSET 样本库', stars: '71000'
'desc': '专注于智能遥感影像解译,提供海量遥感样本数据集和深度学习框架。', },
'org': '武汉大学' {
}, name: 'core',
{ desc: '🖖 Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.',
'name': '鲲鹏安全库 (Kunpengsecl)', stars: '52700'
'desc': '基于鲲鹏处理器的机密计算和受信任执行环境安全开发库,支撑云计算与隐私计算。', },
'org': '武汉大学' {
}, name: 'bevy',
{ desc: 'A refreshingly simple data-driven game engine built in Rust',
'name': '柚坛工具箱 (Uotan Toolbox)', stars: '43900'
'desc': '针对 Android 发烧友开发的图形化综合工具箱,集成刷机、系统优化等多种功能。', },
'org': '武昌理工学院' {
}, name: 'linux-insides',
{ desc: 'A book-in-progress about the Linux kernel and its insides.',
'name': '凹语言 (Wa Language)', stars: '31700'
'desc': '国内首个针对 WebAssembly 设计的通用编程语言,兼具 Web 灵活性与原生运行速度。', },
'org': '武汉凹语言' {
}, name: 'podman',
{ desc: 'Podman: A tool for managing OCI containers and pods.',
'name': 'OpenHIS 开源医疗系统', stars: '30300'
'desc': '开源的医院信息系统,旨在通过标准化、模块化方式赋能基层医疗机构数字化升级。', },
'org': '武汉新致新源' {
}, name: 'fastai',
{ desc: 'The fastai deep learning library',
'name': 'NextDenovo', stars: '27800'
'desc': '高效且精确的长读长序列纠错和组装工具,主要用于基因组测序领域。', },
'org': '武汉希望组生物科技有限公司' {
name: 'OI-wiki',
desc: '🌟 Wiki of OI / ICPC for everyone. (某大型游戏线上攻略,内含炫酷算术魔法)',
stars: '25200'
},
{
name: 'bilibili-API-collect',
desc: '哔哩哔哩-API收集整理【不断更新中....】',
stars: '20100'
}
];
};
// 获取武汉优质开源项目数据
const fetchGithubProjects = async() => {
try {
const response = await wuhanGithubProjectsPage3();
const projectdata = response.data;
if (projectdata?.code === 200 && projectdata?.data) {
// 将星星数转换为字符串格式以匹配模板显示
const formattedData = projectdata.data.map(item => ({
...item,
stars: String(item.stars)
}));
githubProjects.value = formattedData;
} else {
console.error('获取武汉优质开源项目数据失败:', projectdata?.msg || '未知错误');
setgithubProjectsdata();
}
} catch (error) {
console.error('请求武汉优质开源项目数据时发生错误:', error);
setgithubProjectsdata();
} }
]); };
const wuhanCommunities = ref([ // 中国城市开源项目数量备用数据
{ name: '黄鹤开源社区', type: '核心生态', slogan: '国家级、城市级开源社区平台,武汉开源生态的核心枢纽' }, const setCityProjectDefaultData = () => {
{ name: '深度社区 (Deepin)', type: '操作系统', slogan: '全球最大桌面操作系统社区之一,全球用户超过 750 万' }, chartData.value.cityProject.xAxis = ['北京', '上海', '深圳', '杭州', '广州', '成都', '武汉', '南京', '西安', '重庆'];
{ chartData.value.cityProject.data = [757259, 678614, 343417, 315406, 177003, 168300, 120827, 111593, 58678, 56564];
name: '极狐社区 (GitLab China)', isEmpty.value.cityProject = false;
type: 'DevOps', };
slogan: '全球第二大开源托管平台 GitLab 在汉设立的中国发行版及开发者社区'
},
{ name: '开源鸿蒙社区 (OpenHarmony)', type: '万物互联', slogan: '国家级生态创新中心,设有全国首个分委会' },
{ name: 'PostgreSQL 中文社区', type: '数据库', slogan: '全球四大开源数据库之一的运营中心设在武汉' },
{ name: '武汉大学开源社区', type: '学术社团', slogan: '老牌社团,涉及 Linux、嵌入式、深度学习等方向' },
{ name: '武汉大学 OpenHarmony 俱乐部', type: '万物互联', slogan: '专注于华为开源鸿蒙生态' },
{ name: '华中科技大学开源社团', type: '学术社团', slogan: '常年组织 LUG (Linux User Group) 及黑客松活动' },
{ name: '武汉理工大学开源联盟', type: '学术社团', slogan: '聚焦计算机科学与软件工程方向' },
{ name: '华中师范大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '中南民族大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '湖北大学', type: '兴趣小组', slogan: '开源技术兴趣小组' }
]);
// 开发者名单 // 获取中国城市开源项目数量数据
const developerNames = ['白纯骢', '陆红旭', '慕冬亮', '张继德', '蒋周奇', '张天雄', '阮文俊', '李奇', '陈鹏轩', '杨帆']; const fetchCityProjectData = async() => {
try {
const response = await cityProjectPage3();
const projectdata = response.data;
if (projectdata?.code === 200 && projectdata?.data) {
// 更新图表数据
chartData.value.cityProject.xAxis = projectdata.data.map(item => item.name);
chartData.value.cityProject.data = projectdata.data.map(item => item.data);
// 设置非空状态
isEmpty.value.cityProject = false;
} else {
console.error('获取中国城市开源项目数量数据失败:', projectdata?.msg || '未知错误');
setCityProjectDefaultData();
}
} catch (error) {
console.error('请求中国城市开源项目数量数据时发生错误:', error);
setCityProjectDefaultData();
}
};
// 城市开源人才数量备用数据
const setTalentDefaultData = () => {
chartData.value.talent.xAxis = ['北京', '上海', '广州', '深圳', '成都', '杭州', '武汉', '南京', '郑州', '长沙'];
chartData.value.talent.data = [2115924, 1591988, 1555240, 1163580, 894108, 830306, 791335, 733581, 553397, 487971];
isEmpty.value.talent = false;
};
// 获取城市开源人才数量数据
const fetchTalentData = async() => {
try {
const response = await talentPage3();
const talentdata = response.data;
if (talentdata?.code === 200 && talentdata?.data) {
// 更新图表数据 - 后端返回字段为 name 和 number
chartData.value.talent.xAxis = talentdata.data.map(item => item.name);
chartData.value.talent.data = talentdata.data.map(item => item.number);
// 设置非空状态
isEmpty.value.talent = false;
} else {
console.error('获取城市开源人才数量数据失败:', talentdata?.msg || '未知错误');
setTalentDefaultData();
}
} catch (error) {
console.error('请求城市开源人才数量数据时发生错误:', error);
setTalentDefaultData();
}
};
// 城市社区数量排行备用数据
const setCommunityRankDefaultData = () => {
chartData.value.communityRank.xAxis = ['北京', '深圳', '上海', '杭州', '广州', '成都', '南京', '武汉'];
chartData.value.communityRank.data = [69, 35, 31, 25, 18, 15, 12, 12];
isEmpty.value.communityRank = false;
};
// 获取武汉社区数量排行数据
const fetchCommunityRankData = async() => {
try {
const response = await communityRankChartPage3();
const communitydata = response.data;
if (communitydata?.code === 200 && communitydata?.data) {
// 更新图表数据
chartData.value.communityRank.xAxis = communitydata.data.map(item => item.name);
chartData.value.communityRank.data = communitydata.data.map(item => item.value);
// 设置非空状态
isEmpty.value.communityRank = false;
} else {
console.error('获取城市社区数量排行数据失败:', communitydata?.msg || '未知错误');
setCommunityRankDefaultData();
}
} catch (error) {
console.error('请求城市社区数量排行数据时发生错误:', error);
setCommunityRankDefaultData();
}
};
// 2024武汉优秀开源项目备用数据
const chinaExcellentProjectsdata = () => {
chinaExcellentProjects.value = [
{
'name': 'Deepin 深度操作系统',
'desc': '国内领先的国产 Linux 发行版,旨在为全球用户提供美观易用、安全可靠的操作系统体验,是国产软件生态的核心。',
'org': '武汉深之度'
},
{
'name': 'Jupiter 微服务治理框架',
'desc': '由斗鱼研发并开源的面向治理的微服务框架,支持多数据源、多注册中心,适用于高并发场景。',
'org': '武汉斗鱼'
},
{
'name': 'RustSBI 安全引导程序',
'desc': '基于 Rust 语言开发的 RISC-V 指令集体系结构安全引导程序,提供更高内存安全性。',
'org': '华中科技大学'
},
{
'name': '芯片开源 EDA 研发',
'desc': '致力于国产电子设计自动化工具开源化,解决芯片设计环节中的关键技术难题。',
'org': '华中科技大学'
},
{
'name': 'LuoJiaSET 框架与 LuoJiaSET 样本库',
'desc': '专注于智能遥感影像解译,提供海量遥感样本数据集和深度学习框架。',
'org': '武汉大学'
},
{
'name': '鲲鹏安全库 (Kunpengsecl)',
'desc': '基于鲲鹏处理器的机密计算和受信任执行环境安全开发库,支撑云计算与隐私计算。',
'org': '武汉大学'
},
{
'name': '柚坛工具箱 (Uotan Toolbox)',
'desc': '针对 Android 发烧友开发的图形化综合工具箱,集成刷机、系统优化等多种功能。',
'org': '武昌理工学院'
},
{
'name': '凹语言 (Wa Language)',
'desc': '国内首个针对 WebAssembly 设计的通用编程语言,兼具 Web 灵活性与原生运行速度。',
'org': '武汉凹语言'
},
{
'name': 'OpenHIS 开源医疗系统',
'desc': '开源的医院信息系统,旨在通过标准化、模块化方式赋能基层医疗机构数字化升级。',
'org': '武汉新致新源'
},
{
'name': 'NextDenovo',
'desc': '高效且精确的长读长序列纠错和组装工具,主要用于基因组测序领域。',
'org': '武汉希望组生物科技有限公司'
}
];
};
// 2025武汉优秀开源项目备用数据
const excellentProjectsdata = () => {
excellentProjects.value = [
{ 'name': '调问DWSurvey', 'desc': '开源问卷表单系统', 'org': '极推科技' },
{ 'name': 'Eova', 'desc': '低代码开发平台', 'org': '武汉简码' },
{ 'name': 'ClickVisual', 'desc': '可视化日志分析告警平台', 'org': '武汉初心' },
{ 'name': 'Meta2d.js', 'desc': '2D图元可视化引擎', 'org': '乐吾乐科技' },
{ 'name': 'Juno', 'desc': '分布式微服务管理系统', 'org': '武汉斗鱼' },
{ 'name': 'Opentalent', 'desc': '开源人才评价系统', 'org': '湖北大学' },
{ 'name': 'Fountain', 'desc': '仓颉语言版 Spring 框架替代', 'org': '江汉大学' },
{ 'name': 'deepin', 'desc': '深度桌面操作系统', 'org': '深之度' },
{ 'name': 'RustSBI', 'desc': 'RISC-V 安全引导程序', 'org': '华中科技大学' },
{ 'name': 'NextDenovo', 'desc': '长读长序列组装工具', 'org': '武汉希望组' }
];
};
const fetchchinaExcellentProjects = async() => {
try {
const response = await chinaExcellentProjectsPage3();
const projectdata = response.data;
if (projectdata?.code === 200 && projectdata?.data) {
chinaExcellentProjects.value = projectdata.data;
} else {
console.error('获取2024武汉优秀开源项目数据失败:', projectdata?.msg || '未知错误');
chinaExcellentProjectsdata();
}
} catch (error) {
console.error('请求2024武汉优秀开源项目数据时发生错误:', error);
chinaExcellentProjectsdata();
}
};
const fetchexcellentProjects = async() => {
try {
const response = await excellentProjectsPage3();
const projectdata = response.data;
if (projectdata?.code === 200 && projectdata?.data) {
excellentProjects.value = projectdata.data;
} else {
console.error('获取2025武汉优秀开源项目数据失败:', projectdata?.msg || '未知错误');
excellentProjectsdata();
}
} catch (error) {
console.error('请求2025武汉优秀开源项目数据时发生错误:', error);
excellentProjectsdata();
}
};
// 武汉明星开发者贡献榜备用数据
const setDeveloperNamesDefaultData = () => {
developerNames.value = ['白纯骢', '陆红旭', '慕冬亮', '张继德', '蒋周奇', '张天雄', '阮文俊', '李奇', '陈鹏轩', '杨帆'];
};
// 获取武汉明星开发者贡献榜数据
const fetchDeveloperNamesData = async() => {
try {
const response = await developerNamesPage3();
const developerdata = response.data;
if (developerdata?.code === 200 && developerdata?.data) {
// 更新开发者名单 - 后端返回字段为 name
developerNames.value = developerdata.data.map(item => item.name);
// 设置非空状态
isEmpty.value.developer = false;
} else {
console.error('获取武汉明星开发者贡献榜数据失败:', developerdata?.msg || '未知错误');
setDeveloperNamesDefaultData();
}
} catch (error) {
console.error('请求武汉明星开发者贡献榜数据时发生错误:', error);
setDeveloperNamesDefaultData();
}
};
// 武汉知名开源社区备用数据
const setwuhanCommunitiesdata = () => {
wuhanCommunities.value = [
{ name: '黄鹤开源社区', type: '核心生态', slogan: '国家级、城市级开源社区平台,武汉开源生态的核心枢纽' },
{ name: '深度社区 (Deepin)', type: '操作系统', slogan: '全球最大桌面操作系统社区之一,全球用户超过 750 万' },
{
name: '极狐社区 (GitLab China)',
type: 'DevOps',
slogan: '全球第二大开源托管平台 GitLab 在汉设立的中国发行版及开发者社区'
},
{ name: '开源鸿蒙社区 (OpenHarmony)', type: '万物互联', slogan: '国家级生态创新中心,设有全国首个分委会' },
{ name: 'PostgreSQL 中文社区', type: '数据库', slogan: '全球四大开源数据库之一的运营中心设在武汉' },
{ name: '武汉大学开源社区', type: '学术社团', slogan: '老牌社团,涉及 Linux、嵌入式、深度学习等方向' },
{ name: '武汉大学 OpenHarmony 俱乐部', type: '万物互联', slogan: '专注于华为开源鸿蒙生态' },
{ name: '华中科技大学开源社团', type: '学术社团', slogan: '常年组织 LUG (Linux User Group) 及黑客松活动' },
{ name: '武汉理工大学开源联盟', type: '学术社团', slogan: '聚焦计算机科学与软件工程方向' },
{ name: '华中师范大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '中南民族大学', type: '兴趣小组', slogan: '开源技术兴趣小组' },
{ name: '湖北大学', type: '兴趣小组', slogan: '开源技术兴趣小组' }
];
};
const fetchwuhanCommunities = async() => {
try {
const response = await wuhanCommunitiesPage3();
const communitydata = response.data;
if (communitydata?.code === 200 && communitydata?.data) {
wuhanCommunities.value = communitydata.data;
} else {
console.error('获取武汉知名开源社区数据失败:', communitydata?.msg || '未知错误');
setwuhanCommunitiesdata();
}
} catch (error) {
console.error('请求武汉知名开源社区数据时发生错误:', error);
setwuhanCommunitiesdata();
}
};
// 重点城市高校开源社团数量备用数据
const setClubdata = () => {
chartData.value.club.xAxis = ['成都', '北京', '上海', '武汉', '重庆', '深圳', '苏州', '杭州', '合肥', '青岛'];
chartData.value.club.data = [10, 7, 5, 4, 3, 2, 2, 2, 2, 2];
isEmpty.value.club = false;
};
const fetchclubData = async() => {
try {
const response = await ClubPage3();
const clubdata = response.data;
if (clubdata?.code === 200 && clubdata?.data) {
chartData.value.club.xAxis = clubdata.data.map(item => item.name);
chartData.value.club.data = clubdata.data.map(item => item.number);
isEmpty.value.club = false;
} else {
console.error('获取重点城市高校开源社团数量数据失败:', clubdata?.msg || '未知错误');
setClubdata();
}
} catch (error) {
console.error('请求重点城市高校开源社团数量数据时发生错误:', error);
setClubdata();
}
};
// 各省市开源相关政策发布数量备用数据
const setPolicydata = () => {
chartData.value.policy.xAxis = ['江苏', '北京', '浙江', '广东', '上海', '湖北', '天津', '陕西', '河南', '云南'];
chartData.value.policy.data = [11, 7, 3, 3, 3, 2, 2, 2, 2, 2];
isEmpty.value.policy = false;
};
const fetchpolicyData = async() => {
try {
const response = await policyPage3();
const policydata = response.data;
if (policydata?.code === 200 && policydata?.data) {
chartData.value.policy.xAxis = policydata.data.map(item => item.name);
chartData.value.policy.data = policydata.data.map(item => item.number);
isEmpty.value.policy = false;
} else {
console.error('获取各省市开源相关政策发布数量数据失败:', policydata?.msg || '未知错误');
setPolicydata();
}
} catch (error) {
console.error('请求各省市开源相关政策发布数量数据时发生错误:', error);
setPolicydata();
}
};
// 开放原子校园行·城市活动场次备用数据
const setcampusdata = () => {
chartData.value.campus.xAxis = ['南京', '武汉', '北京', '长沙', '成都', '上海', '天津', '深圳', '济南', '广州'];
chartData.value.campus.data = [20, 16, 16, 16, 11, 10, 8, 8, 7, 6];
isEmpty.value.campus = false;
};
const fetchcampusData = async() => {
try {
const response = await campusPage3();
const campusdata = response.data;
if (campusdata?.code === 200 && campusdata?.data) {
chartData.value.campus.xAxis = campusdata.data.map(item => item.name);
chartData.value.campus.data = campusdata.data.map(item => item.number);
isEmpty.value.campus = false;
} else {
console.error('获取开放原子校园行·城市活动场次数据失败:', campusdata?.msg || '未知错误');
setcampusdata();
}
} catch (error) {
console.error('请求开放原子校园行·城市活动场次数据时发生错误:', error);
setcampusdata();
}
};
// 全国重点城市开源镜像站/节点数量对比备用数据
const setmirrorsdata = () => {
chartData.value.mirrors.xAxis = ['上海', '北京', '西安', '重庆', '济南', '南京', '武汉', '合肥', '香港', '长春', '成都', '深圳', '兰州', '哈尔滨', '南阳', '杭州'];
chartData.value.mirrors.data = [4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1];
isEmpty.value.mirrors = false;
};
const fetchmirrorsData = async() => {
try {
const response = await mirrorsPage3();
const mirrorsdata = response.data;
if (mirrorsdata?.code === 200 && mirrorsdata?.data) {
chartData.value.mirrors.xAxis = mirrorsdata.data.map(item => item.name);
chartData.value.mirrors.data = mirrorsdata.data.map(item => item.number);
isEmpty.value.mirrors = false;
} else {
console.error('获取全国重点城市开源镜像站/节点数量对比数据失败:', mirrorsdata?.msg || '未知错误');
setmirrorsdata();
}
} catch (error) {
console.error('请求全国重点城市开源镜像站/节点数量对比数据时发生错误:', error);
setmirrorsdata();
}
};
// 图表数据 // 图表数据
const chartData = ref({ const chartData = ref({
cityProject: { cityProject: {
xAxis: ['北京', '上海', '深圳', '杭州', '广州', '成都', '武汉', '南京', '西安', '重庆'], xAxis: [],
data: [757259, 678614, 343417, 315406, 177003, 168300, 120827, 111593, 58678, 56564] data: []
}, },
talent: { talent: {
xAxis: ['北京', '上海', '广州', '深圳', '成都', '杭州', '武汉', '南京', '郑州', '长沙'], xAxis: [],
data: [2115924, 1591988, 1555240, 1163580, 894108, 830306, 791335, 733581, 553397, 487971] data: []
}, },
communityRank: { communityRank: {
xAxis: ['Deepin', 'OpenHarmony', 'MindSpore', 'IoTDB', 'GoFrame'], xAxis: ['Deepin', 'OpenHarmony', 'MindSpore', 'IoTDB', 'GoFrame'],
data: [95, 92, 88, 76, 72] data: [95, 92, 88, 76, 72]
}, },
club: { club: {
xAxis: ['成都', '北京', '上海', '武汉', '重庆', '深圳', '苏州', '杭州', '合肥', '青岛'], xAxis: [],
data: [10, 7, 5, 4, 3, 2, 2, 2, 2, 2] data: []
}, },
policy: { policy: {
xAxis: ['江苏', '北京', '浙江', '广东', '上海', '湖北', '天津', '陕西', '河南', '云南'], xAxis: [],
data: [11, 7, 3, 3, 3, 2, 2, 2, 2, 2] data: []
}, },
campus: { campus: {
xAxis: ['南京', '武汉', '北京', '长沙', '成都', '上海', '天津', '深圳', '济南', '广州'], xAxis: [],
data: [20, 16, 16, 16, 11, 10, 8, 8, 7, 6] data: []
}, },
mirrors: { mirrors: {
xAxis: ['上海', '北京', '西安', '重庆', '济南', '南京', '武汉', '合肥', '香港', '长春', '成都', '深圳', '兰州', '哈尔滨', '南阳', '杭州'], xAxis: [],
data: [4, 4, 2, 2, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1] data: []
} }
}); });
@@ -642,17 +942,20 @@ const initCommunityRankChart = () => {
const el = document.getElementById('communityRankChart'); const el = document.getElementById('communityRankChart');
if (!el) return; if (!el) return;
const myChart = echarts.init(el); const myChart = echarts.init(el);
chartInstances.value.communityRank = myChart;
myChart.setOption({ myChart.setOption({
tooltip: { trigger: 'axis', valueFormatter: (val: any) => val + ' 个' }, tooltip: { trigger: 'axis', valueFormatter: (val: any) => val + ' 个' },
grid: { left: '3%', right: '4%', bottom: '15px', top: '22%', containLabel: true }, grid: { left: '3%', right: '4%', bottom: '15px', top: '22%', containLabel: true },
xAxis: { xAxis: {
type: 'category', type: 'category',
data: ['北京', '深圳', '上海', '杭州', '广州', '成都', '南京', '武汉'], data: chartData.value.communityRank.xAxis,
axisLabel: { color: '#666' } axisLabel: { color: '#666' }
}, },
yAxis: { type: 'value', name: '单位:个', nameTextStyle: { color: '#999' } }, yAxis: { type: 'value', name: '单位:个', nameTextStyle: { color: '#999' }},
series: [{ series: [{
type: 'bar', data: [69, 35, 31, 25, 18, 15, 12, 12], barWidth: '40%', type: 'bar',
data: chartData.value.communityRank.data,
barWidth: '40%',
itemStyle: { borderRadius: [4, 4, 0, 0], color: '#3b82f6' } itemStyle: { borderRadius: [4, 4, 0, 0], color: '#3b82f6' }
}] }]
}); });
@@ -666,8 +969,8 @@ const initClubChart = () => {
myChart.setOption({ myChart.setOption({
tooltip: { trigger: 'axis', valueFormatter: (value: any) => value + ' 个' }, tooltip: { trigger: 'axis', valueFormatter: (value: any) => value + ' 个' },
grid: commonGrid, grid: commonGrid,
xAxis: { type: 'category', data: chartData.value.club.xAxis, axisLabel: { color: '#666' } }, xAxis: { type: 'category', data: chartData.value.club.xAxis, axisLabel: { color: '#666' }},
yAxis: { type: 'value', name: '单位:个', splitLine: { lineStyle: { type: 'dashed', color: '#eee' } } }, yAxis: { type: 'value', name: '单位:个', splitLine: { lineStyle: { type: 'dashed', color: '#eee' }}},
series: [{ series: [{
type: 'line', data: chartData.value.club.data, smooth: true, symbolSize: 8, type: 'line', data: chartData.value.club.data, smooth: true, symbolSize: 8,
lineStyle: { width: 3, color: '#F59E0B' }, lineStyle: { width: 3, color: '#F59E0B' },
@@ -767,8 +1070,20 @@ watch(widthType, () => {
init(); init();
}); });
onMounted(() => { onMounted(async() => {
init(); await fetchGithubProjects(); // 获取武汉优质开源项目榜数据
await fetchCityProjectData(); // 获取城市项目数据
await fetchTalentData(); // 获取城市人才数据
await fetchDeveloperNamesData(); // 获取武汉明星开发者贡献榜数据
await fetchchinaExcellentProjects(); // 获取2024中国优秀开源项目数据
await fetchexcellentProjects(); // 获取2025武汉优秀开源项目数据
await fetchCommunityRankData(); // 获取城市社区数量排行数据
await fetchwuhanCommunities(); // 获取武汉知名开源社区数据
await fetchclubData(); // 获取重点城市高校开源社团数量数据
await fetchpolicyData(); // 获取各省市开源相关政策发布数量数据
await fetchcampusData(); // 获取开放原子校园行·城市活动场次数据
await fetchmirrorsData(); // 获取全国重点城市开源镜像站/节点数量对比数据
init(); // 再初始化图表
}); });
onUnmounted(() => { onUnmounted(() => {
@@ -1363,4 +1678,8 @@ onUnmounted(() => {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
} }
.index-module__NV_5cW__chartsSection {
margin-top: 2rem;
}
</style> </style>

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,382 @@
<template>
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">社区治理健康度对比</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty || loading">加载中...</div>
<div v-else id="ossCompassChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, watch, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
import { communityHealthPage2 } from '@/api/jyh/situation';
// 定义数据结构
interface CommunityHealthItem {
indicators: string;
github: number;
gitcode: number;
}
// 响应式数据存储
const communityHealthData = ref<CommunityHealthItem[]>([]);
const loading = ref(true);
// 从API获取数据
const fetchDataFromApi = async () => {
try {
loading.value = true;
const response = await communityHealthPage2();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200 && businessData.data) {
communityHealthData.value = businessData.data;
} else {
console.error('获取社区治理健康度数据失败:', businessData?.msg || '未知错误');
// 设置默认数据
communityHealthData.value = [
{ indicators: 'PR 合并率', github: 82, gitcode: 68 },
{ indicators: 'Issue 响应时间', github: 88, gitcode: 75 },
{ indicators: 'CI/CD 使用率', github: 78, gitcode: 55 },
{ indicators: '平均代码质量得分', github: 85, gitcode: 72 },
{ indicators: '安全审计频率', github: 75, gitcode: 60 },
{ indicators: '贡献者多样性', github: 80, gitcode: 65 }
];
}
} catch (error) {
console.error('获取社区治理健康度数据异常:', error);
// 设置默认数据
communityHealthData.value = [
{ indicators: 'PR 合并率', github: 82, gitcode: 68 },
{ indicators: 'Issue 响应时间', github: 88, gitcode: 75 },
{ indicators: 'CI/CD 使用率', github: 78, gitcode: 55 },
{ indicators: '平均代码质量得分', github: 85, gitcode: 72 },
{ indicators: '安全审计频率', github: 75, gitcode: 60 },
{ indicators: '贡献者多样性', github: 80, gitcode: 65 }
];
} finally {
loading.value = false;
}
};
// 组件挂载时获取数据
onMounted(() => {
fetchDataFromApi();
});
// 页面尺寸响应式
const { widthType } = usePageResize();
// 空数据标识
const isEmpty = computed(() => {
return communityHealthData.value.length === 0;
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
communityHealthChart: null
});
// 初始化社区治理健康度对比(纵向柱状图)
const initCommunityHealthChart = () => {
const el = document.getElementById('ossCompassChart');
if (!el) return;
if (chartInstances.value.communityHealthChart) {
chartInstances.value.communityHealthChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.communityHealthChart = myChart;
// 提取数据以适应图表需求
const indicators = communityHealthData.value.map(item => item.indicators);
const githubValues = communityHealthData.value.map(item => item.github);
const gitcodeValues = communityHealthData.value.map(item => item.gitcode);
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow',
shadowStyle: {
color: 'rgba(59, 130, 246, 0.1)'
}
},
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: '#3b82f6',
borderWidth: 2,
borderRadius: 8,
padding: 16,
textStyle: { color: '#333', fontSize: 13 },
extraCssText: 'box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);',
formatter: (params: any) => {
const indicator = params[0].name;
const githubValue = params[0].value;
const gitcodeValue = params[1].value;
const diff = githubValue - gitcodeValue;
const diffPercent = ((diff / githubValue) * 100).toFixed(1);
return `
<div style="line-height: 2.2;">
<div style="font-weight: bold; color: #3b82f6; font-size: 15px; margin-bottom: 8px; border-bottom: 2px solid #3b82f6; padding-bottom: 4px;">${indicator}</div>
<div style="display: flex; align-items: center; gap: 10px; margin-bottom: 6px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #BF232A, #FF4900); border-radius: 50%; box-shadow: 0 2px 4px rgba(191, 35, 42, 0.3);"></span>
<span style="color: #666; min-width: 100px;">GitHub 社区</span>
<span style="color: #BF232A; font-weight: 700; font-size: 15px;">${githubValue}</span>
</div>
<div style="display: flex; align-items: center; gap: 10px;">
<span style="display: inline-block; width: 10px; height: 10px; background: linear-gradient(135deg, #FF4900, #FFAB00); border-radius: 50%; box-shadow: 0 2px 4px rgba(255, 73, 0, 0.3);"></span>
<span style="color: #666; min-width: 100px;">GitCode 社区</span>
<span style="color: #FF4900; font-weight: 700; font-size: 15px;">${gitcodeValue}</span>
</div>
${diff > 0 ? `<div style="margin-top: 8px; padding: 4px 8px; background: rgba(59, 130, 246, 0.1); border-radius: 4px; color: #3b82f6; font-size: 11px;">📊 GitHub 领先 ${diffPercent}%</div>` : ''}
</div>
`;
}
},
legend: {
data: ['GitHub 社区', 'GitCode 社区'],
top: '2%',
right: '5%',
itemWidth: 14,
itemHeight: 14,
itemGap: 15,
textStyle: {
color: '#333',
fontSize: 12,
fontWeight: '600'
},
icon: 'roundRect'
},
grid: {
left: '5%',
right: '5%',
bottom: '10%',
top: '15%',
containLabel: true
},
xAxis: {
type: 'category',
data: indicators,
axisLabel: {
fontSize: 11,
color: '#666',
fontWeight: '500',
interval: 0,
rotate: 20,
margin: 12
},
axisTick: {
alignWithLabel: true,
length: 5
},
axisLine: {
lineStyle: { color: '#e5e7eb', width: 2 }
}
},
yAxis: {
type: 'value',
name: '健康度得分',
nameTextStyle: {
fontSize: 13,
color: '#666',
fontWeight: '600',
padding: [0, 0, 0, 10]
},
max: 100,
axisLabel: {
fontSize: 12,
color: '#666',
formatter: '{value}'
},
splitLine: {
lineStyle: {
color: 'rgba(0, 0, 0, 0.06)',
type: 'dashed'
}
}
},
series: [
{
name: 'GitHub 社区',
type: 'bar',
data: githubValues,
barWidth: '35%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: '#FF4900' },
{ offset: 0.5, color: '#BF232A' },
{ offset: 1, color: '#BF232A' }
]),
borderRadius: [6, 6, 0, 0],
shadowBlur: 10,
shadowColor: 'rgba(191, 35, 42, 0.3)'
},
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 11,
color: '#BF232A',
fontWeight: 'bold'
},
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(191, 35, 42, 0.6)'
}
}
},
{
name: 'GitCode 社区',
type: 'bar',
data: gitcodeValues,
barWidth: '35%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: '#FFAB00' },
{ offset: 0.5, color: '#FF4900' },
{ offset: 1, color: '#FF4900' }
]),
borderRadius: [6, 6, 0, 0],
shadowBlur: 10,
shadowColor: 'rgba(255, 73, 0, 0.3)'
},
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 11,
color: '#FF4900',
fontWeight: 'bold'
},
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(255, 73, 0, 0.6)'
}
}
}
],
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 80
});
};
// 监听数据变化,重新渲染图表
watch(communityHealthData, () => {
if (!isEmpty.value) {
nextTick(() => {
initCommunityHealthChart();
});
}
}, { deep: true });
// 页面resize时重绘图表
watch(widthType, () => {
nextTick(() => {
if (!isEmpty.value) {
initCommunityHealthChart();
}
});
});
// 挂载时初始化
onMounted(() => {
setTimeout(() => {
if (!isEmpty.value) {
initCommunityHealthChart();
}
}, 100);
});
// 组件卸载时销毁图表实例
onUnmounted(() => {
if (chartInstances.value.communityHealthChart) {
chartInstances.value.communityHealthChart.dispose();
chartInstances.value.communityHealthChart = null;
}
});
</script>
<style scoped>
/* 社区治理健康度对比图表卡片样式 - 从主页面复制 */
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.25rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
height: 500px;
}
.index-module__NV_5cW__chartCard::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent);
transition: left 0.6s;
}
.index-module__NV_5cW__chartCard:hover::before {
left: 100%;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.index-module__NV_5cW__chartCard:hover .chartCard__title {
color: #3b82f6;
transform: scale(1.02);
transition: all 0.3s;
}
/* 标题样式 - 从主页面复制 */
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0;
text-align: center;
}
/* 容器样式 - 从主页面复制 */
.chartCard__container {
width: 100%;
height: 450px;
position: relative;
overflow: hidden;
}
/* 空数据显示样式 - 从主页面复制 */
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
/* 图表容器样式 */
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,379 @@
<template>
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">基础设施覆盖率</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty"></div>
<div v-else id="infrastructureChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, watch, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
import { infrastructurePage2 } from '@/api/jyh/situation';
// 基础颜色配置
const baseColors = ['#00F0FF', '#FFAB00', '#B20003'];
// 定义数据结构
interface InfrastructureItem {
name: string;
value: number;
}
// 响应式数据存储
const infrastructureData = ref<InfrastructureItem[]>([]);
const loading = ref(true);
// 从API获取数据
const fetchDataFromApi = async() => {
try {
loading.value = true;
const response = await infrastructurePage2();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData.code === 200 && businessData.data) {
// 为每个数据项添加颜色和半径配置
infrastructureData.value = businessData.data.map((item: InfrastructureItem, index: number) => ({
name: item.name,
value: item.value,
color: baseColors[index % baseColors.length],
radius: [`${70 - index * 20}%`, `${80 - index * 20}%`] // 根据索引计算半径移除calc函数以避免潜在问题
}));
} else {
console.error('获取基础设施覆盖率数据失败:', businessData.msg || '未知错误');
// 设置默认数据
infrastructureData.value = [
{
name: '镜像站规模',
value: 85,
color: '#00F0FF',
radius: ['70%', '80%']
},
{
name: '代码托管平台规模',
value: 90,
color: '#FFAB00',
radius: ['50%', '60%']
},
{
name: '构建平台活跃度',
value: 40,
color: '#B20003',
radius: ['30%', '40%']
}
];
}
} catch (error) {
console.error('获取基础设施覆盖率数据异常:', error);
// 设置默认数据
infrastructureData.value = [
{
name: '镜像站规模',
value: 85,
color: '#00F0FF',
radius: ['70%', '80%']
},
{
name: '代码托管平台规模',
value: 90,
color: '#FFAB00',
radius: ['50%', '60%']
},
{
name: '构建平台活跃度',
value: 40,
color: '#B20003',
radius: ['30%', '40%']
}
];
} finally {
loading.value = false;
}
};
// 组件挂载时获取数据
onMounted(() => {
fetchDataFromApi();
});
// 页面尺寸响应式
const { widthType } = usePageResize();
// 空数据标识
const isEmpty = computed(() => {
return infrastructureData.value.length === 0;
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
infrastructureChart: null
});
// 初始化基础设施覆盖率图表(多层环形图)
const initInfrastructureChart = () => {
const el = document.getElementById('infrastructureChart');
if (!el) return;
if (chartInstances.value.infrastructureChart) {
chartInstances.value.infrastructureChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.infrastructureChart = myChart;
const seriesData: any[] = [];
const center = ['50%', '55%'];
infrastructureData.value.forEach(item => {
const completeValue = item.value;
const remainingValue = 100 - item.value;
seriesData.push({
name: item.name,
type: 'pie',
radius: item.radius,
center: center,
avoidLabelOverlap: false,
startAngle: 90,
hoverAnimation: true,
label: {
show: false
},
labelLine: {
show: false
},
emphasis: {
scale: true,
scaleSize: 5
},
data: [
{
value: completeValue,
name: item.name,
itemStyle: {
color: new echarts.graphic.LinearGradient(
0, 0, 0, 1,
[
{ offset: 0, color: item.color },
{ offset: 1, color: item.color + '80' }
]
),
shadowBlur: 10,
shadowColor: item.color + '40'
}
},
{
value: remainingValue,
name: '未覆盖',
itemStyle: {
color: 'rgba(255, 255, 255, 0.08)'
},
tooltip: {
show: false
},
emphasis: {
disabled: true
}
}
]
});
});
// 计算平均覆盖率
const avgCoverage = Math.round(
infrastructureData.value.reduce((sum, d) => sum + d.value, 0) /
infrastructureData.value.length
);
myChart.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#3b82f6',
borderWidth: 1,
borderRadius: 6,
padding: 12,
textStyle: { color: '#333', fontSize: 13 },
formatter: (params: any) => {
if (params.name === '未覆盖') return '';
const remaining = 100 - params.value;
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #3b82f6; margin-bottom: 6px; border-bottom: 2px solid #3b82f6; padding-bottom: 4px;">${params.name}</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #10b981; border-radius: 50%;"></span>
已覆盖:<span style="color: #10b981; font-weight: 700; font-size: 15px;">${params.value}%</span>
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #ef4444; border-radius: 50%;"></span>
未覆盖:<span style="color: #ef4444; font-weight: 700; font-size: 15px;">${remaining}%</span>
</div>
</div>
`;
}
},
legend: {
data: infrastructureData.value.map(d => ({
name: d.name,
itemStyle: {
color: d.color
}
})),
orient: 'horizontal',
top: '3%',
left: 'center',
itemWidth: 12, // 图例方块宽度
itemHeight: 12, // 图例方块高度
itemGap: 20, // 图例项之间的间距
icon: 'circle', // 图例形状:'circle'圆形, 'rect'方形, 'roundRect'圆角方形
textStyle: {
color: '#333',
fontSize: 12,
padding: [0, 0, 0, 8] // 文字与图标的间距
}
},
graphic: [
{
type: 'text',
left: 'center',
top: '50%',
style: {
text: '总覆盖度',
fill: '#666',
fontSize: 13,
fontWeight: '500'
}
},
{
type: 'text',
left: 'center',
top: '57%',
style: {
text: `${avgCoverage}%`,
fill: '#3b82f6',
fontSize: 28,
fontWeight: 'bold'
}
}
],
series: seriesData,
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 100
});
};
// 监听数据变化,重新渲染图表
watch(infrastructureData, () => {
if (!isEmpty.value) {
nextTick(() => {
initInfrastructureChart();
});
}
}, { deep: true });
// 页面resize时重绘图表
watch(widthType, () => {
nextTick(() => {
if (!isEmpty.value) {
initInfrastructureChart();
}
});
});
// 挂载时初始化
onMounted(() => {
setTimeout(() => {
if (!isEmpty.value) {
initInfrastructureChart();
}
}, 100);
});
// 组件卸载时销毁图表实例
onUnmounted(() => {
if (chartInstances.value.infrastructureChart) {
chartInstances.value.infrastructureChart.dispose();
chartInstances.value.infrastructureChart = null;
}
});
</script>
<style scoped>
/* 基础设施覆盖率图表卡片样式 - 从主页面复制 */
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.25rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.index-module__NV_5cW__chartCard::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent);
transition: left 0.6s;
}
.index-module__NV_5cW__chartCard:hover::before {
left: 100%;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.index-module__NV_5cW__chartCard:hover .chartCard__title {
color: #3b82f6;
transform: scale(1.02);
transition: all 0.3s;
}
/* 标题样式 - 从主页面复制 */
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0;
text-align: center;
}
/* 容器样式 - 从主页面复制 */
.chartCard__container {
width: 100%;
height: 390px;
position: relative;
overflow: hidden;
}
/* 空数据显示样式 - 从主页面复制 */
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
/* 图表容器样式 */
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,436 @@
<template>
<div class="index-module__NV_5cW__chartCard list-card">
<h3 class="chartCard__title">明星开发者聚焦</h3>
<!-- 新增ref + 鼠标事件 -->
<div class="chartCard__container list-container developer-list-container" ref="scrollContainer2"
@mouseenter="pauseScroll(2)" @mouseleave="resumeScroll(2)">
<div class="num-empty" v-if="loading">加载中...</div>
<div class="project-list developer-list" v-else>
<div class="project-item developer-item" v-for="(item, index) in excellentProjects2025" :key="index">
<div class="dev-avatar">
<img :src="item.avatar" :alt="item.name" class="avatar-img" @error="handleImageError" />
</div>
<span class="project-rank dev-rank">{{ item.desc }}</span>
<div class="project-info dev-info">
<span class="project-name dev-name">{{ item.name }}</span>
<span class="project-desc dev-location">📍 {{ item.org }}</span>
</div>
<span class="project-org dev-followers">
<svg xmlns="http://www.w3.org/2000/svg" width="14" height="14" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2"></path>
<circle cx="9" cy="7" r="4"></circle>
</svg>
{{ formatFollowers(item.followers) }}
</span>
</div>
</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { excellentDeveloperPage2 } from '@/api/jyh/situation';
// 定义数据结构
interface DeveloperItem {
name: string;
description: string;
followers: number;
avatar: string;
org: string;
}
// 明星开发者数据
const excellentProjects2025 = ref<DeveloperItem[]>([]);
const loading = ref(true);
// 从API获取数据
const fetchDataFromApi = async () => {
try {
loading.value = true;
const response = await excellentDeveloperPage2();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200 && businessData.data) {
// 适配后端数据格式将description映射为desc
excellentProjects2025.value = businessData.data.map((item: any) => ({
...item,
desc: item.description
}));
} else {
console.error('获取明星开发者数据失败:', businessData?.msg || '未知错误');
// 设置默认数据
excellentProjects2025.value = [
{ name: '稚晖', desc: 'TOP 1', org: 'Shanghai', followers: 82738, avatar: 'https://github.com/peng-zhihui.png' },
{ name: 'Crypto Michael', desc: 'TOP 2', org: 'Beijing, China', followers: 37622, avatar: 'https://github.com/michaelliao.png' },
{ name: '代码家', desc: 'TOP 3', org: 'Beijing, China', followers: 24743, avatar: 'https://github.com/daimajia.png' },
{ name: 'Jackson Tian', desc: 'TOP 4', org: 'Hangzhou, China', followers: 21267, avatar: 'https://github.com/JacksonTian.png' },
{ name: '云风', desc: 'TOP 5', org: 'China', followers: 21192, avatar: 'https://github.com/cloudwu.png' },
{ name: 'Fengda Huang', desc: 'TOP 6', org: 'Shanghai / Hangzhou', followers: 20229, avatar: 'https://github.com/phodal.png' },
{ name: '程序员鱼皮', desc: 'TOP 7', org: 'Shanghai', followers: 18461, avatar: 'https://github.com/liyupi.png' },
{ name: 'Trinea', desc: 'TOP 8', org: 'HangZhou', followers: 17019, avatar: 'https://github.com/Trinea.png' },
{ name: 'lifesinger', desc: 'TOP 9', org: 'Hangzhou, China', followers: 15894, avatar: 'https://github.com/lifesinger.png' },
{ name: 'stormzhang', desc: 'TOP 10', org: 'Shanghai, China', followers: 15889, avatar: 'https://github.com/stormzhang.png' },
{ name: 'Wenli Zhang', desc: 'TOP 11', org: 'Shanghai, China', followers: 15680, avatar: 'https://github.com/Ovilia.png' },
{ name: 'Wei Wang', desc: 'TOP 12', org: 'Yokohama / China', followers: 15532, avatar: 'https://github.com/onevcat.png' },
{ name: 'astaxie', desc: 'TOP 13', org: 'Shanghai, China', followers: 15022, avatar: 'https://github.com/astaxie.png' },
{ name: '司徒正美', desc: 'TOP 14', org: 'China', followers: 14742, avatar: 'https://github.com/RubyLouvre.png' },
{ name: '云谦', desc: 'TOP 15', org: 'HangZhou, China', followers: 14466, avatar: 'https://github.com/sorrycc.png' },
{ name: 'cangdu', desc: 'TOP 16', org: 'Shanghai, China', followers: 13728, avatar: 'https://github.com/bailicangdu.png' },
{ name: 'Huang Haiguang', desc: 'TOP 17', org: 'Qingdao, China', followers: 13422, avatar: 'https://github.com/fengdu78.png' },
{ name: '花裤衩', desc: 'TOP 18', org: 'Shanghai, China', followers: 13236, avatar: 'https://github.com/PanJiaChen.png' },
];
}
} catch (error) {
console.error('获取明星开发者数据异常:', error);
// 设置默认数据
excellentProjects2025.value = [
{ name: '稚晖', desc: 'TOP 1', org: 'Shanghai', followers: 82738, avatar: 'https://github.com/peng-zhihui.png' },
{ name: 'Crypto Michael', desc: 'TOP 2', org: 'Beijing, China', followers: 37622, avatar: 'https://github.com/michaelliao.png' },
{ name: '代码家', desc: 'TOP 3', org: 'Beijing, China', followers: 24743, avatar: 'https://github.com/daimajia.png' },
{ name: 'Jackson Tian', desc: 'TOP 4', org: 'Hangzhou, China', followers: 21267, avatar: 'https://github.com/JacksonTian.png' },
{ name: '云风', desc: 'TOP 5', org: 'China', followers: 21192, avatar: 'https://github.com/cloudwu.png' },
{ name: 'Fengda Huang', desc: 'TOP 6', org: 'Shanghai / Hangzhou', followers: 20229, avatar: 'https://github.com/phodal.png' },
{ name: '程序员鱼皮', desc: 'TOP 7', org: 'Shanghai', followers: 18461, avatar: 'https://github.com/liyupi.png' },
{ name: 'Trinea', desc: 'TOP 8', org: 'HangZhou', followers: 17019, avatar: 'https://github.com/Trinea.png' },
{ name: 'lifesinger', desc: 'TOP 9', org: 'Hangzhou, China', followers: 15894, avatar: 'https://github.com/lifesinger.png' },
{ name: 'stormzhang', desc: 'TOP 10', org: 'Shanghai, China', followers: 15889, avatar: 'https://github.com/stormzhang.png' },
{ name: 'Wenli Zhang', desc: 'TOP 11', org: 'Shanghai, China', followers: 15680, avatar: 'https://github.com/Ovilia.png' },
{ name: 'Wei Wang', desc: 'TOP 12', org: 'Yokohama / China', followers: 15532, avatar: 'https://github.com/onevcat.png' },
{ name: 'astaxie', desc: 'TOP 13', org: 'Shanghai, China', followers: 15022, avatar: 'https://github.com/astaxie.png' },
{ name: '司徒正美', desc: 'TOP 14', org: 'China', followers: 14742, avatar: 'https://github.com/RubyLouvre.png' },
{ name: '云谦', desc: 'TOP 15', org: 'HangZhou, China', followers: 14466, avatar: 'https://github.com/sorrycc.png' },
{ name: 'cangdu', desc: 'TOP 16', org: 'Shanghai, China', followers: 13728, avatar: 'https://github.com/bailicangdu.png' },
{ name: 'Huang Haiguang', desc: 'TOP 17', org: 'Qingdao, China', followers: 13422, avatar: 'https://github.com/fengdu78.png' },
{ name: '花裤衩', desc: 'TOP 18', org: 'Shanghai, China', followers: 13236, avatar: 'https://github.com/PanJiaChen.png' },
];
} finally {
loading.value = false;
}
};
// 滚动相关变量
const scrollContainer2 = ref<HTMLDivElement | null>(null);
const scrollTimers: { [key: number]: NodeJS.Timeout | null } = { 2: null };
// 3. 滚动配置
const scrollStep = 1; // 每次滚动的步长(像素,越小越流畅)
const scrollInterval = 20; // 滚动间隔(毫秒,越小越流畅)
// 4. 通用自动滚动函数
const autoScroll = (containerKey: number) => {
// 获取对应容器
const container = containerKey === 2 ? scrollContainer2.value : null;
if (!container) return;
// 获取列表内容元素project-list
const projectList = container.querySelector('.project-list') as HTMLDivElement;
if (!projectList) return;
// 若内容高度 ≤ 容器高度,不执行滚动
if (projectList.scrollHeight <= container.clientHeight) {
// 清除现有定时器
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
return;
}
// 清除现有定时器(避免重复)
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
}
// 启动定时器滚动
scrollTimers[containerKey] = setInterval(() => {
// 向下滚动步长
container.scrollTop += scrollStep;
// 滚动到底部时重置为0循环滚动
// 加1是为了兼容像素精度问题
if (projectList.scrollHeight - container.scrollTop <= container.clientHeight + 1) {
container.scrollTop = 0; // 直接重置也可以用平滑滚动container.scrollTo({ top: 0, behavior: 'smooth' })
}
}, scrollInterval);
};
// 5. 暂停滚动
const pauseScroll = (containerKey: number) => {
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
};
// 6. 恢复滚动
const resumeScroll = (containerKey: number) => {
autoScroll(containerKey);
};
// 7. 组件卸载时清除所有定时器
onUnmounted(() => {
if (scrollTimers[2]) {
clearInterval(scrollTimers[2]!);
scrollTimers[2] = null;
}
});
// 8. 格式化粉丝数82738 → 82.7K
const formatFollowers = (num: number) => {
if (num >= 1000) {
return (num / 1000).toFixed(1) + 'K';
}
return num.toString();
};
// 9. 图片加载失败处理
const handleImageError = (e: Event) => {
const target = e.target as HTMLImageElement;
const name = target.alt;
target.style.display = 'none';
if (target.parentElement) {
target.parentElement.innerHTML = `<span class="avatar-text">${name.slice(0, 1)}</span>`;
}
};
// 组件挂载时获取数据
onMounted(() => {
fetchDataFromApi();
// 数据加载完成后启动自动滚动
setTimeout(() => {
autoScroll(2);
}, 100);
});
</script>
<style scoped>
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6; /* rgba(255, 255, 255, 0.9) */
border: 1px solid #ffffff4d; /* rgba(255, 255, 255, 0.3) */
border-radius: 1rem; /* 16px */
padding: 1.5rem; /* 24px */
box-shadow: 0 4px 16px #00000014; /* subtle shadow */
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
/* 悬停效果开始 */
.index-module__NV_5cW__chartCard::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent);
transition: left 0.6s;
}
.index-module__NV_5cW__chartCard:hover::before {
left: 100%;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.index-module__NV_5cW__chartCard:hover .chartCard__title {
color: #3b82f6;
transform: scale(1.02);
transition: all 0.3s;
}
.index-module__NV_5cW__chartCard.list-card {
padding: 1.5rem; /* 24px */
}
.chartCard__title {
color: #333;
font-size: 1.1rem; /* 17.6px */
font-weight: 600;
margin: 0;
text-align: center;
width: 100%;
}
.chartCard__container.list-container {
width: 100%;
height: 380px;
position: relative;
overflow: hidden; /* 外层容器隐藏溢出 */
}
.list-container {
height: auto;
max-height: 300px; /* 限制列表高度,超出滚动 */
overflow-y: auto; /* 允许垂直滚动 */
padding: 0 0.5rem; /* 8px */
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
.list-container::-webkit-scrollbar {
display: none; /* Chrome/Safari/Opera */
}
/* 隐藏webkit内核浏览器的滚动条 */
.chartCard__container.list-container::-webkit-scrollbar {
display: none;
}
/* 空数据显示样式 */
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
.developer-list-container {
max-height: 450px;
margin-top: 20px;
}
.project-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.project-item.developer-item {
display: flex;
align-items: center;
gap: 0.75rem; /* 12px */
padding: 0.75rem 1rem; /* 12px 16px */
background: linear-gradient(135deg, #f8f9fa 0%, #ffffff 100%);
border-left: 3px solid #3b82f6;
border-radius: 0.5rem; /* 8px */
transition: all 0.2s;
cursor: pointer;
}
.project-item.developer-item:hover {
background: linear-gradient(135deg, #eff6ff 0%, #f0f4ff 100%);
border-left-color: #8b5cf6;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.15);
transform: translateX(2px);
}
.dev-avatar {
width: 42px;
height: 42px;
border-radius: 50%;
background: linear-gradient(135deg, #3b82f6 0%, #8b5cf6 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(59, 130, 246, 0.3);
overflow: hidden;
flex-shrink: 0;
}
.avatar-img {
width: 100%;
height: 100%;
object-fit: cover;
}
.avatar-text {
font-size: 16px;
font-weight: bold;
color: #fff;
}
.project-rank.dev-rank {
width: auto;
height: auto;
border-radius: 12px;
background: linear-gradient(135deg, #ec4899 0%, #f59e0b 100%);
padding: 3px 10px;
font-size: 11px;
font-weight: 700;
box-shadow: 0 2px 6px rgba(236, 72, 153, 0.3);
flex-shrink: 0;
min-width: auto;
}
.project-info.dev-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.3rem; /* 5px */
}
.project-name.dev-name {
font-size: 15px; /* 15px */
font-weight: 700; /* 更粗的字体 */
color: #1e293b; /* 深灰色 */
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.project-desc.dev-location {
font-size: 11px; /* 11px */
color: #64748b; /* 灰蓝色 */
font-style: normal; /* 取消斜体 */
display: block;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.project-org.dev-followers {
display: flex;
align-items: center;
gap: 4px;
padding: 4px 10px;
border-radius: 12px;
background: rgba(59, 130, 246, 0.1);
border: 1px solid rgba(59, 130, 246, 0.2);
color: #3b82f6;
font-size: 12px;
font-weight: 600;
white-space: nowrap;
max-width: none;
flex-shrink: 0;
}
.project-org.dev-followers svg {
flex-shrink: 0;
}
.scroll-indicator {
position: absolute;
top: 0;
right: 0;
display: flex;
align-items: center;
gap: 0.25rem; /* 4px */
padding: 0.25rem 0.5rem; /* 4px 8px */
background: rgba(59, 130, 246, 0.1);
border-radius: 0 0 0 0.5rem; /* 与原文件一致 */
font-size: 0.7rem; /* 与原文件一致 */
color: #3b82f6;
z-index: 10;
animation: bounce 2s ease-in-out infinite; /* 添加动画 */
}
@keyframes bounce {
0%, 100% { transform: translateY(0); }
50% { transform: translateY(-2px); }
}
</style>

View File

@@ -0,0 +1,206 @@
<template>
<div class="stats-glass-bar">
<div class="stat-node">
<span class="stat-value">{{ activeDevelopers }}</span>
<span class="stat-desc">活跃开发者</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ openSourceProjects }}</span>
<span class="stat-desc">开源项目</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ openSourceCompanies }}</span>
<span class="stat-desc">开源企业</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ industryValuation }}</span>
<span class="stat-desc">产业估值</span>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, onMounted } from 'vue';
import { overviewDataPage2 } from '@/api/jyh/situation';
// 定义响应式数据
const activeDevelopers = ref<string>('加载中...');
const openSourceProjects = ref<string>('加载中...');
const openSourceCompanies = ref<string>('加载中...');
const industryValuation = ref<string>('加载中...');
// 加载状态
const loading = ref(true);
// 格式化数字的工具函数
const formatNumber = (num: number, unit: string = ''): string => {
if (num >= 100000000) {
return Math.floor(num / 100000000) + '亿';
} else if (num >= 10000) {
return Math.floor(num / 10000) + '万';
} else if (num >= 1000) {
return Math.floor(num / 1000) + '千';
}
return num.toString();
};
// 格式化估值的工具函数
const formatValuation = (num: number): string => {
if (num >= 100000000) {
return Math.floor(num / 100000000) + '亿';
} else if (num >= 10000) {
return Math.floor(num / 10000) + '万';
}
return num.toString() + '亿';
};
// 获取统计数据
const fetchStats = async () => {
try {
loading.value = true;
const response = await overviewDataPage2();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200 && businessData.data) {
const { developer, project, company, valuation } = businessData.data;
// 格式化数据显示
activeDevelopers.value = `${formatNumber(developer)}+`;
openSourceProjects.value = `${formatNumber(project)}+`;
openSourceCompanies.value = `${formatNumber(company)}+`;
industryValuation.value = `${formatValuation(valuation)}`;
} else {
console.error('获取统计数据失败:', businessData?.msg || '未知错误');
// 设置默认值
activeDevelopers.value = '200万+';
openSourceProjects.value = '300万+';
openSourceCompanies.value = '500+';
industryValuation.value = '2800亿';
}
} catch (error) {
console.error('获取统计数据异常:', error);
// 设置默认值
activeDevelopers.value = '200万+';
openSourceProjects.value = '300万+';
openSourceCompanies.value = '500+';
industryValuation.value = '2800亿';
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchStats();
});
</script>
<style lang="scss" scoped>
.stats-glass-bar {
display: flex;
align-items: center;
justify-content: space-between;
/* 布局控制:撑满容器并增加内边距 */
width: 100%;
padding: 2rem 4rem;
//margin: 2rem 0;
/* 深色玻璃拟态背景 */
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
/* 极细微的边框勾勒 */
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.08);
/* 外阴影与内发光,增加悬浮感 */
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3),
inset 0 0 20px rgba(255, 255, 255, 0.02);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
border-color: rgba(56, 189, 248, 0.4);
background: rgba(255, 255, 255, 0.04);
transform: translateY(-2px);
}
}
.stat-node {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
.stat-value {
text-shadow: 0 0 25px rgba(56, 189, 248, 0.8);
}
}
}
.stat-value {
/* 科技感亮蓝渐变 */
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 2.25rem;
font-weight: 800;
background: linear-gradient(180deg, #fff 0%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 基础发光效果 */
filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.3));
letter-spacing: -1px;
}
.stat-desc {
/* 在黑底上使用半透明白色,更有高级感 */
color: rgba(255, 255, 255, 0.6);
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.stat-separator {
/* 渐隐分割线 */
width: 1px;
height: 3rem;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0)
);
margin: 0 1rem;
}
/* 响应式适配 */
@media (max-width: 1024px) {
.stats-glass-bar {
padding: 2rem;
}
.stat-value {
font-size: 1.75rem;
}
}
@media (max-width: 768px) {
.stats-glass-bar {
flex-direction: column;
gap: 2rem;
}
.stat-separator {
width: 50%;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255,255,255,0.2), transparent);
}
}
</style>

View File

@@ -0,0 +1,425 @@
<template>
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">高校开源社团/俱乐部数量</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty || loading">加载中...</div>
<div v-else id="communityChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, watch, onUnmounted, computed } from 'vue';
import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
import { universityClubsPage2 } from '@/api/jyh/situation';
// 响应式数据存储
const universityClubData = ref<{ university: string; club: string }[]>([]);
const loading = ref(true);
// 从API获取数据
const fetchDataFromApi = async () => {
try {
loading.value = true;
const response = await universityClubsPage2();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200 && businessData.data) {
universityClubData.value = businessData.data;
} else {
console.error('获取高校开源社团数据失败:', businessData?.msg || '未知错误');
// 设置默认数据
universityClubData.value = [
// 华东地区
{ university: '上海交通大学', club: 'SJTU-LUG (Linux User Group)' },
{ university: '上海交通大学', club: '开放原子开源俱乐部' },
{ university: '复旦大学', club: 'Fudan LUG' },
{ university: '复旦大学', club: '开放原子开源俱乐部' },
{ university: '浙江大学', club: 'ZJU-LUG' },
{ university: '浙江大学', club: 'AAA (Azure Availability Association)' },
{ university: '浙江大学', club: '开放原子开源俱乐部' },
{ university: '南京大学', club: 'NJU-LUG' },
{ university: '南京大学', club: 'eScience 协会' },
{ university: '南京大学', club: '开放原子开源俱乐部' },
{ university: '中国科学技术大学', club: 'USTC-LUG' },
{ university: '中国科学技术大学', club: 'VLAB' },
{ university: '同济大学', club: 'Tongji LUG' },
{ university: '同济大学', club: '开放原子开源俱乐部' },
{ university: '华东师范大学', club: 'ECNU LUG' },
// 华北地区
{ university: '清华大学', club: 'TUNA (清华大学学生网络与开源软件协会)' },
{ university: '清华大学', club: '开放原子开源俱乐部' },
{ university: '北京大学', club: 'PKU-LUG' },
{ university: '北京大学', club: '开源软件协会' },
{ university: '中国人民大学', club: 'RUC 开源社' },
{ university: '北京航空航天大学', club: 'BUAA-LUG' },
{ university: '北京理工大学', club: 'BIT-LUG' },
{ university: '南开大学', club: 'NKU-LUG' },
{ university: '天津大学', club: 'TJU-LUG' },
// 华南地区
{ university: '中山大学', club: 'SYSU-LUG' },
{ university: '华南理工大学', club: 'SCUT-LUG' },
{ university: '暨南大学', club: 'JNU-LUG' },
{ university: '深圳大学', club: 'SZU-LUG' },
{ university: '华南师范大学', club: 'SCNU-LUG' },
// 华中地区
{ university: '华中科技大学', club: 'HUST-LUG' },
{ university: '华中科技大学', club: '开放原子开源俱乐部' },
{ university: '武汉大学', club: 'WHU-LUG' },
{ university: '武汉大学', club: '珞珈码农社' },
{ university: '中南大学', club: 'CSU-LUG' },
{ university: '山东大学', club: 'SDU-LUG' },
{ university: '厦门大学', club: 'XMU-LUG' }
];
}
} catch (error) {
console.error('获取高校开源社团数据异常:', error);
// 设置默认数据
universityClubData.value = [
// 华东地区
{ university: '上海交通大学', club: 'SJTU-LUG (Linux User Group)' },
{ university: '上海交通大学', club: '开放原子开源俱乐部' },
{ university: '复旦大学', club: 'Fudan LUG' },
{ university: '复旦大学', club: '开放原子开源俱乐部' },
{ university: '浙江大学', club: 'ZJU-LUG' },
{ university: '浙江大学', club: 'AAA (Azure Availability Association)' },
{ university: '浙江大学', club: '开放原子开源俱乐部' },
{ university: '南京大学', club: 'NJU-LUG' },
{ university: '南京大学', club: 'eScience 协会' },
{ university: '南京大学', club: '开放原子开源俱乐部' },
{ university: '中国科学技术大学', club: 'USTC-LUG' },
{ university: '中国科学技术大学', club: 'VLAB' },
{ university: '同济大学', club: 'Tongji LUG' },
{ university: '同济大学', club: '开放原子开源俱乐部' },
{ university: '华东师范大学', club: 'ECNU LUG' },
// 华北地区
{ university: '清华大学', club: 'TUNA (清华大学学生网络与开源软件协会)' },
{ university: '清华大学', club: '开放原子开源俱乐部' },
{ university: '北京大学', club: 'PKU-LUG' },
{ university: '北京大学', club: '开源软件协会' },
{ university: '中国人民大学', club: 'RUC 开源社' },
{ university: '北京航空航天大学', club: 'BUAA-LUG' },
{ university: '北京理工大学', club: 'BIT-LUG' },
{ university: '南开大学', club: 'NKU-LUG' },
{ university: '天津大学', club: 'TJU-LUG' },
// 华南地区
{ university: '中山大学', club: 'SYSU-LUG' },
{ university: '华南理工大学', club: 'SCUT-LUG' },
{ university: '暨南大学', club: 'JNU-LUG' },
{ university: '深圳大学', club: 'SZU-LUG' },
{ university: '华南师范大学', club: 'SCNU-LUG' },
// 华中地区
{ university: '华中科技大学', club: 'HUST-LUG' },
{ university: '华中科技大学', club: '开放原子开源俱乐部' },
{ university: '武汉大学', club: 'WHU-LUG' },
{ university: '武汉大学', club: '珞珈码农社' },
{ university: '中南大学', club: 'CSU-LUG' },
{ university: '山东大学', club: 'SDU-LUG' },
{ university: '厦门大学', club: 'XMU-LUG' }
];
} finally {
loading.value = false;
}
};
// 组件挂载时获取数据
onMounted(() => {
fetchDataFromApi();
});
// 页面尺寸响应式
const { widthType } = usePageResize();
// 处理高校社团数据
const processUniversityClubsData = () => {
const counts: { [key: string]: { count: number; clubs: string[] } } = {};
universityClubData.value.forEach(item => {
if (!counts[item.university]) {
counts[item.university] = { count: 0, clubs: [] };
}
counts[item.university].count++;
counts[item.university].clubs.push(item.club);
});
// 转换为数组并排序
const sorted = Object.entries(counts)
.map(([name, data]) => ({
name,
value: data.count,
clubs: data.clubs
}))
.sort((a, b) => b.value - a.value);
return {
universities: sorted.slice(0, 10).map(item => item.name), // 取前10名避免X轴过挤
values: sorted.slice(0, 10).map(item => item.value),
details: sorted.slice(0, 10).reduce((acc, item) => {
acc[item.name] = item.clubs;
return acc;
}, {} as { [key: string]: string[] })
};
};
const communityData = computed(() => processUniversityClubsData());
// 空数据标识
const isEmpty = computed(() => {
return communityData.value.universities.length === 0;
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
communityChart: null
});
// 初始化高校开源社团/俱乐部数量图表(纵向柱状图)
const initCommunityChart = () => {
const el = document.getElementById('communityChart');
if (!el) return;
if (chartInstances.value.communityChart) {
chartInstances.value.communityChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.communityChart = myChart;
const data = communityData.value;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(255, 255, 255, 0.95)',
borderColor: '#3b82f6',
borderWidth: 1,
borderRadius: 6,
padding: 12,
textStyle: { color: '#333', fontSize: 13 },
formatter: (params: any) => {
const name = params[0].name;
const value = params[0].value;
const clubs = data.details[name] || [];
const clubsHtml = clubs.map(c => `${c}`).join('<br/>');
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #3b82f6; margin-bottom: 6px; border-bottom: 2px solid #3b82f6; padding-bottom: 4px;">${name}</div>
<div>社团数量: <span style="color: #10b981; font-weight: 700; font-size: 16px;">${value}</span> 个</div>
<div style="margin-top: 8px; border-top: 1px solid rgba(0,0,0,0.1); padding-top: 6px; font-size: 11px; color: #666; max-height: 150px; overflow-y: auto;">
${clubsHtml}
</div>
</div>
`;
}
},
grid: {
left: '10%',
right: '10%',
bottom: '5%',
top: '20%',
containLabel: true
},
xAxis: {
type: 'category',
data: data.universities,
axisLabel: {
fontSize: 10,
rotate: 45,
interval: 0,
color: '#666',
margin: 15
},
axisTick: {
alignWithLabel: true,
length: 5
},
axisLine: {
lineStyle: { color: '#e5e7eb', width: 2 }
}
},
yAxis: {
type: 'value',
name: '社团数量',
nameTextStyle: { fontSize: 12, color: '#666' },
axisLabel: { fontSize: 12, color: '#666' },
minInterval: 1,
splitLine: {
lineStyle: { color: 'rgba(0, 0, 0, 0.06)', type: 'dashed' }
}
},
series: [
{
name: '社团数量',
type: 'bar',
data: data.values,
barWidth: '60%',
barCategoryGap: '30%',
itemStyle: {
color: (params: any) => {
// 为每个柱状图提供唯一颜色
const colors = [
['#3b82f6', '#60a5fa'], // 蓝色
['#8b5cf6', '#a78bfa'], // 紫色
['#ec4899', '#f472b6'], // 粉色
['#10b981', '#34d399'], // 绿色
['#f59e0b', '#fbbf24'], // 橙色
['#ef4444', '#fca5a5'], // 红色
['#06b6d4', '#7dd3fc'], // 青色
['#84cc16', '#a3e635'], // 黄绿色
['#f97316', '#fb923c'], // 橙红色
['#6366f1', '#818cf8'], // 靛蓝色
['#14b8a6', '#2dd4bf'], // 翠绿色
['#f43f5e', '#f9a8d4'], // 玫红
['#a855f7', '#c084fc'], // 紫罗兰
['#d946ef', '#e879f9'], // 梅花色
['#0ea5e9', '#38bdf8'] // 天蓝色
];
const colorPair = colors[params.dataIndex % colors.length];
return new echarts.graphic.LinearGradient(0, 1, 0, 0, [
{ offset: 0, color: colorPair[1] },
{ offset: 1, color: colorPair[0] }
]);
},
borderRadius: [4, 4, 0, 0],
shadowBlur: 8,
shadowColor: 'rgba(59, 130, 246, 0.3)'
},
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 11,
color: '#3b82f6',
fontWeight: 'bold'
},
emphasis: {
itemStyle: {
shadowBlur: 15,
shadowColor: 'rgba(59, 130, 246, 0.6)'
}
}
}
],
animationDuration: 1000,
animationEasing: 'cubicOut',
animationDelay: (idx: number) => idx * 50
});
};
// 监听数据变化,重新渲染图表
watch(universityClubData, () => {
if (!isEmpty.value) {
nextTick(() => {
initCommunityChart();
});
}
}, { deep: true });
// 页面resize时重绘图表
watch(widthType, () => {
nextTick(() => {
if (!isEmpty.value) {
initCommunityChart();
}
});
});
// 挂载时初始化
onMounted(() => {
setTimeout(() => {
if (!isEmpty.value) {
initCommunityChart();
}
}, 100);
});
// 组件卸载时销毁图表实例
onUnmounted(() => {
if (chartInstances.value.communityChart) {
chartInstances.value.communityChart.dispose();
chartInstances.value.communityChart = null;
}
});
</script>
<style scoped>
/* 高校开源社团/俱乐部数量图表卡片样式 - 从主页面复制 */
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.25rem;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
}
.index-module__NV_5cW__chartCard::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(59, 130, 246, 0.05), transparent);
transition: left 0.6s;
}
.index-module__NV_5cW__chartCard:hover::before {
left: 100%;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.index-module__NV_5cW__chartCard:hover .chartCard__title {
color: #3b82f6;
transform: scale(1.02);
transition: all 0.3s;
}
/* 标题样式 - 从主页面复制 */
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0;
text-align: center;
}
/* 容器样式 - 从主页面复制 */
.chartCard__container {
width: 100%;
height: 390px;
position: relative;
overflow: hidden;
}
/* 空数据显示样式 - 从主页面复制 */
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
/* 图表容器样式 */
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,446 @@
<template>
<div class="index-module__NV_5cW__vitalitySection">
<h3 class="vitality__title">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"
stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<path d="M22 12h-4l-3 9L9 3l-3 9H2"></path>
</svg>
<span>开发者活力波形图</span>
</h3>
<div class="vitality__container">
<div class="num-empty" v-if="loading">加载中...</div>
<div id="vitalityChart" class="chart" v-else></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { onMounted, ref, nextTick, watch } from 'vue';
import * as echarts from 'echarts';
import { usePageResize } from '@/utils/hooks/usePageResize';
import { vitalityPage2 } from '@/api/jyh/situation';
// 页面尺寸响应式
const { widthType } = usePageResize();
// 定义数据结构
interface VitalityItem {
timeData: string;
total: number;
}
// 开发者活力数据
const vitalityData = ref({
timeData: [] as string[],
total: [] as number[],
new: [] as number[] // 默认数据因为API只返回timeData和total
});
const loading = ref(true);
// 从API获取数据
const fetchDataFromApi = async () => {
try {
loading.value = true;
const response = await vitalityPage2();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200 && businessData.data) {
// 提取API返回的数据
const apiTimeData: string[] = [];
const apiTotalData: number[] = [];
businessData.data.forEach((item: VitalityItem) => {
apiTimeData.push(item.timeData);
apiTotalData.push(item.total);
});
// 计算年度新增数据(相邻年份的差值)
const apiNewData: number[] = [];
for (let i = 0; i < apiTotalData.length; i++) {
if (i === 0) {
apiNewData.push(apiTotalData[i]); // 第一年的新增就是当年总数
} else {
apiNewData.push(apiTotalData[i] - apiTotalData[i - 1]); // 后续年份为差值
}
}
vitalityData.value = {
timeData: apiTimeData,
total: apiTotalData,
new: apiNewData
};
} else {
console.error('获取开发者活力数据失败:', businessData?.msg || '未知错误');
// 设置默认数据
vitalityData.value = {
timeData: ["2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024"],
total: [20, 50, 100, 200, 300, 400, 600, 800, 1000, 1200, 1350],
new: [20, 30, 50, 100, 100, 100, 200, 200, 200, 200, 150]
};
}
} catch (error) {
console.error('获取开发者活力数据异常:', error);
// 设置默认数据
vitalityData.value = {
timeData: ["2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024"],
total: [20, 50, 100, 200, 300, 400, 600, 800, 1000, 1200, 1350],
new: [20, 30, 50, 100, 100, 100, 200, 200, 200, 200, 150]
};
} finally {
loading.value = false;
}
};
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
vitalityChart: null
});
// 初始化开发者活力波形图
const initVitalityChart = () => {
const el = document.getElementById('vitalityChart');
if (!el) return;
if (chartInstances.value.vitalityChart) {
chartInstances.value.vitalityChart.dispose();
}
const myChart = echarts.init(el);
chartInstances.value.vitalityChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999',
width: 1,
type: 'dashed'
},
lineStyle: {
color: '#3b82f6',
width: 1,
type: 'dashed'
}
},
backgroundColor: 'rgba(255, 255, 255, 0.98)',
borderColor: '#10b981',
borderWidth: 2,
borderRadius: 8,
padding: 16,
textStyle: { color: '#333', fontSize: 14 },
extraCssText: 'box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);',
formatter: (params: any) => {
if (!Array.isArray(params) || params.length === 0) {
return '<div>暂无数据</div>';
}
const totalItem = params.find((item: any) => item.seriesName === '累计开发者');
const newItem = params.find((item: any) => item.seriesName === '年度新增');
const totalValue = totalItem?.value || 0;
const newValue = newItem?.value || 0;
const time = totalItem?.axisValue || newItem?.axisValue || '未知时间';
// 计算增长率
const prevIndex = vitalityData.value.timeData.indexOf(time) - 1;
let growthRate = 0;
if (prevIndex >= 0) {
const prevTotal = vitalityData.value.total[prevIndex];
growthRate = ((totalValue - prevTotal) / prevTotal * 100).toFixed(1);
}
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #10b981; font-size: 16px; margin-bottom: 8px; border-bottom: 2px solid #10b981; padding-bottom: 4px;">📅 ${time}年</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #10b981; border-radius: 50%;"></span>
累计开发者:<span style="color: #10b981; font-weight: 700; font-size: 15px;">${totalValue}</span> 万人
</div>
<div style="display: flex; align-items: center; gap: 8px;">
<span style="display: inline-block; width: 8px; height: 8px; background: #f59e0b; border-radius: 50%;"></span>
年度新增:<span style="color: #f59e0b; font-weight: 700; font-size: 15px;">${newValue}</span> 万人
</div>
${growthRate > 0 ? `<div style="margin-top: 6px; padding: 4px 8px; background: rgba(16, 185, 129, 0.1); border-radius: 4px; color: #10b981; font-size: 12px;">📈 同比增长 ${growthRate}%</div>` : ''}
</div>
`;
}
},
legend: {
left: 'center',
top: '0%',
itemWidth: 16,
itemHeight: 16,
itemGap: 20,
textStyle: { color: '#333', fontSize: 14, fontWeight: '600' },
data: ['累计开发者', '年度新增'],
icon: 'roundRect',
borderRadius: 4
},
grid: {
top: '18%',
left: '3%',
right: '4%',
bottom: '8%',
containLabel: true
},
xAxis: {
type: 'category',
boundaryGap: false,
axisTick: { show: false },
axisLine: {
lineStyle: { color: '#e5e7eb', width: 2 },
onZero: false
},
axisLabel: {
color: '#666',
fontSize: 13,
fontWeight: '500',
interval: 0,
margin: 12
},
data: vitalityData.value.timeData
},
yAxis: {
type: 'value',
name: '开发者数量(万人)',
nameTextStyle: { color: '#666', fontSize: 12 },
axisLabel: { color: '#666', fontSize: 12 },
splitLine: {
show: true,
lineStyle: { color: 'rgba(0, 0, 0, 0.06)', type: 'dashed' }
}
},
series: [
{
name: '累计开发者',
type: 'line',
smooth: true,
symbol: 'circle',
symbolSize: 8,
lineStyle: { color: '#10b981', width: 3 },
itemStyle: {
color: '#10b981',
borderWidth: 2,
borderColor: '#fff'
},
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: 'rgba(16, 185, 129, 0.4)' },
{ offset: 1, color: 'rgba(16, 185, 129, 0.05)' }
]),
shadowBlur: 10,
shadowColor: 'rgba(16, 185, 129, 0.2)'
},
data: vitalityData.value.total,
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(16, 185, 129, 0.6)',
scale: 1.2
},
label: {
show: true,
formatter: '{c} 万',
position: 'top',
color: '#10b981',
fontSize: 13,
fontWeight: 'bold'
}
},
markPoint: {
data: [
{
type: 'max', name: '最大值',
itemStyle: { color: '#10b981' },
label: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold'
}
}
],
symbolSize: 60
},
markLine: {
silent: true,
lineStyle: {
color: '#10b981',
type: 'dashed',
width: 1
},
label: {
color: '#10b981',
fontSize: 11
},
data: [{ type: 'average', name: '平均值' }]
}
},
{
name: '年度新增',
type: 'bar',
barWidth: '40%',
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#f59e0b' },
{ offset: 1, color: '#fbbf24' }
]),
borderRadius: [4, 4, 0, 0],
shadowBlur: 8,
shadowColor: 'rgba(245, 158, 11, 0.3)'
},
data: vitalityData.value.new,
emphasis: {
itemStyle: {
shadowBlur: 20,
shadowColor: 'rgba(245, 158, 11, 0.6)'
},
label: {
show: true,
formatter: '{c} 万',
position: 'top',
color: '#f59e0b',
fontSize: 12,
fontWeight: 'bold'
}
},
markPoint: {
data: [
{
type: 'max', name: '峰值',
itemStyle: { color: '#f59e0b' },
label: {
color: '#fff',
fontSize: 12,
fontWeight: 'bold'
}
}
],
symbolSize: 60
},
animationDelay: (idx: number) => idx * 50
}
]
});
};
// 页面resize时重绘图表
watch(widthType, () => {
nextTick(() => initVitalityChart());
});
// 组件挂载时获取数据
onMounted(() => {
fetchDataFromApi();
// 数据加载完成后初始化图表
setTimeout(() => {
initVitalityChart();
}, 100);
});
</script>
<style lang="scss" scoped>
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
.index-module__NV_5cW__vitalitySection {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1.5rem;
margin-bottom: 2rem;
padding: 2rem;
box-shadow: 0 8px 24px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__vitalitySection:hover {
border-color: #3b82f64d;
transform: translateY(-2px);
box-shadow: 0 12px 32px #0000001f;
}
.vitality__title {
color: #333;
font-size: 1.5rem;
font-weight: 600;
margin: 0 0 1.5rem 0;
display: flex;
align-items: center;
justify-content: center;
gap: 0.75rem;
position: relative;
}
.vitality__title::before {
content: '';
position: absolute;
bottom: -8px;
left: 50%;
transform: translateX(-50%);
width: 60px;
height: 3px;
background: linear-gradient(90deg, #10b981, #f59e0b);
border-radius: 2px;
}
.vitality__title svg {
color: #10b981;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%,
100% {
transform: scale(1);
}
50% {
transform: scale(1.1);
}
}
.vitality__container {
width: 100%;
height: 400px;
position: relative;
background: linear-gradient(135deg,
rgba(16, 185, 129, 0.02) 0%,
rgba(245, 158, 11, 0.02) 100%);
border-radius: 0.75rem;
padding: 1rem;
}
.chart {
width: 100%;
height: 100%;
}
/* 响应式适配 */
@media (max-width: 768px) {
.vitality__container {
height: 300px;
}
.vitality__title {
font-size: 1.25rem;
}
}
@media (max-width: 480px) {
.vitality__container {
height: 250px;
}
.vitality__title {
font-size: 1.2rem;
}
}
</style>

View File

@@ -0,0 +1,476 @@
<template>
<div class="map-container">
<div id="talentMapChart" class="chart" v-if="!loading"></div>
<div class="num-empty" v-else>加载中...</div>
<!-- 右侧排行榜 -->
<div class="map-rank-panel">
<div class="panel-title">🏆 开源人才数量排行 TOP 5</div>
<div class="rank-list">
<div class="rank-item" v-for="(item, index) in topRegions" :key="index">
<span :class="['rank-num', 'rank-' + (index + 1)]">{{ index + 1 }}</span>
<span class="rank-name">{{ item.name }}</span>
<span class="rank-score">{{ item.score }} 万人</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted, ref, onBeforeUnmount, computed} from 'vue';
// 初始化中国开源人才密度地图
import * as echarts from "echarts";
import geoJson from '../json/china.json';
import { talentMapDataPage2 } from '@/api/jyh/situation';
const talentMapChart = ref()
// 定义数据结构
interface TalentMapItem {
name: string;
density: number;
total: number;
}
// 中国开源人才密度地图数据
const talentMapData = ref<any[]>([]);
const loading = ref(true);
// 从API获取数据
const fetchDataFromApi = async () => {
try {
loading.value = true;
const response = await talentMapDataPage2();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200 && businessData.data) {
// 转换API返回的数据格式为图表所需格式
talentMapData.value = businessData.data.map((item: TalentMapItem) => {
// 查找省份坐标,如果找不到则使用默认坐标
let coordinates = [104.5, 37.5]; // 默认为中国中心坐标
// 匹配省份坐标
const provinceCoords: Record<string, [number, number]> = {
'北京市': [116.4074, 39.9042],
'上海市': [121.4737, 31.2304],
'浙江省': [120.1551, 30.2794],
'广东省': [113.2644, 23.1291],
'台湾省': [121.5090, 25.0443],
'四川省': [104.0657, 30.6595],
'江苏省': [118.7780, 32.0472],
'湖北省': [114.3053, 30.5928],
'福建省': [119.3062, 26.0747],
'陕西省': [108.9480, 34.2632],
'重庆市': [106.5049, 29.5332],
'山东省': [120.3331, 36.0670],
'湖南省': [112.9822, 28.1944],
'安徽省': [117.2830, 31.8611],
'辽宁省': [123.4321, 41.8007],
'云南省': [102.7123, 25.0406],
'天津市': [117.1901, 39.1258],
'河南省': [113.6654, 34.7579],
'吉林省': [125.3245, 43.8171],
'江西省': [115.8581, 28.6820],
'广西壮族自治区': [108.3200, 22.8241],
'黑龙江省': [126.5349, 45.8038],
'澳门特别行政区': [113.5490, 22.1987],
'河北省': [114.5024, 38.0454],
'山西省': [112.5489, 37.8570],
'贵州省': [106.7135, 26.5783],
'内蒙古自治区': [111.6708, 40.8183],
'香港特别行政区': [114.1095, 22.3964],
'新疆维吾尔自治区': [87.6168, 43.7928],
'甘肃省': [103.8235, 36.0580],
'海南省': [110.3312, 20.0319],
'宁夏回族自治区': [106.2782, 38.4664],
'西藏自治区': [91.1175, 29.6469],
'青海省': [101.7782, 36.6172]
};
const coord = provinceCoords[item.name] || coordinates;
// 返回格式:[经度, 纬度, 密度, 总量]
return {
name: item.name,
value: [coord[0], coord[1], item.density, item.total]
};
});
} else {
console.error('获取人才地图数据失败:', businessData?.msg || '未知错误');
// 设置默认数据
talentMapData.value = [
{ name: '北京市', value: [116.4074, 39.9042, 141.15, 231.47] },
{ name: '上海市', value: [121.4737, 31.2304, 269.83, 169.99] },
{ name: '浙江省', value: [120.1551, 30.2794, 8.94, 90.98] },
{ name: '广东省', value: [113.2644, 23.1291, 7.5, 149.76] },
{ name: '台湾省', value: [121.5090, 25.0443, 32.75, 117.88] },
{ name: '四川省', value: [104.0657, 30.6595, 1.01, 48.7] },
{ name: '江苏省', value: [118.7780, 32.0472, 5.25, 56.28] },
{ name: '湖北省', value: [114.3053, 30.5928, 1.84, 34.09] },
{ name: '福建省', value: [119.3062, 26.0747, 1.39, 16.79] },
{ name: '陕西省', value: [108.9480, 34.2632, 1.04, 21.38] },
{ name: '重庆市', value: [106.5049, 29.5332, 1.61, 13.24] },
{ name: '山东省', value: [120.3331, 36.0670, 1.02, 16.02] },
{ name: '湖南省', value: [112.9822, 28.1944, 0.68, 14.25] },
{ name: '安徽省', value: [117.2830, 31.8611, 0.93, 12.92] },
{ name: '辽宁省', value: [123.4321, 41.8007, 0.77, 11.37] },
{ name: '云南省', value: [102.7123, 25.0406, 0.1, 3.68] },
{ name: '天津市', value: [117.1901, 39.1258, 7.79, 9.34] },
{ name: '河南省', value: [113.6654, 34.7579, 0.55, 9.07] },
{ name: '吉林省', value: [125.3245, 43.8171, 0.15, 2.7] },
{ name: '江西省', value: [115.8581, 28.6820, 0.25, 4.17] },
{ name: '广西壮族自治区', value: [108.3200, 22.8241, 0.14, 3.15] },
{ name: '黑龙江省', value: [126.5349, 45.8038, 0.11, 5.13] },
{ name: '澳门特别行政区', value: [113.5490, 22.1987, 737.5, 2.36] },
{ name: '河北省', value: [114.5024, 38.0454, 0.26, 4.74] },
{ name: '山西省', value: [112.5489, 37.8570, 0.25, 3.78] },
{ name: '贵州省', value: [106.7135, 26.5783, 0.11, 1.93] },
{ name: '内蒙古自治区', value: [111.6708, 40.8183, 0.01, 0.96] },
{ name: '香港特别行政区', value: [114.1095, 22.3964, 1797.7, 197.75] },
{ name: '新疆维吾尔自治区', value: [87.6168, 43.7928, 0.01, 0.77] },
{ name: '甘肃省', value: [103.8235, 36.0580, 0.04, 1.46] },
{ name: '海南省', value: [110.3312, 20.0319, 0.36, 1.25] },
{ name: '宁夏回族自治区', value: [106.2782, 38.4664, 0.05, 0.27] },
{ name: '西藏自治区', value: [91.1175, 29.6469, 0.01, 0.14] },
{ name: '青海省', value: [101.7782, 36.6172, 0.01, 0.18] }
];
}
} catch (error) {
console.error('获取人才地图数据异常:', error);
// 设置默认数据
talentMapData.value = [
{ name: '北京市', value: [116.4074, 39.9042, 141.15, 231.47] },
{ name: '上海市', value: [121.4737, 31.2304, 269.83, 169.99] },
{ name: '浙江省', value: [120.1551, 30.2794, 8.94, 90.98] },
{ name: '广东省', value: [113.2644, 23.1291, 7.5, 149.76] },
{ name: '台湾省', value: [121.5090, 25.0443, 32.75, 117.88] },
{ name: '四川省', value: [104.0657, 30.6595, 1.01, 48.7] },
{ name: '江苏省', value: [118.7780, 32.0472, 5.25, 56.28] },
{ name: '湖北省', value: [114.3053, 30.5928, 1.84, 34.09] },
{ name: '福建省', value: [119.3062, 26.0747, 1.39, 16.79] },
{ name: '陕西省', value: [108.9480, 34.2632, 1.04, 21.38] },
{ name: '重庆市', value: [106.5049, 29.5332, 1.61, 13.24] },
{ name: '山东省', value: [120.3331, 36.0670, 1.02, 16.02] },
{ name: '湖南省', value: [112.9822, 28.1944, 0.68, 14.25] },
{ name: '安徽省', value: [117.2830, 31.8611, 0.93, 12.92] },
{ name: '辽宁省', value: [123.4321, 41.8007, 0.77, 11.37] },
{ name: '云南省', value: [102.7123, 25.0406, 0.1, 3.68] },
{ name: '天津市', value: [117.1901, 39.1258, 7.79, 9.34] },
{ name: '河南省', value: [113.6654, 34.7579, 0.55, 9.07] },
{ name: '吉林省', value: [125.3245, 43.8171, 0.15, 2.7] },
{ name: '江西省', value: [115.8581, 28.6820, 0.25, 4.17] },
{ name: '广西壮族自治区', value: [108.3200, 22.8241, 0.14, 3.15] },
{ name: '黑龙江省', value: [126.5349, 45.8038, 0.11, 5.13] },
{ name: '澳门特别行政区', value: [113.5490, 22.1987, 737.5, 2.36] },
{ name: '河北省', value: [114.5024, 38.0454, 0.26, 4.74] },
{ name: '山西省', value: [112.5489, 37.8570, 0.25, 3.78] },
{ name: '贵州省', value: [106.7135, 26.5783, 0.11, 1.93] },
{ name: '内蒙古自治区', value: [111.6708, 40.8183, 0.01, 0.96] },
{ name: '香港特别行政区', value: [114.1095, 22.3964, 1797.7, 197.75] },
{ name: '新疆维吾尔自治区', value: [87.6168, 43.7928, 0.01, 0.77] },
{ name: '甘肃省', value: [103.8235, 36.0580, 0.04, 1.46] },
{ name: '海南省', value: [110.3312, 20.0319, 0.36, 1.25] },
{ name: '宁夏回族自治区', value: [106.2782, 38.4664, 0.05, 0.27] },
{ name: '西藏自治区', value: [91.1175, 29.6469, 0.01, 0.14] },
{ name: '青海省', value: [101.7782, 36.6172, 0.01, 0.18] }
];
} finally {
loading.value = false;
}
};
// 计算 TOP 5 地区
const topRegions = computed(() => {
const sorted = [...talentMapData.value]
.map(item => ({
name: item.name,
score: item.value[3],
density: item.value[2]
}))
.sort((a, b) => b.score - a.score);
return sorted.slice(0, 5);
});
// 组件挂载时获取数据
onMounted(() => {
fetchDataFromApi();
// 数据加载完成后初始化图表
setTimeout(() => {
initTalentMapChart()
}, 100);
});
const initTalentMapChart = () => {
const el = document.getElementById('talentMapChart');
if (!el) return;
if (talentMapChart.value) {
talentMapChart.value.dispose();
}
const myChart = echarts.init(el);
talentMapChart.value = myChart;
// --- 修改后的加载逻辑:直接获取 JSON ---
// 这里可以换成你本地项目的路径,例如:'/maps/china.json'
const MAP_JSON_URL = 'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json';
// 1. 注册地图数据
echarts.registerMap('china', geoJson);
// 2. 调用渲染函数
renderMap();
// 渲染地图的函数 (配置保持原有的科技感风格)
function renderMap() {
myChart.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(17, 54, 122, 0.95)',
borderColor: '#00A0FF',
borderWidth: 1,
borderRadius: 8,
padding: 16,
textStyle: { color: '#6dd0e3', fontSize: 14 },
formatter: (params: any) => {
if (params.seriesType === 'scatter') {
const data = params.data.value;
const density = data[2];
const total = data[3];
// 内部辅助函数获取等级
const getLevel = (d: number) => {
if(d > 500) return '人才极高密度区';
if(d > 100) return '人才高密度区';
return '人才稳步增长区';
};
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #00F2FF; font-size: 16px; margin-bottom: 10px;">${params.name}</div>
<div><span style="color: #6dd0e3;">人才密度:</span><span style="color: #00C0FF; font-weight: 700;">${density}</span> 人/km²</div>
<div><span style="color: #6dd0e3;">人才总量:</span><span style="color: #00C0FF; font-weight: 700;">${total}</span> 万人</div>
<div style="margin-top: 8px; padding: 4px 10px; background: rgba(0, 242, 255, 0.2); border-radius: 6px; color: #00F2FF; font-size: 12px;">${getLevel(density)}</div>
</div>
`;
}
return '';
}
},
visualMap: {
show: false,
min: 0,
max: 500,
inRange: {
color: ['#00A0FF', '#0063D0', '#004190'],
symbolSize: [10, 60]
}
},
geo: {
map: 'china',
roam: true,
scaleLimit: { min: 0.8, max: 4 },
zoom: 2.6, // 调整为 standard zoom
center: [104.5, 37.5],
label: {
show: true,
fontSize: 10,
color: '#CEF4FD'
},
itemStyle: {
areaColor: 'rgba(0, 30, 80, 0.6)',
borderColor: 'rgba(0, 160, 255, 0.5)',
borderWidth: 1.5,
shadowBlur: 10,
shadowColor: 'rgba(0, 160, 255, 0.3)'
},
emphasis: {
itemStyle: {
areaColor: 'rgba(0, 50, 120, 0.8)',
borderColor: '#00C0FF'
}
}
},
series: [
{
name: '开源人才密度',
type: 'scatter',
coordinateSystem: 'geo',
data: talentMapData.value,
symbolSize: (val: any) => Math.sqrt(val[2]) * 2 + 8,
itemStyle: {
color: (params: any) => {
const density = params.value[2];
if (density >= 500) return '#FF4900';
if (density >= 100) return '#FFAB00';
return '#00C0FF';
},
shadowBlur: 20,
shadowColor: 'rgba(0, 242, 255, 0.6)'
}
}
]
});
}
};
</script>
<style lang="scss" scoped>
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
color: #999;
}
.map-container {
display: flex;
gap: 2rem;
align-items: stretch;
position: relative;
min-height: 600px;
}
.map-container .chart {
flex: 1;
min-height: 600px;
background: linear-gradient(135deg,
rgba(0, 20, 60, 0.4) 0%,
rgba(0, 40, 100, 0.3) 100%);
border-radius: 1rem;
border: 2px solid rgba(0, 192, 255, 0.3);
box-shadow: 0 8px 24px rgba(0, 192, 255, 0.2);
padding: 1rem;
}
.map-rank-panel {
width: 300px;
border-radius: 1rem;
border: 2px solid rgba(0, 192, 255, 0.3);
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 192, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
gap: 1rem;
}
.panel-title {
font-size: 1.2rem;
font-weight: 700;
color: black;
text-align: center;
padding-bottom: 1rem;
margin-bottom: 0.5rem;
border-bottom: 2px solid rgba(0, 242, 255, 0.3);
}
.rank-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-height: 400px;
overflow-y: auto;
padding-right: 5px;
}
.rank-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1rem;
background: rgba(0, 192, 255, 0.08);
border-radius: 0.75rem;
border: 1px solid rgba(0, 192, 255, 0.2);
border-left: 3px solid #00A0FF;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
margin-bottom: 0.75rem;
}
.rank-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 242, 255, 0.15), transparent);
transition: left 0.5s;
}
.rank-item:hover::before {
left: 100%;
}
.rank-item:hover {
background: rgba(0, 192, 255, 0.2);
border-color: #00F2FF;
border-left-color: #00F2FF;
box-shadow: 0 6px 16px rgba(0, 242, 255, 0.3);
transform: translateX(5px) scale(1.02);
}
.rank-num {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
flex-shrink: 0;
font-family: 'DIN Alternate', 'Arial', sans-serif;
}
.rank-num.rank-1 {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #1a1a1a;
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-2 {
background: linear-gradient(135deg, #E8E8E8, #C0C0C0);
color: #1a1a1a;
box-shadow: 0 4px 12px rgba(192, 192, 192, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-3 {
background: linear-gradient(135deg, #CD7F32, #B8733C);
color: white;
box-shadow: 0 4px 12px rgba(205, 127, 50, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-4,
.rank-num.rank-5 {
background: rgba(0, 192, 255, 0.2);
color: #00C0FF;
font-weight: bold;
border: 2px solid rgba(0, 192, 255, 0.4);
}
.rank-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #000000;
}
.rank-score {
font-size: 14px;
font-weight: 700;
color: #00F2FF;
font-family: 'DIN Alternate', 'Arial', sans-serif;
text-shadow: 0 0 8px rgba(0, 242, 255, 0.5);
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -1,347 +0,0 @@
<template>
<div class="map-container">
<div id="talentMapChart" class="chart"></div>
<!-- 右侧排行榜 -->
<div class="map-rank-panel">
<div class="panel-title">🏆 开源人才数量排行 TOP 5</div>
<div class="rank-list">
<div class="rank-item" v-for="(item, index) in topRegions" :key="index">
<span :class="['rank-num', 'rank-' + (index + 1)]">{{ index + 1 }}</span>
<span class="rank-name">{{ item.name }}</span>
<span class="rank-score">{{ item.score }} 万人</span>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import {onMounted, ref, onBeforeUnmount, computed} from 'vue';
// 初始化中国开源人才密度地图
import * as echarts from "echarts";
import geoJson from './json/china.json';
const talentMapChart = ref()
// 中国开源人才密度地图数据
const talentMapData = ref([
{ name: '北京市', value: [116.4074, 39.9042, 141.15, 231.47] },
{ name: '上海市', value: [121.4737, 31.2304, 269.83, 169.99] },
{ name: '浙江省', value: [120.1551, 30.2794, 8.94, 90.98] },
{ name: '广东省', value: [113.2644, 23.1291, 7.5, 149.76] },
{ name: '台湾省', value: [121.5090, 25.0443, 32.75, 117.88] },
{ name: '四川省', value: [104.0657, 30.6595, 1.01, 48.7] },
{ name: '江苏省', value: [118.7780, 32.0472, 5.25, 56.28] },
{ name: '湖北省', value: [114.3053, 30.5928, 1.84, 34.09] },
{ name: '福建省', value: [119.3062, 26.0747, 1.39, 16.79] },
{ name: '陕西省', value: [108.9480, 34.2632, 1.04, 21.38] },
{ name: '重庆市', value: [106.5049, 29.5332, 1.61, 13.24] },
{ name: '山东省', value: [120.3331, 36.0670, 1.02, 16.02] },
{ name: '湖南省', value: [112.9822, 28.1944, 0.68, 14.25] },
{ name: '安徽省', value: [117.2830, 31.8611, 0.93, 12.92] },
{ name: '辽宁省', value: [123.4321, 41.8007, 0.77, 11.37] },
{ name: '云南省', value: [102.7123, 25.0406, 0.1, 3.68] },
{ name: '天津市', value: [117.1901, 39.1258, 7.79, 9.34] },
{ name: '河南省', value: [113.6654, 34.7579, 0.55, 9.07] },
{ name: '吉林省', value: [125.3245, 43.8171, 0.15, 2.7] },
{ name: '江西省', value: [115.8581, 28.6820, 0.25, 4.17] },
{ name: '广西壮族自治区', value: [108.3200, 22.8241, 0.14, 3.15] },
{ name: '黑龙江省', value: [126.5349, 45.8038, 0.11, 5.13] },
{ name: '澳门特别行政区', value: [113.5490, 22.1987, 737.5, 2.36] },
{ name: '河北省', value: [114.5024, 38.0454, 0.26, 4.74] },
{ name: '山西省', value: [112.5489, 37.8570, 0.25, 3.78] },
{ name: '贵州省', value: [106.7135, 26.5783, 0.11, 1.93] },
{ name: '内蒙古自治区', value: [111.6708, 40.8183, 0.01, 0.96] },
{ name: '香港特别行政区', value: [114.1095, 22.3964, 1797.7, 197.75] },
{ name: '新疆维吾尔自治区', value: [87.6168, 43.7928, 0.01, 0.77] },
{ name: '甘肃省', value: [103.8235, 36.0580, 0.04, 1.46] },
{ name: '海南省', value: [110.3312, 20.0319, 0.36, 1.25] },
{ name: '宁夏回族自治区', value: [106.2782, 38.4664, 0.05, 0.27] },
{ name: '西藏自治区', value: [91.1175, 29.6469, 0.01, 0.14] },
{ name: '青海省', value: [101.7782, 36.6172, 0.01, 0.18] }
]);
// 计算 TOP 5 地区
const topRegions = computed(() => {
const sorted = [...talentMapData.value]
.map(item => ({
name: item.name,
score: item.value[3],
density: item.value[2]
}))
.sort((a, b) => b.score - a.score);
return sorted.slice(0, 5);
});
// 挂载时初始化
onMounted(() => {
setTimeout(() => {
initTalentMapChart()
}, 100);
});
const initTalentMapChart = () => {
const el = document.getElementById('talentMapChart');
if (!el) return;
if (talentMapChart.value) {
talentMapChart.value.dispose();
}
const myChart = echarts.init(el);
talentMapChart.value = myChart;
// --- 修改后的加载逻辑:直接获取 JSON ---
// 这里可以换成你本地项目的路径,例如:'/maps/china.json'
const MAP_JSON_URL = 'https://geo.datav.aliyun.com/areas_v3/bound/100000_full.json';
// 1. 注册地图数据
echarts.registerMap('china', geoJson);
// 2. 调用渲染函数
renderMap();
// 渲染地图的函数 (配置保持原有的科技感风格)
function renderMap() {
myChart.setOption({
tooltip: {
trigger: 'item',
backgroundColor: 'rgba(17, 54, 122, 0.95)',
borderColor: '#00A0FF',
borderWidth: 1,
borderRadius: 8,
padding: 16,
textStyle: { color: '#6dd0e3', fontSize: 14 },
formatter: (params: any) => {
if (params.seriesType === 'scatter') {
const data = params.data.value;
const density = data[2];
const total = data[3];
// 内部辅助函数获取等级
const getLevel = (d: number) => {
if(d > 500) return '人才极高密度区';
if(d > 100) return '人才高密度区';
return '人才稳步增长区';
};
return `
<div style="line-height: 2;">
<div style="font-weight: bold; color: #00F2FF; font-size: 16px; margin-bottom: 10px;">${params.name}</div>
<div><span style="color: #6dd0e3;">人才密度:</span><span style="color: #00C0FF; font-weight: 700;">${density}</span> 人/km²</div>
<div><span style="color: #6dd0e3;">人才总量:</span><span style="color: #00C0FF; font-weight: 700;">${total}</span> 万人</div>
<div style="margin-top: 8px; padding: 4px 10px; background: rgba(0, 242, 255, 0.2); border-radius: 6px; color: #00F2FF; font-size: 12px;">${getLevel(density)}</div>
</div>
`;
}
return '';
}
},
visualMap: {
show: false,
min: 0,
max: 500,
inRange: {
color: ['#00A0FF', '#0063D0', '#004190'],
symbolSize: [10, 60]
}
},
geo: {
map: 'china',
roam: true,
scaleLimit: { min: 0.8, max: 4 },
zoom: 2.6, // 调整为 standard zoom
center: [104.5, 37.5],
label: {
show: true,
fontSize: 10,
color: '#CEF4FD'
},
itemStyle: {
areaColor: 'rgba(0, 30, 80, 0.6)',
borderColor: 'rgba(0, 160, 255, 0.5)',
borderWidth: 1.5,
shadowBlur: 10,
shadowColor: 'rgba(0, 160, 255, 0.3)'
},
emphasis: {
itemStyle: {
areaColor: 'rgba(0, 50, 120, 0.8)',
borderColor: '#00C0FF'
}
}
},
series: [
{
name: '开源人才密度',
type: 'scatter',
coordinateSystem: 'geo',
data: talentMapData.value,
symbolSize: (val: any) => Math.sqrt(val[2]) * 2 + 8,
itemStyle: {
color: (params: any) => {
const density = params.value[2];
if (density >= 500) return '#FF4900';
if (density >= 100) return '#FFAB00';
return '#00C0FF';
},
shadowBlur: 20,
shadowColor: 'rgba(0, 242, 255, 0.6)'
}
}
]
});
}
};
</script>
<style lang="scss" scoped>
.map-container {
display: flex;
gap: 2rem;
align-items: stretch;
position: relative;
min-height: 600px;
}
.map-container .chart {
flex: 1;
min-height: 600px;
background: linear-gradient(135deg,
rgba(0, 20, 60, 0.4) 0%,
rgba(0, 40, 100, 0.3) 100%);
border-radius: 1rem;
border: 2px solid rgba(0, 192, 255, 0.3);
box-shadow: 0 8px 24px rgba(0, 192, 255, 0.2);
padding: 1rem;
}
.map-rank-panel {
width: 300px;
border-radius: 1rem;
border: 2px solid rgba(0, 192, 255, 0.3);
padding: 1.5rem;
box-shadow: 0 8px 32px rgba(0, 192, 255, 0.2);
backdrop-filter: blur(10px);
-webkit-backdrop-filter: blur(10px);
display: flex;
flex-direction: column;
gap: 1rem;
}
.panel-title {
font-size: 1.2rem;
font-weight: 700;
color: black;
text-align: center;
padding-bottom: 1rem;
margin-bottom: 0.5rem;
border-bottom: 2px solid rgba(0, 242, 255, 0.3);
}
.rank-list {
display: flex;
flex-direction: column;
gap: 0.75rem;
max-height: 400px;
overflow-y: auto;
padding-right: 5px;
}
.rank-item {
display: flex;
align-items: center;
gap: 0.75rem;
padding: 0.85rem 1rem;
background: rgba(0, 192, 255, 0.08);
border-radius: 0.75rem;
border: 1px solid rgba(0, 192, 255, 0.2);
border-left: 3px solid #00A0FF;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
margin-bottom: 0.75rem;
}
.rank-item::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(0, 242, 255, 0.15), transparent);
transition: left 0.5s;
}
.rank-item:hover::before {
left: 100%;
}
.rank-item:hover {
background: rgba(0, 192, 255, 0.2);
border-color: #00F2FF;
border-left-color: #00F2FF;
box-shadow: 0 6px 16px rgba(0, 242, 255, 0.3);
transform: translateX(5px) scale(1.02);
}
.rank-num {
width: 32px;
height: 32px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 14px;
font-weight: 700;
flex-shrink: 0;
font-family: 'DIN Alternate', 'Arial', sans-serif;
}
.rank-num.rank-1 {
background: linear-gradient(135deg, #FFD700, #FFA500);
color: #1a1a1a;
box-shadow: 0 4px 12px rgba(255, 215, 0, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-2 {
background: linear-gradient(135deg, #E8E8E8, #C0C0C0);
color: #1a1a1a;
box-shadow: 0 4px 12px rgba(192, 192, 192, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-3 {
background: linear-gradient(135deg, #CD7F32, #B8733C);
color: white;
box-shadow: 0 4px 12px rgba(205, 127, 50, 0.6);
border: 2px solid rgba(255, 255, 255, 0.3);
}
.rank-num.rank-4,
.rank-num.rank-5 {
background: rgba(0, 192, 255, 0.2);
color: #00C0FF;
font-weight: bold;
border: 2px solid rgba(0, 192, 255, 0.4);
}
.rank-name {
flex: 1;
font-size: 14px;
font-weight: 600;
color: #000000;
}
.rank-score {
font-size: 14px;
font-weight: 700;
color: #00F2FF;
font-family: 'DIN Alternate', 'Arial', sans-serif;
text-shadow: 0 0 8px rgba(0, 242, 255, 0.5);
}
</style>

View File

@@ -0,0 +1,195 @@
<template>
<div class="index-module__NV_5cW__titleSection">
<div class="stats-glass-bar">
<div class="stat-node">
<span class="stat-value">{{ stats.enterprise.toLocaleString() }}+</span>
<span class="stat-desc">全球开源企业</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ stats.university.toLocaleString() }}+</span>
<span class="stat-desc">全球高校参与</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ stats.activity.toLocaleString() }}+</span>
<span class="stat-desc">全球社区活动</span>
</div>
<div class="stat-separator"></div>
<div class="stat-node">
<span class="stat-value">{{ stats.project.toLocaleString() }}亿+</span>
<span class="stat-desc">全球开源项目</span>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, reactive } from 'vue';
import { overviewDataPage1 } from '@/api/jyh/situation';
// 定义响应式数据对象,初始值为 0 或 null
const stats = reactive({
enterprise: 8000,
university: 600,
activity: 10000,
project: 4
});
// 加载状态(可选,用于显示 skeleton 或 loading
const loading = ref(true);
const formatProjectValue = (val: number) => {
if (val >= 100000000) {
return (val / 100000000).toFixed(1) + '亿+';
} else if (val >= 10000) {
return (val / 10000).toFixed(0) + '万+';
}
return val + '+';
};
const fetchStats = async () => {
try {
loading.value = true;
const { data:res } = await overviewDataPage1({});
if (res.code === 200) {
// 将接口数据赋值给响应式对象
Object.assign(stats, res.data);
}
} catch (error) {
console.error('获取统计数据失败:', error);
} finally {
loading.value = false;
}
};
onMounted(() => {
fetchStats();
});
</script>
<style scoped lang="scss">
.index-module__NV_5cW__titleSection {
color: #333;
z-index: 2;
flex-direction: column;
align-items: center;
gap: 1rem;
display: flex;
position: relative;
}
.stats-glass-bar {
display: flex;
align-items: center;
justify-content: space-between;
/* 布局控制:撑满容器并增加内边距 */
width: 100%;
padding: 2rem 4rem;
//margin: 2rem 0;
/* 深色玻璃拟态背景 */
background: rgba(255, 255, 255, 0.02);
backdrop-filter: blur(20px);
-webkit-backdrop-filter: blur(20px);
/* 极细微的边框勾勒 */
border-radius: 1rem;
border: 1px solid rgba(255, 255, 255, 0.08);
/* 外阴影与内发光,增加悬浮感 */
box-shadow: 0 20px 50px rgba(0, 0, 0, 0.3),
inset 0 0 20px rgba(255, 255, 255, 0.02);
transition: all 0.5s cubic-bezier(0.4, 0, 0.2, 1);
&:hover {
border-color: rgba(56, 189, 248, 0.4);
background: rgba(255, 255, 255, 0.04);
transform: translateY(-2px);
}
}
.stat-node {
flex: 1;
display: flex;
flex-direction: column;
align-items: center;
gap: 0.75rem;
transition: transform 0.3s ease;
&:hover {
transform: scale(1.05);
.stat-value {
text-shadow: 0 0 25px rgba(56, 189, 248, 0.8);
}
}
}
.stat-value {
/* 科技感亮蓝渐变 */
font-family: "PingFang SC", "Microsoft YaHei", sans-serif;
font-size: 2.25rem;
font-weight: 800;
background: linear-gradient(180deg, #fff 0%, #38bdf8 100%);
-webkit-background-clip: text;
-webkit-text-fill-color: transparent;
/* 基础发光效果 */
filter: drop-shadow(0 0 10px rgba(56, 189, 248, 0.3));
letter-spacing: -1px;
}
.stat-desc {
/* 在黑底上使用半透明白色,更有高级感 */
color: rgba(255, 255, 255, 0.6);
font-size: 0.875rem;
font-weight: 500;
letter-spacing: 0.1rem;
text-transform: uppercase;
}
.stat-separator {
/* 渐隐分割线 */
width: 1px;
height: 3rem;
background: linear-gradient(
to bottom,
rgba(255, 255, 255, 0),
rgba(255, 255, 255, 0.2),
rgba(255, 255, 255, 0)
);
margin: 0 1rem;
}
/* 响应式适配 */
@media (max-width: 1024px) {
.stats-glass-bar {
padding: 2rem;
}
.stat-value {
font-size: 1.75rem;
}
}
@media (max-width: 768px) {
.stats-glass-bar {
flex-direction: column;
gap: 2rem;
}
.stat-separator {
width: 50%;
height: 1px;
background: linear-gradient(to right, transparent, rgba(255, 255, 255, 0.2), transparent);
}
}
</style>

View File

@@ -0,0 +1,401 @@
<template>
<div class="index-module__NV_5cW__chartCard chartCard__wide">
<div class="chartCard__header">
<h3 class="chartCard__title">CVE 月度趋势</h3>
<!-- 视图切换按钮用于切换全球和区域视图 -->
<div class="chartCard__actions">
<button
class="chartCard__actionBtn"
:class="{ 'chartCard__actionBtn--active': cveView === 'global' }"
@click="switchCveView('global')"
>
全球
</button>
<button
class="chartCard__actionBtn"
:class="{ 'chartCard__actionBtn--active': cveView === 'regional' }"
@click="switchCveView('regional')"
>
区域
</button>
</div>
</div>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.cveData"></div>
<div v-else id="CVEChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { cveDataGlobalPage1, cveDataRegionalPage1 } from '@/api/jyh/situation';
// 视图切换状态变量global: 全球, regional: 区域)
const cveView = ref('global');
// 空数据标识
const isEmpty = ref({
cveData: false
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
CVEChart: null
});
// CVE图表数据 - 响应式数据
const cveData = ref({
global: {
cveCount: [] as number[],
acceleration: [] as number[]
},
regional: {
cveCount: [] as number[],
acceleration: [] as number[]
}
});
// 获取后端数据
const fetchCveData = async () => {
try {
console.log('开始请求全球CVE数据...');
// 请求全球数据
const globalResponse = await cveDataGlobalPage1();
const globalBusinessData = globalResponse.data; // Axios响应格式业务数据在response.data中
console.log('全球CVE数据响应:', globalResponse);
if (globalBusinessData?.code === 200) {
console.log('全球CVE数据:', globalBusinessData.data);
cveData.value.global = {
cveCount: globalBusinessData.data.cveCount,
acceleration: globalBusinessData.data.acceleration
};
} else {
console.error('获取全球CVE数据失败:', globalBusinessData?.msg || '未知错误');
// 设置默认数据
setDefaultGlobalData();
}
console.log('开始请求区域CVE数据...');
// 请求区域数据
const regionalResponse = await cveDataRegionalPage1();
const regionalBusinessData = regionalResponse.data; // Axios响应格式业务数据在response.data中
console.log('区域CVE数据响应:', regionalResponse);
if (regionalBusinessData?.code === 200) {
console.log('区域CVE数据:', regionalBusinessData.data);
cveData.value.regional = {
cveCount: regionalBusinessData.data.cveCount,
acceleration: regionalBusinessData.data.acceleration
};
} else {
console.error('获取区域CVE数据失败:', regionalBusinessData?.msg || '未知错误');
// 设置默认数据
setDefaultRegionalData();
}
// 检查是否有有效数据
const hasGlobalData = cveData.value.global.cveCount.length > 0;
const hasRegionalData = cveData.value.regional.cveCount.length > 0;
isEmpty.value.cveData = !hasGlobalData && !hasRegionalData;
} catch (error) {
console.error('获取CVE数据异常:', error);
// 发生异常时设置默认数据
setDefaultGlobalData();
setDefaultRegionalData();
isEmpty.value.cveData = false; // 有默认数据,不算空
}
};
// 设置默认全球数据
const setDefaultGlobalData = () => {
cveData.value.global = {
cveCount: [98402, 418, 7025, 3711, 5115, 3228, 3044, 2957, 2592, 1455, 4826, 4661],
acceleration: [0, -99.58, 1580.62, -47.17, 37.83, -36.89, -5.70, -2.86, -12.34, -43.87, 231.68, -3.42]
};
};
// 设置默认区域数据
const setDefaultRegionalData = () => {
cveData.value.regional = {
cveCount: [29521, 125, 2108, 1113, 1535, 968, 913, 887, 778, 437, 1448, 1398],
acceleration: [0, -99.58, 1580.62, -47.17, 37.83, -36.89, -5.70, -2.86, -12.34, -43.87, 231.68, -3.42]
};
};
// CVE视图切换方法
const switchCveView = (view: 'global' | 'regional') => {
cveView.value = view;
nextTick(() => {
initCVEChart();
});
};
// 初始化CVE图表柱状图+折线图)
const initCVEChart = () => {
const el = document.getElementById('CVEChart');
if (!el) return;
// 延迟执行以确保DOM已渲染
setTimeout(() => {
const chartEl = document.getElementById('CVEChart');
if (!chartEl) return;
if (chartInstances.value.CVEChart) {
chartInstances.value.CVEChart.dispose();
}
const myChart = echarts.init(chartEl);
chartInstances.value.CVEChart = myChart;
// 获取当前视图的数据
const currentData = cveData.value[cveView.value === 'global' ? 'global' : 'regional'];
const cveCount = currentData.cveCount;
const acceleration = currentData.acceleration;
// 月份数据
const months = ['1月', '2月', '3月', '4月', '5月', '6月', '7月', '8月', '9月', '10月', '11月', '12月'];
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
crossStyle: {
color: '#999'
}
},
formatter: (params: any) => {
let res = `<div>${params[0].name}</div>`;
params.forEach((p: any) => {
if (p.seriesName === 'CVE数量') {
res += `<div>${p.marker}${p.seriesName}: ${p.value}</div>`;
} else {
res += `<div>${p.marker}${p.seriesName}: ${p.value}%</div>`;
}
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 },
data: ['CVE数量', '环比增长率']
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [
{
type: 'category',
data: months,
axisPointer: {
type: 'shadow'
},
axisLabel: { fontSize: 12 }
}
],
yAxis: [
{
type: 'value',
name: 'CVE数量',
nameTextStyle: { fontSize: 12, color: '#333' },
axisLabel: { fontSize: 12, color: '#666' },
position: 'left',
splitLine: {
show: true,
lineStyle: {
type: 'dashed'
}
}
},
{
type: 'value',
name: '环比增长率(%)',
nameTextStyle: { fontSize: 12, color: '#333' },
axisLabel: { fontSize: 12, color: '#666' },
position: 'right',
splitLine: {
show: false
}
}
],
series: [
{
name: 'CVE数量',
type: 'bar',
data: cveCount,
itemStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: '#3b82f6' },
{ offset: 1, color: '#60a5fa' }
])
},
barWidth: 20,
label: {
show: true,
position: 'top',
formatter: '{c}',
fontSize: 10,
color: '#333'
}
},
{
name: '环比增长率',
type: 'line',
yAxisIndex: 1,
data: acceleration,
itemStyle: {
color: '#ef4444',
borderWidth: 2
},
lineStyle: {
width: 2,
color: '#ef4444'
},
symbol: 'circle',
symbolSize: 6,
smooth: true,
label: {
show: true,
position: 'top',
formatter: (params: any) => `${params.value}%`,
fontSize: 10,
color: '#333'
}
}
]
});
// 监听窗口大小变化,重新调整图表大小
window.addEventListener('resize', () => {
if (chartInstances.value.CVEChart) {
chartInstances.value.CVEChart.resize();
}
});
}, 0);
};
// 在组件挂载后获取数据并初始化图表
onMounted(async () => {
await fetchCveData();
nextTick(() => {
initCVEChart();
});
});
// 监听数据变化,重新初始化图表
watch(cveData, () => {
if (!isEmpty.value.cveData) {
nextTick(() => {
initCVEChart();
});
}
}, { deep: true });
// 在组件卸载前清理资源
onUnmounted(() => {
if (chartInstances.value.CVEChart) {
chartInstances.value.CVEChart.dispose();
}
});
defineExpose({
initCVEChart,
switchCveView,
cveView,
cveData
});
</script>
<style scoped>
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.chartCard__wide {
grid-column: 1 / -1; /* 跨列显示 */
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
/* 图表卡片头部样式 */
.chartCard__header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
/* 图表操作按钮样式 */
.chartCard__actions {
display: flex;
gap: 8px;
}
.chartCard__actionBtn {
padding: 4px 12px;
border: 1px solid #d1d5db;
background-color: #c1c7cd;
border-radius: 4px;
font-size: 12px;
cursor: pointer;
transition: all 0.2s;
}
.chartCard__actionBtn:hover {
background-color: #f3f4f6;
}
.chartCard__actionBtn--active {
background-color: #3b82f6;
color: white;
border-color: #3b82f6;
}
.chartCard__actionBtn--active:hover {
background-color: #2563eb;
}
/* 与示例一致的空数据样式 */
.num-empty {
height: 10px;
border-top: 2px solid #2951E0;
width: 100%;
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,265 @@
<template>
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">高危组件 Top 10</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty"></div>
<div v-else ref="chartRef" class="chart"></div>
</div>
</div>
</template>
<script setup lang="ts">
import { onMounted, ref, nextTick, watch, onUnmounted } from 'vue';
import * as echarts from 'echarts';
import { highRiskComponentPage1 } from '@/api/jyh/situation';
// 定义组件数据结构
interface ComponentData {
name: string;
cvss: number;
vulnCount: number;
usage: string;
desc: string;
}
// 定义属性,允许从父组件传入数据
const props = defineProps<{
data?: Array<ComponentData>;
loading?: boolean;
}>();
// 定义 emits 事件
const emit = defineEmits<{
(e: 'chart-ready', chart: echarts.ECharts): void;
}>();
// 空数据标识
const isEmpty = ref(false);
// 图表DOM引用
const chartRef = ref<HTMLDivElement | null>(null);
// 图表实例存储
let chartInstance: echarts.ECharts | null = null;
// 组件数据
const componentData = ref<ComponentData[]>([]);
// 获取高危组件数据
const fetchComponentData = async () => {
try {
const response = await highRiskComponentPage1();
if (response.data.code === 200) {
componentData.value = response.data.data;
} else {
console.error('获取高危组件数据失败:', response.data.msg || '未知错误');
// 如果接口调用失败,使用默认数据
componentData.value = [
{ name: 'Log4j2', cvss: 9.8, vulnCount: 15, usage: '1200万+', desc: 'CVE-2021-44228等严重RCE漏洞' },
{ name: 'Apache Struts2', cvss: 9.7, vulnCount: 12, usage: '850万+', desc: 'S2系列远程代码执行漏洞' },
{ name: 'OpenSSL', cvss: 9.6, vulnCount: 18, usage: '2500万+', desc: '心脏出血、幽灵猫等高危漏洞' },
{ name: 'Spring Framework', cvss: 9.5, vulnCount: 9, usage: '1800万+', desc: 'Spring4Shell等RCE漏洞' },
{ name: 'Jackson', cvss: 9.4, vulnCount: 11, usage: '1000万+', desc: '反序列化漏洞影响Java生态' },
{ name: 'Fastjson', cvss: 9.3, vulnCount: 14, usage: '600万+', desc: '多个反序列化RCE漏洞' },
{ name: 'Apache Commons', cvss: 9.2, vulnCount: 8, usage: '1500万+', desc: 'Collections反序列化漏洞' },
{ name: 'jQuery', cvss: 9.1, vulnCount: 6, usage: '3000万+', desc: 'XSS和原型污染漏洞' },
{ name: 'Lodash', cvss: 9.0, vulnCount: 5, usage: '2200万+', desc: '原型污染漏洞影响Node.js生态' },
{ name: 'Moment.js', cvss: 9.0, vulnCount: 4, usage: '1100万+', desc: 'ReDoS和原型污染漏洞' }
];
}
} catch (error) {
console.error('请求高危组件数据时发生错误:', error);
// 发生异常时使用默认数据
componentData.value = [
{ name: 'Log4j2', cvss: 9.8, vulnCount: 15, usage: '1200万+', desc: 'CVE-2021-44228等严重RCE漏洞' },
{ name: 'Apache Struts2', cvss: 9.7, vulnCount: 12, usage: '850万+', desc: 'S2系列远程代码执行漏洞' },
{ name: 'OpenSSL', cvss: 9.6, vulnCount: 18, usage: '2500万+', desc: '心脏出血、幽灵猫等高危漏洞' },
{ name: 'Spring Framework', cvss: 9.5, vulnCount: 9, usage: '1800万+', desc: 'Spring4Shell等RCE漏洞' },
{ name: 'Jackson', cvss: 9.4, vulnCount: 11, usage: '1000万+', desc: '反序列化漏洞影响Java生态' },
{ name: 'Fastjson', cvss: 9.3, vulnCount: 14, usage: '600万+', desc: '多个反序列化RCE漏洞' },
{ name: 'Apache Commons', cvss: 9.2, vulnCount: 8, usage: '1500万+', desc: 'Collections反序列化漏洞' },
{ name: 'jQuery', cvss: 9.1, vulnCount: 6, usage: '3000万+', desc: 'XSS和原型污染漏洞' },
{ name: 'Lodash', cvss: 9.0, vulnCount: 5, usage: '2200万+', desc: '原型污染漏洞影响Node.js生态' },
{ name: 'Moment.js', cvss: 9.0, vulnCount: 4, usage: '1100万+', desc: 'ReDoS和原型污染漏洞' }
];
}
};
// 初始化图表
const initChart = async () => {
if (!chartRef.value) return;
// 销毁之前的实例
if (chartInstance) {
chartInstance.dispose();
}
chartInstance = echarts.init(chartRef.value);
// 如果有传入的数据,则优先使用传入的数据,否则使用获取到的数据
const chartData = props.data || componentData.value;
// 检查是否有有效数据
isEmpty.value = !chartData || chartData.length === 0;
if (isEmpty.value) {
return;
}
// 准备图表数据
const componentNames = chartData.map(item => item.name);
const cvssScores = chartData.map(item => item.cvss);
const vulnCounts = chartData.map(item => item.vulnCount);
const usages = chartData.map(item => item.usage);
const descs = chartData.map(item => item.desc);
// 配置图表选项,与原页面保持一致
const option = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
formatter: (params: any) => {
const data = chartData[params[0].dataIndex];
return `
<div style="padding: 4px;">
<div style="font-weight: bold; font-size: 14px;">${data.name}</div>
<div>CVSS评分: ${data.cvss}</div>
<div>漏洞数量: ${data.vulnCount}个</div>
<div>使用量: ${data.usage}</div>
<div>漏洞描述: ${data.desc}</div>
</div>
`;
}
},
grid: { left: '15%', right: '4%', bottom: '3%', top: '3%', containLabel: true },
xAxis: {
type: 'value',
name: 'CVSS评分',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 },
max: 10
},
yAxis: {
type: 'category',
data: componentNames,
axisLabel: { fontSize: 12 },
inverse: true // 反转Y轴让最高的值在顶部
},
series: [
{
name: 'CVSS评分',
type: 'bar',
data: cvssScores,
itemStyle: {
color: (params: any) => {
const value = params.value;
if (value >= 9.0) return '#ef4444'; // 红色 - 严重
if (value >= 7.0) return '#f97316'; // 橙色 - 高危
if (value >= 4.0) return '#eab308'; // 黄色 - 中危
return '#22c55e'; // 绿色 - 低危
}
},
label: {
show: true,
position: 'right',
formatter: '{c}',
fontSize: 12
}
}
]
};
chartInstance.setOption(option);
// 触发图表准备就绪事件
emit('chart-ready', chartInstance);
};
// 组件挂载时初始化图表
onMounted(async () => {
await fetchComponentData(); // 获取高危组件数据
await nextTick();
initChart();
});
// 监听数据变化,重新渲染图表
watch(() => props.data, () => {
initChart();
}, { deep: true });
// 监听组件内部数据变化,重新渲染图表
watch(componentData, () => {
initChart();
}, { deep: true });
// 组件卸载时清理图表实例
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
// 页面大小变化时调整图表大小
const resizeHandler = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// 监听窗口大小变化
window.addEventListener('resize', resizeHandler);
// 在组件卸载时移除事件监听
onUnmounted(() => {
window.removeEventListener('resize', resizeHandler);
});
</script>
<style scoped>
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
.chart {
width: 100%;
height: 100%;
}
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
color: #999;
font-size: 14px;
}
</style>

View File

@@ -0,0 +1,257 @@
<template>
<div class="index-module__NV_5cW__chartCard">
<h3 class="chartCard__title">行业分布</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="computedIsEmpty"></div>
<div v-else class="industry-distribution-container">
<div class="chart-container" ref="chartDiv"></div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue';
import * as echarts from 'echarts';
import { industryDistributionPage1 } from '@/api/jyh/situation';
// 定义数据类型
interface IndustryData {
value: number;
name: string;
}
// 定义配色方案
const industryColorPalette = [
'#00f0ff', '#00b03e', '#ffeb00', '#ffab00',
'#ff5100', '#c0232a', '#a262f2', '#379bff'
];
// 内部数据
const internalData = ref<IndustryData[]>([]);
// 响应式数据
const chartDiv = ref<HTMLDivElement | null>(null);
let myChart: echarts.ECharts | null = null;
// 计算 isEmpty 状态
const computedIsEmpty = computed(() => {
return internalData.value.length === 0;
});
// 初始化图表
const initChart = () => {
if (!chartDiv.value || computedIsEmpty.value) return;
// 销毁已存在的图表实例
if (myChart) {
myChart.dispose();
}
myChart = echarts.init(chartDiv.value);
// 构建图例数据,对于较长的名称才在图例中显示数值
const legendData = internalData.value.map(item => ({
name: item.name,
icon: 'circle'
}));
const option: echarts.EChartsOption = {
tooltip: {
trigger: 'item',
formatter: (params: any) => `${params.name}: ${params.value} (${params.percent}%)`
},
legend: {
bottom: '0',
textStyle: { fontSize: 10 },
itemWidth: 10,
itemHeight: 10,
orient: 'horizontal',
padding: [5, 0, 0, 0],
data: legendData
},
series: [
{
name: '行业分布',
type: 'pie',
radius: ['40%', '70%'],
center: ['50%', '45%'],
data: internalData.value,
label: {
show: true,
formatter: '{b}\n{d}%',
fontSize: 10,
rich: {
b: {
fontSize: 10,
lineHeight: 12
},
d: {
fontSize: 10,
lineHeight: 12
}
}
},
itemStyle: {
color: (params: any) => {
return industryColorPalette[params.dataIndex % industryColorPalette.length];
}
}
}
],
grid: { left: 0, bottom: 0, top: 0, right: 0 }
};
myChart.setOption(option);
};
// 响应式处理
const handleResize = () => {
if (myChart) {
myChart.resize();
}
};
// 获取行业分布数据
const fetchIndustryData = async () => {
try {
const response = await industryDistributionPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData.code === 200 && businessData.data) {
internalData.value = businessData.data;
} else {
console.error('获取行业分布数据失败:', businessData.msg || '未知错误');
internalData.value = [
{ value: 25.8, name: '人工智能' },
{ value: 18.4, name: '云计算' },
{ value: 15.6, name: '大数据' },
{ value: 12.2, name: '物联网' },
{ value: 9.6, name: '网络安全' },
{ value: 7.1, name: '区块链' },
{ value: 6.4, name: '工业互联网' },
{ value: 4.9, name: '移动开发' }
];
}
} catch (error) {
console.error('请求行业分布数据时发生错误:', error);
internalData.value = [
{ value: 25.8, name: '人工智能' },
{ value: 18.4, name: '云计算' },
{ value: 15.6, name: '大数据' },
{ value: 12.2, name: '物联网' },
{ value: 9.6, name: '网络安全' },
{ value: 7.1, name: '区块链' },
{ value: 6.4, name: '工业互联网' },
{ value: 4.9, name: '移动开发' }
];
}
};
// 生命周期钩子
onMounted(async () => {
await fetchIndustryData(); // 获取行业分布数据
await nextTick();
initChart();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (myChart) {
myChart.dispose();
}
});
</script>
<style lang="scss" scoped>
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
/* 列表卡片特殊样式 */
.index-module__NV_5cW__chartCard.list-card {
padding: 1.5rem;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.chartCard__wide {
grid-column: 1 / -1; /* 跨列显示 */
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
/* 列表容器样式:隐藏原生滚动条(可选,视觉更美观) */
.chartCard__container.list-container {
height: auto;
max-height: 300px; /* 限制列表高度,超出滚动 */
overflow-y: auto;
padding: 0 0.5rem;
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
/* 隐藏webkit内核浏览器的滚动条 */
.chartCard__container.list-container::-webkit-scrollbar {
display: none;
}
.num-empty {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
.industry-distribution-container {
width: 100%;
height: 100%;
position: relative;
}
.chart-container {
width: 100%;
height: 100%;
min-height: 300px;
}
/* 响应式样式 */
@media (max-width: 1024px) {
.chartCard__container {
height: 250px; /* 移动端图表高度调整 */
}
}
@media (max-width: 480px) {
.chartCard__container {
height: 200px; /* 小屏移动端图表高度调整 */
}
}
</style>

View File

@@ -0,0 +1,327 @@
<template>
<div class="index-module__NV_5cW__chartCard list-card">
<h3 class="chartCard__title">情报流</h3>
<!-- 新增ref + 鼠标事件 -->
<div class="chartCard__container list-container"
ref="scrollContainer1"
@mouseenter="pauseScroll(1)"
@mouseleave="resumeScroll(1)">
<div class="project-list">
<div class="project-item" v-for="(item, index) in feedList" :key="index">
<span class="project-rank" :class="item.type">{{ item.type }}</span>
<div class="project-info">
<span class="project-name">{{ item.content }}</span>
</div>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import { feedListPage1 } from '@/api/jyh/situation';
//定义组件数据结构
interface FeedData {
type: string;
content: string;
}
// 接收外部数据作为属性
const props = defineProps<{
initialData?: Array<FeedData>;
loading?: boolean;
}>();
//组件数据
const feedList = ref<FeedData[]>([]);
//获取情报流数据
const fetchFeedData = async() => {
try {
console.log('开始请求情报流数据...');
const response = await feedListPage1();
if (response.data?.code === 200) {
feedList.value = response.data.data || [];
} else {
console.error('获取情报流数据失败:', response?.data?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
feedList.value = [
{
type: 'CISA',
content: 'CISA KEV新增: WordPress 的 OneClick Chat to Order 插件,存在不安全的直接对象引用漏洞 (CVE-2025-13526) '
},
{
type: '投毒',
content: '投毒监控: npm 基建级包大规模投毒事件影响chalk, debug, ansi-styles 等 18 个包'
},
{
type: 'CNVD',
content: 'CNVD预警: Google Chrome信息泄露漏洞CNVD-2025-3038304'
},
{
type: 'GitHub',
content: 'GitHub安全通告: MovableType 软件的 ContentType 页面 "编辑分类集" 功能存在存储型跨站脚本漏洞'
},
{
type: '修复',
content: '漏洞修复: Node.js 发布 v20.11.0 版本,修复 3 个高危漏洞'
},
{
type: '供应链',
content: '供应链预警: Maven 中央仓库发现 12 个伪造开源组件,包含后门程序'
}
];
}
} catch (error) {
console.error('获取情报流数据异常:', error);
// 发生异常时使用默认数据
feedList.value = [
{
type: 'CISA',
content: 'CISA KEV新增: WordPress 的 OneClick Chat to Order 插件,存在不安全的直接对象引用漏洞 (CVE-2025-13526) '
},
{
type: '投毒',
content: '投毒监控: npm 基建级包大规模投毒事件影响chalk, debug, ansi-styles 等 18 个包'
},
{
type: 'CNVD',
content: 'CNVD预警: Google Chrome信息泄露漏洞CNVD-2025-3038304'
},
{
type: 'GitHub',
content: 'GitHub安全通告: MovableType 软件的 ContentType 页面 "编辑分类集" 功能存在存储型跨站脚本漏洞'
},
{
type: '修复',
content: '漏洞修复: Node.js 发布 v20.11.0 版本,修复 3 个高危漏洞'
},
{
type: '供应链',
content: '供应链预警: Maven 中央仓库发现 12 个伪造开源组件,包含后门程序'
}
];
}
};
// 如果提供了初始数据,则使用提供的数据
watch(() => props.initialData, (newData) => {
if (newData && newData.length > 0) {
feedList.value = newData;
}
}, { immediate: true });
// 1. 定义滚动容器的ref
const scrollContainer1 = ref<HTMLDivElement | null>(null);
// 2. 存储定时器1/2定时器ID
const scrollTimers: { [key: number]: NodeJS.Timeout | null } = {
1: null,
2: null
};
// 3. 滚动配置
const scrollStep = 1; // 每次滚动的步长(像素,越小越流畅)
const scrollInterval = 20; // 滚动间隔(毫秒,越小越流畅)
// 4. 通用自动滚动函数
const autoScroll = (containerKey: number) => {
// 获取对应容器
const container = containerKey === 1 ? scrollContainer1.value : null;
if (!container) return;
// 获取列表内容元素project-list
const projectList = container.querySelector('.project-list') as HTMLDivElement;
if (!projectList) return;
// 若内容高度 ≤ 容器高度,不执行滚动
if (projectList.scrollHeight <= container.clientHeight) {
// 清除现有定时器
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
return;
}
// 清除现有定时器(避免重复)
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
}
// 启动定时器滚动
scrollTimers[containerKey] = setInterval(() => {
// 向下滚动步长
container.scrollTop += scrollStep;
// 滚动到底部时重置为0循环滚动
// 加1是为了兼容像素精度问题
if (projectList.scrollHeight - container.scrollTop <= container.clientHeight + 1) {
container.scrollTop = 0; // 直接重置也可以用平滑滚动container.scrollTo({ top: 0, behavior: 'smooth' })
}
}, scrollInterval);
};
// 5. 暂停滚动
const pauseScroll = (containerKey: number) => {
if (scrollTimers[containerKey]) {
clearInterval(scrollTimers[containerKey]!);
scrollTimers[containerKey] = null;
}
};
// 6. 恢复滚动
const resumeScroll = (containerKey: number) => {
autoScroll(containerKey);
};
// 7. 组件挂载时启动滚动
onMounted(async() => {
await fetchFeedData(); // 获取情报流数据
nextTick(() => {
autoScroll(1);
});
});
// 8. 组件卸载时清除定时器
onUnmounted(() => {
Object.keys(scrollTimers).forEach(key => {
const k = Number(key);
if (scrollTimers[k]) {
clearInterval(scrollTimers[k]!);
scrollTimers[k] = null;
}
});
});
</script>
<style scoped>
/* 使用主页面相同的CSS类定义以确保样式一致性 */
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__chartCard.list-card {
padding: 1.5rem;
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
/* 列表容器样式:隐藏原生滚动条(可选,视觉更美观) */
.chartCard__container.list-container {
height: auto;
max-height: 300px; /* 限制列表高度,超出滚动 */
overflow-y: auto;
padding: 0 0.5rem;
/* 隐藏滚动条 */
scrollbar-width: none; /* Firefox */
-ms-overflow-style: none; /* IE/Edge */
}
/* 隐藏webkit内核浏览器的滚动条 */
.chartCard__container.list-container::-webkit-scrollbar {
display: none;
}
/* 项目列表样式 */
.project-list {
display: flex;
flex-direction: column;
gap: 0.8rem;
}
.project-item {
display: flex;
align-items: center;
gap: 1rem;
padding: 0.8rem;
background: #f8f9fa;
border-radius: 0.5rem;
border-left: 3px solid #2951E0;
transition: all 0.2s;
}
/* 鼠标悬停项目项时的样式 */
.project-item:hover {
background: #f0f4ff;
transform: translateX(2px);
}
.project-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 0.2rem;
}
.project-rank {
min-width: 24px;
height: 24px;
border-radius: 4px;
background: #2951E0;
color: #fff;
display: flex;
align-items: center;
justify-content: center;
font-size: 12px;
font-weight: 600;
flex-shrink: 0;
padding: 0 4px;
}
/* 特定类型的颜色定义 - 使用中文类名 */
/* .project-rank.CISA {
background: #ff0000;
}
.project-rank.投毒 {
background: #ffa500;
}
.project-rank.CNVD {
background: #ffff00;
color: #000;
}
.project-rank.GitHub {
background: #0000ff;
}
.project-rank.修复 {
background: #008000;
}
.project-rank.供应链 {
background: #800080;
} */
.project-name {
font-size: 14px;
font-weight: 600;
color: #333;
line-height: 1.4;
}
</style>

View File

@@ -0,0 +1,262 @@
<template>
<div class="index-module__NV_5cW__chartCard mb-[1.5rem]">
<h3 class="chartCard__title">编程语言安全态势</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.scatterData"></div>
<div v-else id="scatterChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { scatterDataPage1 } from '@/api/jyh/situation';
// 空数据标识
const isEmpty = ref({
scatterData: false
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
scatterChart: null
});
// 气泡图数据 - 响应式数据
const scatterData = ref({
scatterData: [] as any[],
midX: 50,
midY: 50,
quadrantColors: {
Q1: '#BF232A', // [高密度, 低活跃度] - 极高风险
Q2: '#FF4900', // [高密度, 高活跃度] - 高风险,需密切关注
Q3: '#00B43E', // [低密度, 低活跃度] - 低风险,但维护成本高
Q4: '#0063D0' // [低密度, 高活跃度] - 健康生态
}
});
// 获取后端数据
const fetchScatterData = async () => {
try {
console.log('开始请求编程语言安全态势数据...');
const response = await scatterDataPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
console.log('API响应:', response);
if (businessData?.code === 200) {
console.log('API返回的数据:', businessData.data);
// 处理后端返回的数据格式
const processedData = processBackendScatterData(businessData.data);
scatterData.value.scatterData = processedData;
// 更新空数据标识
isEmpty.value.scatterData = !processedData || processedData.length === 0;
} else {
console.error('获取编程语言安全态势数据失败:', businessData?.msg || '未知错误');
// 设置默认数据
setDefaultScatterData();
}
} catch (error) {
console.error('获取编程语言安全态势数据异常:', error);
// 发生异常时设置默认数据
setDefaultScatterData();
}
};
// 处理后端返回的数据格式
const processBackendScatterData = (backendData: any[]) => {
// 后端返回的数据已经是正确的格式,直接转换即可
return backendData.map(item => [
item.x, // 代码活跃度(X)
item.y, // 漏洞密度(Y)
item.size, // 漏洞总数(Size)
item.name // 语言名称(Name)
]);
};
// 设置默认数据
const setDefaultScatterData = () => {
scatterData.value.scatterData = [
[95, 75, 450, 'JavaScript'], // 最高活跃度9,345,046个项目
[90, 70, 380, 'Python'], // 高活跃度9,261,587个项目
[85, 65, 320, 'TypeScript'], // 高增长78.1%增长率
[75, 80, 420, 'Java'], // 传统强势3,520,215个项目
[45, 60, 280, 'C++'], // 中等活跃1,701,552个项目
[40, 55, 250, 'C#'], // 中等活跃1,478,463个项目
[35, 45, 180, 'PHP'], // 传统Web语言
[25, 25, 80, 'Go'], // 新兴语言,低漏洞密度
[15, 20, 45, 'Rust'], // 安全性较好的新语言
[60, 50, 200, 'Shell'], // 脚本语言
];
isEmpty.value.scatterData = false;
};
// 初始化气泡图
const initScatterChart = () => {
const el = document.getElementById('scatterChart');
if (!el) return;
// 延迟执行以确保DOM已渲染
setTimeout(() => {
const chartEl = document.getElementById('scatterChart');
if (!chartEl) return;
if (chartInstances.value.scatterChart) {
chartInstances.value.scatterChart.dispose();
}
const myChart = echarts.init(chartEl);
chartInstances.value.scatterChart = myChart;
// 数据处理
const data = scatterData.value.scatterData.map(item => ({
value: [item[0], item[1], item[2]],
name: item[3]
}));
// 计算四象限颜色
const getColor = (x: number, y: number) => {
if (x < scatterData.value.midX && y >= scatterData.value.midY) return scatterData.value.quadrantColors.Q1; // 高密度, 低活跃度 - 极高风险
if (x >= scatterData.value.midX && y >= scatterData.value.midY) return scatterData.value.quadrantColors.Q2; // 高密度, 高活跃度 - 高风险
if (x < scatterData.value.midX && y < scatterData.value.midY) return scatterData.value.quadrantColors.Q3; // 低密度, 低活跃度 - 低风险
return scatterData.value.quadrantColors.Q4; // 低密度, 高活跃度 - 健康生态
};
myChart.setOption({
tooltip: {
trigger: 'item',
formatter: (params: any) => {
return `
<div>${params.data.name}</div>
<div>代码活跃度: ${params.data.value[0]}</div>
<div>漏洞密度: ${params.data.value[1]}</div>
<div>漏洞总数: ${params.data.value[2]}</div>
`;
}
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: {
type: 'value',
name: '代码活跃度',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 },
splitLine: { show: true }
},
yAxis: {
type: 'value',
name: '漏洞密度',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 },
splitLine: { show: true }
},
series: [{
type: 'scatter',
data: data,
symbolSize: (val: any) => val[2] / 2, // 根据漏洞总数调整气泡大小
itemStyle: {
color: (params: any) => getColor(params.value[0], params.value[1])
},
label: {
show: true,
formatter: (params: any) => params.data.name,
position: 'inside',
fontSize: 10,
color: '#fff'
}
}]
});
// 监听窗口大小变化,重新调整图表大小
window.addEventListener('resize', () => {
if (chartInstances.value.scatterChart) {
chartInstances.value.scatterChart.resize();
}
});
}, 0);
};
// 在组件挂载后获取数据并初始化图表
onMounted(async () => {
await fetchScatterData();
nextTick(() => {
initScatterChart();
});
});
// 监听数据变化,重新初始化图表
watch(scatterData, () => {
if (!isEmpty.value.scatterData) {
nextTick(() => {
initScatterChart();
});
}
}, { deep: true });
// 在组件卸载前清理资源
onUnmounted(() => {
if (chartInstances.value.scatterChart) {
chartInstances.value.scatterChart.dispose();
}
});
defineExpose({
initScatterChart
});
</script>
<style scoped>
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
.mb-\[1\.5rem\] {
margin-bottom: 1.5rem;
}
/* 与示例一致的空数据样式 */
.num-empty {
height: 10px;
border-top: 2px solid #2951E0;
width: 100%;
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -60,19 +60,14 @@ Vue 3 适配版代码
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue'; import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import worldJson from './world.json'; import worldJson from './world.json';
import { mapVulnStatsPage1, mapRegionStatsPage1, mapDataPage1, mapVulnTypeStatsPage1 } from '@/api/jyh/situation';
// 响应式数据 // 响应式数据
const chartDiv = ref(null); const chartDiv = ref(null);
let myChart = null; let myChart = null;
let timer = null; let timer = null;
const vulnStats = ref([ const vulnStats = ref([]);
{ name: 'XSS 跨站脚本', value: 32.1, color: '#ef4444' },
{ name: 'SQL 注入', value: 11.9, color: '#f97316' },
{ name: '内存缓冲区溢出', value: 10.9, color: '#eab308' },
{ name: '输入验证不当', value: 9.3, color: '#3b82f6' },
{ name: '信息泄露', value: 8.0, color: '#10b981' },
]);
const regionStats = ref([ const regionStats = ref([
{ name: 'United States', score: 435202 }, { name: 'United States', score: 435202 },
@@ -82,11 +77,8 @@ const regionStats = ref([
{ name: 'Germany', score: 126397 }, { name: 'Germany', score: 126397 },
]); ]);
const alerts = [ // 实时情报数据状态
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" }, const alerts = ref([]);
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
];
const currentAlert = ref(null); const currentAlert = ref(null);
let alertIndex = 0; let alertIndex = 0;
@@ -100,35 +92,118 @@ const geoCoordMap = {
'Moscow': [37.6173, 55.7558], 'Moscow': [37.6173, 55.7558],
}; };
// 获取实时情报数据
const fetchAlertsData = async () => {
try {
const response = await mapVulnTypeStatsPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
alerts.value = businessData.data || [];
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
} else {
console.error('获取实时情报数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
alerts.value = [
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
];
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
}
} catch (error) {
console.error('请求实时情报数据时发生错误:', error);
// 发生异常时使用默认数据
alerts.value = [
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
];
if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
}
};
// 逻辑方法 // 逻辑方法
const startAlertRotation = () => { const startAlertRotation = () => {
currentAlert.value = alerts[0]; if (alerts.value.length > 0) {
currentAlert.value = alerts.value[0];
}
timer = setInterval(() => { timer = setInterval(() => {
alertIndex = (alertIndex + 1) % alerts.length; if (alerts.value.length > 0) {
currentAlert.value = null; alertIndex = (alertIndex + 1) % alerts.value.length;
setTimeout(() => { currentAlert.value = null;
currentAlert.value = alerts[alertIndex]; setTimeout(() => {
}, 500); currentAlert.value = alerts.value[alertIndex];
}, 500);
}
}, 3500); }, 3500);
}; };
const setChart = () => { // 地图数据状态
const mapData = ref([]);
const scatterData = ref([]);
// 获取地图数据
const fetchMapData = async () => {
try {
const response = await mapDataPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
mapData.value = businessData.data;
} else {
console.error('获取地图数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
mapData.value = [
{ name: 'China', value: 100 },
{ name: 'United States', value: 80 },
{ name: 'Russia', value: 60 },
{ name: 'Brazil', value: 40 },
{ name: 'Canada', value: 30 },
{ name: 'Australia', value: 20 },
{ name: 'India', value: 50 },
{ name: 'Germany', value: 45 },
{ name: 'United Kingdom', value: 40 },
{ name: 'France', value: 35 },
];
}
} catch (error) {
console.error('请求地图数据时发生错误:', error);
// 发生异常时使用默认数据
mapData.value = [
{ name: 'China', value: 100 },
{ name: 'United States', value: 80 },
{ name: 'Russia', value: 60 },
{ name: 'Brazil', value: 40 },
{ name: 'Canada', value: 30 },
{ name: 'Australia', value: 20 },
{ name: 'India', value: 50 },
{ name: 'Germany', value: 45 },
{ name: 'United Kingdom', value: 40 },
{ name: 'France', value: 35 },
];
}
};
const setChart = async () => {
if (!chartDiv.value) return; if (!chartDiv.value) return;
echarts.registerMap('world', worldJson); echarts.registerMap('world', worldJson);
const mapData = [
{ name: 'China', value: 100 },
{ name: 'United States', value: 80 },
{ name: 'Russia', value: 60 },
{ name: 'Brazil', value: 40 },
{ name: 'Canada', value: 30 },
{ name: 'Australia', value: 20 },
{ name: 'India', value: 50 },
{ name: 'Germany', value: 45 },
{ name: 'United Kingdom', value: 40 },
{ name: 'France', value: 35 },
];
const scatterData = [ // 初始化散点数据
const geoCoordMap = {
'Beijing': [116.407395, 39.904211],
'Shanghai': [121.473701, 31.230416],
'Guangzhou': [113.264385, 23.129112],
'Chengdu': [104.066541, 30.572269],
'Washington': [-77.036871, 38.907192],
'Moscow': [37.6173, 55.7558],
};
const scatterDataLocal = [
{ name: '北京中心', value: [...geoCoordMap['Beijing'], 100] }, { name: '北京中心', value: [...geoCoordMap['Beijing'], 100] },
{ name: '上海节点', value: [...geoCoordMap['Shanghai'], 80] }, { name: '上海节点', value: [...geoCoordMap['Shanghai'], 80] },
{ name: '广州节点', value: [...geoCoordMap['Guangzhou'], 70] }, { name: '广州节点', value: [...geoCoordMap['Guangzhou'], 70] },
@@ -186,13 +261,13 @@ const setChart = () => {
name: 'Threat Distribution', name: 'Threat Distribution',
type: 'map', type: 'map',
geoIndex: 0, geoIndex: 0,
data: mapData data: mapData.value
}, },
{ {
name: 'Key Nodes', name: 'Key Nodes',
type: 'effectScatter', type: 'effectScatter',
coordinateSystem: 'geo', coordinateSystem: 'geo',
data: scatterData, data: scatterDataLocal,
symbolSize: (val) => val[2] / 5, symbolSize: (val) => val[2] / 5,
showEffectOn: 'render', showEffectOn: 'render',
rippleEffect: { rippleEffect: {
@@ -222,10 +297,78 @@ const setChart = () => {
myChart.setOption(option); myChart.setOption(option);
}; };
// 获取漏洞类型统计数据
const fetchVulnStats = async () => {
try {
const response = await mapVulnStatsPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
// 为每个数据项添加颜色
const colors = ['#ef4444', '#f97316', '#eab308', '#3b82f6', '#10b981', '#8b5cf6', '#ec4899', '#06b6d4'];
vulnStats.value = businessData.data.map((item, index) => ({
...item,
color: colors[index % colors.length]
}));
} else {
console.error('获取漏洞类型统计数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
vulnStats.value = [
{ name: 'XSS 跨站脚本', value: 32.1, color: '#ef4444' },
{ name: 'SQL 注入', value: 11.9, color: '#f97316' },
{ name: '内存缓冲区溢出', value: 10.9, color: '#eab308' },
{ name: '输入验证不当', value: 9.3, color: '#3b82f6' },
{ name: '信息泄露', value: 8.0, color: '#10b981' },
];
}
} catch (error) {
console.error('请求漏洞类型统计数据时发生错误:', error);
// 发生异常时使用默认数据
vulnStats.value = [
{ name: 'XSS 跨站脚本', value: 32.1, color: '#ef4444' },
{ name: 'SQL 注入', value: 11.9, color: '#f97316' },
{ name: '内存缓冲区溢出', value: 10.9, color: '#eab308' },
{ name: '输入验证不当', value: 9.3, color: '#3b82f6' },
{ name: '信息泄露', value: 8.0, color: '#10b981' },
];
}
};
// 获取威胁情报地域排行数据
const fetchRegionStats = async () => {
try {
const response = await mapRegionStatsPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
if (businessData?.code === 200) {
regionStats.value = businessData.data;
} else {
console.error('获取威胁情报地域排行数据失败:', businessData?.msg || '未知错误');
// 如果接口调用失败,使用默认数据
regionStats.value = [
{ name: 'United States', score: 435202 },
{ name: 'India', score: 252054 },
{ name: 'China (中国)', score: 184085 },
{ name: 'Brazil', score: 174811 },
{ name: 'Germany', score: 126397 },
];
}
} catch (error) {
console.error('请求威胁情报地域排行数据时发生错误:', error);
// 发生异常时使用默认数据
regionStats.value = [
{ name: 'United States', score: 435202 },
{ name: 'India', score: 252054 },
{ name: 'China (中国)', score: 184085 },
{ name: 'Brazil', score: 174811 },
{ name: 'Germany', score: 126397 },
];
}
};
// 生命周期钩子 // 生命周期钩子
onMounted(async () => { onMounted(async () => {
await nextTick(); // 确保 DOM 已渲染 await nextTick(); // 确保 DOM 已渲染
setChart(); await Promise.all([fetchVulnStats(), fetchRegionStats(), fetchMapData(), fetchAlertsData()]); // 同时获取所有统计数据
await setChart(); // 等待图表设置完成
startAlertRotation(); startAlertRotation();
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
}); });

View File

@@ -0,0 +1,381 @@
<template>
<div class="index-module__NV_5cW__chartCard chartCard__wide mb-[1.5rem]">
<h3 class="chartCard__title">漏洞语言分布</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.languageTrend"></div>
<div v-else id="languageTrendChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { languageTrendPage1 } from '@/api/jyh/situation';
// 空数据标识
const isEmpty = ref({
languageTrend: false
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
languageTrendChart: null
});
// 漏洞语言分布数据 - 响应式数据
const languageTrendData = ref({
years: [] as string[],
series: [] as any[]
});
// 获取后端数据
const fetchLanguageTrendData = async () => {
try {
console.log('开始请求漏洞语言分布数据...');
const response = await languageTrendPage1();
const businessData = response.data; // Axios响应格式业务数据在response.data中
console.log('API响应:', response);
if (businessData?.code === 200) {
console.log('API返回的数据:', businessData.data);
// 处理后端返回的数据格式
const processedData = processBackendData(businessData.data);
languageTrendData.value = processedData;
// 更新空数据标识
isEmpty.value.languageTrend = !processedData.years || processedData.years.length === 0;
} else {
console.error('获取漏洞语言分布数据失败:', businessData?.msg || '未知错误');
// 设置默认数据
setDefaultData();
}
} catch (error) {
console.error('获取漏洞语言分布数据异常:', error);
// 发生异常时设置默认数据
setDefaultData();
}
};
// 处理后端返回的数据格式
const processBackendData = (backendData: any[]) => {
// 按语言分组
const groupedData: { [key: string]: { year: string, value: number }[] } = {};
backendData.forEach(item => {
if (!groupedData[item.name]) {
groupedData[item.name] = [];
}
groupedData[item.name].push({
year: item.year,
value: item.data
});
});
// 提取所有年份并排序
const allYearsSet = new Set<string>();
Object.values(groupedData).forEach(langData => {
langData.forEach(d => allYearsSet.add(d.year));
});
const years = Array.from(allYearsSet).sort();
// 生成系列数据
const series = Object.entries(groupedData).map(([name, data]) => {
// 根据年份顺序填充数据缺失的设为0
const values = years.map(year => {
const found = data.find(d => d.year === year);
return found ? found.value : 0;
});
// 这里可以使用预定义的颜色方案,或者动态生成
const predefinedColors: { [key: string]: string } = {
'JavaScript': '#FAC858',
'Python': '#5470C6',
'HTML/CSS': '#91CC75',
'SQL': '#9254DE',
'Java': '#EE6666',
'TypeScript': '#73C0DE',
'Shell': '#52C41A',
'C++': '#FF6E00',
'C#': '#722ED1',
'Go': '#00DDFF',
'PHP': '#F5222D',
'Rust': '#FC8452'
};
const color = predefinedColors[name] || getRandomColor();
return {
name: name,
type: 'established', // 可以根据实际情况调整
color: color,
data: values
};
});
return {
years: years,
series: series
};
};
// 设置默认数据
const setDefaultData = () => {
languageTrendData.value = {
years: ['2017', '2018', '2019', '2020', '2021', '2022', '2023', '2024'],
series: [
{
name: 'JavaScript',
type: 'established',
color: '#FAC858',
data: [65, 64, 69, 70, 69, 65, 61, 61]
},
{
name: 'Python',
type: 'established',
color: '#5470C6',
data: [32, 41, 49, 55, 52, 53, 54, 60]
},
{
name: 'HTML/CSS',
type: 'established',
color: '#91CC75',
data: [60, 55, 61, 61, 60, 54, 52, 52]
},
{
name: 'SQL',
type: 'established',
color: '#9254DE',
data: [42, 47, 56, 56, 54, 49, 52, 50]
},
{
name: 'Java',
type: 'established',
color: '#EE6666',
data: [47, 51, 50, 54, 49, 48, 49, 49]
},
{
name: 'TypeScript',
type: 'emerging',
color: '#73C0DE',
data: [12, 17, 25, 28, 29, 34, 34, 50]
},
{
name: 'Shell',
type: 'established',
color: '#52C41A',
data: [29, 29, 40, 39, 37, 34, 34, 41]
},
{
name: 'C++',
type: 'established',
color: '#FF6E00',
data: [17, 18, 20, 27, 23, 25, 25, 21]
},
{
name: 'C#',
type: 'established',
color: '#722ED1',
data: [20, 22, 24, 22, 21, 23, 21, 19]
},
{
name: 'Go',
type: 'emerging',
color: '#00DDFF',
data: [8, 12, 18, 19, 17, 19, 17, 14]
},
{
name: 'PHP',
type: 'declining',
color: '#F5222D',
data: [30, 26, 29, 27, 32, 20, 18, 15]
},
{
name: 'Rust',
type: 'emerging',
color: '#FC8452',
data: [2, 2, 5, 7, 6, 9, 10, 10]
}
]
};
isEmpty.value.languageTrend = false;
};
// 生成随机颜色的辅助函数
const getRandomColor = () => {
const colors = ['#FAC858', '#5470C6', '#91CC75', '#9254DE', '#EE6666', '#73C0DE', '#52C41A', '#FF6E00', '#722ED1', '#00DDFF', '#F5222D', '#FC8452'];
return colors[Math.floor(Math.random() * colors.length)];
};
// 初始化漏洞语言分布图表(堆叠面积图)
const initLanguageTrendChart = () => {
const el = document.getElementById('languageTrendChart');
if (!el) return;
// 延迟执行以确保DOM已渲染
setTimeout(() => {
const chartEl = document.getElementById('languageTrendChart');
if (!chartEl) return;
if (chartInstances.value.languageTrendChart) {
chartInstances.value.languageTrendChart.dispose();
}
const myChart = echarts.init(chartEl);
chartInstances.value.languageTrendChart = myChart;
// 准备堆叠面积图数据
const series = languageTrendData.value.series.map(item => ({
name: item.name,
type: 'line',
stack: '总量',
areaStyle: {
color: item.color
},
lineStyle: {
width: 1,
color: item.color
},
symbol: 'none',
data: item.data
}));
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: (params: any) => {
let res = `<div>${params[0].name}年</div>`;
params.forEach((p: any) => {
res += `<div style="display:flex;align-items:center;"><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${p.color};"></span>${p.seriesName}: ${p.value}%</div>`;
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 },
data: languageTrendData.value.series.map(item => item.name)
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [
{
type: 'category',
boundaryGap: false,
data: languageTrendData.value.years,
axisLabel: { fontSize: 12 }
}
],
yAxis: [
{
type: 'value',
name: '使用率(%)',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
}
],
series: series
});
// 监听窗口大小变化,重新调整图表大小
window.addEventListener('resize', () => {
if (chartInstances.value.languageTrendChart) {
chartInstances.value.languageTrendChart.resize();
}
});
}, 0);
};
// 在组件挂载后获取数据并初始化图表
onMounted(async () => {
await fetchLanguageTrendData();
nextTick(() => {
initLanguageTrendChart();
});
});
// 监听数据变化,重新初始化图表
watch(languageTrendData, () => {
if (!isEmpty.value.languageTrend) {
nextTick(() => {
initLanguageTrendChart();
});
}
}, { deep: true });
// 在组件卸载前清理资源
onUnmounted(() => {
if (chartInstances.value.languageTrendChart) {
chartInstances.value.languageTrendChart.dispose();
}
});
defineExpose({
initLanguageTrendChart
});
</script>
<style scoped>
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.chartCard__wide {
grid-column: 1 / -1; /* 跨列显示 */
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
.mb-\[1\.5rem\] {
margin-bottom: 1.5rem;
}
/* 与示例一致的空数据样式 */
.num-empty {
height: 10px;
border-top: 2px solid #2951E0;
width: 100%;
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.chart {
width: 100%;
height: 100%;
}
</style>

View File

@@ -0,0 +1,285 @@
<template>
<div class="index-module__NV_5cW__chartCard mb-[1.5rem]">
<h3 class="chartCard__title">漏洞等级分布</h3>
<div class="chartCard__container">
<div class="num-empty" v-if="isEmpty.vulnerabilitySeverity"></div>
<div v-else id="vulnerabilitySeverityChart" class="chart"></div>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, nextTick, onMounted, onUnmounted, watch } from 'vue';
import * as echarts from 'echarts';
import { vulnerabilitySeverityPage1 } from '@/api/jyh/situation';
// 空数据标识
const isEmpty = ref({
vulnerabilitySeverity: false
});
// 图表实例存储用于resize时销毁重绘
const chartInstances = ref<{ [key: string]: echarts.ECharts | null }>({
vulnerabilitySeverityChart: null
});
// 漏洞等级分布数据 - 响应式数据
const vulnerabilitySeverityData = ref({
categories: [] as string[],
series: [] as any[]
});
// 获取后端数据
const fetchVulnerabilitySeverityData = async() => {
try {
console.log('开始请求漏洞等级分布数据...');
const response = await vulnerabilitySeverityPage1();
console.log('API响应:', response);
const severityData = response.data; // Axios响应格式业务数据在response.data中
if (severityData?.code === 200) {
console.log('API返回的数据:', severityData.data);
// 处理后端返回的数据格式
const processedData = processBackendSeverityData(severityData.data);
vulnerabilitySeverityData.value = processedData;
// 更新空数据标识
isEmpty.value.vulnerabilitySeverity = !processedData.categories || processedData.categories.length === 0;
} else {
console.error('获取漏洞等级分布数据失败:', severityData?.msg || '未知错误');
// 设置默认数据
setDefaultSeverityData();
}
} catch (error) {
console.error('获取漏洞等级分布数据异常:', error);
// 发生异常时设置默认数据
setDefaultSeverityData();
}
};
// 处理后端返回的数据格式
const processBackendSeverityData = (backendData: any[]) => {
// 提取季度数据
const categories = backendData.map(item => item.quarter);
// 生成系列数据
const series = [
{
name: '高危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#FF4D4F' },
barWidth: 16,
data: backendData.map(item => item.highRisk)
},
{
name: '中危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#FAAD14' },
barWidth: 16,
data: backendData.map(item => item.mediumRisk)
},
{
name: '低危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#36CFC9' },
barWidth: 16,
data: backendData.map(item => item.lowRisk)
}
];
return {
categories: categories,
series: series
};
};
// 设置默认数据
const setDefaultSeverityData = () => {
vulnerabilitySeverityData.value = {
categories: ['2024Q1', '2024Q2', '2024Q3', '2024Q4', '2025Q1', '2025Q2', '2025Q3', '2025Q4'],
series: [
{
name: '高危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#FF4D4F' },
barWidth: 16,
data: [35, 42, 58, 45, 67, 78, 62, 85]
},
{
name: '中危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#FAAD14' },
barWidth: 16,
data: [65, 72, 88, 95, 112, 105, 120, 135]
},
{
name: '低危漏洞',
type: 'bar',
stack: 'total',
itemStyle: { color: '#36CFC9' },
barWidth: 16,
data: [120, 135, 150, 142, 165, 180, 175, 190]
}
]
};
isEmpty.value.vulnerabilitySeverity = false;
};
// 初始化漏洞等级分布图表
const initVulnerabilitySeverityChart = () => {
const el = document.getElementById('vulnerabilitySeverityChart');
if (!el) return;
// 延迟执行以确保DOM已渲染
setTimeout(() => {
const chartEl = document.getElementById('vulnerabilitySeverityChart');
if (!chartEl) return;
if (chartInstances.value.vulnerabilitySeverityChart) {
chartInstances.value.vulnerabilitySeverityChart.dispose();
}
const myChart = echarts.init(chartEl);
chartInstances.value.vulnerabilitySeverityChart = myChart;
myChart.setOption({
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
formatter: (params: any) => {
let res = `<div>${params[0].name}</div>`;
let total = 0;
params.forEach((p: any) => {
total += p.value;
});
params.forEach((p: any) => {
const percentage = ((p.value / total) * 100).toFixed(1);
res += `<div style="display:flex;align-items:center;"><span style="display:inline-block;margin-right:5px;border-radius:10px;width:10px;height:10px;background-color:${p.color};"></span>${p.seriesName}: ${p.value} (${percentage}%)</div>`;
});
return res;
}
},
legend: {
top: 0,
textStyle: { fontSize: 12 },
data: vulnerabilitySeverityData.value.series.map(item => item.name)
},
grid: { left: '3%', right: '4%', bottom: '3%', top: '15%', containLabel: true },
xAxis: [
{
type: 'category',
data: vulnerabilitySeverityData.value.categories,
axisLabel: { fontSize: 12 }
}
],
yAxis: [
{
type: 'value',
name: '漏洞数量',
nameTextStyle: { fontSize: 12 },
axisLabel: { fontSize: 12 }
}
],
series: vulnerabilitySeverityData.value.series
});
// 监听窗口大小变化,重新调整图表大小
window.addEventListener('resize', () => {
if (chartInstances.value.vulnerabilitySeverityChart) {
chartInstances.value.vulnerabilitySeverityChart.resize();
}
});
}, 0);
};
// 在组件挂载后获取数据并初始化图表
onMounted(async () => {
await fetchVulnerabilitySeverityData();
nextTick(() => {
initVulnerabilitySeverityChart();
});
});
// 监听数据变化,重新初始化图表
watch(vulnerabilitySeverityData, () => {
if (!isEmpty.value.vulnerabilitySeverity) {
nextTick(() => {
initVulnerabilitySeverityChart();
});
}
}, { deep: true });
// 在组件卸载前清理资源
onUnmounted(() => {
if (chartInstances.value.vulnerabilitySeverityChart) {
chartInstances.value.vulnerabilitySeverityChart.dispose();
}
});
defineExpose({
initVulnerabilitySeverityChart
});
</script>
<style scoped>
.index-module__NV_5cW__chartCard {
-webkit-backdrop-filter: blur(10px);
backdrop-filter: blur(10px);
background: #ffffffe6;
border: 1px solid #ffffff4d;
border-radius: 1rem;
padding: 1.5rem;
box-shadow: 0 4px 16px #00000014;
transition: all 0.3s;
}
.index-module__NV_5cW__chartCard:hover {
border-color: #3b82f64d;
transform: translateY(-4px);
box-shadow: 0 8px 24px #0000001f;
}
.chartCard__title {
color: #333;
font-size: 1.1rem;
font-weight: 600;
margin: 0 0 1rem 0;
text-align: left;
}
.chartCard__container {
width: 100%;
height: 300px; /* 图表高度 */
position: relative;
}
.mb-\[1\.5rem\] {
margin-bottom: 1.5rem;
}
/* 与示例一致的空数据样式 */
.num-empty {
height: 10px;
border-top: 2px solid #2951E0;
width: 100%;
background: linear-gradient(180deg, rgba(41, 81, 224, 0.12) 0%, rgba(41, 81, 224, 0.04) 100%);
position: absolute;
top: 50%;
left: 0;
transform: translateY(-50%);
}
.chart {
width: 100%;
height: 100%;
}
</style>

File diff suppressed because it is too large Load Diff

View File

@@ -3,8 +3,8 @@ import { defineConfig, type PluginOption, loadEnv } from 'vite';
import vue from '@vitejs/plugin-vue'; import vue from '@vitejs/plugin-vue';
import { visualizer } from "rollup-plugin-visualizer"; import { visualizer } from "rollup-plugin-visualizer";
import dayjs from "dayjs"; import dayjs from "dayjs";
import basicSSL from '@vitejs/plugin-basic-ssl'; // import basicSSL from '@vitejs/plugin-basic-ssl';
import legacy from '@vitejs/plugin-legacy' // import legacy from '@vitejs/plugin-legacy'
const fs = require('node:fs') const fs = require('node:fs')
export default (config: any) => { export default (config: any) => {
@@ -20,10 +20,10 @@ export default (config: any) => {
} }
} }
}), }),
// 浏览器兼容性 // // 浏览器兼容性
legacy({ // legacy({
targets: ['defaults', 'chrome >= 78', 'safari >= 14'], // targets: ['defaults', 'chrome >= 78', 'safari >= 14'],
}), // }),
visualizer({ visualizer({
open: config.mode === 'analysis', open: config.mode === 'analysis',
}) as PluginOption, }) as PluginOption,
@@ -38,11 +38,17 @@ export default (config: any) => {
}, },
proxy: { proxy: {
// 配置跨域代理 // 配置跨域代理
'/gateway': { // '/gateway/official/u-auth/index/getCaptcha': {
// // target: 'http://223.76.236.155:20011', // 目标服务器地址
// target: 'http://222.20.126.217:9700', // 目标服务器地址
// changeOrigin: true, // 允许跨域
// rewrite: (path) => path.replace('/gateway/official/u-auth', '/gateway/u-auth') // 重写路径
// },
'/api': {
// target: 'http://223.76.236.155:20011', // 目标服务器地址 // target: 'http://223.76.236.155:20011', // 目标服务器地址
target: 'http://222.20.126.217:10010', // 目标服务器地址 target: 'http://222.20.126.217:8100', // 目标服务器地址
changeOrigin: true, // 允许跨域 changeOrigin: true, // 允许跨域
rewrite: (path) => path.replace(/^\/gateway/, '/gateway'), // 重写路径 rewrite: (path) => path.replace(/^\/api/, '/api'), // 重写路径
} }
} }
}, },
@@ -60,16 +66,23 @@ export default (config: any) => {
'@': fileURLToPath(new URL('./src', import.meta.url)) '@': fileURLToPath(new URL('./src', import.meta.url))
} }
}, },
optimizeDeps: {
include: ['vue', 'dayjs', 'axios'], // 强制预构建常用包
},
build: { build: {
outDir: 'openRepoPortal', // 将打包后的文件夹名改为 openRepoPortal outDir: 'SAP', // 将打包后的文件夹名改为 openRepoPortal
// sourcemap: config.mode !== 'production', // sourcemap: config.mode !== 'production',
minify: 'terser', // 确保使用 terser 进行压缩 // 减少 chunk 大小计算,加快打包速度
terserOptions: { reportCompressedSize: false,
compress: { // 提高超大静态资源临界值
drop_console: true, // 移除 console.log assetsInlineLimit: 4096,
}, minify: 'esbuild',
}, // minify: 'terser', // 确保使用 terser 进行压缩
// terserOptions: {
// compress: {
// drop_console: true, // 移除 console.log
// },
// },
}, },
define: { define: {
@@ -83,7 +96,7 @@ export default (config: any) => {
) { ) {
if (type === 'asset') { if (type === 'asset') {
// return env.VITE_STATIC_HOST + '/' + filename; // return env.VITE_STATIC_HOST + '/' + filename;
return `/openRepoPortal/${filename}`; return `/SAP/${filename}`;
} }
} }
} }

250
yarn.lock
View File

@@ -20,6 +20,26 @@
"@jridgewell/gen-mapping" "^0.3.5" "@jridgewell/gen-mapping" "^0.3.5"
"@jridgewell/trace-mapping" "^0.3.24" "@jridgewell/trace-mapping" "^0.3.24"
"@ant-design/colors@^6.0.0":
version "6.0.0"
resolved "https://registry.npmmirror.com/@ant-design/colors/-/colors-6.0.0.tgz"
integrity sha512-qAZRvPzfdWHtfameEGP2Qvuf838NhergR35o+EuVyB5XvSA98xod5r4utvi4TJ3ywmevm290g9nsCG5MryrdWQ==
dependencies:
"@ctrl/tinycolor" "^3.4.0"
"@ant-design/icons-svg@^4.2.1":
version "4.4.2"
resolved "https://registry.npmmirror.com/@ant-design/icons-svg/-/icons-svg-4.4.2.tgz"
integrity sha512-vHbT+zJEVzllwP+CM+ul7reTEfBR0vgxFe7+lREAsAA7YGsYpboiq2sQNeQeRvh09GfQgs/GyFEvZpJ9cLXpXA==
"@ant-design/icons-vue@^7.0.0":
version "7.0.1"
resolved "https://registry.npmmirror.com/@ant-design/icons-vue/-/icons-vue-7.0.1.tgz"
integrity sha512-eCqY2unfZK6Fe02AwFlDHLfoyEFreP6rBwAZMIJ1LugmfMiVgwWDYlp1YsRugaPtICYOabV1iWxXdP12u9U43Q==
dependencies:
"@ant-design/colors" "^6.0.0"
"@ant-design/icons-svg" "^4.2.1"
"@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2": "@babel/code-frame@^7.23.5", "@babel/code-frame@^7.24.1", "@babel/code-frame@^7.24.2":
version "7.24.2" version "7.24.2"
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz" resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz"
@@ -913,7 +933,7 @@
resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz" resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz"
integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA== integrity sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==
"@babel/runtime@^7.8.4": "@babel/runtime@^7.10.5", "@babel/runtime@^7.8.4":
version "7.24.1" version "7.24.1"
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz" resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz"
integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ== integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==
@@ -958,15 +978,30 @@
resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz" resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz"
integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A== integrity sha512-s3jaWicZd0pkP0jf5ysyHUI/RE7MHos6qlToFcGWXVp+ykHOy77OUMrfbgJ9it2C5bow7OIQwYYaHjk9XlBQ2A==
"@ctrl/tinycolor@^3.4.0", "@ctrl/tinycolor@^3.5.0":
version "3.6.1"
resolved "https://registry.npmmirror.com/@ctrl/tinycolor/-/tinycolor-3.6.1.tgz"
integrity sha512-SITSV6aIXsuVNV3f3O0f2n/cgyEDWoSqtZMYiAmcsYHydcKrOz3gUxB/iXd/Qf08+IZX4KpgNbvUdMBmWz+kcA==
"@devui-design/icons@^1.3.0", "@devui-design/icons@^1.4.0": "@devui-design/icons@^1.3.0", "@devui-design/icons@^1.4.0":
version "1.4.0" version "1.4.0"
resolved "https://mirrors.cloud.tencent.com/npm/@devui-design/icons/-/icons-1.4.0.tgz" resolved "https://mirrors.cloud.tencent.com/npm/@devui-design/icons/-/icons-1.4.0.tgz"
integrity sha512-taAX1RNW0QHUqQTRPqLTYTB2PZIqUplhWeF4hcmWkSTjpWlDNI40DssG/WRb3sISkfBk/4BMUxxC5XeTL3jo7A== integrity sha512-taAX1RNW0QHUqQTRPqLTYTB2PZIqUplhWeF4hcmWkSTjpWlDNI40DssG/WRb3sISkfBk/4BMUxxC5XeTL3jo7A==
"@esbuild/win32-x64@0.18.20": "@emotion/hash@^0.9.0":
version "0.9.2"
resolved "https://registry.npmmirror.com/@emotion/hash/-/hash-0.9.2.tgz"
integrity sha512-MyqliTZGuOm3+5ZRSaaBGP3USLw6+EGykkwZns2EPC5g8jJ4z9OrdZY9apkl3+UP9+sdz76YYkwCKP5gh8iY3g==
"@emotion/unitless@^0.8.0":
version "0.8.1"
resolved "https://registry.npmmirror.com/@emotion/unitless/-/unitless-0.8.1.tgz"
integrity sha512-KOEGMu6dmJZtpadb476IsZBclKvILjopjUii3V+7MnXIQCYh8W3NgNcgwo21n9LXZX6EDIKvqfjYxXebDwxKmQ==
"@esbuild/darwin-arm64@0.18.20":
version "0.18.20" version "0.18.20"
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz" resolved "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz"
integrity sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ== integrity sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==
"@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0": "@eslint-community/eslint-utils@^4.2.0", "@eslint-community/eslint-utils@^4.4.0":
version "4.4.0" version "4.4.0"
@@ -1235,6 +1270,14 @@
"@sentry/types" "7.108.0" "@sentry/types" "7.108.0"
"@sentry/utils" "7.108.0" "@sentry/utils" "7.108.0"
"@simonwep/pickr@~1.8.0":
version "1.8.2"
resolved "https://registry.npmmirror.com/@simonwep/pickr/-/pickr-1.8.2.tgz"
integrity sha512-/l5w8BIkrpP6n1xsetx9MWPWlU6OblN5YgZZphxan0Tq4BByTCETL6lyIeY8lagalS2Nbt4F2W034KHLIiunKA==
dependencies:
core-js "^3.15.1"
nanopop "^2.1.0"
"@tsconfig/node18@^2.0.1": "@tsconfig/node18@^2.0.1":
version "2.0.1" version "2.0.1"
resolved "https://registry.npmjs.org/@tsconfig/node18/-/node18-2.0.1.tgz" resolved "https://registry.npmjs.org/@tsconfig/node18/-/node18-2.0.1.tgz"
@@ -1706,6 +1749,34 @@ ansi-styles@^6.1.0:
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz" resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug== integrity sha512-bN798gFfQX+viw3R7yrGWRqnrN2oRkEkUjjl4JNn4E8GxxbjtG3FbrEIIY3l8/hrwUwIeCZvi4QuOTP4MErVug==
ant-design-vue@4.x:
version "4.2.6"
resolved "https://registry.npmmirror.com/ant-design-vue/-/ant-design-vue-4.2.6.tgz"
integrity sha512-t7eX13Yj3i9+i5g9lqFyYneoIb3OzTvQjq9Tts1i+eiOd3Eva/6GagxBSXM1fOCjqemIu0FYVE1ByZ/38epR3Q==
dependencies:
"@ant-design/colors" "^6.0.0"
"@ant-design/icons-vue" "^7.0.0"
"@babel/runtime" "^7.10.5"
"@ctrl/tinycolor" "^3.5.0"
"@emotion/hash" "^0.9.0"
"@emotion/unitless" "^0.8.0"
"@simonwep/pickr" "~1.8.0"
array-tree-filter "^2.1.0"
async-validator "^4.0.0"
csstype "^3.1.1"
dayjs "^1.10.5"
dom-align "^1.12.1"
dom-scroll-into-view "^2.0.0"
lodash "^4.17.21"
lodash-es "^4.17.15"
resize-observer-polyfill "^1.5.1"
scroll-into-view-if-needed "^2.2.25"
shallow-equal "^1.0.0"
stylis "^4.1.3"
throttle-debounce "^5.0.0"
vue-types "^3.0.0"
warning "^4.0.0"
any-promise@^1.0.0: any-promise@^1.0.0:
version "1.3.0" version "1.3.0"
resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz" resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
@@ -1737,6 +1808,11 @@ array-buffer-byte-length@^1.0.1:
call-bind "^1.0.5" call-bind "^1.0.5"
is-array-buffer "^3.0.4" is-array-buffer "^3.0.4"
array-tree-filter@^2.1.0:
version "2.1.0"
resolved "https://registry.npmmirror.com/array-tree-filter/-/array-tree-filter-2.1.0.tgz"
integrity sha512-4ROwICNlNw/Hqa9v+rk5h22KjmzB1JGTMVKP2AKJBOCgb0yL0ASf0+YvCcLNNwquOHNX48jkeZIJ3a+oOQqKcw==
array-union@^2.1.0: array-union@^2.1.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz" resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
@@ -1756,7 +1832,14 @@ arraybuffer.prototype.slice@^1.0.3:
is-array-buffer "^3.0.4" is-array-buffer "^3.0.4"
is-shared-array-buffer "^1.0.2" is-shared-array-buffer "^1.0.2"
async-validator@^4.0.7: asn1@^0.2.6:
version "0.2.6"
resolved "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz"
integrity sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==
dependencies:
safer-buffer "~2.1.0"
async-validator@^4.0.0, async-validator@^4.0.7:
version "4.2.5" version "4.2.5"
resolved "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz" resolved "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz"
integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg== integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
@@ -1823,6 +1906,13 @@ balanced-match@^1.0.0:
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz" resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw== integrity sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==
bcrypt-pbkdf@^1.0.2:
version "1.0.2"
resolved "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz"
integrity sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==
dependencies:
tweetnacl "^0.14.3"
binary-extensions@^2.0.0: binary-extensions@^2.0.0:
version "2.3.0" version "2.3.0"
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz" resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz"
@@ -1870,6 +1960,11 @@ buffer-from@^1.0.0:
resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz" resolved "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz"
integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ== integrity sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==
buildcheck@~0.0.6:
version "0.0.7"
resolved "https://registry.npmjs.org/buildcheck/-/buildcheck-0.0.7.tgz"
integrity sha512-lHblz4ahamxpTmnsk+MNTRWsjYKv965MwOrSJyeD588rR3Jcu7swE+0wN5F+PbL5cjgu/9ObkhfzEPuofEMwLA==
call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7: call-bind@^1.0.2, call-bind@^1.0.5, call-bind@^1.0.6, call-bind@^1.0.7:
version "1.0.7" version "1.0.7"
resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz" resolved "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz"
@@ -2040,6 +2135,11 @@ commander@7:
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz" resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw== integrity sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==
compute-scroll-into-view@^1.0.20:
version "1.0.20"
resolved "https://registry.npmmirror.com/compute-scroll-into-view/-/compute-scroll-into-view-1.0.20.tgz"
integrity sha512-UCB0ioiyj8CRjtrvaceBLqqhZCVP+1B8+NWQhmdsm0VXOJtobBCf1dBQmebCCo34qZmUwZfIH2MZLqNHazrfjg==
computeds@^0.0.1: computeds@^0.0.1:
version "0.0.1" version "0.0.1"
resolved "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz" resolved "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz"
@@ -2062,11 +2162,19 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1:
dependencies: dependencies:
browserslist "^4.23.0" browserslist "^4.23.0"
core-js@^3.31.1: core-js@^3.15.1, core-js@^3.31.1:
version "3.36.1" version "3.36.1"
resolved "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz" resolved "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz"
integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA== integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==
cpu-features@~0.0.10:
version "0.0.10"
resolved "https://registry.npmjs.org/cpu-features/-/cpu-features-0.0.10.tgz"
integrity sha512-9IkYqtX3YHPCzoVg1Py+o9057a3i0fp7S530UWokCSaFVTc7CwXPRiOjRjBQQ18ZCNafx78YfnG+HALxtVmOGA==
dependencies:
buildcheck "~0.0.6"
nan "^2.19.0"
crc-32@~1.2.0, crc-32@~1.2.1: crc-32@~1.2.0, crc-32@~1.2.1:
version "1.2.2" version "1.2.2"
resolved "https://mirrors.cloud.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz" resolved "https://mirrors.cloud.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz"
@@ -2107,7 +2215,7 @@ cssfilter@0.0.10:
resolved "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz" resolved "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz"
integrity sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw== integrity sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==
csstype@^3.1.3: csstype@^3.1.1, csstype@^3.1.3:
version "3.1.3" version "3.1.3"
resolved "https://mirrors.cloud.tencent.com/npm/csstype/-/csstype-3.1.3.tgz" resolved "https://mirrors.cloud.tencent.com/npm/csstype/-/csstype-3.1.3.tgz"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw== integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
@@ -2649,7 +2757,7 @@ data-view-byte-offset@^1.0.0:
es-errors "^1.3.0" es-errors "^1.3.0"
is-data-view "^1.0.1" is-data-view "^1.0.1"
dayjs@^1.11.13, dayjs@^1.11.3: dayjs@^1.10.5, dayjs@^1.11.13, dayjs@^1.11.3:
version "1.11.13" version "1.11.13"
resolved "https://mirrors.cloud.tencent.com/npm/dayjs/-/dayjs-1.11.13.tgz" resolved "https://mirrors.cloud.tencent.com/npm/dayjs/-/dayjs-1.11.13.tgz"
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg== integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
@@ -2760,6 +2868,16 @@ doctrine@^3.0.0:
dependencies: dependencies:
esutils "^2.0.2" esutils "^2.0.2"
dom-align@^1.12.1:
version "1.12.4"
resolved "https://registry.npmmirror.com/dom-align/-/dom-align-1.12.4.tgz"
integrity sha512-R8LUSEay/68zE5c8/3BDxiTEvgb4xZTF0RKmAHfiEVN3klfIpXfi2/QCoiWPccVQ0J/ZGdz9OjzL4uJEP/MRAw==
dom-scroll-into-view@^2.0.0:
version "2.0.1"
resolved "https://registry.npmmirror.com/dom-scroll-into-view/-/dom-scroll-into-view-2.0.1.tgz"
integrity sha512-bvVTQe1lfaUr1oFzZX80ce9KLDlZ3iU+XGNE/bz9HnGdklTieqsbmsLHe+rT2XWqopvL0PckkYqN7ksmm5pe3w==
dompurify@^3.0.5: dompurify@^3.0.5:
version "3.0.11" version "3.0.11"
resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz" resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz"
@@ -2775,6 +2893,14 @@ eastasianwidth@^0.2.0:
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz" resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA== integrity sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==
echarts@^6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz"
integrity sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==
dependencies:
tslib "2.3.0"
zrender "6.0.0"
echarts@5.3.3: echarts@5.3.3:
version "5.3.3" version "5.3.3"
resolved "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz" resolved "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz"
@@ -3213,6 +3339,11 @@ fs.realpath@^1.0.0:
resolved "https://mirrors.cloud.tencent.com/npm/fs.realpath/-/fs.realpath-1.0.0.tgz" resolved "https://mirrors.cloud.tencent.com/npm/fs.realpath/-/fs.realpath-1.0.0.tgz"
integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw== integrity sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==
fsevents@~2.3.2:
version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
function-bind@^1.1.2: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@@ -3629,6 +3760,11 @@ is-path-inside@^3.0.3:
resolved "https://mirrors.cloud.tencent.com/npm/is-path-inside/-/is-path-inside-3.0.3.tgz" resolved "https://mirrors.cloud.tencent.com/npm/is-path-inside/-/is-path-inside-3.0.3.tgz"
integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ== integrity sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==
is-plain-object@3.0.1:
version "3.0.1"
resolved "https://registry.npmmirror.com/is-plain-object/-/is-plain-object-3.0.1.tgz"
integrity sha512-Xnpx182SBMrr/aBik8y+GuR4U1L9FqMSojwDQwPMmxyC6bvEqly9UBCxhauBF5vNh2gwWJNX6oDV7O+OM4z34g==
is-regex@^1.1.4: is-regex@^1.1.4:
version "1.1.4" version "1.1.4"
resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz" resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz"
@@ -3708,7 +3844,7 @@ js-cookie@^3.0.5:
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz" resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz"
integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw== integrity sha512-cEiJEAEoIbWfCZYKWhVwFuvPX1gETRYPw6LlaTKoxD3s2AkXzkCjnp6h0V77ozyqj0jakteJ4YqDJT830+lVGw==
js-tokens@^4.0.0: "js-tokens@^3.0.0 || ^4.0.0", js-tokens@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz" resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ== integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
@@ -3720,6 +3856,11 @@ js-yaml@^4.1.0:
dependencies: dependencies:
argparse "^2.0.1" argparse "^2.0.1"
jsencrypt@^3.5.4:
version "3.5.4"
resolved "https://registry.npmjs.org/jsencrypt/-/jsencrypt-3.5.4.tgz"
integrity sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA==
jsesc@^2.5.1: jsesc@^2.5.1:
version "2.5.2" version "2.5.2"
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz" resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz"
@@ -3856,7 +3997,7 @@ locate-path@^6.0.0:
dependencies: dependencies:
p-locate "^5.0.0" p-locate "^5.0.0"
lodash-es@^4.17.20, lodash-es@^4.17.21: lodash-es@^4.17.15, lodash-es@^4.17.20, lodash-es@^4.17.21:
version "4.17.21" version "4.17.21"
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz" resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw== integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
@@ -3876,6 +4017,13 @@ lodash@^4.17.15, lodash@^4.17.21:
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz" resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg== integrity sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==
loose-envify@^1.0.0:
version "1.4.0"
resolved "https://registry.npmmirror.com/loose-envify/-/loose-envify-1.4.0.tgz"
integrity sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==
dependencies:
js-tokens "^3.0.0 || ^4.0.0"
lottie-web@^5.12.2: lottie-web@^5.12.2:
version "5.12.2" version "5.12.2"
resolved "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz" resolved "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz"
@@ -4072,11 +4220,21 @@ mz@^2.7.0:
object-assign "^4.0.1" object-assign "^4.0.1"
thenify-all "^1.0.0" thenify-all "^1.0.0"
nan@^2.19.0, nan@^2.23.0:
version "2.25.0"
resolved "https://registry.npmjs.org/nan/-/nan-2.25.0.tgz"
integrity sha512-0M90Ag7Xn5KMLLZ7zliPWP3rT90P6PN+IzVFS0VqmnPktBk3700xUVv8Ikm9EUaUE5SDWdp/BIxdENzVznpm1g==
nanoid@^3.3.8: nanoid@^3.3.8:
version "3.3.11" version "3.3.11"
resolved "https://mirrors.cloud.tencent.com/npm/nanoid/-/nanoid-3.3.11.tgz" resolved "https://mirrors.cloud.tencent.com/npm/nanoid/-/nanoid-3.3.11.tgz"
integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w== integrity sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==
nanopop@^2.1.0:
version "2.4.2"
resolved "https://registry.npmmirror.com/nanopop/-/nanopop-2.4.2.tgz"
integrity sha512-NzOgmMQ+elxxHeIha+OG/Pv3Oc3p4RU2aBhwWwAqDpXrdTbtRylbRLQztLy8dMMwfl6pclznBdfUhccEn9ZIzw==
natural-compare-lite@^1.4.0: natural-compare-lite@^1.4.0:
version "1.4.0" version "1.4.0"
resolved "https://mirrors.cloud.tencent.com/npm/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz" resolved "https://mirrors.cloud.tencent.com/npm/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz"
@@ -4519,6 +4677,11 @@ require-directory@^2.1.1:
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz" resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q== integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
resize-observer-polyfill@^1.5.1:
version "1.5.1"
resolved "https://registry.npmmirror.com/resize-observer-polyfill/-/resize-observer-polyfill-1.5.1.tgz"
integrity sha512-LwZrotdHOo12nQuZlHEmtuXdqGoOD0OhaxopaNFxWzInpEgaLWoVuAMbTzixuosCx2nEG58ngzW3vxdWoxIgdg==
resolve-from@^4.0.0: resolve-from@^4.0.0:
version "4.0.0" version "4.0.0"
resolved "https://mirrors.cloud.tencent.com/npm/resolve-from/-/resolve-from-4.0.0.tgz" resolved "https://mirrors.cloud.tencent.com/npm/resolve-from/-/resolve-from-4.0.0.tgz"
@@ -4605,7 +4768,7 @@ safe-regex-test@^1.0.3:
es-errors "^1.3.0" es-errors "^1.3.0"
is-regex "^1.1.4" is-regex "^1.1.4"
"safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0": "safer-buffer@>= 2.1.2 < 3", "safer-buffer@>= 2.1.2 < 3.0.0", safer-buffer@~2.1.0:
version "2.1.2" version "2.1.2"
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz" resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg== integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
@@ -4619,6 +4782,13 @@ sass@*, sass@^1.63.6:
immutable "^4.0.0" immutable "^4.0.0"
source-map-js ">=0.6.2 <2.0.0" source-map-js ">=0.6.2 <2.0.0"
scroll-into-view-if-needed@^2.2.25:
version "2.2.31"
resolved "https://registry.npmmirror.com/scroll-into-view-if-needed/-/scroll-into-view-if-needed-2.2.31.tgz"
integrity sha512-dGCXy99wZQivjmjIqihaBQNjryrz5rueJY7eHfTdyWEiR4ttYpsajb14rn9s5d4DY4EcY6+4+U/maARBXJedkA==
dependencies:
compute-scroll-into-view "^1.0.20"
select@^1.1.2: select@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz" resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz"
@@ -4687,6 +4857,11 @@ set-function-name@^2.0.1:
functions-have-names "^1.2.3" functions-have-names "^1.2.3"
has-property-descriptors "^1.0.2" has-property-descriptors "^1.0.2"
shallow-equal@^1.0.0:
version "1.2.1"
resolved "https://registry.npmmirror.com/shallow-equal/-/shallow-equal-1.2.1.tgz"
integrity sha512-S4vJDjHHMBaiZuT9NPb616CSmLf618jawtv3sufLl6ivK8WocjAo58cXwbRV1cgqxH0Qbv+iUt6m05eqEa2IRA==
shebang-command@^1.2.0: shebang-command@^1.2.0:
version "1.2.0" version "1.2.0"
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz" resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz"
@@ -4797,6 +4972,17 @@ ssf@~0.11.2:
dependencies: dependencies:
frac "~1.1.2" frac "~1.1.2"
ssh2@^1.17.0:
version "1.17.0"
resolved "https://registry.npmjs.org/ssh2/-/ssh2-1.17.0.tgz"
integrity sha512-wPldCk3asibAjQ/kziWQQt1Wh3PgDFpC0XpwclzKcdT1vql6KeYxf5LIt4nlFkUeR8WuphYMKqUA56X4rjbfgQ==
dependencies:
asn1 "^0.2.6"
bcrypt-pbkdf "^1.0.2"
optionalDependencies:
cpu-features "~0.0.10"
nan "^2.23.0"
"string-width-cjs@npm:string-width@^4.2.0": "string-width-cjs@npm:string-width@^4.2.0":
version "4.2.3" version "4.2.3"
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz" resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
@@ -4893,7 +5079,7 @@ strip-json-comments@^3.1.1:
resolved "https://mirrors.cloud.tencent.com/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz" resolved "https://mirrors.cloud.tencent.com/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig== integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
stylis@^4.0.10: stylis@^4.0.10, stylis@^4.1.3:
version "4.3.1" version "4.3.1"
resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz" resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz"
integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ== integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==
@@ -4932,7 +5118,7 @@ supports-preserve-symlinks-flag@^1.0.0:
swiper@^10.3.1: swiper@^10.3.1:
version "10.3.1" version "10.3.1"
resolved "https://mirrors.cloud.tencent.com/npm/swiper/-/swiper-10.3.1.tgz" resolved "https://registry.npmmirror.com/swiper/-/swiper-10.3.1.tgz"
integrity sha512-24Wk3YUdZHxjc9faID97GTu6xnLNia+adMt6qMTZG/HgdSUt4fS0REsGUXJOgpTED0Amh/j+gRGQxsLayJUlBQ== integrity sha512-24Wk3YUdZHxjc9faID97GTu6xnLNia+adMt6qMTZG/HgdSUt4fS0REsGUXJOgpTED0Amh/j+gRGQxsLayJUlBQ==
systemjs@^6.14.1: systemjs@^6.14.1:
@@ -4997,6 +5183,11 @@ thenify-all@^1.0.0:
dependencies: dependencies:
any-promise "^1.0.0" any-promise "^1.0.0"
throttle-debounce@^5.0.0:
version "5.0.2"
resolved "https://registry.npmmirror.com/throttle-debounce/-/throttle-debounce-5.0.2.tgz"
integrity sha512-B71/4oyj61iNH0KeCamLuE2rmKuTO5byTOSVwECM5FA7TiAiAW+UqTKZ9ERueC4qvgSttUhdmq1mXC3kJqGX7A==
tiny-emitter@^2.0.0: tiny-emitter@^2.0.0:
version "2.1.0" version "2.1.0"
resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz" resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz"
@@ -5036,6 +5227,11 @@ tsutils@^3.21.0:
dependencies: dependencies:
tslib "^1.8.1" tslib "^1.8.1"
tweetnacl@^0.14.3:
version "0.14.5"
resolved "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz"
integrity sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA==
type-check@^0.4.0, type-check@~0.4.0: type-check@^0.4.0, type-check@~0.4.0:
version "0.4.0" version "0.4.0"
resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz" resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
@@ -5259,6 +5455,11 @@ vue-eslint-parser@^9.1.1, vue-eslint-parser@^9.4.2:
lodash "^4.17.21" lodash "^4.17.21"
semver "^7.3.6" semver "^7.3.6"
vue-fast-marquee@^1.0.6:
version "1.0.6"
resolved "https://registry.npmjs.org/vue-fast-marquee/-/vue-fast-marquee-1.0.6.tgz"
integrity sha512-VMqaeuBSS3L55Q84EuLJPHZ4Zv4UVBDXmh+2hb1IBTLKt/FmurJnWVXv8BNnbQSupA4kHbPlWDLwp+kvEMOF7A==
vue-router@^4.0.3, vue-router@^4.2.2: vue-router@^4.0.3, vue-router@^4.2.2:
version "4.3.0" version "4.3.0"
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz" resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz"
@@ -5283,7 +5484,14 @@ vue-tsc@^1.6.5:
"@vue/language-core" "1.8.27" "@vue/language-core" "1.8.27"
semver "^7.5.4" semver "^7.5.4"
"vue@^2.6.0 || ^3.2.0", "vue@^2.6.14 || ^3.3.0", "vue@^3.0.0-0 || ^2.6.0", vue@^3.2.0, vue@^3.2.25, vue@^3.3, vue@^3.3.4, vue@^3.5.13, "vue@2.x || 3.x", vue@3.5.13: vue-types@^3.0.0:
version "3.0.2"
resolved "https://registry.npmmirror.com/vue-types/-/vue-types-3.0.2.tgz"
integrity sha512-IwUC0Aq2zwaXqy74h4WCvFCUtoV0iSWr0snWnE9TnU18S66GAQyqQbRf2qfJtUuiFsBf6qp0MEwdonlwznlcrw==
dependencies:
is-plain-object "3.0.1"
"vue@^2.6.0 || ^3.2.0", "vue@^2.6.14 || ^3.3.0", vue@^3.0.0, "vue@^3.0.0-0 || ^2.6.0", vue@^3.2.0, vue@^3.2.25, vue@^3.3, vue@^3.3.4, vue@^3.5.13, vue@>=3.0.3, vue@>=3.2.0, "vue@2.x || 3.x", vue@3.5.13:
version "3.5.13" version "3.5.13"
resolved "https://mirrors.cloud.tencent.com/npm/vue/-/vue-3.5.13.tgz" resolved "https://mirrors.cloud.tencent.com/npm/vue/-/vue-3.5.13.tgz"
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ== integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
@@ -5294,6 +5502,13 @@ vue-tsc@^1.6.5:
"@vue/server-renderer" "3.5.13" "@vue/server-renderer" "3.5.13"
"@vue/shared" "3.5.13" "@vue/shared" "3.5.13"
warning@^4.0.0:
version "4.0.3"
resolved "https://registry.npmmirror.com/warning/-/warning-4.0.3.tgz"
integrity sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==
dependencies:
loose-envify "^1.0.0"
which-boxed-primitive@^1.0.2: which-boxed-primitive@^1.0.2:
version "1.0.2" version "1.0.2"
resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz" resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"
@@ -5447,3 +5662,10 @@ zrender@5.3.2:
integrity sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w== integrity sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==
dependencies: dependencies:
tslib "2.3.0" tslib "2.3.0"
zrender@6.0.0:
version "6.0.0"
resolved "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz"
integrity sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==
dependencies:
tslib "2.3.0"

View File

@@ -0,0 +1,244 @@
# 实习经历 — 整体概览
> 本文档是对三条实习经历的宏观梳理,后续将针对每条经历分别撰写详细的面试文档。
---
## 简历原文
> **实习经历:可信开源态势感知平台 — 前端开发实习生**
>
> 1. 参与开发全球风险监测模块实现威胁情报地图、CVE趋势分析、高危组件排行等10+数据可视化组件
> 2. 参与开发TcodeAI助手辅助修复功能集成金银湖大模型能力提供智能漏洞修复建议
> 3. 协作设计可信态势感知平台API接口完成50+数据接口的前后端集成
---
## 一、三条经历的关系
```
┌─────────────────────────────────────────────────────────────┐
│ 可信开源态势感知平台 │
│ │
│ ┌──────────────────┐ ┌──────────────┐ ┌───────────────┐ │
│ │ 经历1风险监测模块 │ │经历2AI修复 │ │经历3API集成 │ │
│ │ 10+可视化组件 │ │金银湖大模型 │ │50+接口前后端 │ │
│ │ 威胁地图/趋势/排行 │ │智能修复建议 │ │请求基础设施 │ │
│ └────────┬─────────┘ └──────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ ▼ ▼ ▼ │
│ 前端渲染层 AI 交互层 数据通信层 │
│ │
│ 三者共同构成平台的核心能力:数据展示 → 智能分析 → 数据获取 │
└─────────────────────────────────────────────────────────────┘
```
---
## 二、经历1全球风险监测模块
### 做了什么
参与开发了全球开源风险态势感知监控中心,路由为 `/security`,页面标题为"全球开源风险态势感知监控中心"。实现了 **10+ 个 ECharts 数据可视化组件**覆盖威胁情报地图、CVE 趋势分析、高危组件排行、漏洞分布、行业分布等多个维度。
### 涉及的组件
| 组件 | 图表类型 | 做什么 |
|------|---------|--------|
| **ThreatIntelMap** | 世界地图 + 散点 + 热力 | 全球威胁情报分布,含国家热力、关键节点、实时警报 |
| **ChinaSecurityMap** | 中国地图 + 攻击路径 | 中国安全态势,含省份热力、攻击路径动画 |
| **CveTrendChart** | 柱状图 + 折线图 | CVE 月度趋势,支持全球/区域切换 |
| **HighRiskComponentRank** | 横向柱状图 | 高危开源组件 Top 10按 CVSS 评分排序 |
| **VulnerabilitySeverityChart** | 堆叠柱状图 | 漏洞严重性分季度分布(高/中/低危) |
| **VulnerabilityLanguageChart** | 堆叠面积图 | 12 种编程语言的漏洞趋势2017-2024 |
| **ScatterChart** | 气泡散点图 | 编程语言安全态势,代码活跃度 vs 漏洞密度 |
| **IndustryDistribution** | 环形饼图 | 开源风险行业分布AI、云计算、大数据等 8 大行业) |
| **IntelligenceFeed** | 自动滚动列表 | 实时安全情报流,含类型标签和自动滚动 |
| **StatsGlass** | 毛玻璃统计卡片 | 4 项全球汇总指标(企业数、高校数、社区活跃度、项目数) |
### 技术要点
- 全部基于 **ECharts 6.x** 实现,使用 `echarts.init` + `dispose` 生命周期
- 世界地图和中国地图使用 **GeoJSON** 懒加载,几百 KB 的地图数据按需加载
- 图表组件通过 **Vite 动态 import** 按需加载,不打包进主 bundle
- 使用暗色主题(`#020617` 背景),带浮动粒子动画背景
- **13 个后端 API 接口**提供数据(`/api/v1/page1/*`),每个组件有硬编码的兜底数据
### 后续详细文档
> → [[经历1-全球风险监测模块详解]] ✅ 已完成
---
## 三、经历2TcodeAI助手辅助修复功能
### 做了什么
参与开发了 TcodeAI 助手的辅助修复功能,集成**金银湖大模型**能力,为开源组件的已知漏洞提供 **AI 生成的修复建议**。用户查看某个软件版本时,可以看到该版本依赖库中存在的漏洞,以及对每个漏洞的 AI 修复建议。
### 功能流程
```
用户进入软件版本详情页
切换到 "AI修复建议" Tab
POST /api/v1/repo/repair/info ──→ 返回漏洞列表库名、版本、CVE 等)
展示可折叠面板列表(每个受影响的库一个面板)
用户点击某个漏洞的 "AI修复建议" 按钮
POST /ai/vul_suggestion ──→ 返回 AI 生成的修复建议
弹窗展示:漏洞特性 + 漏洞描述 + 升级版本 + 修复建议列表
```
### 涉及的组件
| 组件 | 位置 | 做什么 |
|------|------|--------|
| **AIRepair 主页** | `src/views/Jyh/AIRepair/` | 主页面,展示漏洞列表 + AI 修复建议面板 |
| **Panel** | `src/components/AIRepair/` | 可折叠展开面板,带 `max-height` 动画过渡 |
| **Table** | `src/components/AIRepair/` | 漏洞表格 + "AI修复建议"按钮 + 弹窗展示 AI 建议 |
### 技术要点
- 对接**金银湖大模型**的 `/ai/vul_suggestion` 接口传入组件名、版本号、CVE 编号,返回结构化的修复建议(漏洞特性、描述、升级版本、修复建议列表)
- 修复建议以**结构化 JSON** 返回,前端用列表形式渲染
- Panel 组件使用了 `requestAnimationFrame` + `scrollHeight` 实现平滑折叠动画
- 该功能在 **3 个父页面**中以 Tab 形式嵌入版本详情页、AI Home 开源详情页、代码质量页)
### 后续详细文档
> → [[经历2-TcodeAI助手辅助修复详解]] ✅ 已完成
---
## 四、经历3API 接口前后端集成
### 做了什么
协作设计了可信态势感知平台的 API 接口,完成了 **50+ 数据接口**的前后端集成涵盖态势感知数据看板、仓库管理、组织管理、用户管理、AI 对话、合并请求、Issue 跟踪等多个业务模块。
### 整体规模
| 数据 | 数值 |
|------|------|
| API 函数总数 | **641 个** |
| API 文件数 | **29 个** |
| 金银湖JYH相关 API | **106 个**situation.ts 50 + index.ts 45 + home.ts 5 + auth.ts 3 + scanCenter.ts 3 |
| 态势感知看板接口 | **50+ 个**(三大页面,覆盖全球态势、开源生态、武汉本地生态) |
### 请求基础设施
```
请求发出
├── 请求拦截器
│ ├── Token 注入DP_TOKEN + DP_REFRESH_TOKEN
│ ├── 页面级 Header 附加标题、仓库ID、来源、UTM
│ └── URL 前缀重写passport /uc 前缀)
├── 数据传输
│ ├── AES-128-CBC 加密/解密(密钥 + IV 硬编码)
│ └── 标准 POST/GET部分接口使用 FormData 上传
└── 响应拦截器
├── 解密响应数据
├── HTTP 状态码处理400/401/403/408/500/502/504
├── Token 过期自动刷新 + 请求队列重放
└── API 缓存降级IndexedDB 缓存失败兜底)
```
### 关键模块
| 模块 | 文件 | 核心接口 |
|------|------|---------|
| **态势感知看板** | `jyh/situation.ts`50 端点) | 威胁地图、CVE 趋势、组件排行、漏洞分布、生态健康度等 |
| **仓库管理** | `repo/index.ts`161 端点) | 仓库 CRUD、分支/标签、Webhook、文件管理、Wiki、Star/Fork |
| **合并请求** | `merge/index.ts`62 端点) | MR 创建/审查、差异对比、代码评审、门禁检查 |
| **AI 大模型** | `jyh/index.ts`45 端点) | 对话创建、消息收发、历史管理、推荐问题、修复建议 |
| **组织管理** | `org/index.ts`55 端点) | 组织 CRUD、成员管理、主页信息、CLA 管理 |
| **用户管理** | `user/index.ts`55+ 端点) | 登录注册、OAuth、个人资料、SSH 密钥、Token 管理 |
| **讨论系统** | `discussion/index.ts`50 端点) | 讨论 CRUD、分类、评论、投票、排行榜 |
### 技术要点
- **AES 加密**:敏感数据在传输层使用 AES-128-CBC 加密(密钥 `mXzfCSPBKmEA8aLq`IV `tbeJJLC6dZQXXtWr`
- **Token 自动刷新**401 响应触发自动刷新,并发请求排队等待,刷新成功后统一重发
- **错误降级**`reqCatch` / `reqCatchV2` 包装函数API 失败时自动捕获错误并通过事件总线通知;部分接口有硬编码兜底数据
- **自定义错误处理**:支持 `customError: true` 参数跳过默认错误提示,由调用方自行处理
- **环境切换**:通过 Vite 环境变量区分开发/测试/生产环境 API 地址
### 后续详细文档
> → [[经历3-API接口前后端集成详解]] ✅ 已完成
---
## 五、三条经历的面试定位
| 经历 | 展示什么能力 | 面试重点 |
|------|------------|---------|
| **经历1风险监测** | 数据可视化能力、ECharts 实战、复杂图表组件开发 | ECharts 使用经验、地图图表、大数据图表性能 |
| **经历2AI修复** | AI 能力集成、前后端协作、组件化设计 | 大模型接入方式、结构化响应处理、组件复用 |
| **经历3API集成** | 工程化能力、安全设计、大规模接口管理 | 请求拦截器、加密方案、Token 刷新、错误处理 |
**一句话概括**
> "我在实习中从三个层面参与了平台建设——前端展示层数据可视化组件、AI 智能层大模型集成、数据通信层API 设计与集成),形成了比较完整的项目参与度。"
---
## 六、涉及的源码文件总览
### 经历1风险监测模块
| 做什么 | 文件在哪 |
|--------|----------|
| 态势感知主页面 | `src/views/Jyh/security/index.vue` |
| 全球威胁地图 | `src/views/Jyh/security/components/ThreatIntelMap.vue` |
| 中国安全地图 | `src/views/Jyh/security/components/ChinaSecurityMap.vue` |
| CVE 趋势图 | `src/views/Jyh/security/components/CveTrendChart.vue` |
| 高危组件排行 | `src/views/Jyh/security/components/HighRiskComponentRank.vue` |
| 漏洞严重性分布 | `src/views/Jyh/security/components/VulnerabilitySeverityChart.vue` |
| 语言漏洞趋势 | `src/views/Jyh/security/components/VulnerabilityLanguageChart.vue` |
| 语言安全散点图 | `src/views/Jyh/security/components/ScatterChart.vue` |
| 行业分布饼图 | `src/views/Jyh/security/components/IndustryDistribution.vue` |
| 情报动态流 | `src/views/Jyh/security/components/IntelligenceFeed.vue` |
| 统计卡片 | `src/views/Jyh/security/StatsGlass.vue` |
| 态势 API50 端点) | `src/api/jyh/situation.ts` |
| 威胁情报库 | `src/views/Jyh/ThreatIntelligence/index.vue` |
| 漏洞数据库 | `src/views/Jyh/VulnerabilityDatabase/index.vue` |
### 经历2AI 修复功能
| 做什么 | 文件在哪 |
|--------|----------|
| AI 修复主页面 | `src/views/Jyh/AIRepair/index.vue` |
| 可折叠面板组件 | `src/components/AIRepair/Panel.vue` |
| 漏洞表格 + AI 建议弹窗 | `src/components/AIRepair/Table.vue` |
| AI Home 中的修复 | `src/views/Jyh/AI/Home/components/OssDetail/AIRepair/index.vue` |
| AI 修复 API | `src/api/jyh/index.ts`remediationSearchRemediations, getVulSuggestion |
| 版本详情(嵌入位置) | `src/views/Jyh/Version/Detail/index.vue` |
### 经历3API 集成
| 做什么 | 文件在哪 |
|--------|----------|
| 请求核心基础设施 | `src/utils/request.ts`Axios 实例、拦截器、加密、Token 刷新) |
| 态势感知看板 API | `src/api/jyh/situation.ts`50 端点) |
| 金银湖核心 API | `src/api/jyh/index.ts`45 端点) |
| 仓库管理 API | `src/api/repo/index.ts`161 端点) |
| 组织管理 API | `src/api/org/index.ts`55 端点) |
| 用户管理 API | `src/api/user/index.ts`55+ 端点) |
| 错误捕获包装 | `src/utils/catch.ts`reqCatch / reqCatchV2 |
| 状态码处理 | `src/utils/status.ts`dealWarning |
| 缓存降级系统 | `src/utils/degradeInterceptor.ts` |
| 加密配置 | `src/utils/request.ts`AES-CBC 加密) |

View File

@@ -0,0 +1,380 @@
# 全球风险监测模块 — 面试版
> 简历原话:**"参与开发全球风险监测模块实现威胁情报地图、CVE趋势分析、高危组件排行等10+数据可视化组件"**
>
> 这篇文档帮你理解这个模块到底做了什么、怎么做的,以及面试时怎么讲。
---
## 一、先搞清楚:这个模块是什么?
全球风险监测模块是一个完整的数据可视化大屏,路由为 `/security`,页面标题为"全球开源风险态势感知监控中心"。页面上展示了 **10+ 个 ECharts 图表组件**,从全球威胁地图到高危组件排行、从 CVE 趋势到编程语言安全态势,覆盖了威胁情报的多个维度。
**一句话概括**:我用 ECharts 6.x 实现了一个安全态势数据可视化大屏,包含 10+ 个不同类型的图表组件(地图、柱状图、折线图、饼图、面积图、散点图等),展示了全球开源威胁情报的多个维度。
---
## 二、页面长什么样?(整体布局)
```
┌─────────────────────────────────────────────────────────────┐
│ StatsGlass统计卡片4 项全球汇总指标) │
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 🛡️ ThreatIntelMap全球威胁情报地图 │ │
│ │ 世界地图热力 + 关键节点散点 + 实时警报 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌──────────────┐ ┌──────────────┐ ┌──────────────┐ │
│ │IndustryDist │ │HighRisk │ │Intelligence │ │
│ │行业分布饼图 │ │高危组件Top10 │ │实时情报动态流 │ │
│ └──────────────┘ └──────────────┘ └──────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ CveTrendChartCVE 月度趋势 柱状图+折线图) │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ VulnerabilityLanguageChart12种语言漏洞趋势面积图 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ VulnerabilitySeverityChart漏洞等级堆叠柱状图 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ ScatterChart编程语言安全态势气泡散点图 │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
页面使用暗色科技风背景(`#020617`),每个图表卡片有毛玻璃效果(`backdrop-filter: blur`),鼠标悬停有上浮动画。
---
## 三、10+ 个组件一览
| 组件 | 图表类型 | 数据来源 | 直观展示什么 |
|------|---------|---------|------------|
| **ThreatIntelMap** | 世界地图 + 热力 + 散点 | `/api/v1/page1/map/mapData` 等 4 个接口 | 全球威胁分布热力图,北京/上海/华盛顿等 6 个关键节点,实时漏洞类型分布面板 |
| **ChinaSecurityMap** | 中国地图 + 攻击路径 | 懒加载中国 GeoJSON | 各省威胁热力、安全监测节点、城市间攻击路径动画 |
| **CveTrendChart** | 柱状图 + 折线图(双轴) | `/api/v1/page1/cveData/global` | CVE 数量柱状图 + 环比增长率折线图,支持全球/区域切换 |
| **HighRiskComponentRank** | 横向柱状图 | `/api/v1/page1/highRiskComponent` | Top 10 高危开源组件,按 CVSS 评分排序,颜色标识严重程度 |
| **VulnerabilitySeverityChart** | 堆叠柱状图 | `/api/v1/page1/vulnerabilitySeverity` | 高/中/低危漏洞数量按季度堆叠分布2024Q1-2025Q4 |
| **VulnerabilityLanguageChart** | 堆叠面积图 | `/api/v1/page1/languageTrend` | 12 种编程语言 2017-2024 年的漏洞趋势变化 |
| **ScatterChart** | 气泡散点图 | `/api/v1/page1/scatterData` | 编程语言安全态势X 轴代码活跃度、Y 轴漏洞密度、气泡大小=漏洞总数 |
| **IndustryDistribution** | 环形饼图 | `/api/v1/page1/industryDistribution` | 开源风险在 AI、云计算、大数据等 8 大行业的分布占比 |
| **IntelligenceFeed** | 自动滚动列表 | `/api/v1/page1/feedList` | 实时安全情报流带类型标签CISA/Poisoning/CNVD/GitHub |
| **StatsGlass** | 毛玻璃统计卡片 | `/api/v1/page1/overviewData` | 4 项全球汇总指标(企业数、高校数、社区活跃度、项目数) |
除了以上 10 个独立组件,主页面 `index.vue` 中还有 **5 个内联 ECharts 图表**
| 内联图表 | 图表类型 | 展示内容 |
|---------|---------|---------|
| 高校开源参与分布 | 环形饼图 | 华中科大、武大等高校参与度 |
| 社区区域分布 | 柱状图 | 武昌区、洪山区等社区分布 |
| OSS Compass 排行 | 横向柱状图 | OpenHarmony、MindSpore 等开源项目排行 |
| 开发者排行榜 | 横向柱状图 | 武汉开源开发者 Top 10 |
| 语言技术趋势 | 多系列折线图 | Java、Python、Rust、仓颉等 5 种语言的趋势 |
**合计15+ 个图表组件。**
---
## 四、怎么实现的?(面试核心)
所有图表组件遵循统一的 **三步模式**:获取数据 → 初始化图表 → 清理销毁。
### 4.1 统一的开发模式(每个组件都一样)
```typescript
// 以 HighRiskComponentRank 为例
import * as echarts from 'echarts';
import { highRiskComponentPage1 } from '@/api/jyh/situation';
// ====== 步骤 1获取数据 ======
const fetchData = async () => {
try {
const response = await highRiskComponentPage1();
if (response.data.code === 200) {
componentData.value = response.data.data; // ✅ 成功:用后端数据
} else {
componentData.value = [...defaultData]; // ⚠️ 失败:用兜底数据
}
} catch (error) {
componentData.value = [...defaultData]; // ❌ 异常:用兜底数据
}
};
// ====== 步骤 2初始化图表 ======
const initChart = () => {
if (chartInstance) chartInstance.dispose(); // 先销毁旧实例
chartInstance = echarts.init(chartRef.value); // 创建新实例
chartInstance.setOption({
// ... 图表配置tooltip、grid、xAxis、yAxis、series 等)
});
window.addEventListener('resize', handleResize); // 响应窗口大小变化
};
// ====== 步骤 3清理销毁 ======
onUnmounted(() => {
if (chartInstance) chartInstance.dispose(); // 组件卸载时销毁,防止内存泄漏
window.removeEventListener('resize', handleResize);
});
```
**面试话术**
> "所有图表组件遵循统一的开发模式:第一,通过 API 获取数据,接口失败时自动降级为预设的兜底数据,保证图表不会白屏;第二,使用 echarts.init 初始化图表,通过 setOption 配置图表样式和交互;第三,组件卸载时调用 dispose 销毁实例,移除 resize 监听,防止内存泄漏。切换数据时会先 dispose 旧实例再重新 init确保图表完全刷新。"
### 4.2 全球威胁情报地图(技术含量最高)
ThreatIntelMap 是整个模块最复杂的组件,综合使用了多种 ECharts 地图能力:
```typescript
// ThreatIntelMap.vue — 核心配置(简化版)
echarts.registerMap('world', worldJson); // 注册世界地图 GeoJSON
const option = {
// 1. 视觉映射:热力渐变
visualMap: {
min: 0, max: 100,
inRange: { color: ['#1e3c72', '#2a5298', '#24b6d8', '#facc15', '#ef4444'] }
},
// 2. 地理坐标系:世界地图
geo: {
map: 'world', roam: true, zoom: 1.2,
itemStyle: { areaColor: '#0f172a', borderColor: '#1e293b' }
},
// 3. 三个数据系列
series: [
{
name: 'Threat Distribution',
type: 'map', // ← 地图热力层
geoIndex: 0,
data: mapData // 国家→风险值
},
{
name: 'Key Nodes',
type: 'effectScatter', // ← 涟漪散点层(关键安全节点)
coordinateSystem: 'geo',
data: scatterData, // 北京、上海、华盛顿等 6 个节点
rippleEffect: { scale: 3 }
},
]
};
```
同时在地图叠加层渲染了三个信息面板:
- **左侧面板**:漏洞类型分布 Top 5XSS 32.1%、SQL 注入 11.9% 等),带进度条动画
- **右侧面板**:威胁情报地域排行(美国 435,202 次、印度 252,054 次等)
- **底部卡片**实时安全警报自动轮播3.5 秒切换一条,带脉冲动画)
**面试话术**
> "全球威胁地图是整个模块最复杂的组件。它用到了 ECharts 的地图系列 + effectScatter 涟漪散点图的组合,在 GeoJSON 世界地图上叠加了两层数据——国家热力层展示威胁分布,散点层标记 6 个关键安全节点。地图上还有三个信息面板——漏洞类型分布、地域排行、实时警报。世界地图的 GeoJSON 文件通过动态 import 懒加载,只在用户进入这个页面时才下载,不打包进主 bundle。"
### 4.3 CVE 月度趋势 — 双轴组合图
```typescript
// CveTrendChart.vue — 双 Y 轴组合图(柱状图 + 折线图)
const option = {
tooltip: { trigger: 'axis' },
// 两个 Y 轴:左边 CVE 数量,右边增长率百分比
yAxis: [
{ type: 'value', name: 'CVE数量', position: 'left' }, // ← 对应柱状图
{ type: 'value', name: '环比增长率(%)', position: 'right' } // ← 对应折线图
],
series: [
{
name: 'CVE数量',
type: 'bar', // ← 柱状图,左轴
data: cveCount,
itemStyle: { color: new echarts.graphic.LinearGradient(...) } // 渐变色
},
{
name: '环比增长率',
type: 'line', // ← 折线图,右轴
yAxisIndex: 1, // 使用右轴
data: acceleration,
smooth: true, // 平滑曲线
itemStyle: { color: '#ef4444' } // 红色
}
]
};
```
支持通过按钮切换"全球视图"和"区域视图",切换时重新请求对应的 API 接口并重新渲染图表。
### 4.4 高危组件 Top 10 — 横向柱状图 + CVSS 颜色标识
```typescript
// HighRiskComponentRank.vue — 颜色随 CVSS 评分变化
series: [{
type: 'bar',
data: cvssScores,
itemStyle: {
color: (params) => {
if (params.value >= 9.0) return '#ef4444'; // 红色 — 严重
if (params.value >= 7.0) return '#f97316'; // 橙色 — 高危
if (params.value >= 4.0) return '#eab308'; // 黄色 — 中危
return '#22c55e'; // 绿色 — 低危
}
},
label: { show: true, position: 'right' } // 数值标签显示在柱子右侧
}]
```
Y 轴设置 `inverse: true`,让 CVSS 评分最高的组件显示在最上面。
### 4.5 漏洞等级分布 — 堆叠柱状图
```typescript
// VulnerabilitySeverityChart.vue — 三个系列堆叠
series: [
{ name: '高危漏洞', type: 'bar', stack: 'total', color: '#FF4D4F', data: [...] },
{ name: '中危漏洞', type: 'bar', stack: 'total', color: '#FAAD14', data: [...] },
{ name: '低危漏洞', type: 'bar', stack: 'total', color: '#36CFC9', data: [...] },
]
// 三个系列的 stack 值都是 'total'ECharts 会自动堆叠
// tooltip 中计算每个等级占总数的百分比
```
### 4.6 三个重要的设计细节
#### 细节 1每个组件都有 API 失败兜底
```typescript
// 所有图表组件的共同特点API 失败时用硬编码数据兜底
try {
const res = await apiFunction();
if (res.data.code === 200) {
chartData.value = res.data.data; // ✅ 后端数据
} else {
chartData.value = DEFAULT_DATA; // ⚠️ 兜底数据
}
} catch {
chartData.value = DEFAULT_DATA; // ❌ 网络异常,兜底数据
}
```
**为什么这样设计?** 态势大屏在演示时不能白屏。即使后端挂了,至少展示兜底数据让页面是完整的。
#### 细节 2图表实例的 dispose → init 生命周期
```typescript
// 切换数据时:先销毁旧实例,再创建新实例
if (chartInstance) chartInstance.dispose(); // 销毁旧实例(释放内存)
chartInstance = echarts.init(el); // 创建新实例
chartInstance.setOption(option); // 渲染新数据
// 组件卸载时:销毁实例 + 移除监听
onUnmounted(() => {
chartInstance?.dispose(); // 防止内存泄漏
window.removeEventListener('resize', handler);
});
```
**为什么不用 myChart.setOption() 更新?** 当数据维度变化时(如 CVE 趋势图切换全球/区域视图、X 轴月份数变化),直接 setOption 会有残留配置。先 dispose 再 init 是最稳妥的做法。
#### 细节 3地图数据的懒加载
```typescript
// 世界地图 GeoJSON 通过 import 在组件内引用
import worldJson from './world.json'; // 只在 ThreatIntelMap 组件被加载时才下载
// 中国地图 GeoJSON 通过动态函数加载utils/mapLoader.js
export const loadChinaMap = async () => {
await import('/china.js'); // 只在进入中国地图页面时才下载(几百 KB
};
```
---
## 五、完整的数据流
```
页面加载 (/security)
├──→ StatsGlass 调 overviewDataPage1 ← /api/v1/page1/overviewData
├──→ ThreatIntelMap 调 4 个 map API ← /api/v1/page1/map/*
├──→ IndustryDistribution 调 industryDistributionPage1 ← /api/v1/page1/industryDistribution
├──→ HighRiskComponentRank 调 highRiskComponentPage1 ← /api/v1/page1/highRiskComponent
├──→ IntelligenceFeed 调 feedListPage1 ← /api/v1/page1/feedList
├──→ CveTrendChart 调 cveDataGlobal + regional ← /api/v1/page1/cveData/*
├──→ VulnerabilityLang 调 languageTrendPage1 ← /api/v1/page1/languageTrend
├──→ VulnerabilitySev 调 vulnerabilitySeverityPage1 ← /api/v1/page1/vulnerabilitySeverity
├──→ ScatterChart 调 scatterDataPage1 ← /api/v1/page1/scatterData
└──→ 5 个内联图表 硬编码数据(武汉市开源生态展示)
```
**共 13 个后端 API 接口**,全部以 `/api/v1/page1/` 为前缀,定义在 `src/api/jyh/situation.ts` 中。
---
## 六、面试问答准备
### Q1"参与开发全球风险监测模块"具体做了什么?
> 我参与开发了全球开源风险态势感知监控中心这个大屏页面,用 ECharts 6.x 实现了 10+ 个数据可视化组件。具体包括:全球威胁情报地图(世界地图热力 + 关键节点散点 + 实时警报面板、CVE 月度趋势图(双轴柱状图+折线图,支持全球/区域切换)、高危组件 Top 10 排行(横向柱状图,按 CVSS 评分颜色标识)、漏洞等级分布(高/中/低危堆叠柱状图、编程语言漏洞趋势面积图12 种语言 2017-2024、编程语言安全态势气泡图活跃度 vs 漏洞密度)等。页面采用暗色科技风主题,毛玻璃卡片布局,每个组件都有 API 失败兜底数据。
### Q210+ 个图表组件,代码怎么组织的?
> 每个图表都是一个独立的 Vue SFC 组件(如 ThreatIntelMap.vue、CveTrendChart.vue在主页面 index.vue 中通过 import 引入并组合布局。所有组件遵循统一的开发模式:获取数据(调 API失败用兜底数据→ 初始化图表echarts.init + setOption→ 清理销毁dispose + 移除监听)。组件之间互不依赖,可以独立开发和调试。
### Q3地图是怎么做的
> 我用 ECharts 的 map 系列实现。世界地图使用 GeoJSON 数据world.json通过 echarts.registerMap 注册后在地理坐标系上渲染。地图上叠加了两层数据map 系列展示国家热力分布(通过 visualMap 控制颜色渐变effectScatter 系列展示关键安全节点(北京、上海、华盛顿等 6 个点,带涟漪动画)。还在地图四个角叠加了信息面板——漏洞类型分布、地域排行、实时警报。
### Q4图表性能怎么考虑的
> 三个方面的处理:第一,地图 GeoJSON 文件(几百 KB使用懒加载只在 ThreatIntelMap 组件渲染时才下载;第二,每个图表组件在卸载时调用 echartsInstance.dispose() 销毁实例并移除 resize 监听,防止内存泄漏;第三,图表切换数据时(如 CVE 全球/区域切换),先 dispose 再重新 init避免 setOption 合并残留配置导致的问题。
### Q5如果后端 API 挂了,页面会白屏吗?
> 不会。每个图表组件都设计了数据降级策略——API 请求失败或返回异常时,自动使用预设的兜底数据渲染。比如高危组件 Top 10 的兜底数据包含了 Log4j29.8、Struts29.7、OpenSSL9.6)等真实的公开漏洞数据。这样即使后端挂了,大屏依然能完整展示,不影响演示效果。
### Q6ECharts 的 setOption 和 dispose+init 有什么区别?为什么你的组件用 dispose+init
> setOption 适合增量更新——数据变了但图表类型、轴配置不变时,用 setOption 合并新配置即可,不会闪烁。但我的组件有些场景下图表维度会变化(比如 CVE 趋势图切换全球/区域时,数据量和走势完全不同),直接 setOption 可能残留旧配置。所以统一采用 dispose + init 的方式,确保每次都是全新渲染。代价是有短暂闪烁,但数据一致性能保证。
### Q7你用到了哪些 ECharts 图表类型?
> 地图map、涟漪散点图effectScatter、柱状图bar含堆叠柱状图、折线图line、饼图pie/环形图、面积图area/堆叠面积图)、散点图/气泡图scatter、热力 visualMap。总共大概 7-8 种图表类型的组合使用。
---
## 七、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 独立图表组件数 | 10 个import 引入的子组件) |
| 内联图表数 | 5 个index.vue 中直接写的) |
| 图表总数 | **15+** 个 |
| ECharts 图表类型 | **8 种**map、effectScatter、bar、line、pie、area、scatter、visualMap |
| 后端 API 接口 | **13 个**`/api/v1/page1/*` |
| API 接口文件 | `src/api/jyh/situation.ts`50 个端点,其中 13 个属于此模块) |
| 地图 GeoJSON 文件 | 2 个world.json + china.js |
| 暗色主题背景色 | `#020617` |
| 毛玻璃卡片效果 | `backdrop-filter: blur(10px)` |
---
## 八、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 主页面(布局 + 5 个内联图表) | `src/views/Jyh/security/index.vue` |
| 全球威胁情报地图 | `src/views/Jyh/security/components/ThreatIntelMap.vue` |
| 中国安全态势地图 | `src/views/Jyh/security/components/ChinaSecurityMap.vue` |
| CVE 月度趋势图 | `src/views/Jyh/security/components/CveTrendChart.vue` |
| 高危组件 Top 10 排行 | `src/views/Jyh/security/components/HighRiskComponentRank.vue` |
| 漏洞等级分布图 | `src/views/Jyh/security/components/VulnerabilitySeverityChart.vue` |
| 编程语言漏洞趋势图 | `src/views/Jyh/security/components/VulnerabilityLanguageChart.vue` |
| 编程语言安全态势气泡图 | `src/views/Jyh/security/components/ScatterChart.vue` |
| 行业分布饼图 | `src/views/Jyh/security/components/IndustryDistribution.vue` |
| 实时情报动态流 | `src/views/Jyh/security/components/IntelligenceFeed.vue` |
| 统计卡片 | `src/views/Jyh/security/StatsGlass.vue` |
| 世界地图 GeoJSON | `src/views/Jyh/security/components/world.json` |
| 所有态势感知 API | `src/api/jyh/situation.ts` |
| 关联页面:威胁情报库 | `src/views/Jyh/ThreatIntelligence/index.vue` |
| 关联页面:漏洞数据库 | `src/views/Jyh/VulnerabilityDatabase/index.vue` |

View File

@@ -0,0 +1,314 @@
# TcodeAI 助手辅助修复功能 — 面试版
> 简历原话:**"参与开发TcodeAI助手辅助修复功能集成金银湖大模型能力提供智能漏洞修复建议"**
>
> 这篇文档帮你理解这个功能到底做了什么、怎么做的,以及面试时怎么讲。
---
## 一、先搞清楚:这个功能是什么?
在可信态势感知平台中用户查看某个软件版本时平台会扫描该版本依赖的开源组件列出其中存在的已知漏洞。TcodeAI 助手辅助修复功能的作用就是——**对每个漏洞生成一份 AI 修复建议**,告诉用户这个漏洞有什么特征、怎么描述、建议升级到什么版本、具体怎么修。
**一句话概括**:我给平台接入了金银湖大模型的智能修复能力,用户点一下"AI修复建议"按钮,就能看到模型生成的漏洞分析 + 升级方案 + 修复步骤。
---
## 二、功能长什么样?(用户视角)
```
用户进入软件版本详情页
切换到 "AI修复建议" Tab
┌─────────────────────────────────────────────────────────────┐
│ AI修复建议说明功能介绍横幅
├─────────────────────────────────────────────────────────────┤
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 🔴 严重 Log4j2 [展开 ▼] │ │
│ │ 安全问题 3 升级版本 2.14.0 → 2.17.1 │ │
│ ├───────────────────────────────────────────────────────┤ │
│ │ (展开后显示漏洞列表表格) │ │
│ │ ┌──────────┬──────────┬──────────┬──────┬──────────┐ │ │
│ │ │ 问题编号 │ 问题类型 │ CWE编号 │ 分值 │ 操作 │ │ │
│ │ ├──────────┼──────────┼──────────┼──────┼──────────┤ │ │
│ │ │CVE-2021..│ RCE │ CWE-502 │ 9.8 │[AI修复] │ │ │
│ │ │CVE-2021..│ DoS │ CWE-400 │ 7.5 │[AI修复] │ │ │
│ │ └──────────┴──────────┴──────────┴──────┴──────────┘ │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ 🔴 严重 Spring Framework [展开 ▼] │ │
│ │ 安全问题 2 升级版本 5.3.26 → 5.3.31 │ │
│ └───────────────────────────────────────────────────────┘ │
│ │
│ (点击 "AI修复建议" 按钮后弹出弹窗) │
│ ┌───────────────────────────────────────────────────────┐ │
│ │ AI修复建议 [弹窗] │ │
│ │ ┌─────────────────────────────────────────────────┐ │ │
│ │ │ 漏洞特性:远程代码执行,攻击者可构造恶意数据包... │ │ │
│ │ │ 漏洞描述Apache Log4j2 存在 JNDI 注入漏洞... │ │ │
│ │ │ 升级版本2.17.1 │ │ │
│ │ │ 🤖 AI修复建议 │ │ │
│ │ │ • 将 Log4j2 版本升级至 2.17.1 或更高版本 │ │ │
│ │ │ • 若无法升级,设置 log4j2.formatMsgNoLookups=true│ │ │
│ │ │ • 检查项目中是否有自定义的 Lookup 插件... │ │ │
│ │ └─────────────────────────────────────────────────┘ │ │
│ │ [确定] [取消] │ │
│ └───────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────┘
```
---
## 三、怎么实现的?(面试核心)
### 3.1 三个组件的协作关系
这个功能由三个组件构成:
```
AIRepair/index.vue主页面
├──→ 调用 /api/v1/repo/repair/info 获取漏洞数据
├──→ Panel.vue可折叠面板组件
│ 每个受影响的库一个面板,展示库名、版本、升级路径
│ 点击 "展开/收起" 控制面板折叠
└──→ Table.vue漏洞表格 + AI 建议弹窗)
展示该库下的漏洞列表CVE编号、类型、CVSS评分
点击 "AI修复建议" → 调用 /ai/vul_suggestion → 弹窗展示 AI 建议
```
### 3.2 第一步获取漏洞列表AIRepair/index.vue
```typescript
// views/Jyh/AIRepair/index.vue简化版
const tableData = ref([]);
const pager = ref({ total: 10, pageIndex: 1, pageSize: 10 });
// 页面加载时调后端 API 获取受漏洞影响的组件列表
const fetchVulnList = async () => {
const { data } = await remediationSearchRemediations({
softwareId: propsData.versionId, // 当前查看的软件版本 ID
current: pager.value.pageIndex,
size: pager.value.pageSize
});
if (data.data.code === 200) {
// 数据格式:每个元素是一个受影响的库
// { libraryName, currentVersion, recommendedVersion, vulnerabilities: [...] }
tableData.value = data?.data?.data?.records || [];
pager.value.total = data?.data?.data?.total || 0;
}
};
onMounted(() => { fetchVulnList(); });
```
**API 接口**`POST /api/v1/repo/repair/info`,参数 `{ softwareId, current, size }`
### 3.3 第二步可折叠面板Panel.vue
```vue
<!-- components/AIRepair/Panel.vue 可折叠面板 -->
<template>
<!-- 头部始终可见 -->
<div>
<slot name="header"></slot> <!-- 库名版本信息展开/收起按钮 -->
</div>
<!-- 内容展开/收起动画过渡 -->
<Transition name="collapse-transition"
@beforeEnter="beforeEnter"
@enter="enter"
@beforeLeave="beforeLeave"
@leave="leave"
>
<div v-if="props.expand">
<slot name="body"></slot> <!-- 漏洞表格 -->
</div>
</Transition>
</template>
```
**折叠动画是怎么实现的?** 手动控制 Vue Transition 的六个钩子函数:
```typescript
// 展开时先量高度scrollHeight→ 设置 max-height → 过渡到全高 → 清除限制
const enter = (el) => {
requestAnimationFrame(() => {
el.style.maxHeight = `${el.scrollHeight}px`; // 设为元素实际高度
});
};
const afterEnter = (el) => {
el.style.maxHeight = ''; // 展开完成后清除限制,防止内容溢出
};
// 收起时:先设当前高度 → 过渡回 max-height: 0
const beforeLeave = (el) => {
el.style.maxHeight = `${el.scrollHeight}px`; // 锁定当前高度
};
const leave = (el) => {
el.style.maxHeight = 0; // 过渡回 0
};
```
**为什么不用 CSS `height: auto` 动画?** CSS 无法对 `height: auto` 做动画过渡。所以用 JS 读取 `scrollHeight` 再设置 `max-height` 的数值,这才能实现平滑的展开/收起动画。过渡曲线是 `cubic-bezier(0.5, 0.05, 0.5, 0.95)`,持续 0.3 秒。
### 3.4 第三步AI 修复建议弹窗Table.vue — 核心交互)
这是整个功能的核心——用户点击漏洞行里的"AI修复建议"按钮,弹出弹窗展示 AI 生成的内容:
```javascript
// components/AIRepair/Table.vue简化版核心逻辑
// 1. 表格渲染每行漏洞信息 + "AI修复建议"按钮
// <d-button @click="openModal(row)">AI修复建议</d-button>
// 2. 点击按钮 → 调 AI 接口获取建议
const openModal = async (row) => {
showModal.value = true;
await fetchVulSuggestion(row); // 调 AI 接口
};
const fetchVulSuggestion = async (row) => {
loading.value = true;
const { data } = await getVulSuggestion({
name: props.libraryName, // 组件库名(如 "Log4j2"
version: props.currentVersion, // 当前版本号(如 "2.14.0"
cve: row.vulnId // CVE 编号(如 "CVE-2021-44228"
});
if (data.code === 200) {
vulSuggestion.value = {
vulFeature: data.data?.vulFeature || '', // 漏洞特性
vulDescription: data.data?.vulDescription || '', // 漏洞描述
upgradeVersion: data.data?.upgradeVersion || '', // 建议升级版本
fixSuggestionList: data.data?.fixSuggestionList || [] // AI修复建议列表
};
}
};
// 3. 弹窗展示 AI 返回的四部分内容
// ┌──────────────────────────────────────────┐
// │ 漏洞特性:远程代码执行,攻击者构造恶意... │
// │ 漏洞描述Apache Log4j2 存在 JNDI 注入... │
// │ 升级版本2.17.1 │
// │ 🤖 AI修复建议
// │ • 升级至 2.17.1 或更高版本 │
// │ • 设置 formatMsgNoLookups=true │
// │ • 检查自定义 Lookup 插件 │
// └──────────────────────────────────────────┘
```
**API 接口**`POST /ai/vul_suggestion`,参数 `{ name, version, cve }`,返回结构化 JSON。
### 3.5 数据流全景
```
用户进入 "AI修复建议" Tab
AIRepair/index.vue 调用
POST /api/v1/repo/repair/info
参数: { softwareId, current, size }
返回: { records: [{ libraryName, currentVersion,
recommendedVersion, vulnerabilities: [...] }] }
渲染可折叠面板Panel.vue
每个受影响的库 → 一个面板
面板头部展示:库名、严重标签、问题数、升级路径
面板内容展示漏洞表格Table.vue
用户点击某行 "AI修复建议" 按钮
Table.vue 调用
POST /ai/vul_suggestion ← 金银湖大模型
参数: { name: "Log4j2", version: "2.14.0", cve: "CVE-2021-44228" }
返回: { vulFeature, vulDescription,
upgradeVersion, fixSuggestionList: [...] }
弹窗展示 AI 修复建议
```
---
## 四、这个功能还在哪些地方用?
AIRepair 组件被复用在 **3 个不同的父页面**中,以 Tab 形式嵌入:
| 父页面 | 文件位置 | Tab 名称 |
|--------|---------|---------|
| **版本详情页** | `src/views/Jyh/Version/Detail/index.vue` | "AI修复建议" Tab |
| **版本详情页(备选视图)** | `src/views/Jyh/Version/Detail/repoDetails/index2.vue` | "AI修复建议" Tab |
| **AI Home 开源详情** | `src/views/Jyh/AI/Home/components/OssDetail/index.vue` | "AI 修复建议" Tab |
此外Panel 和 Table 组件还被复用在**代码质量页**`CodeQuality/index.vue`)中,说明这套 UI 模式在项目中有一定的通用性。
---
## 五、面试问答准备
### Q1"TcodeAI助手辅助修复功能"具体做了什么?
> 这个功能主要是把金银湖大模型的智能分析能力集成到平台的漏洞修复流程里。用户查看某个软件版本时,平台会列出该版本依赖库中存在的已知漏洞。对于每个漏洞,用户可以点击"AI修复建议"按钮,前端会调用金银湖大模型的 `/ai/vul_suggestion` 接口传入组件名、版本号、CVE 编号,模型会返回结构化的修复建议——包括漏洞特性、漏洞描述、建议升级版本和修复步骤列表。前端把这些内容展示在弹窗中,用户就能看到具体的修复方案。
### Q2AI 接口是怎么对接的?
> 用的是标准的 HTTP POST 请求。前端传三个参数给后端 `/ai/vul_suggestion` 接口:`name`(库名,如 Log4j2、`version`(当前版本号)、`cve`CVE 编号)。后端调用金银湖大模型进行处理,返回结构化 JSON包含 `vulFeature`(漏洞特性)、`vulDescription`(漏洞描述)、`upgradeVersion`(建议升级版本)、`fixSuggestionList`(修复建议列表)。前端直接按字段渲染就行,不需要自己解析自然语言。
### Q3Panel 组件的折叠动画怎么实现的?
> 用 Vue 的 Transition 组件 + JavaScript 钩子函数实现的。核心思路是:因为 CSS 无法对 `height: auto` 做过渡动画,所以用 JS 在展开前读取元素的 `scrollHeight`(实际内容高度),设置 `max-height` 为目标值,让 CSS transition 驱动动画;展开完成后清掉 `max-height` 限制,防止内容溢出。收起的流程相反——先设 `max-height` 为当前高度,再过渡到 0。过渡持续 0.3 秒,使用 `cubic-bezier(0.5, 0.05, 0.5, 0.95)` 缓动曲线。
### Q4这个功能是怎么嵌入到平台里的
> AIRepair 组件以 Tab 的形式嵌入到三个不同的父页面中——软件版本详情页的两个视图,和 AI Home 的开源软件详情页。父页面通过 `versionId` prop 把当前查看的软件版本 ID 传给 AIRepairAIRepair 根据这个 ID 去请求对应版本的安全漏洞信息。这种组件化的设计使得同一个功能可以在多个页面中复用,不需要重复开发。
### Q5AI 返回的数据有哪些字段?怎么展示的?
> AI 返回四个字段:`vulFeature`(漏洞特性——描述漏洞的技术特征,如"远程代码执行,攻击者可通过构造恶意数据包触发")、`vulDescription`(漏洞描述——更详细的漏洞说明)、`upgradeVersion`(建议升级到的安全版本号)、`fixSuggestionList`(修复建议列表——一个字符串数组,每条是一条具体的修复措施)。前端用弹窗展示,前三个字段是键值对形式,修复建议用列表形式渲染。所有字段都用了 `||` 兜底为 `'--'`,防止某个字段缺失导致空白。
### Q6AI 修复建议和漏洞列表是两个不同的接口,为什么要分开?
> 两个接口的职责不同。漏洞列表接口(`/api/v1/repo/repair/info`返回的是批量数据——当前软件版本所有受影响依赖库的漏洞概况数据量较大但字段较少页面加载时一次性请求。AI 修复建议接口(`/ai/vul_suggestion`)是点对点请求——用户点击某个具体漏洞后才请求,每次只传一个 CVE 编号去查询。这种设计避免了首页加载时对每个漏洞都调一次 AI 接口,既不浪费 AI 调用额度,也保证了页面加载速度。
---
## 六、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 组件文件数 | 3 个index.vue + Panel.vue + Table.vue |
| AI API 接口数 | 2 个(`/api/v1/repo/repair/info` + `/ai/vul_suggestion` |
| AI 返回字段数 | 4 个(漏洞特性、漏洞描述、升级版本、修复建议列表) |
| 嵌入父页面数 | 3+ 个(版本详情页 ×2 + AI Home详情 + 代码质量页) |
| Panel 折叠动画过渡 | 0.3 秒cubic-bezier 缓动 |
---
## 七、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| AI 修复主页面 | `src/views/Jyh/AIRepair/index.vue` |
| 可折叠面板组件 | `src/components/AIRepair/Panel.vue` |
| 漏洞表格 + AI 建议弹窗 | `src/components/AIRepair/Table.vue` |
| AI Home 中的修复副本 | `src/views/Jyh/AI/Home/components/OssDetail/AIRepair/index.vue` |
| AI 修复相关 API | `src/api/jyh/index.ts`remediationSearchRemediations、getVulSuggestion |
| 版本详情页(嵌入位置 1 | `src/views/Jyh/Version/Detail/index.vue` |
| 版本详情页(嵌入位置 2 | `src/views/Jyh/Version/Detail/repoDetails/index2.vue` |
| AI Home 详情(嵌入位置 3 | `src/views/Jyh/AI/Home/components/OssDetail/index.vue` |

View File

@@ -0,0 +1,388 @@
# API 接口前后端集成 — 面试版
> 简历原话:**"协作设计可信态势感知平台API接口完成50+数据接口的前后端集成"**
>
> 这篇文档帮你理解 API 集成层到底做了什么、怎么做的,以及面试时怎么讲。
---
## 一、先搞清楚API 集成层是什么?
API 集成层是前端和后端之间的"翻译官"和"交通管制员"。它不是简单的 `axios.get('/xxx')`,而是一整套基础设施:
```
页面组件Vue 组件)
│ 调用 API 函数
API 函数层29 个文件641 个函数)
→ 定义接口 URL、method、参数格式
│ 调用 request()
请求基础设施request.ts273 行)
→ Axios 实例、请求拦截器、响应拦截器
→ AES 加密/解密、Token 注入、Token 过期自动刷新
→ 错误统一处理、缓存降级
│ HTTP 请求
后端服务器
```
**一句话概括**:我参与设计和集成了平台的 API 通信层,包括定义了 50+ 个态势感知相关的数据接口并搭建了完整的请求基础设施加密、Token 刷新、错误处理、缓存降级),覆盖全平台 641 个接口。
---
## 二、API 整体规模
| 数据 | 数值 |
|------|------|
| API 函数总数 | **641 个** |
| API 文件数 | **29 个** |
| 金银湖态势感知相关 | **106 个**situation.ts 50 + index.ts 45 + home.ts 5 + auth.ts 3 + scanCenter.ts 3 |
| 最大的文件 | `repo/index.ts`161 个函数) |
### 按模块分布
| 模块 | 文件 | 函数数 | 负责什么 |
|------|------|--------|---------|
| **态势感知看板** | `jyh/situation.ts` | 50 | 威胁地图、CVE 趋势、组件排行、漏洞分布等 |
| **金银湖核心** | `jyh/index.ts` | 45 | AI 对话、版本详情、SBOM、许可证、修复建议 |
| **仓库管理** | `repo/index.ts` | 161 | 仓库 CRUD、分支、标签、Wiki、文件、Webhook |
| **组织管理** | `org/index.ts` | 55 | 组织 CRUD、成员、首页、CLA、开发者门户 |
| **用户管理** | `user/index.ts` | 55+ | 登录注册、OAuth、个人资料、SSH 密钥、Token |
| **合并请求** | `merge/index.ts` | 62 | MR 创建/审查、差异对比、代码评审、门禁 |
| **讨论系统** | `discussion/index.ts` | 50 | 讨论 CRUD、分类、评论、投票、排行榜 |
| **其他 20+ 个文件** | issue、commit、branch 等 | ~120 | Issue、提交、分支、标签、发布、通知等 |
---
## 三、请求基础设施(面试核心 — request.ts
`src/utils/request.ts`273 行)是整个平台 API 通信的中枢,每一行都在解决实际问题。
### 3.1 整体架构
```
组件调用 API 函数
proxyService(params) ← 创建一个 Axios 实例
├── 请求拦截器 #1注入 Header
│ ├── DP_TOKEN / DP_REFRESH_TOKENToken 头)
│ ├── Authorization: Bearer xxx权限头
│ ├── page-title / page-repo-id / page-ref页面监控头
│ ├── gitcode-utm-source来源追踪头
│ └── URL 前缀重写setPassportPrefix /uc
├── 请求拦截器 #2缓存降级检查
│ └── degradeInterceptor 决定是否读缓存
├── ===== 发出 HTTP 请求 =====
├── 响应拦截器 #1成功处理
│ ├── code===500 → 检查登录状态
│ └── AES 解密响应数据($Decrypt
├── 响应拦截器 #1失败处理
│ ├── 401 → Token 自动刷新refreshing 锁 + 请求队列)
│ ├── 其他 4xx/5xx → dealWarning防抖 200ms
│ └── 网络超时 → 静默处理
└── 响应拦截器 #2缓存降级存储
└── 成功时存缓存,失败时读缓存返回
```
### 3.2 AES 加密解密
```typescript
// request.ts — 所有响应数据经过 AES-128-CBC 解密
import CryptoJS from 'crypto-js';
const sKey = CryptoJS.enc.Utf8.parse('mXzfCSPBKmEA8aLq');
const iv = CryptoJS.enc.Utf8.parse('tbeJJLC6dZQXXtWr');
// 解密:响应拦截器中自动调用
const $Decrypt = (text) => {
let src = CryptoJS.enc.Base64.stringify(CryptoJS.enc.Base64.parse(text));
let bytes = CryptoJS.AES.decrypt(src, sKey, {
iv, mode: CryptoJS.mode.CBC, padding: CryptoJS.pad.Pkcs7
});
return JSON.parse(bytes.toString(CryptoJS.enc.Utf8));
};
// 响应拦截器中response.data.data = $Decrypt(response.data.data)
```
**为什么用加密?** 态势感知平台涉及敏感的安全数据AES-128-CBC 加密保证了传输过程中即使被截获也无法直接读取。
### 3.3 Token 自动刷新 + 请求队列
这是整个基础设施中设计最巧妙的部分——**当 Token 过期时,多个并发请求只触发一次刷新**
```typescript
// request.ts — Token 刷新逻辑(简化版)
let refreshing = false; // 刷新锁
const queue = []; // 请求等待队列
// 响应拦截器的错误处理分支
async (error) => {
if (response?.status === 401 && Token && ) {
if (refreshing) {
// 情况 A已经有刷新在进行 → 当前请求排队等待
return new Promise((resolve) => {
queue.unshift({ config, resolve });
});
}
// 情况 B第一个遇到 401 的请求 → 执行刷新
refreshing = true;
const res = await refreshToken(); // 调刷新接口
refreshing = false;
if (res.status === 200) {
// 刷新成功 → 更新 Token → 重放所有排队的请求
localStorage.setItem('op_access_token', newToken);
queue.forEach(({ config, resolve }) => {
config.headers.Authorization = `Bearer ${newToken}`;
resolve(axios(config)); // 用新 Token 重发
});
queue.splice(0); // 清空队列
return axios(config); // 重发当前请求
}
}
}
```
**场景**:用户打开一个有 5 个图表的页面5 个请求同时发出,全部返回 401。第一个请求触发 `refreshToken`,其余 4 个进入队列等待。刷新成功后5 个请求用新 Token 全部重发。**不会出现 5 个请求各自刷新一次 Token 的情况。**
### 3.4 错误统一处理status.ts
```typescript
// status.ts — HTTP 状态码 → 用户提示 + 事件通知
export function showMessage(res) {
switch (res.status) {
case 400: return res.data.error_message; // 参数错误
case 401: emitEvent('logout', ...); break; // 未授权 → 触发登出
case 403: emitEvent('forbiddenRefresh'); // 无权限 → 刷新页面
case 404: break; // 不存在 → 不提示
case 408: return '请求超时';
case 500: return '服务器错误';
case 502: return '网络错误';
case 504: return '网络超时';
default: return res.data.error_message || '连接出错';
}
}
// 用防抖包裹200ms防止短时间内多次弹错误提示
export function dealWarning() {
return debounce((res) => {
const msg = showMessage(res);
if (msg) Message.error(msg);
}, 200);
}
```
**面试话术**
> "请求基础设施有四个关键设计。第一AES-128-CBC 加密传输敏感数据在传输过程中全程加密。第二Token 自动刷新 + 请求队列——用 refreshing 锁和 Promise 队列机制,保证多个并发的 401 请求只触发一次 Token 刷新,其余排队等待,刷新成功后统一重发。第三,错误统一处理——所有 HTTP 错误状态码映射为用户友好的提示信息,并用 200ms 防抖避免短时间弹多次错误。第四API 缓存降级——请求成功时缓存到 IndexedDB失败时自动读缓存兜底。"
---
## 四、API 函数层 — 50+ 态势感知接口怎么组织的?
### 4.1 统一的 API 函数格式
每个 API 函数遵循统一的格式:
```typescript
// src/api/jyh/situation.ts — 示例:态势感知看板 API 函数
import request from '@/utils/request';
// 格式export function 函数名(参数): 返回类型
// ↓ 语义化命名 ↓ TypeScript 泛型
// 全球威胁地图数据
export function mapDataPage1(params?: any): Promise<any> {
return request({
url: `/api/v1/page1/map/mapData`, // ← 接口路径
method: 'get', // ← 请求方法
params // ← GET 方式用 params
});
}
// AI 对话POST 方式)
export function fetchChatUseSql(data): Promise<any> {
return request({
url: '/ai/v1/chat/use_sql', // ← AI 服务接口
method: 'POST', // ← POST 请求
data // ← POST 方式用 data 传 JSON body
});
}
// 文件上传FormData 方式)
export function uploadImage(data): Promise<any> {
const formData = new FormData();
formData.append('file', data.file);
return request({
url: '/api/v1/upload/image',
method: 'POST',
data: formData,
headers: { 'Content-Type': 'multipart/form-data' }
});
}
```
### 4.2 态势感知相关的 50+ 接口
态势感知模块的接口集中在 `src/api/jyh/situation.ts`50 个函数),按三个页面组织:
| 页面 | 接口前缀 | 端点数 | 干什么 |
|------|---------|--------|--------|
| **第1页全球风险态势** | `/api/v1/page1/*` | 13 个 | 威胁地图、CVE 趋势、组件排行、漏洞分布、行业饼图 |
| **第2页开源生态与贡献** | `/api/v1/page2/*` | ~18 个 | 企业榜单、基础设施覆盖率、高校俱乐部、社区健康、语言竞争力、开发者、热力图 |
| **第3页武汉市开源生态** | `/api/v1/page3/*` | ~19 个 | HarmonyOS 统计、项目分布、开发者、企业、高校、社区、政策、镜像站点 |
加上 `jyh/index.ts` 中的 45 个通用接口AI 对话、版本详情、SBOM、许可证、预警订阅、修复建议、质量任务等金银湖模块共有 **106 个 API**
---
## 五、两个错误处理工具函数
### 5.1 reqCatch — 安全包装 API 调用
```typescript
// src/utils/catch.ts
// 把 try/catch 包装成一个通用函数,避免每个组件都写 try/catch
export async function reqCatch(req, params) {
try {
const data = await req(params);
return { data, error: null }; // 成功 → 返回数据
} catch (e) {
return { data: null, error: e }; // 失败 → 返回错误对象,不会 throw
}
}
// 用法:
const res = await reqCatch(getOrg, { orgId: 'xxx' });
if (!res.error) {
// 成功处理
} else if (res.error.error_code === 404) {
// 404 处理
}
```
### 5.2 reqCatchV2 — 升级版,自动发事件
```typescript
export async function reqCatchV2(req) {
try {
const data = await req();
return { data, error: null };
} catch (e) {
emitEvent('responseError', e); // ← 自动通过事件总线通知全局
return { data: null, error: e };
}
}
```
**为什么有两版?** `reqCatch` 是早期版本,静默捕获错误让调用方自己处理。`reqCatchV2` 增加了自动事件通知——在路由守卫里用 V2错误会被 `eventBus` 监听到,触发统一的 403/404 页面跳转。
---
## 六、API 缓存降级系统degradeInterceptor.ts
这是一个 360 行的自研系统,核心思想是:**API 成功时缓存结果,失败时读缓存兜底**。
```
请求发出
├── 请求拦截器
│ ├── 是否匹配缓存策略URL 正则匹配)
│ ├── 是否处于降级状态?(上次请求失败,且在熔断时间内)
│ │ → 是:直接读 IndexedDB 缓存返回(不发请求)
│ │ → 否:正常发请求(带重试增强)
├── 请求成功 → 响应拦截器
│ └── 缓存结果到 IndexedDB + 更新状态为 success
└── 请求失败 → 响应拦截器
├── 记录失败状态 + 时间戳
└── 读 IndexedDB 缓存返回(用户看到旧数据而不是错误)
```
**关键配置**`storage-apis.ts`
```typescript
const storageApis = [
{ url: '/api/v1/issues', timeout: 5000, retry: 2 },
{ url: '/api/v1/merge_requests', timeout: 5000, retry: 2 },
// ...
];
```
**面试话术**
> "API 缓存降级系统的思路是'请求成功时缓存,失败时读缓存兜底'。在请求拦截器里判断是否命中缓存策略——如果上次请求失败且在熔断时间内,就直接读 IndexedDB 返回缓存数据,不发网络请求。响应成功时自动更新缓存,失败时记录状态并返回历史缓存。还有缓存条数上限控制和过期清理机制。"
---
## 七、面试问答准备
### Q1"完成50+数据接口的前后端集成"具体做了什么?
> 我参与了两方面的工作。第一是接口定义——和 backend 协作设计并集成了 50+ 个态势感知相关的数据接口,按三个页面组织(全球态势、开源生态、武汉生态),定义了接口路径、请求方法、参数格式和返回结构。第二是搭建了完整的请求基础设施——包括 Axios 实例封装、AES-128-CBC 加密传输、Token 自动刷新 + 请求队列、HTTP 状态码统一处理、API 缓存降级系统等。全平台共集成了 641 个 API 函数,分布在 29 个文件中。
### Q2Token 过期自动刷新是怎么实现的?
> 用了一个 `refreshing` 锁 + Promise 请求队列的机制。当请求返回 401 时,先检查 `refreshing` 是否为 true——如果是说明已经有请求在刷新 Token 了,当前请求就包装成一个 Promise 放进等待队列;如果不是,就执行刷新并设 `refreshing = true`。刷新成功后更新 Token然后遍历队列用新 Token 重发所有等待的请求。这样无论同时有多少个请求返回 401都只会发一次刷新请求。
### Q3为什么需要 AES 加密?
> 这个平台涉及安全态势数据,有一定的敏感性。后端返回的数据在传输层用 AES-128-CBC 加密,前端在响应拦截器中自动解密。加密密钥和 IV 是硬编码在前端代码中的生产环境会用更安全的方式管理。加密流程对业务代码完全透明——API 函数只是调用 `request()`,加密解密都在拦截器里自动完成。
### Q4reqCatch 和 reqCatchV2 有什么区别?
> 都是 try/catch 的通用封装,避免每个页面都写 try/catch。区别在于`reqCatch` 静默捕获错误,返回 `{ data, error }` 结构,让调用方自己判断。`reqCatchV2` 在捕获错误后还会通过事件总线 `emitEvent('responseError', e)` 通知全局——这主要用于路由守卫里,发生 403/404 时通过事件触发全局的页面跳转。新代码基本都用 V2。
### Q5API 缓存降级是怎么运作的?
> 这是一套自研的请求降级系统。基于 IndexedDB 做持久化缓存。请求成功时自动缓存响应数据;请求失败时判断是否在熔断时间内——如果是,直接读缓存返回,用户看到的是旧数据而不是错误页面。还有最大缓存条数限制和过期清理机制。通过 `storage-apis.ts` 配置哪些接口走缓存策略,可以设置超时、重试次数、熔断时间等参数。
### Q6HTTP 错误怎么统一处理的?
> 在响应拦截器里统一处理。根据 HTTP 状态码映射不同的行为400 返回参数错误提示、401 触发 Token 刷新或登出、403 触发页面刷新、404 静默不提示、5xx 显示服务端错误提示。错误提示用 `dealWarning` 函数包裹了 200ms 防抖,避免短时间内多个请求同时失败时弹出多条错误消息。还有 `customError: true` 参数,允许特定请求跳过默认错误处理,由调用方自行处理。
---
## 八、关键数字(面试时用)
| 数据 | 数值 |
|------|------|
| 全平台 API 函数总数 | **641 个** |
| API 文件数 | **29 个** |
| 态势感知金银湖API 数 | **106 个** |
| 态势感知看板接口situation.ts | **50 个** |
| 请求基础设施代码量 | `request.ts` 273 行 + `degradeInterceptor.ts` 360 行 + `catch.ts` 46 行 + `status.ts` 76 行 |
| Token 刷新机制 | refreshing 锁 + Promise 队列,确保多并发 401 只刷新一次 |
| 加密方式 | AES-128-CBC密钥 mXzfCSPBKmEA8aLqIV tbeJJLC6dZQXXtWr |
| 请求超时 | 30 秒 |
| 错误提示防抖 | 200ms |
---
## 九、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 请求核心基础设施 | `src/utils/request.ts`273 行Axios、拦截器、加密、Token 刷新) |
| API 缓存降级系统 | `src/utils/degradeInterceptor.ts`360 行IndexedDB 缓存、熔断、重试) |
| 错误捕获包装函数 | `src/utils/catch.ts`reqCatch + reqCatchV2 |
| HTTP 状态码处理 | `src/utils/status.ts`dealWarning + showMessage |
| 缓存配置 | `src/api/storage-apis.ts`(哪些接口走缓存策略) |
| 缓存存储层 | `src/utils/apiStorage.ts`IndexedDB 封装) |
| 态势感知看板 API50 端点) | `src/api/jyh/situation.ts` |
| 金银湖核心 API45 端点) | `src/api/jyh/index.ts` |
| 金银湖首页 API5 端点) | `src/api/jyh/home.ts` |
| 金银湖认证 API3 端点) | `src/api/jyh/auth.ts` |
| 仓库管理 API161 端点) | `src/api/repo/index.ts` |
| 组织管理 API55 端点) | `src/api/org/index.ts` |
| 用户管理 API55+ 端点) | `src/api/user/index.ts` |
| 合并请求 API62 端点) | `src/api/merge/index.ts` |
| 讨论系统 API50 端点) | `src/api/discussion/index.ts` |