Compare commits
207 Commits
master
...
a5c3b03eb8
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a5c3b03eb8 | ||
|
|
46eb668c31 | ||
|
|
1aaefae7e8 | ||
|
|
00652dab3b | ||
|
|
7553c7ebd0 | ||
|
|
4f58ace30c | ||
|
|
40be92afc3 | ||
|
|
693fb32bb3 | ||
|
|
4a6d0137a1 | ||
|
|
bbad6ce7e2 | ||
|
|
d280d32326 | ||
|
|
42d4f24c4b | ||
|
|
ce325e8518 | ||
|
|
157048054f | ||
|
|
1fd3c46b42 | ||
|
|
4b695b932a | ||
|
|
483c6d5c6b | ||
|
|
3fc27a684b | ||
|
|
b2b04763d3 | ||
|
|
3d487c8d6c | ||
|
|
613b801da8 | ||
|
|
2affb8296e | ||
|
|
88612f25ef | ||
|
|
520b030fab | ||
|
|
ecc78c82b3 | ||
|
|
82005a43a3 | ||
|
|
7b59d3ac9c | ||
|
|
15b04e6788 | ||
|
|
4ddc7aa90f | ||
|
|
c09541afb7 | ||
|
|
5c0d46904b | ||
|
|
0fcbf2ba73 | ||
|
|
a832c64d41 | ||
|
|
f640eca25c | ||
|
|
97ab1a1e79 | ||
|
|
33a617ef91 | ||
|
|
9caa5322ba | ||
|
|
3eef874bb8 | ||
|
|
e51e9a2176 | ||
|
|
e67c386cb7 | ||
|
|
bd9decfed8 | ||
|
|
bf25fcfc92 | ||
|
|
05cce185e6 | ||
|
|
2876d23502 | ||
|
|
5ce6380dea | ||
|
|
424a0a1de6 | ||
|
|
231d8bd14e | ||
|
|
6f79cf1f9c | ||
|
|
89b5a01446 | ||
|
|
6c2a33996f | ||
|
|
119b2f0ec5 | ||
|
|
1853de9c3a | ||
|
|
bcab3014ac | ||
|
|
e66ca622f5 | ||
|
|
d2603e1a38 | ||
|
|
03a099ea10 | ||
|
|
3ffe0716e6 | ||
|
|
3ac1ef0d4d | ||
|
|
fa07a197f0 | ||
|
|
f36602bab3 | ||
|
|
e9d8e48514 | ||
|
|
e495e3ec9f | ||
|
|
3dba3a2189 | ||
|
|
aae51d0b6a | ||
|
|
5c8f9273c4 | ||
|
|
d0be493658 | ||
|
|
75085051ff | ||
|
|
a8207b44ae | ||
|
|
08ee73c225 | ||
|
|
1643ea6e63 | ||
|
|
9fd93c460c | ||
|
|
d62710e910 | ||
|
|
f68ced16f8 | ||
|
|
5d74295706 | ||
|
|
a70db39a08 | ||
|
|
d9d6fbdba6 | ||
|
|
43ca3c13a1 | ||
|
|
1085a8f8c8 | ||
|
|
21f068bc9d | ||
|
|
a8d8773c63 | ||
|
|
25f2c6d50a | ||
|
|
181caeb231 | ||
|
|
a9e688a4b0 | ||
|
|
666ca8c5a7 | ||
|
|
9f41da5df8 | ||
|
|
4d9f1034d7 | ||
|
|
e0281f3b7c | ||
|
|
0aa47a7827 | ||
|
|
f113ea5582 | ||
|
|
b7f0ce70ae | ||
|
|
1aef4f7d55 | ||
|
|
336b4c2a47 | ||
|
|
c8262ea17d | ||
|
|
944fb16bf4 | ||
|
|
b2a702324a | ||
|
|
7e39d3164b | ||
|
|
7b0e00bed5 | ||
|
|
5f22b7d30e | ||
|
|
d63b9f32bd | ||
|
|
cc1514d3c4 | ||
|
|
907a26cc94 | ||
|
|
4d9c8ed52c | ||
|
|
31da7bdea9 | ||
|
|
e90006368a | ||
|
|
f3b63fdfd9 | ||
|
|
29fa1a2199 | ||
|
|
b80ff647c9 | ||
| d0d9adfde9 | |||
| 81e4f3693b | |||
|
|
6fe0681152 | ||
| e18b165979 | |||
|
|
4edc5527b0 | ||
|
|
b54e84f62e | ||
|
|
8a021d0adc | ||
|
|
5a1b50ba7f | ||
|
|
dc1f4d30ad | ||
|
|
594e982469 | ||
|
|
75a2b30776 | ||
|
|
5e9c7686e5 | ||
|
|
abaaeff09e | ||
|
|
365ebe9964 | ||
|
|
562eae4342 | ||
|
|
ed608275ed | ||
|
|
bd1c1902b0 | ||
|
|
7c87be069f | ||
|
|
785d783226 | ||
|
|
dda0aecfa7 | ||
|
|
38f8b376af | ||
| 0f0cbb9998 | |||
|
|
a7d802a40f | ||
| 798f98e194 | |||
| adc4a67221 | |||
|
|
1714534d3b | ||
|
|
5c793a2759 | ||
|
|
d9baf96cce | ||
|
|
e698c4f166 | ||
|
|
e3ee78b9f4 | ||
|
|
12cf936916 | ||
|
|
42aa1eda2b | ||
|
|
7e6321a7b9 | ||
| 8cab400008 | |||
| e79572cd34 | |||
| 678dd3842a | |||
| be36557c0c | |||
| 634726b75e | |||
| b51d981d83 | |||
| db4ad5744c | |||
|
|
bb17c69ba9 | ||
|
|
58579b7ad3 | ||
|
|
8885af40c2 | ||
|
|
2906b515e4 | ||
| ffab7ede4a | |||
|
|
43ab6ec1c3 | ||
|
|
dc09f426b4 | ||
|
|
8f208d4b73 | ||
|
|
a812a96d82 | ||
|
|
59d5988e2a | ||
|
|
168a885a7d | ||
|
|
d8a8629c4b | ||
|
|
ca7eee0fc2 | ||
| 722a4c2a30 | |||
| ea34772999 | |||
| 5114f2cfee | |||
| a8b8192ed4 | |||
|
|
68ef282e65 | ||
|
|
1ee6a6c330 | ||
|
|
ea2bd0aa5a | ||
|
|
97dbec995f | ||
|
|
51078acc1d | ||
|
|
d6b5456e43 | ||
|
|
0f84f23c63 | ||
|
|
f13c2d06b3 | ||
|
|
6bd1db5ed7 | ||
|
|
6c477d923b | ||
|
|
a61dbfd821 | ||
|
|
7924714d0e | ||
|
|
a630f40b76 | ||
|
|
61f8e017de | ||
|
|
356810f316 | ||
|
|
fd4f9bc73d | ||
|
|
44467bc22a | ||
|
|
e8eabfab9f | ||
|
|
c249b9d70f | ||
|
|
d72546c81b | ||
|
|
87c2777923 | ||
|
|
af7578ec2a | ||
|
|
1e2d8f775d | ||
|
|
e7150457c4 | ||
|
|
5550b0fb7f | ||
|
|
28ea0cc27f | ||
|
|
18319e66c5 | ||
|
|
b032e2b074 | ||
|
|
63d4751838 | ||
|
|
c7de332275 | ||
|
|
fcb247395b | ||
|
|
9e86d1e6bc | ||
|
|
e76110239d | ||
|
|
86d26f161a | ||
|
|
e01a050f80 | ||
|
|
4440333736 | ||
|
|
d7dfa9bf8d | ||
|
|
0ab4649909 | ||
|
|
ae00eb3d6f | ||
|
|
cf25a6feb1 | ||
|
|
0d266e8a03 | ||
|
|
f1f05c11f6 | ||
|
|
f5b59fadd9 |
2
.env
2
.env
@@ -1,4 +1,4 @@
|
||||
VITE_APP_TITLE = '可信开源代码库'
|
||||
VITE_APP_TITLE = 'Tcode'
|
||||
VITE_TEST_REPOID = 34
|
||||
|
||||
VITE_SECRET_KEY = 'SA!nUNPZ5o!OSV&B'
|
||||
|
||||
@@ -5,10 +5,10 @@ VITE_ENV = 'development'
|
||||
VITE_HOST = 'https://test.gitcode.net'
|
||||
|
||||
# 接口地址
|
||||
VITE_API_HOST = '/gateway/official'
|
||||
VITE_API_HOST = 'http://222.20.126.217:8100/'
|
||||
|
||||
# devPress接口地址
|
||||
VITE_DEV_API_HOST = '/gateway/official'
|
||||
VITE_DEV_API_HOST = 'http://222.20.126.217:8100/'
|
||||
|
||||
# devPress地址
|
||||
VITE_DEV_HOST = 'https://test.gitcode.net'
|
||||
|
||||
2
.env.loc
2
.env.loc
@@ -39,7 +39,7 @@ VITE_USER_CENTER_HOST = 'https://local.gitcode.net:8002'
|
||||
# ai-copilot 子应用
|
||||
VITE_COPILOT_HOST = 'https://local.gitcode.net:8003/child/aichat-app'
|
||||
|
||||
VITE_BASE_URL=/openRepoPortal/
|
||||
VITE_BASE_URL=/SAP/
|
||||
|
||||
# AI大模型接口地址
|
||||
VITE_AI_API_HOST = 'http://222.20.126.208:8089'
|
||||
|
||||
2
.env.pre
2
.env.pre
@@ -37,7 +37,7 @@ VITE_USER_CENTER_HOST = 'https://pre-usercenter-app.gitcode.com'
|
||||
# ai-copilot 子应用
|
||||
VITE_COPILOT_HOST = 'https://pre-copilot-app.gitcode.com'
|
||||
|
||||
VITE_BASE_URL=/openRepoPortal/
|
||||
VITE_BASE_URL=/SAP/
|
||||
|
||||
# AI大模型接口地址
|
||||
VITE_AI_API_HOST = 'http://222.20.126.208:8089'
|
||||
|
||||
@@ -2,13 +2,13 @@
|
||||
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接口地址
|
||||
VITE_DEV_API_HOST = '/gateway/official'
|
||||
VITE_DEV_API_HOST = 'http://222.20.126.217:8100/'
|
||||
|
||||
# devPress地址
|
||||
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_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'
|
||||
@@ -37,7 +37,7 @@ VITE_USER_CENTER_HOST = 'https://usercenter-app.gitcode.com'
|
||||
# ai-copilot 子应用
|
||||
VITE_COPILOT_HOST = 'https://copilot-app.gitcode.com'
|
||||
|
||||
VITE_BASE_URL=/openRepoPortal/
|
||||
VITE_BASE_URL=/SAP/
|
||||
|
||||
# AI大模型接口地址
|
||||
VITE_AI_API_HOST = 'http://222.20.126.208:8087'
|
||||
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -12,7 +12,7 @@ lerna-debug.log*
|
||||
node_modules
|
||||
.DS_Store
|
||||
dist
|
||||
openRepoPortal
|
||||
SAP
|
||||
nginx
|
||||
dist-ssr
|
||||
coverage
|
||||
@@ -39,3 +39,5 @@ components.d.ts
|
||||
|
||||
# 组件汇总文件,仅限开发本地浏览
|
||||
component.md
|
||||
|
||||
./deploy.js
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
# 可信开源代码库
|
||||
# Tcode
|
||||
|
||||
架构:基于[create-vue](https://github.com/vuejs/create-vue)构建(vite+vue3+pinia+scss)
|
||||
组件库:[Vue DevUI](https://vue-devui.github.io/)
|
||||
|
||||
75
deploy.js
Normal file
75
deploy.js
Normal 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();
|
||||
17
index.html
17
index.html
@@ -9,7 +9,7 @@
|
||||
<link rel="dns-prefetch" href="//usercenter-app.gitcode.com">
|
||||
<link rel="dns-prefetch" href="//res.hc-cdn.com">
|
||||
<!-- <meta name="referrer" content="no-referrer"> -->
|
||||
<title>可信开源代码库</title>
|
||||
<title>Tcode</title>
|
||||
<style type="text/css">
|
||||
.icon {
|
||||
width: 1em; height: 1em;
|
||||
@@ -23,19 +23,4 @@
|
||||
<div id="app"></div>
|
||||
<script type="module" src="/src/main.ts"></script>
|
||||
</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>
|
||||
|
||||
733
package-lock.json
generated
733
package-lock.json
generated
@@ -1,11 +1,11 @@
|
||||
{
|
||||
"name": "gitcode-v2",
|
||||
"name": "SAP",
|
||||
"version": "0.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "gitcode-v2",
|
||||
"name": "SAP",
|
||||
"version": "0.0.0",
|
||||
"dependencies": {
|
||||
"@devui-design/icons": "^1.4.0",
|
||||
@@ -14,15 +14,18 @@
|
||||
"@sentry/vue": "^7.101.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"ant-design-vue": "4.x",
|
||||
"axios": "^1.6.8",
|
||||
"canvas-confetti": "^1.9.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"devui-theme": "^0.0.7",
|
||||
"dompurify": "^3.0.5",
|
||||
"echarts": "^6.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"highlightjs-line-numbers.js": "^2.9.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-web": "^5.12.2",
|
||||
@@ -37,6 +40,7 @@
|
||||
"swiper": "^10.3.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-devui": "^1.6.31",
|
||||
"vue-fast-marquee": "^1.0.6",
|
||||
"vue-router": "^4.2.2",
|
||||
"xlsx": "^0.18.5",
|
||||
"yaml": "^2.4.1"
|
||||
@@ -61,6 +65,7 @@
|
||||
"prettier": "^2.8.8",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.63.6",
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "~5.0.4",
|
||||
@@ -103,6 +108,34 @@
|
||||
"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": {
|
||||
"version": "7.24.2",
|
||||
"resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz",
|
||||
@@ -1699,7 +1732,6 @@
|
||||
"version": "7.24.1",
|
||||
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz",
|
||||
"integrity": "sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"regenerator-runtime": "^0.14.0"
|
||||
},
|
||||
@@ -1710,8 +1742,7 @@
|
||||
"node_modules/@babel/runtime/node_modules/regenerator-runtime": {
|
||||
"version": "0.14.1",
|
||||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
|
||||
"dev": true
|
||||
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw=="
|
||||
},
|
||||
"node_modules/@babel/template": {
|
||||
"version": "7.24.0",
|
||||
@@ -1765,11 +1796,389 @@
|
||||
"resolved": "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz",
|
||||
"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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/@devui-design/icons/-/icons-1.4.0.tgz",
|
||||
"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": {
|
||||
"version": "0.18.20",
|
||||
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
|
||||
@@ -2322,6 +2731,16 @@
|
||||
"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": {
|
||||
"version": "2.0.1",
|
||||
"resolved": "https://registry.npmjs.org/@tsconfig/node18/-/node18-2.0.1.tgz",
|
||||
@@ -3196,6 +3615,46 @@
|
||||
"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": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz",
|
||||
@@ -3242,6 +3701,12 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz",
|
||||
@@ -3273,6 +3738,16 @@
|
||||
"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": {
|
||||
"version": "4.2.5",
|
||||
"resolved": "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz",
|
||||
@@ -3390,6 +3865,16 @@
|
||||
"integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==",
|
||||
"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": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz",
|
||||
@@ -3468,6 +3953,16 @@
|
||||
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
|
||||
"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": {
|
||||
"version": "1.0.7",
|
||||
"resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.7.tgz",
|
||||
@@ -3686,6 +4181,12 @@
|
||||
"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": {
|
||||
"version": "0.0.1",
|
||||
"resolved": "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz",
|
||||
@@ -3708,7 +4209,6 @@
|
||||
"version": "3.36.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz",
|
||||
"integrity": "sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==",
|
||||
"dev": true,
|
||||
"hasInstallScript": true,
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
@@ -3728,6 +4228,21 @@
|
||||
"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": {
|
||||
"version": "1.2.2",
|
||||
"resolved": "https://mirrors.cloud.tencent.com/npm/crc-32/-/crc-32-1.2.2.tgz",
|
||||
@@ -4693,6 +5208,18 @@
|
||||
"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": {
|
||||
"version": "3.0.11",
|
||||
"resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz",
|
||||
@@ -4705,12 +5232,13 @@
|
||||
"dev": true
|
||||
},
|
||||
"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==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/echarts/-/echarts-6.0.0.tgz",
|
||||
"integrity": "sha512-Tte/grDQRiETQP4xz3iZWSvoHrkCQtwqd6hs+mifXcjrCuo2iKWbajFObuLJVBlDIJlOzgQPd1hsaKt/3+OMkQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0",
|
||||
"zrender": "5.3.2"
|
||||
"zrender": "6.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/echarts/node_modules/tslib": {
|
||||
@@ -5517,6 +6045,21 @@
|
||||
"dev": 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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
|
||||
@@ -6144,6 +6687,15 @@
|
||||
"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": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz",
|
||||
@@ -6294,8 +6846,7 @@
|
||||
"node_modules/js-tokens": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz",
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==",
|
||||
"dev": true
|
||||
"integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="
|
||||
},
|
||||
"node_modules/js-yaml": {
|
||||
"version": "4.1.0",
|
||||
@@ -6310,6 +6861,12 @@
|
||||
"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": {
|
||||
"version": "2.5.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
|
||||
@@ -6516,6 +7073,18 @@
|
||||
"dev": 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": {
|
||||
"version": "5.12.2",
|
||||
"resolved": "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz",
|
||||
@@ -6725,6 +7294,14 @@
|
||||
"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": {
|
||||
"version": "3.3.11",
|
||||
"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_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": {
|
||||
"version": "1.4.0",
|
||||
"resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz",
|
||||
@@ -7667,6 +8250,12 @@
|
||||
"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": {
|
||||
"version": "1.22.8",
|
||||
"resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz",
|
||||
@@ -7866,6 +8455,15 @@
|
||||
"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": {
|
||||
"version": "1.1.2",
|
||||
"resolved": "https://registry.npmjs.org/select/-/select-1.1.2.tgz",
|
||||
@@ -7911,6 +8509,12 @@
|
||||
"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": {
|
||||
"version": "2.0.0",
|
||||
"resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz",
|
||||
@@ -8063,6 +8667,24 @@
|
||||
"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": {
|
||||
"version": "4.2.3",
|
||||
"resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz",
|
||||
@@ -8314,7 +8936,7 @@
|
||||
},
|
||||
"node_modules/swiper": {
|
||||
"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==",
|
||||
"funding": [
|
||||
{
|
||||
@@ -8326,6 +8948,7 @@
|
||||
"url": "http://opencollective.com/swiper"
|
||||
}
|
||||
],
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">= 4.7.0"
|
||||
}
|
||||
@@ -8425,6 +9048,15 @@
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"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": {
|
||||
"version": "0.4.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "2.1.0",
|
||||
"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",
|
||||
"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": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/uc.micro/-/uc.micro-1.0.6.tgz",
|
||||
"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": {
|
||||
"version": "9.4.2",
|
||||
"resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-9.4.2.tgz",
|
||||
@@ -9028,6 +9692,15 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"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": {
|
||||
"version": "4.3.0",
|
||||
"resolved": "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz",
|
||||
@@ -9102,6 +9775,30 @@
|
||||
"integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==",
|
||||
"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": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",
|
||||
@@ -9392,9 +10089,10 @@
|
||||
}
|
||||
},
|
||||
"node_modules/zrender": {
|
||||
"version": "5.3.2",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-5.3.2.tgz",
|
||||
"integrity": "sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==",
|
||||
"version": "6.0.0",
|
||||
"resolved": "https://registry.npmjs.org/zrender/-/zrender-6.0.0.tgz",
|
||||
"integrity": "sha512-41dFXEEXuJpNecuUQq6JlbybmnHaqqpGlbH1yxnA5V9MMP4SbohSVZsJIwz+zdjQXSSlR1Vc34EgH1zxyTDvhg==",
|
||||
"license": "BSD-3-Clause",
|
||||
"dependencies": {
|
||||
"tslib": "2.3.0"
|
||||
}
|
||||
@@ -9402,7 +10100,8 @@
|
||||
"node_modules/zrender/node_modules/tslib": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.3.0.tgz",
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg=="
|
||||
"integrity": "sha512-N82ooyxVNm6h1riLCoyS9e3fuJ3AMG2zIZs2Gd1ATcSFjSA23Q0fzjjZeh0jbJvWVDZ0cJT8yaNNaaXHzueNjg==",
|
||||
"license": "0BSD"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
10
package.json
10
package.json
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"name": "gitcode-v2",
|
||||
"name": "SAP",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
@@ -16,7 +16,8 @@
|
||||
"preview": "vite preview",
|
||||
"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",
|
||||
"format": "prettier --write src/"
|
||||
"format": "prettier --write src/",
|
||||
"deploy": "node deploy.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"@devui-design/icons": "^1.4.0",
|
||||
@@ -25,15 +26,18 @@
|
||||
"@sentry/vue": "^7.101.1",
|
||||
"@types/crypto-js": "^4.1.1",
|
||||
"@vueuse/core": "^10.2.1",
|
||||
"ant-design-vue": "4.x",
|
||||
"axios": "^1.6.8",
|
||||
"canvas-confetti": "^1.9.2",
|
||||
"crypto-js": "^4.2.0",
|
||||
"dayjs": "^1.11.13",
|
||||
"devui-theme": "^0.0.7",
|
||||
"dompurify": "^3.0.5",
|
||||
"echarts": "^6.0.0",
|
||||
"highlight.js": "^11.11.1",
|
||||
"highlightjs-line-numbers.js": "^2.9.0",
|
||||
"js-cookie": "^3.0.5",
|
||||
"jsencrypt": "^3.5.4",
|
||||
"localforage": "^1.10.0",
|
||||
"lodash": "^4.17.21",
|
||||
"lottie-web": "^5.12.2",
|
||||
@@ -48,6 +52,7 @@
|
||||
"swiper": "^10.3.1",
|
||||
"vue": "^3.3.4",
|
||||
"vue-devui": "^1.6.31",
|
||||
"vue-fast-marquee": "^1.0.6",
|
||||
"vue-router": "^4.2.2",
|
||||
"xlsx": "^0.18.5",
|
||||
"yaml": "^2.4.1"
|
||||
@@ -72,6 +77,7 @@
|
||||
"prettier": "^2.8.8",
|
||||
"rollup-plugin-visualizer": "^5.9.2",
|
||||
"sass": "^1.63.6",
|
||||
"ssh2": "^1.17.0",
|
||||
"tailwindcss": "^3.3.3",
|
||||
"terser": "^5.39.0",
|
||||
"typescript": "~5.0.4",
|
||||
|
||||
453
src/api/jyh/situation.ts
Normal file
453
src/api/jyh/situation.ts
Normal 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
|
||||
});
|
||||
}
|
||||
@@ -52,7 +52,7 @@ export const injectRouter = (router) => {
|
||||
// 判断命名空间类型
|
||||
const globalStore = useGlobalInfoStore();
|
||||
globalStore.setNamespaceType(-1); // 重置namespaceType,防止非组织路由请求组织信息
|
||||
globalStore.setLanguageList();
|
||||
// globalStore.setLanguageList();
|
||||
if (to.meta.pageType === 'userOrOrg') {
|
||||
const { namespace } = to.params;
|
||||
const params = {
|
||||
|
||||
137
src/views/Jyh/Community/StatsBar.vue
Normal file
137
src/views/Jyh/Community/StatsBar.vue
Normal 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>
|
||||
210
src/views/Jyh/Community/components/HMStatsBar.vue
Normal file
210
src/views/Jyh/Community/components/HMStatsBar.vue
Normal 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
@@ -1,36 +1,90 @@
|
||||
<template>
|
||||
<div class="stats-glass-bar">
|
||||
<div class="stat-node">
|
||||
<span class="stat-value">1000W+</span>
|
||||
<span class="stat-desc">鸿蒙开发者</span>
|
||||
<span class="stat-value">{{ whCompany }}</span>
|
||||
<span class="stat-desc">武汉开源企业</span>
|
||||
</div>
|
||||
<div class="stat-separator"></div>
|
||||
<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-value">{{ whCommunity }}</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-value">{{ whDeveloper }}</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-value">{{ whProject }}</span>
|
||||
<span class="stat-desc">武汉开源项目数量</span>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
export default {
|
||||
name: "StatsBar"
|
||||
}
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
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>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
@@ -42,7 +96,7 @@ name: "StatsBar"
|
||||
/* 布局控制:撑满容器并增加内边距 */
|
||||
width: 100%;
|
||||
padding: 2rem 4rem;
|
||||
//margin: 2rem 0;
|
||||
margin: 0 0 1rem;
|
||||
|
||||
/* 深色玻璃拟态背景 */
|
||||
background: rgba(255, 255, 255, 0.02);
|
||||
|
||||
@@ -14,7 +14,6 @@
|
||||
汇聚武汉开源力量,共建城市开源生态
|
||||
</p>
|
||||
</div>
|
||||
<StatsBar />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -30,9 +29,12 @@
|
||||
class="lucide lucide-activity" aria-hidden="true">
|
||||
<path d="M22 12h-4l-3 9L9 3l-3 9H2" />
|
||||
</svg>
|
||||
<span>武汉开源生态数据可视化</span>
|
||||
<span>武汉开源生态建设总览</span>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="index-module__NV_5cW__sectionHeader">
|
||||
<StatsBar />
|
||||
</div>
|
||||
|
||||
<div class="index-module__NV_5cW__chartsGrid">
|
||||
|
||||
@@ -309,129 +311,255 @@ import * as echarts from 'echarts';
|
||||
import { usePageResize } from '@/utils/hooks/usePageResize';
|
||||
import StatsBar from './components/StatsBar.vue';
|
||||
import HarmonyEcoCharts from './components/HarmonyEcoCharts.vue';
|
||||
import { wuhanGithubProjectsPage3, cityProjectPage3, chinaExcellentProjectsPage3, excellentProjectsPage3, talentPage3 } from '@/api/jyh/situation';
|
||||
|
||||
const { widthType } = usePageResize();
|
||||
|
||||
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 githubProjects = ref([]);
|
||||
|
||||
// 武汉优质开源项目备用数据
|
||||
const setgithubProjectsdata = () => {
|
||||
githubProjects.value = [
|
||||
{
|
||||
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 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 setCityProjectDefaultData = () => {
|
||||
chartData.value.cityProject.xAxis = ['北京', '上海', '深圳', '杭州', '广州', '成都', '武汉', '南京', '西安', '重庆'];
|
||||
chartData.value.cityProject.data = [757259, 678614, 343417, 315406, 177003, 168300, 120827, 111593, 58678, 56564];
|
||||
isEmpty.value.cityProject = false;
|
||||
};
|
||||
|
||||
// 获取中国城市开源项目数量数据
|
||||
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 activeTab = ref(0);
|
||||
|
||||
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 excellentProjects = ref([]);
|
||||
|
||||
const chinaExcellentProjects = ref([
|
||||
{
|
||||
'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': '武汉希望组生物科技有限公司'
|
||||
const chinaExcellentProjects = ref([]);
|
||||
|
||||
// 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 wuhanCommunities = ref([
|
||||
{ name: '黄鹤开源社区', type: '核心生态', slogan: '国家级、城市级开源社区平台,武汉开源生态的核心枢纽' },
|
||||
@@ -458,12 +586,12 @@ const developerNames = ['白纯骢', '陆红旭', '慕冬亮', '张继德', '蒋
|
||||
// 图表数据
|
||||
const chartData = ref({
|
||||
cityProject: {
|
||||
xAxis: ['北京', '上海', '深圳', '杭州', '广州', '成都', '武汉', '南京', '西安', '重庆'],
|
||||
data: [757259, 678614, 343417, 315406, 177003, 168300, 120827, 111593, 58678, 56564]
|
||||
xAxis: [],
|
||||
data: []
|
||||
},
|
||||
talent: {
|
||||
xAxis: ['北京', '上海', '广州', '深圳', '成都', '杭州', '武汉', '南京', '郑州', '长沙'],
|
||||
data: [2115924, 1591988, 1555240, 1163580, 894108, 830306, 791335, 733581, 553397, 487971]
|
||||
xAxis: [],
|
||||
data: []
|
||||
},
|
||||
communityRank: {
|
||||
xAxis: ['Deepin', 'OpenHarmony', 'MindSpore', 'IoTDB', 'GoFrame'],
|
||||
@@ -642,6 +770,7 @@ const initCommunityRankChart = () => {
|
||||
const el = document.getElementById('communityRankChart');
|
||||
if (!el) return;
|
||||
const myChart = echarts.init(el);
|
||||
chartInstances.value.communityRank = myChart;
|
||||
myChart.setOption({
|
||||
tooltip: { trigger: 'axis', valueFormatter: (val: any) => val + ' 个' },
|
||||
grid: { left: '3%', right: '4%', bottom: '15px', top: '22%', containLabel: true },
|
||||
@@ -767,8 +896,13 @@ watch(widthType, () => {
|
||||
init();
|
||||
});
|
||||
|
||||
onMounted(() => {
|
||||
init();
|
||||
onMounted(async() => {
|
||||
await fetchGithubProjects();
|
||||
await fetchCityProjectData(); // 获取城市项目数据
|
||||
await fetchTalentData(); // 获取城市人才数据
|
||||
await fetchchinaExcellentProjects(); // 获取2024中国优秀开源项目数据
|
||||
await fetchexcellentProjects(); // 获取2025武汉优秀开源项目数据
|
||||
init(); // 再初始化图表
|
||||
});
|
||||
|
||||
onUnmounted(() => {
|
||||
@@ -1363,4 +1497,8 @@ onUnmounted(() => {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
}
|
||||
|
||||
.index-module__NV_5cW__chartsSection {
|
||||
margin-top: 2rem;
|
||||
}
|
||||
</style>
|
||||
|
||||
1408
src/views/Jyh/Community/indexOld.vue
Normal file
1408
src/views/Jyh/Community/indexOld.vue
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
@@ -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>
|
||||
436
src/views/Jyh/ecosystem/components/StarDevelopersChart.vue
Normal file
436
src/views/Jyh/ecosystem/components/StarDevelopersChart.vue
Normal 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>
|
||||
206
src/views/Jyh/ecosystem/components/StatsGlass.vue
Normal file
206
src/views/Jyh/ecosystem/components/StatsGlass.vue
Normal 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>
|
||||
425
src/views/Jyh/ecosystem/components/UniversityClubStatsChart.vue
Normal file
425
src/views/Jyh/ecosystem/components/UniversityClubStatsChart.vue
Normal 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>
|
||||
446
src/views/Jyh/ecosystem/components/VitalityWaveChart.vue
Normal file
446
src/views/Jyh/ecosystem/components/VitalityWaveChart.vue
Normal 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>
|
||||
476
src/views/Jyh/ecosystem/components/talentMapChart.vue
Normal file
476
src/views/Jyh/ecosystem/components/talentMapChart.vue
Normal 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
@@ -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>
|
||||
195
src/views/Jyh/security/StatsGlass.vue
Normal file
195
src/views/Jyh/security/StatsGlass.vue
Normal 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: 0,
|
||||
university: 0,
|
||||
activity: 0,
|
||||
project: 0
|
||||
});
|
||||
|
||||
// 加载状态(可选,用于显示 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>
|
||||
401
src/views/Jyh/security/components/CveTrendChart.vue
Normal file
401
src/views/Jyh/security/components/CveTrendChart.vue
Normal 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>
|
||||
265
src/views/Jyh/security/components/HighRiskComponentRank.vue
Normal file
265
src/views/Jyh/security/components/HighRiskComponentRank.vue
Normal 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>
|
||||
257
src/views/Jyh/security/components/IndustryDistribution.vue
Normal file
257
src/views/Jyh/security/components/IndustryDistribution.vue
Normal 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>
|
||||
327
src/views/Jyh/security/components/IntelligenceFeed.vue
Normal file
327
src/views/Jyh/security/components/IntelligenceFeed.vue
Normal 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>
|
||||
262
src/views/Jyh/security/components/ScatterChart.vue
Normal file
262
src/views/Jyh/security/components/ScatterChart.vue
Normal 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>
|
||||
@@ -60,19 +60,14 @@ Vue 3 适配版代码
|
||||
import { ref, onMounted, onBeforeUnmount, nextTick } from 'vue';
|
||||
import * as echarts from 'echarts';
|
||||
import worldJson from './world.json';
|
||||
import { mapVulnStatsPage1, mapRegionStatsPage1, mapDataPage1, mapVulnTypeStatsPage1 } from '@/api/jyh/situation';
|
||||
|
||||
// 响应式数据
|
||||
const chartDiv = ref(null);
|
||||
let myChart = null;
|
||||
let timer = null;
|
||||
|
||||
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 vulnStats = ref([]);
|
||||
|
||||
const regionStats = ref([
|
||||
{ name: 'United States', score: 435202 },
|
||||
@@ -82,11 +77,8 @@ const regionStats = ref([
|
||||
{ name: 'Germany', score: 126397 },
|
||||
]);
|
||||
|
||||
const alerts = [
|
||||
{ title: "高危漏洞通告", desc: "Apache Log4j2 远程代码执行漏洞复现" },
|
||||
{ title: "僵尸网络活动", desc: "Mirai 变种正在扫描 IoT 设备端口 23" },
|
||||
{ title: "APT 组织活动", desc: "OceanLotus 组织针对金融行业的钓鱼攻击" },
|
||||
];
|
||||
// 实时情报数据状态
|
||||
const alerts = ref([]);
|
||||
|
||||
const currentAlert = ref(null);
|
||||
let alertIndex = 0;
|
||||
@@ -100,35 +92,118 @@ const geoCoordMap = {
|
||||
'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 = () => {
|
||||
currentAlert.value = alerts[0];
|
||||
if (alerts.value.length > 0) {
|
||||
currentAlert.value = alerts.value[0];
|
||||
}
|
||||
timer = setInterval(() => {
|
||||
alertIndex = (alertIndex + 1) % alerts.length;
|
||||
currentAlert.value = null;
|
||||
setTimeout(() => {
|
||||
currentAlert.value = alerts[alertIndex];
|
||||
}, 500);
|
||||
if (alerts.value.length > 0) {
|
||||
alertIndex = (alertIndex + 1) % alerts.value.length;
|
||||
currentAlert.value = null;
|
||||
setTimeout(() => {
|
||||
currentAlert.value = alerts.value[alertIndex];
|
||||
}, 500);
|
||||
}
|
||||
}, 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;
|
||||
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['Shanghai'], 80] },
|
||||
{ name: '广州节点', value: [...geoCoordMap['Guangzhou'], 70] },
|
||||
@@ -186,13 +261,13 @@ const setChart = () => {
|
||||
name: 'Threat Distribution',
|
||||
type: 'map',
|
||||
geoIndex: 0,
|
||||
data: mapData
|
||||
data: mapData.value
|
||||
},
|
||||
{
|
||||
name: 'Key Nodes',
|
||||
type: 'effectScatter',
|
||||
coordinateSystem: 'geo',
|
||||
data: scatterData,
|
||||
data: scatterDataLocal,
|
||||
symbolSize: (val) => val[2] / 5,
|
||||
showEffectOn: 'render',
|
||||
rippleEffect: {
|
||||
@@ -222,10 +297,78 @@ const setChart = () => {
|
||||
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 () => {
|
||||
await nextTick(); // 确保 DOM 已渲染
|
||||
setChart();
|
||||
await Promise.all([fetchVulnStats(), fetchRegionStats(), fetchMapData(), fetchAlertsData()]); // 同时获取所有统计数据
|
||||
await setChart(); // 等待图表设置完成
|
||||
startAlertRotation();
|
||||
window.addEventListener('resize', handleResize);
|
||||
});
|
||||
|
||||
381
src/views/Jyh/security/components/VulnerabilityLanguageChart.vue
Normal file
381
src/views/Jyh/security/components/VulnerabilityLanguageChart.vue
Normal 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>
|
||||
285
src/views/Jyh/security/components/VulnerabilitySeverityChart.vue
Normal file
285
src/views/Jyh/security/components/VulnerabilitySeverityChart.vue
Normal 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
@@ -3,8 +3,8 @@ import { defineConfig, type PluginOption, loadEnv } from 'vite';
|
||||
import vue from '@vitejs/plugin-vue';
|
||||
import { visualizer } from "rollup-plugin-visualizer";
|
||||
import dayjs from "dayjs";
|
||||
import basicSSL from '@vitejs/plugin-basic-ssl';
|
||||
import legacy from '@vitejs/plugin-legacy'
|
||||
// import basicSSL from '@vitejs/plugin-basic-ssl';
|
||||
// import legacy from '@vitejs/plugin-legacy'
|
||||
const fs = require('node:fs')
|
||||
|
||||
export default (config: any) => {
|
||||
@@ -20,10 +20,10 @@ export default (config: any) => {
|
||||
}
|
||||
}
|
||||
}),
|
||||
// 浏览器兼容性
|
||||
legacy({
|
||||
targets: ['defaults', 'chrome >= 78', 'safari >= 14'],
|
||||
}),
|
||||
// // 浏览器兼容性
|
||||
// legacy({
|
||||
// targets: ['defaults', 'chrome >= 78', 'safari >= 14'],
|
||||
// }),
|
||||
visualizer({
|
||||
open: config.mode === 'analysis',
|
||||
}) as PluginOption,
|
||||
@@ -38,11 +38,17 @@ export default (config: any) => {
|
||||
},
|
||||
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://222.20.126.217:10010', // 目标服务器地址
|
||||
target: 'http://222.20.126.217:8100', // 目标服务器地址
|
||||
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))
|
||||
}
|
||||
},
|
||||
|
||||
optimizeDeps: {
|
||||
include: ['vue', 'dayjs', 'axios'], // 强制预构建常用包
|
||||
},
|
||||
build: {
|
||||
outDir: 'openRepoPortal', // 将打包后的文件夹名改为 openRepoPortal
|
||||
outDir: 'SAP', // 将打包后的文件夹名改为 openRepoPortal
|
||||
// sourcemap: config.mode !== 'production',
|
||||
minify: 'terser', // 确保使用 terser 进行压缩
|
||||
terserOptions: {
|
||||
compress: {
|
||||
drop_console: true, // 移除 console.log
|
||||
},
|
||||
},
|
||||
// 减少 chunk 大小计算,加快打包速度
|
||||
reportCompressedSize: false,
|
||||
// 提高超大静态资源临界值
|
||||
assetsInlineLimit: 4096,
|
||||
minify: 'esbuild',
|
||||
// minify: 'terser', // 确保使用 terser 进行压缩
|
||||
// terserOptions: {
|
||||
// compress: {
|
||||
// drop_console: true, // 移除 console.log
|
||||
// },
|
||||
// },
|
||||
},
|
||||
|
||||
define: {
|
||||
@@ -83,7 +96,7 @@ export default (config: any) => {
|
||||
) {
|
||||
if (type === 'asset') {
|
||||
// return env.VITE_STATIC_HOST + '/' + filename;
|
||||
return `/openRepoPortal/${filename}`;
|
||||
return `/SAP/${filename}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
222
yarn.lock
222
yarn.lock
@@ -2,6 +2,7 @@
|
||||
# yarn lockfile v1
|
||||
|
||||
|
||||
\
|
||||
"@aashutoshrathi/word-wrap@^1.2.3":
|
||||
version "1.2.6"
|
||||
resolved "https://registry.npmjs.org/@aashutoshrathi/word-wrap/-/word-wrap-1.2.6.tgz"
|
||||
@@ -20,6 +21,26 @@
|
||||
"@jridgewell/gen-mapping" "^0.3.5"
|
||||
"@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":
|
||||
version "7.24.2"
|
||||
resolved "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz"
|
||||
@@ -913,7 +934,7 @@
|
||||
resolved "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz"
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.1.tgz"
|
||||
integrity sha512-+BIznRzyqBf+2wCTxcKE3wDjfGeCoVE61KSHGpkzqrLi8qxqFwBeUFyId2cxkTmm55fzDGnm0+yCxaxygrLUnQ==
|
||||
@@ -958,11 +979,26 @@
|
||||
resolved "https://registry.npmjs.org/@braintree/sanitize-url/-/sanitize-url-6.0.4.tgz"
|
||||
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":
|
||||
version "1.4.0"
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/@devui-design/icons/-/icons-1.4.0.tgz"
|
||||
integrity sha512-taAX1RNW0QHUqQTRPqLTYTB2PZIqUplhWeF4hcmWkSTjpWlDNI40DssG/WRb3sISkfBk/4BMUxxC5XeTL3jo7A==
|
||||
|
||||
"@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/win32-x64@0.18.20":
|
||||
version "0.18.20"
|
||||
resolved "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz"
|
||||
@@ -1235,6 +1271,14 @@
|
||||
"@sentry/types" "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":
|
||||
version "2.0.1"
|
||||
resolved "https://registry.npmjs.org/@tsconfig/node18/-/node18-2.0.1.tgz"
|
||||
@@ -1706,6 +1750,34 @@ ansi-styles@^6.1.0:
|
||||
resolved "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.1.tgz"
|
||||
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:
|
||||
version "1.3.0"
|
||||
resolved "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz"
|
||||
@@ -1737,6 +1809,11 @@ array-buffer-byte-length@^1.0.1:
|
||||
call-bind "^1.0.5"
|
||||
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:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz"
|
||||
@@ -1756,7 +1833,14 @@ arraybuffer.prototype.slice@^1.0.3:
|
||||
is-array-buffer "^3.0.4"
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/async-validator/-/async-validator-4.2.5.tgz"
|
||||
integrity sha512-7HhHjtERjqlNbZtqNqy2rckN/SpOOlmDliet+lP7k+eKZEjPk3DgyeU9lIXLdeLz0uBbbVp+9Qdow9wJWgwwfg==
|
||||
@@ -1823,6 +1907,13 @@ balanced-match@^1.0.0:
|
||||
resolved "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz"
|
||||
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:
|
||||
version "2.3.0"
|
||||
resolved "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz"
|
||||
@@ -2040,6 +2131,11 @@ commander@7:
|
||||
resolved "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz"
|
||||
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:
|
||||
version "0.0.1"
|
||||
resolved "https://registry.npmjs.org/computeds/-/computeds-0.0.1.tgz"
|
||||
@@ -2062,7 +2158,7 @@ core-js-compat@^3.31.0, core-js-compat@^3.36.1:
|
||||
dependencies:
|
||||
browserslist "^4.23.0"
|
||||
|
||||
core-js@^3.31.1:
|
||||
core-js@^3.15.1, core-js@^3.31.1:
|
||||
version "3.36.1"
|
||||
resolved "https://registry.npmjs.org/core-js/-/core-js-3.36.1.tgz"
|
||||
integrity sha512-BTvUrwxVBezj5SZ3f10ImnX2oRByMxql3EimVqMysepbC9EeMUOpLwdy6Eoili2x6E4kf+ZUB5k/+Jv55alPfA==
|
||||
@@ -2107,7 +2203,7 @@ cssfilter@0.0.10:
|
||||
resolved "https://registry.npmjs.org/cssfilter/-/cssfilter-0.0.10.tgz"
|
||||
integrity sha512-FAaLDaplstoRsDR8XGYH51znUN0UY7nMc6Z9/fvE8EXGwvJE9hu7W2vHwx1+bd6gCYnln9nLbzxFTrcO9YQDZw==
|
||||
|
||||
csstype@^3.1.3:
|
||||
csstype@^3.1.1, csstype@^3.1.3:
|
||||
version "3.1.3"
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/csstype/-/csstype-3.1.3.tgz"
|
||||
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
|
||||
@@ -2649,7 +2745,7 @@ data-view-byte-offset@^1.0.0:
|
||||
es-errors "^1.3.0"
|
||||
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"
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/dayjs/-/dayjs-1.11.13.tgz"
|
||||
integrity sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==
|
||||
@@ -2760,6 +2856,16 @@ doctrine@^3.0.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "3.0.11"
|
||||
resolved "https://registry.npmjs.org/dompurify/-/dompurify-3.0.11.tgz"
|
||||
@@ -2775,6 +2881,14 @@ eastasianwidth@^0.2.0:
|
||||
resolved "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz"
|
||||
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:
|
||||
version "5.3.3"
|
||||
resolved "https://registry.npmjs.org/echarts/-/echarts-5.3.3.tgz"
|
||||
@@ -3629,6 +3743,11 @@ is-path-inside@^3.0.3:
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/is-path-inside/-/is-path-inside-3.0.3.tgz"
|
||||
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:
|
||||
version "1.1.4"
|
||||
resolved "https://registry.npmjs.org/is-regex/-/is-regex-1.1.4.tgz"
|
||||
@@ -3708,7 +3827,7 @@ js-cookie@^3.0.5:
|
||||
resolved "https://registry.npmjs.org/js-cookie/-/js-cookie-3.0.5.tgz"
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz"
|
||||
integrity sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==
|
||||
@@ -3720,6 +3839,11 @@ js-yaml@^4.1.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.5.2"
|
||||
resolved "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz"
|
||||
@@ -3856,7 +3980,7 @@ locate-path@^6.0.0:
|
||||
dependencies:
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/lodash-es/-/lodash-es-4.17.21.tgz"
|
||||
integrity sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==
|
||||
@@ -3876,6 +4000,13 @@ lodash@^4.17.15, lodash@^4.17.21:
|
||||
resolved "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz"
|
||||
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:
|
||||
version "5.12.2"
|
||||
resolved "https://registry.npmjs.org/lottie-web/-/lottie-web-5.12.2.tgz"
|
||||
@@ -4077,6 +4208,11 @@ nanoid@^3.3.8:
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/nanoid/-/nanoid-3.3.11.tgz"
|
||||
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:
|
||||
version "1.4.0"
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/natural-compare-lite/-/natural-compare-lite-1.4.0.tgz"
|
||||
@@ -4519,6 +4655,11 @@ require-directory@^2.1.1:
|
||||
resolved "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz"
|
||||
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:
|
||||
version "4.0.0"
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/resolve-from/-/resolve-from-4.0.0.tgz"
|
||||
@@ -4605,7 +4746,7 @@ safe-regex-test@^1.0.3:
|
||||
es-errors "^1.3.0"
|
||||
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"
|
||||
resolved "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz"
|
||||
integrity sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==
|
||||
@@ -4619,6 +4760,13 @@ sass@*, sass@^1.63.6:
|
||||
immutable "^4.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:
|
||||
version "1.1.2"
|
||||
resolved "https://registry.npmjs.org/select/-/select-1.1.2.tgz"
|
||||
@@ -4687,6 +4835,11 @@ set-function-name@^2.0.1:
|
||||
functions-have-names "^1.2.3"
|
||||
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:
|
||||
version "1.2.0"
|
||||
resolved "https://registry.npmjs.org/shebang-command/-/shebang-command-1.2.0.tgz"
|
||||
@@ -4797,6 +4950,17 @@ ssf@~0.11.2:
|
||||
dependencies:
|
||||
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":
|
||||
version "4.2.3"
|
||||
resolved "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz"
|
||||
@@ -4893,7 +5057,7 @@ strip-json-comments@^3.1.1:
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/strip-json-comments/-/strip-json-comments-3.1.1.tgz"
|
||||
integrity sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==
|
||||
|
||||
stylis@^4.0.10:
|
||||
stylis@^4.0.10, stylis@^4.1.3:
|
||||
version "4.3.1"
|
||||
resolved "https://registry.npmjs.org/stylis/-/stylis-4.3.1.tgz"
|
||||
integrity sha512-EQepAV+wMsIaGVGX1RECzgrcqRRU/0sYOHkeLsZ3fzHaHXZy4DaOOX0vOlGQdlsjkh3mFHAIlVimpwAs4dslyQ==
|
||||
@@ -4932,7 +5096,7 @@ supports-preserve-symlinks-flag@^1.0.0:
|
||||
|
||||
swiper@^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==
|
||||
|
||||
systemjs@^6.14.1:
|
||||
@@ -4997,6 +5161,11 @@ thenify-all@^1.0.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "2.1.0"
|
||||
resolved "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz"
|
||||
@@ -5036,6 +5205,11 @@ tsutils@^3.21.0:
|
||||
dependencies:
|
||||
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:
|
||||
version "0.4.0"
|
||||
resolved "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz"
|
||||
@@ -5259,6 +5433,11 @@ vue-eslint-parser@^9.1.1, vue-eslint-parser@^9.4.2:
|
||||
lodash "^4.17.21"
|
||||
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:
|
||||
version "4.3.0"
|
||||
resolved "https://registry.npmjs.org/vue-router/-/vue-router-4.3.0.tgz"
|
||||
@@ -5283,7 +5462,14 @@ vue-tsc@^1.6.5:
|
||||
"@vue/language-core" "1.8.27"
|
||||
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"
|
||||
resolved "https://mirrors.cloud.tencent.com/npm/vue/-/vue-3.5.13.tgz"
|
||||
integrity sha512-wmeiSMxkZCSc+PM2w2VRsOYAZC8GdipNFRTsLSfodVqI9mbejKeXEGr8SckuLnrQPGe3oJN5c3K0vpoU9q/wCQ==
|
||||
@@ -5294,6 +5480,13 @@ vue-tsc@^1.6.5:
|
||||
"@vue/server-renderer" "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:
|
||||
version "1.0.2"
|
||||
resolved "https://registry.npmjs.org/which-boxed-primitive/-/which-boxed-primitive-1.0.2.tgz"
|
||||
@@ -5447,3 +5640,10 @@ zrender@5.3.2:
|
||||
integrity sha512-8IiYdfwHj2rx0UeIGZGGU4WEVSDEdeVCaIg/fomejg1Xu6OifAL1GVzIPHg2D+MyUkbNgPWji90t0a8IDk+39w==
|
||||
dependencies:
|
||||
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"
|
||||
|
||||
Reference in New Issue
Block a user