提交 4868c113 authored 作者: 朱政's avatar 朱政

feat:展开默认展开第一条,同时ai智能总结默认展示,多智库分析流式默认展示,图表图例颜色与对应领域的tag颜色相同,报告原文搜索功能开发,点击多智库分析后的报告题目跳转到对应原文

上级 8b627b15
...@@ -27,7 +27,7 @@ ...@@ -27,7 +27,7 @@
"json5": "^2.2.3", "json5": "^2.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449", "pdfjs-dist": "^5.5.207",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
...@@ -198,7 +198,6 @@ ...@@ -198,7 +198,6 @@
"resolved": "https://registry.npmmirror.com/@antv/g6/-/g6-4.8.25.tgz", "resolved": "https://registry.npmmirror.com/@antv/g6/-/g6-4.8.25.tgz",
"integrity": "sha512-8mdTnN9QMVNQZtlXmftL8fvRsa4L+GajK58Zp51wyrGLFyjeop8R0QSkCALW45DWP2TaQeZAPtjhQUU/wf5hIg==", "integrity": "sha512-8mdTnN9QMVNQZtlXmftL8fvRsa4L+GajK58Zp51wyrGLFyjeop8R0QSkCALW45DWP2TaQeZAPtjhQUU/wf5hIg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@antv/g6-pc": "0.8.25" "@antv/g6-pc": "0.8.25"
} }
...@@ -965,6 +964,7 @@ ...@@ -965,6 +964,7 @@
"integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/gen-mapping": "^0.3.5",
"@jridgewell/trace-mapping": "^0.3.25" "@jridgewell/trace-mapping": "^0.3.25"
...@@ -1104,9 +1104,10 @@ ...@@ -1104,9 +1104,10 @@
} }
}, },
"node_modules/@napi-rs/canvas": { "node_modules/@napi-rs/canvas": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas/-/canvas-0.1.97.tgz",
"integrity": "sha512-hOkywnrkdFdVpsuaNsZWfEY7kc96eROV2DuMTTvGF15AZfwobzdG2w0eDlU5UBx3Lg/XlWUnqVT5zLUWyo5h6A==", "integrity": "sha512-8cFniXvrIEnVwuNSRCW9wirRZbHvrD3JVujdS2P5n5xiJZNZMOZcfOvJ1pb66c7jXMKHHglJEDVJGbm8XWFcXQ==",
"license": "MIT",
"optional": true, "optional": true,
"workspaces": [ "workspaces": [
"e2e/*" "e2e/*"
...@@ -1119,23 +1120,23 @@ ...@@ -1119,23 +1120,23 @@
"url": "https://github.com/sponsors/Brooooooklyn" "url": "https://github.com/sponsors/Brooooooklyn"
}, },
"optionalDependencies": { "optionalDependencies": {
"@napi-rs/canvas-android-arm64": "0.1.86", "@napi-rs/canvas-android-arm64": "0.1.97",
"@napi-rs/canvas-darwin-arm64": "0.1.86", "@napi-rs/canvas-darwin-arm64": "0.1.97",
"@napi-rs/canvas-darwin-x64": "0.1.86", "@napi-rs/canvas-darwin-x64": "0.1.97",
"@napi-rs/canvas-linux-arm-gnueabihf": "0.1.86", "@napi-rs/canvas-linux-arm-gnueabihf": "0.1.97",
"@napi-rs/canvas-linux-arm64-gnu": "0.1.86", "@napi-rs/canvas-linux-arm64-gnu": "0.1.97",
"@napi-rs/canvas-linux-arm64-musl": "0.1.86", "@napi-rs/canvas-linux-arm64-musl": "0.1.97",
"@napi-rs/canvas-linux-riscv64-gnu": "0.1.86", "@napi-rs/canvas-linux-riscv64-gnu": "0.1.97",
"@napi-rs/canvas-linux-x64-gnu": "0.1.86", "@napi-rs/canvas-linux-x64-gnu": "0.1.97",
"@napi-rs/canvas-linux-x64-musl": "0.1.86", "@napi-rs/canvas-linux-x64-musl": "0.1.97",
"@napi-rs/canvas-win32-arm64-msvc": "0.1.86", "@napi-rs/canvas-win32-arm64-msvc": "0.1.97",
"@napi-rs/canvas-win32-x64-msvc": "0.1.86" "@napi-rs/canvas-win32-x64-msvc": "0.1.97"
} }
}, },
"node_modules/@napi-rs/canvas-android-arm64": { "node_modules/@napi-rs/canvas-android-arm64": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-android-arm64/-/canvas-android-arm64-0.1.97.tgz",
"integrity": "sha512-IjkZFKUr6GzMzzrawJaN3v+yY3Fvpa71e0DcbePfxWelFKnESIir+XUcdAbim29JOd0JE0/hQJdfUCb5t/Fjrw==", "integrity": "sha512-V1c/WVw+NzH8vk7ZK/O8/nyBSCQimU8sfMsB/9qeSvdkGKNU7+mxy/bIF0gTgeBFmHpj30S4E9WHMSrxXGQuVQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -1153,9 +1154,9 @@ ...@@ -1153,9 +1154,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-darwin-arm64": { "node_modules/@napi-rs/canvas-darwin-arm64": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-arm64/-/canvas-darwin-arm64-0.1.97.tgz",
"integrity": "sha512-PUCxDq0wSSJbtaOqoKj3+t5tyDbtxWumziOTykdn3T839hu6koMaBFpGk9lXpsGaPNgyFpPqjxhtsPljBGnDHg==", "integrity": "sha512-ok+SCEF4YejcxuJ9Rm+WWunHHpf2HmiPxfz6z1a/NFQECGXtsY7A4B8XocK1LmT1D7P174MzwPF9Wy3AUAwEPw==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -1173,9 +1174,9 @@ ...@@ -1173,9 +1174,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-darwin-x64": { "node_modules/@napi-rs/canvas-darwin-x64": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-darwin-x64/-/canvas-darwin-x64-0.1.97.tgz",
"integrity": "sha512-rlCFLv4Rrg45qFZq7mysrKnsUbMhwdNg3YPuVfo9u4RkOqm7ooAJvdyDFxiqfSsJJTqupYqa9VQCUt8WKxKhNQ==", "integrity": "sha512-PUP6e6/UGlclUvAQNnuXCcnkpdUou6VYZfQOQxExLp86epOylmiwLkqXIvpFmjoTEDmPmXrI+coL/9EFU1gKPA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -1193,9 +1194,9 @@ ...@@ -1193,9 +1194,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm-gnueabihf": { "node_modules/@napi-rs/canvas-linux-arm-gnueabihf": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm-gnueabihf/-/canvas-linux-arm-gnueabihf-0.1.97.tgz",
"integrity": "sha512-6xWwyMc9BlDBt+9XHN/GzUo3MozHta/2fxQHMb80x0K2zpZuAdDKUYHmYzx9dFWDY3SbPYnx6iRlQl6wxnwS1w==", "integrity": "sha512-XyXH2L/cic8eTNtbrXCcvqHtMX/nEOxN18+7rMrAM2XtLYC/EB5s0wnO1FsLMWmK+04ZSLN9FBGipo7kpIkcOw==",
"cpu": [ "cpu": [
"arm" "arm"
], ],
...@@ -1213,9 +1214,9 @@ ...@@ -1213,9 +1214,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm64-gnu": { "node_modules/@napi-rs/canvas-linux-arm64-gnu": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-gnu/-/canvas-linux-arm64-gnu-0.1.97.tgz",
"integrity": "sha512-r2OX3w50xHxrToTovOSQWwkVfSq752CUzH9dzlVXyr8UDKFV8dMjfa9hePXvAJhN3NBp4TkHcGx15QCdaCIwnA==", "integrity": "sha512-Kuq/M3djq0K8ktgz6nPlK7Ne5d4uWeDxPpyKWOjWDK2RIOhHVtLtyLiJw2fuldw7Vn4mhw05EZXCEr4Q76rs9w==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -1233,9 +1234,9 @@ ...@@ -1233,9 +1234,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-arm64-musl": { "node_modules/@napi-rs/canvas-linux-arm64-musl": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-arm64-musl/-/canvas-linux-arm64-musl-0.1.97.tgz",
"integrity": "sha512-jbXuh8zVFUPw6a9SGpgc6EC+fRbGGyP1NFfeQiVqGLs6bN93ROtPLPL6MH9Bp6yt0CXUFallk2vgKdWDbmW+bw==", "integrity": "sha512-kKmSkQVnWeqg7qdsiXvYxKhAFuHz3tkBjW/zyQv5YKUPhotpaVhpBGv5LqCngzyuRV85SXoe+OFj+Tv0a0QXkQ==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -1253,9 +1254,9 @@ ...@@ -1253,9 +1254,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-riscv64-gnu": { "node_modules/@napi-rs/canvas-linux-riscv64-gnu": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-riscv64-gnu/-/canvas-linux-riscv64-gnu-0.1.97.tgz",
"integrity": "sha512-9IwHR2qbq2HceM9fgwyL7x37Jy3ptt1uxvikQEuWR0FisIx9QEdt7F3huljCky76aoouF2vSd0R2fHo3ESRoPw==", "integrity": "sha512-Jc7I3A51jnEOIAXeLsN/M/+Z28LUeakcsXs07FLq9prXc0eYOtVwsDEv913Gr+06IRo34gJJVgT0TXvmz+N2VA==",
"cpu": [ "cpu": [
"riscv64" "riscv64"
], ],
...@@ -1273,9 +1274,9 @@ ...@@ -1273,9 +1274,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-x64-gnu": { "node_modules/@napi-rs/canvas-linux-x64-gnu": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-gnu/-/canvas-linux-x64-gnu-0.1.97.tgz",
"integrity": "sha512-Jor+rhRN6ubix+D2QkNn9XlPPVAYl+2qFrkZ4oZN9UgtqIUZ+n+HljxhlkkDFRaX1mlxXOXPQjxaZg17zDSFcQ==", "integrity": "sha512-iDUBe7AilfuBSRbSa8/IGX38Mf+iCSBqoVKLSQ5XaY2JLOaqz1TVyPFEyIck7wT6mRQhQt5sN6ogfjIDfi74tg==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -1293,9 +1294,9 @@ ...@@ -1293,9 +1294,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-linux-x64-musl": { "node_modules/@napi-rs/canvas-linux-x64-musl": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-linux-x64-musl/-/canvas-linux-x64-musl-0.1.97.tgz",
"integrity": "sha512-A28VTy91DbclopSGZ2tIon3p8hcVI1JhnNpDpJ5N9rYlUnVz1WQo4waEMh+FICTZF07O3coxBNZc4Vu4doFw7A==", "integrity": "sha512-AKLFd/v0Z5fvgqBDqhvqtAdx+fHMJ5t9JcUNKq4FIZ5WH+iegGm8HPdj00NFlCSnm83Fp3Ln8I2f7uq1aIiWaA==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
...@@ -1313,9 +1314,9 @@ ...@@ -1313,9 +1314,9 @@
} }
}, },
"node_modules/@napi-rs/canvas-win32-arm64-msvc": { "node_modules/@napi-rs/canvas-win32-arm64-msvc": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-arm64-msvc/-/canvas-win32-arm64-msvc-0.1.97.tgz",
"integrity": "sha512-q6G1YXUt3gBCAS2bcDMCaBL4y20di8eVVBi1XhjUqZSVyZZxxwIuRQHy31NlPJUCMiyNiMuc6zeI0uqgkWwAmA==", "integrity": "sha512-u883Yr6A6fO7Vpsy9YE4FVCIxzzo5sO+7pIUjjoDLjS3vQaNMkVzx5bdIpEL+ob+gU88WDK4VcxYMZ6nmnoX9A==",
"cpu": [ "cpu": [
"arm64" "arm64"
], ],
...@@ -1333,12 +1334,13 @@ ...@@ -1333,12 +1334,13 @@
} }
}, },
"node_modules/@napi-rs/canvas-win32-x64-msvc": { "node_modules/@napi-rs/canvas-win32-x64-msvc": {
"version": "0.1.86", "version": "0.1.97",
"resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.86.tgz", "resolved": "https://registry.npmmirror.com/@napi-rs/canvas-win32-x64-msvc/-/canvas-win32-x64-msvc-0.1.97.tgz",
"integrity": "sha512-X0g46uRVgnvCM1cOjRXAOSFSG63ktUFIf/TIfbKCUc7QpmYUcHmSP9iR6DGOYfk+SggLsXoJCIhPTotYeZEAmg==", "integrity": "sha512-sWtD2EE3fV0IzN+iiQUqr/Q1SwqWhs2O1FKItFlxtdDkikpEj5g7DKQpY3x55H/MAOnL8iomnlk3mcEeGiUMoQ==",
"cpu": [ "cpu": [
"x64" "x64"
], ],
"license": "MIT",
"optional": true, "optional": true,
"os": [ "os": [
"win32" "win32"
...@@ -2124,7 +2126,6 @@ ...@@ -2124,7 +2126,6 @@
"resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz", "resolved": "https://registry.npmmirror.com/@types/lodash-es/-/lodash-es-4.17.12.tgz",
"integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==", "integrity": "sha512-0NgftHUcV4v34VhXm8QBSftKVXtbkBG3ViCjs6+eJ5a6y6Mi/jiFGPc1sC7QK+9BFhWrURE3EOggmWaSxL9OzQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/lodash": "*" "@types/lodash": "*"
} }
...@@ -2241,7 +2242,6 @@ ...@@ -2241,7 +2242,6 @@
"resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz", "resolved": "https://registry.npmmirror.com/@vue/compiler-sfc/-/compiler-sfc-3.5.21.tgz",
"integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==", "integrity": "sha512-SXlyk6I5eUGBd2v8Ie7tF6ADHE9kCR6mBEuPyH1nUZ0h6Xx6nZI29i12sJKQmzbDyr2tUHMhhTt51Z6blbkTTQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@babel/parser": "^7.28.3", "@babel/parser": "^7.28.3",
"@vue/compiler-core": "3.5.21", "@vue/compiler-core": "3.5.21",
...@@ -2839,7 +2839,6 @@ ...@@ -2839,7 +2839,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"baseline-browser-mapping": "^2.9.0", "baseline-browser-mapping": "^2.9.0",
"caniuse-lite": "^1.0.30001759", "caniuse-lite": "^1.0.30001759",
...@@ -2859,7 +2858,8 @@ ...@@ -2859,7 +2858,8 @@
"resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz", "resolved": "https://registry.npmmirror.com/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true, "dev": true,
"optional": true "optional": true,
"peer": true
}, },
"node_modules/cache-base": { "node_modules/cache-base": {
"version": "1.0.1", "version": "1.0.1",
...@@ -3196,7 +3196,6 @@ ...@@ -3196,7 +3196,6 @@
"resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz", "resolved": "https://registry.npmmirror.com/cytoscape/-/cytoscape-3.33.1.tgz",
"integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==", "integrity": "sha512-iJc4TwyANnOGR1OmWhsS9ayRS3s+XQ185FmuHObThD+5AeJCakAAbWv8KimMTt08xCCLNgneQwFp+JRJOr9qGQ==",
"license": "MIT", "license": "MIT",
"peer": true,
"engines": { "engines": {
"node": ">=0.10" "node": ">=0.10"
} }
...@@ -3594,7 +3593,6 @@ ...@@ -3594,7 +3593,6 @@
"resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz", "resolved": "https://registry.npmmirror.com/d3-selection/-/d3-selection-3.0.0.tgz",
"integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==", "integrity": "sha512-fmTRWbNMmsmWq6xJV8D19U/gw/bwrHfNXxrIN+HfZgnzqTHp9jOmKMhsTUjXOJnZOdZY9Q28y4yebKzqDKlxlQ==",
"license": "ISC", "license": "ISC",
"peer": true,
"engines": { "engines": {
"node": ">=12" "node": ">=12"
} }
...@@ -3683,7 +3681,6 @@ ...@@ -3683,7 +3681,6 @@
"version": "0.8.5", "version": "0.8.5",
"resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz", "resolved": "https://registry.npmmirror.com/dagre/-/dagre-0.8.5.tgz",
"integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==", "integrity": "sha512-/aTqmnRta7x7MCCpExk7HQL2O4owCT2h8NT//9I1OQ9vt29Pa0BzSAkR5lwFUcQ7491yVi/3CXU9jQ5o0Mn2Sw==",
"peer": true,
"dependencies": { "dependencies": {
"graphlib": "^2.1.8", "graphlib": "^2.1.8",
"lodash": "^4.17.15" "lodash": "^4.17.15"
...@@ -3891,7 +3888,6 @@ ...@@ -3891,7 +3888,6 @@
"resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz", "resolved": "https://registry.npmmirror.com/echarts/-/echarts-5.6.0.tgz",
"integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==", "integrity": "sha512-oTbVTsXfKuEhxftHqL5xprgLoc0k7uScAwtryCgWF6hPYFLRwOUHiFmHGCBKP5NPFNkDVopOieyUqYGH8Fa3kA==",
"license": "Apache-2.0", "license": "Apache-2.0",
"peer": true,
"dependencies": { "dependencies": {
"tslib": "2.3.0", "tslib": "2.3.0",
"zrender": "5.6.1" "zrender": "5.6.1"
...@@ -4243,6 +4239,24 @@ ...@@ -4243,6 +4239,24 @@
"reusify": "^1.0.4" "reusify": "^1.0.4"
} }
}, },
"node_modules/fdir": {
"version": "6.5.0",
"resolved": "https://registry.npmmirror.com/fdir/-/fdir-6.5.0.tgz",
"integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==",
"dev": true,
"license": "MIT",
"engines": {
"node": ">=12.0.0"
},
"peerDependencies": {
"picomatch": "^3 || ^4"
},
"peerDependenciesMeta": {
"picomatch": {
"optional": true
}
}
},
"node_modules/fecha": { "node_modules/fecha": {
"version": "4.2.3", "version": "4.2.3",
"resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz", "resolved": "https://registry.npmmirror.com/fecha/-/fecha-4.2.3.tgz",
...@@ -5252,15 +5266,13 @@ ...@@ -5252,15 +5266,13 @@
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/lodash-es": { "node_modules/lodash-es": {
"version": "4.17.21", "version": "4.17.21",
"resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz", "resolved": "https://registry.npmmirror.com/lodash-es/-/lodash-es-4.17.21.tgz",
"integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==", "integrity": "sha512-mKnC+QJ9pWVzv+C4/U3rRsHapFfHvQFoFB92e52xeyGMcX6/OlIl78je1u8vePzYZSkkogMPJ2yjxxsb89cxyw==",
"license": "MIT", "license": "MIT"
"peer": true
}, },
"node_modules/lodash-unified": { "node_modules/lodash-unified": {
"version": "1.0.3", "version": "1.0.3",
...@@ -5308,7 +5320,6 @@ ...@@ -5308,7 +5320,6 @@
"resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz", "resolved": "https://registry.npmmirror.com/markdown-it/-/markdown-it-14.1.0.tgz",
"integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==", "integrity": "sha512-a54IwgWPaeBCAAsv13YgmALOF1elABB08FxO9i+r4VFk5Vl4pKokRPeX8u5TCgSsPi6ec1otfLjdOpVcgbpshg==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"argparse": "^2.0.1", "argparse": "^2.0.1",
"entities": "^4.4.0", "entities": "^4.4.0",
...@@ -6158,6 +6169,13 @@ ...@@ -6158,6 +6169,13 @@
"license": "MIT", "license": "MIT",
"optional": true "optional": true
}, },
"node_modules/node-readable-to-web-readable-stream": {
"version": "0.4.2",
"resolved": "https://registry.npmmirror.com/node-readable-to-web-readable-stream/-/node-readable-to-web-readable-stream-0.4.2.tgz",
"integrity": "sha512-/cMZNI34v//jUTrI+UIo4ieHAB5EZRY/+7OmXZgBxaWBMcW2tGdceIw06RFxWxrKZ5Jp3sI2i5TsRo+CBhtVLQ==",
"license": "MIT",
"optional": true
},
"node_modules/node-releases": { "node_modules/node-releases": {
"version": "2.0.27", "version": "2.0.27",
"resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz", "resolved": "https://registry.npmmirror.com/node-releases/-/node-releases-2.0.27.tgz",
...@@ -6351,14 +6369,16 @@ ...@@ -6351,14 +6369,16 @@
"license": "MIT" "license": "MIT"
}, },
"node_modules/pdfjs-dist": { "node_modules/pdfjs-dist": {
"version": "5.4.449", "version": "5.5.207",
"resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-5.4.449.tgz", "resolved": "https://registry.npmmirror.com/pdfjs-dist/-/pdfjs-dist-5.5.207.tgz",
"integrity": "sha512-CegnUaT0QwAyQMS+7o2POr4wWUNNe8VaKKlcuoRHeYo98cVnqPpwOXNSx6Trl6szH02JrRcsPgletV6GmF3LtQ==", "integrity": "sha512-WMqqw06w1vUt9ZfT0gOFhMf3wHsWhaCrxGrckGs5Cci6ybDW87IvPaOd2pnBwT6BJuP/CzXDZxjFgmSULLdsdw==",
"license": "Apache-2.0",
"engines": { "engines": {
"node": ">=20.16.0 || >=22.3.0" "node": ">=20.19.0 || >=22.13.0 || >=24"
}, },
"optionalDependencies": { "optionalDependencies": {
"@napi-rs/canvas": "^0.1.81" "@napi-rs/canvas": "^0.1.95",
"node-readable-to-web-readable-stream": "^0.4.2"
} }
}, },
"node_modules/perfect-debounce": { "node_modules/perfect-debounce": {
...@@ -6475,7 +6495,6 @@ ...@@ -6475,7 +6495,6 @@
} }
], ],
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"nanoid": "^3.3.11", "nanoid": "^3.3.11",
"picocolors": "^1.1.1", "picocolors": "^1.1.1",
...@@ -6864,7 +6883,6 @@ ...@@ -6864,7 +6883,6 @@
"integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==", "integrity": "sha512-BgLRGy7tNS9H66aIMASq1qSYbAAJV6Z6WR4QYTvj5FgF15rZ/ympT1uixHXwzbZUBDbkvqUI1KR0fH1FhMaQ9w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@types/estree": "1.0.8" "@types/estree": "1.0.8"
}, },
...@@ -7216,6 +7234,7 @@ ...@@ -7216,6 +7234,7 @@
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"buffer-from": "^1.0.0", "buffer-from": "^1.0.0",
"source-map": "^0.6.0" "source-map": "^0.6.0"
...@@ -7227,6 +7246,7 @@ ...@@ -7227,6 +7246,7 @@
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"peer": true,
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
...@@ -7483,6 +7503,7 @@ ...@@ -7483,6 +7503,7 @@
"integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==",
"dev": true, "dev": true,
"optional": true, "optional": true,
"peer": true,
"dependencies": { "dependencies": {
"@jridgewell/source-map": "^0.3.3", "@jridgewell/source-map": "^0.3.3",
"acorn": "^8.15.0", "acorn": "^8.15.0",
...@@ -7501,13 +7522,54 @@ ...@@ -7501,13 +7522,54 @@
"resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz", "resolved": "https://registry.npmmirror.com/commander/-/commander-2.20.3.tgz",
"integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==",
"dev": true, "dev": true,
"optional": true "optional": true,
"peer": true
},
"node_modules/thenify": {
"version": "3.3.1",
"resolved": "https://registry.npmmirror.com/thenify/-/thenify-3.3.1.tgz",
"integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"any-promise": "^1.0.0"
}
},
"node_modules/thenify-all": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/thenify-all/-/thenify-all-1.6.0.tgz",
"integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==",
"dev": true,
"license": "MIT",
"dependencies": {
"thenify": ">= 3.1.0 < 4"
},
"engines": {
"node": ">=0.8"
}
}, },
"node_modules/tinycolor2": { "node_modules/tinycolor2": {
"version": "1.6.0", "version": "1.6.0",
"resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz", "resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw==" "integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
}, },
"node_modules/tinyglobby": {
"version": "0.2.15",
"resolved": "https://registry.npmmirror.com/tinyglobby/-/tinyglobby-0.2.15.tgz",
"integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"fdir": "^6.5.0",
"picomatch": "^4.0.3"
},
"engines": {
"node": ">=12.0.0"
},
"funding": {
"url": "https://github.com/sponsors/SuperchupuDev"
}
},
"node_modules/to-object-path": { "node_modules/to-object-path": {
"version": "0.3.0", "version": "0.3.0",
"resolved": "https://registry.npmmirror.com/to-object-path/-/to-object-path-0.3.0.tgz", "resolved": "https://registry.npmmirror.com/to-object-path/-/to-object-path-0.3.0.tgz",
...@@ -8008,7 +8070,6 @@ ...@@ -8008,7 +8070,6 @@
"integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==", "integrity": "sha512-j3lYzGC3P+B5Yfy/pfKNgVEg4+UtcIJcVRt2cDjIOmhLourAqPqf8P7acgxeiSgUB7E3p2P8/3gNIgDLpwzs4g==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"esbuild": "^0.21.3", "esbuild": "^0.21.3",
"postcss": "^8.4.43", "postcss": "^8.4.43",
...@@ -8068,7 +8129,6 @@ ...@@ -8068,7 +8129,6 @@
"resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz", "resolved": "https://registry.npmmirror.com/vue/-/vue-3.5.21.tgz",
"integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==", "integrity": "sha512-xxf9rum9KtOdwdRkiApWL+9hZEMWE90FHh8yS1+KJAiWYh+iGWV1FquPjoO9VUHQ+VIhsCXNNyZ5Sf4++RVZBA==",
"license": "MIT", "license": "MIT",
"peer": true,
"dependencies": { "dependencies": {
"@vue/compiler-dom": "3.5.21", "@vue/compiler-dom": "3.5.21",
"@vue/compiler-sfc": "3.5.21", "@vue/compiler-sfc": "3.5.21",
......
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"json5": "^2.2.3", "json5": "^2.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449", "pdfjs-dist": "^5.5.207",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
......
...@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) { ...@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) {
params: { params: {
startDate: params.startDate, startDate: params.startDate,
endDate: params.endDate endDate: params.endDate
} },
// 无数据年份(如 2026)后端可能返回 HTTP 400/500,避免走全局错误提示
validateStatus: (status) =>
(status >= 200 && status < 300) || status === 400 || status === 500
}); });
} }
......
...@@ -160,7 +160,7 @@ ...@@ -160,7 +160,7 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
<div class="box2"> <div class="box2">
<AnalysisBox title="共识观点列表" :showAllBtn="true" v-if="isBox2 || isAnalysisLoading"> <AnalysisBox title="核心观点分析" :showAllBtn="true" v-if="isBox2 || isAnalysisLoading">
<div class="box2-main"> <div class="box2-main">
<div class="empty-image" v-if="isBox2 && !isAnalysisLoading"> <div class="empty-image" v-if="isBox2 && !isAnalysisLoading">
<img src="../assets/images/empty-image.png" alt="" /> <img src="../assets/images/empty-image.png" alt="" />
...@@ -228,7 +228,11 @@ ...@@ -228,7 +228,11 @@
<div v-for="(sv, svIdx) in item.sourceViewDetails" :key="`${sv.report_id}-${sv.view_id}-${svIdx}`" <div v-for="(sv, svIdx) in item.sourceViewDetails" :key="`${sv.report_id}-${sv.view_id}-${svIdx}`"
class="source-view-detail"> class="source-view-detail">
<div class="source-view-detail-title"> <div class="source-view-detail-title">
<span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx) }}</span> <span
class="source-view-detail-title-text"
:class="{ 'is-clickable-report': hasReportLinkForSourceView(sv) }"
@click.stop="handleOpenReportOriginalFromSource(sv)"
>{{ getSourceViewDisplayTitle(sv) }}</span>
<span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl"> <span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" /> <img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span> <span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
...@@ -274,8 +278,11 @@ ...@@ -274,8 +278,11 @@
<div v-for="(sv, svIdx) in item.sourceViewDetails" <div v-for="(sv, svIdx) in item.sourceViewDetails"
:key="`${sv.report_id}-${sv.view_id}-${svIdx}`" class="source-view-detail"> :key="`${sv.report_id}-${sv.view_id}-${svIdx}`" class="source-view-detail">
<div class="source-view-detail-title"> <div class="source-view-detail-title">
<span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx) <span
}}</span> class="source-view-detail-title-text"
:class="{ 'is-clickable-report': hasReportLinkForSourceView(sv) }"
@click.stop="handleOpenReportOriginalFromSource(sv)"
>{{ getSourceViewDisplayTitle(sv) }}</span>
<span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl"> <span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" /> <img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span> <span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
...@@ -323,7 +330,7 @@ const sort = ref(""); ...@@ -323,7 +330,7 @@ const sort = ref("");
const searchPolicy = ref(""); const searchPolicy = ref("");
const isBox2 = ref(true) const isBox2 = ref(true)
const isAnalysisLoading = ref(false) const isAnalysisLoading = ref(false)
const isBeingAnalysisExpanded = ref(false) const isBeingAnalysisExpanded = ref(true)
const beingAnalysisContent = ref("") const beingAnalysisContent = ref("")
const beingAnalysisContentRef = ref(null) const beingAnalysisContentRef = ref(null)
const activeOpinionTab = ref('consensus') const activeOpinionTab = ref('consensus')
...@@ -435,11 +442,23 @@ const getViewpointDetailForSource = (reportId, viewId) => { ...@@ -435,11 +442,23 @@ const getViewpointDetailForSource = (reportId, viewId) => {
thinktankLogoUrl: "" thinktankLogoUrl: ""
} }
} }
/** 展开区标题:优先中文标题,否则英文;无标题则返回空串(由上游过滤) */ /** 根据 report_id 找到报告名(用于展开区标题展示) */
const getSourceViewDisplayTitle = (sv, idx) => { const getReportNameById = (reportId) => {
const zh = String(sv.titleZh ?? "").trim() const id = String(reportId ?? "")
if (!id) return ""
const list = selectedReportList.value || []
const hit = Array.isArray(list) ? list.find((r) => String(r?.id ?? "") === id) : null
return String(hit?.name ?? "").trim()
}
/** 展开区标题:显示报告名《xxx》(优先);否则回退中文标题/英文标题;无标题则返回空串(由上游过滤) */
const getSourceViewDisplayTitle = (sv) => {
const reportName = String(sv?.reportName ?? "").trim()
if (reportName) return `《${reportName}》`
const fromId = getReportNameById(sv?.report_id)
if (fromId) return `《${fromId}》`
const zh = String(sv?.titleZh ?? "").trim()
if (zh) return zh if (zh) return zh
const en = String(sv.title ?? "").trim() const en = String(sv?.title ?? "").trim()
if (en) return en if (en) return en
return "" return ""
} }
...@@ -449,6 +468,20 @@ const getSourceViewDisplayContent = (sv) => { ...@@ -449,6 +468,20 @@ const getSourceViewDisplayContent = (sv) => {
if (zh) return zh if (zh) return zh
return String(sv.content ?? "").trim() return String(sv.content ?? "").trim()
} }
/** 是否存在可跳转的报告 id(source_views 的 report_id) */
const hasReportLinkForSourceView = (sv) => Boolean(String(sv?.report_id ?? "").trim())
/** 点击报告标题:新标签打开该报告原文页 */
const handleOpenReportOriginalFromSource = (sv) => {
const id = String(sv?.report_id ?? "").trim()
if (!id) return
const route = router.resolve({
name: "ReportOriginal",
params: { id }
})
window.open(route.href, "_blank")
}
const tryParseAnswerFromStreamText = (text) => { const tryParseAnswerFromStreamText = (text) => {
const lines = String(text || "") const lines = String(text || "")
.split(/\r?\n/) .split(/\r?\n/)
...@@ -492,9 +525,11 @@ const consensusList = computed(() => { ...@@ -492,9 +525,11 @@ const consensusList = computed(() => {
const sourceViewDetails = sourceViews const sourceViewDetails = sourceViews
.map((v) => { .map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id) const detail = getViewpointDetailForSource(v.report_id, v.view_id)
const reportName = getReportNameById(v.report_id)
return { return {
report_id: v.report_id, report_id: v.report_id,
view_id: v.view_id, view_id: v.view_id,
reportName,
titleZh: detail.titleZh, titleZh: detail.titleZh,
contentZh: detail.contentZh, contentZh: detail.contentZh,
title: detail.title, title: detail.title,
...@@ -504,14 +539,15 @@ const consensusList = computed(() => { ...@@ -504,14 +539,15 @@ const consensusList = computed(() => {
} }
}) })
.filter((sv) => { .filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0) const title = getSourceViewDisplayTitle(sv)
return Boolean(title) return Boolean(title)
}) })
const uniqueReportCount = new Set(sourceViewDetails.map((sv) => String(sv.report_id ?? ""))).size
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";") const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return { return {
id: `consensus-${index + 1}`, id: `consensus-${index + 1}`,
consensusContent: item?.consensus_content || "", consensusContent: item?.consensus_content || "",
reportCount: sourceViewDetails.length, reportCount: uniqueReportCount,
sourceViewText, sourceViewText,
sourceViewDetails sourceViewDetails
} }
...@@ -525,9 +561,11 @@ const differenceList = computed(() => { ...@@ -525,9 +561,11 @@ const differenceList = computed(() => {
const sourceViewDetails = sourceViews const sourceViewDetails = sourceViews
.map((v) => { .map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id) const detail = getViewpointDetailForSource(v.report_id, v.view_id)
const reportName = getReportNameById(v.report_id)
return { return {
report_id: v.report_id, report_id: v.report_id,
view_id: v.view_id, view_id: v.view_id,
reportName,
titleZh: detail.titleZh, titleZh: detail.titleZh,
contentZh: detail.contentZh, contentZh: detail.contentZh,
title: detail.title, title: detail.title,
...@@ -537,19 +575,43 @@ const differenceList = computed(() => { ...@@ -537,19 +575,43 @@ const differenceList = computed(() => {
} }
}) })
.filter((sv) => { .filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0) const title = getSourceViewDisplayTitle(sv)
return Boolean(title) return Boolean(title)
}) })
const uniqueReportCount = new Set(sourceViewDetails.map((sv) => String(sv.report_id ?? ""))).size
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";") const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return { return {
id: `difference-${index + 1}`, id: `difference-${index + 1}`,
disagreementContent: item?.disagreement_content || "", disagreementContent: item?.disagreement_content || "",
reportCount: sourceViewDetails.length, reportCount: uniqueReportCount,
sourceViewText, sourceViewText,
sourceViewDetails sourceViewDetails
} }
}) })
}) })
// 默认展开:每次分析结果就绪后,共识/分歧列表第一条展开,其余关闭
watch(
consensusList,
(list) => {
if (!Array.isArray(list) || list.length === 0) return
if (openConsensusIds.value.size > 0) return
if (!list[0]?.id) return
openConsensusIds.value = new Set([list[0].id])
},
{ immediate: true }
)
watch(
differenceList,
(list) => {
if (!Array.isArray(list) || list.length === 0) return
if (openDifferencesIds.value.size > 0) return
if (!list[0]?.id) return
openDifferencesIds.value = new Set([list[0].id])
},
{ immediate: true }
)
// 近N年发布(用于 startDate) // 近N年发布(用于 startDate)
const selectedYears = ref(5); const selectedYears = ref(5);
const yearsOptions = [ const yearsOptions = [
...@@ -585,9 +647,12 @@ const handleAnalysis = async () => { ...@@ -585,9 +647,12 @@ const handleAnalysis = async () => {
if (!canProceed.value) return if (!canProceed.value) return
isBox2.value = false isBox2.value = false
isAnalysisLoading.value = true isAnalysisLoading.value = true
isBeingAnalysisExpanded.value = false isBeingAnalysisExpanded.value = true
beingAnalysisContent.value = "" beingAnalysisContent.value = ""
domainViewAnalysisRes.value = null domainViewAnalysisRes.value = null
// 默认:共识/分歧第一条展开,其余关闭(每次开始分析先清空旧展开状态)
openConsensusIds.value = new Set()
openDifferencesIds.value = new Set()
// 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位 // 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位
activeOpinionTab.value = 'consensus' activeOpinionTab.value = 'consensus'
await handlePostReportDomainViewAnalysis() await handlePostReportDomainViewAnalysis()
...@@ -601,6 +666,8 @@ const handleBack = () => { ...@@ -601,6 +666,8 @@ const handleBack = () => {
beingAnalysisContent.value = "" beingAnalysisContent.value = ""
// 返回选择时也重置,确保下次进入分析展示一致 // 返回选择时也重置,确保下次进入分析展示一致
activeOpinionTab.value = 'consensus' activeOpinionTab.value = 'consensus'
openConsensusIds.value = new Set()
openDifferencesIds.value = new Set()
} }
const pageSize = 10; const pageSize = 10;
const total = ref(0); const total = ref(0);
...@@ -1029,7 +1096,7 @@ onMounted(async () => { ...@@ -1029,7 +1096,7 @@ onMounted(async () => {
.being-analysis-detail-box { .being-analysis-detail-box {
width: 1063px; width: 1063px;
height: 160px; height: 260px;
background-color: rgb(246, 250, 255); background-color: rgb(246, 250, 255);
border-radius: 10px; border-radius: 10px;
...@@ -1087,7 +1154,7 @@ onMounted(async () => { ...@@ -1087,7 +1154,7 @@ onMounted(async () => {
.being-analysis-box-content { .being-analysis-box-content {
width: 983px; width: 983px;
height: 104px; height: 204px;
margin-left: 40px; margin-left: 40px;
overflow-y: auto; overflow-y: auto;
overflow-x: hidden; overflow-x: hidden;
......
...@@ -50,24 +50,24 @@ ...@@ -50,24 +50,24 @@
</div> </div>
<div class="text">{{ "查看官网" }}</div> <div class="text">{{ "查看官网" }}</div>
</div> --> </div> -->
<div class="btn"> <!-- <div class="btn">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon2.png" alt="" /> <img src="./images/btn-icon2.png" alt="" />
</div> </div>
<div class="text" @click="goToOfficialWebsite()">{{ "查看官网" }}</div> <div class="text" @click="goToOfficialWebsite()">{{ "查看官网" }}</div>
</div> </div> -->
<div class="btn"> <div class="btn">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon2.png" alt="" /> <img src="./images/btn-icon2.png" alt="" />
</div> </div>
<div class="text" @click="toReport()">{{ "报告原文" }}</div> <div class="text" @click="toReport()">{{ "报告原文" }}</div>
</div> </div>
<div class="btn" @click="handleDownloadDocument"> <!-- <div class="btn" @click="handleDownloadDocument">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon3.png" alt="" /> <img src="./images/btn-icon3.png" alt="" />
</div> </div>
<div class="text">{{ "文档下载" }}</div> <div class="text">{{ "文档下载" }}</div>
</div> </div> -->
<div class="btn btn1" @click="handleAnalysisClick"> <div class="btn btn1" @click="handleAnalysisClick">
<div class="icon"> <div class="icon">
<img src="./images/btn-icon4.png" alt="" /> <img src="./images/btn-icon4.png" alt="" />
......
...@@ -43,7 +43,7 @@ ...@@ -43,7 +43,7 @@
<!-- 多个作者:显示第一个 + 等 --> <!-- 多个作者:显示第一个 + 等 -->
<span v-else> <span v-else>
{{ reportAuthors[0].name }} {{ reportAuthors[0].name }}{{ reportAuthors.length }}
</span> </span>
</template> </template>
...@@ -57,7 +57,7 @@ ...@@ -57,7 +57,7 @@
@error="() => { if (author.avatar) author.avatar = null; }" /></div> @error="() => { if (author.avatar) author.avatar = null; }" /></div>
<div class="author-text"> <div class="author-text">
<div class="author-name">{{ author.name }}</div> <div class="author-name">{{ author.name }}</div>
<div class="author-position">{{ author.job }}</div>
</div> </div>
</div> </div>
</div> </div>
...@@ -67,6 +67,10 @@ ...@@ -67,6 +67,10 @@
<div class="box5"> <div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true"> <AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main"> <div class="box5-main">
<template v-if="!hasBox5ChartData">
<el-empty class="box5-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="box5Chart"> <div class="box5Chart">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 --> <!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%" <WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%"
...@@ -77,12 +81,12 @@ ...@@ -77,12 +81,12 @@
</div> </div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)"> <div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton /> <AiButton />
</div> </div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)"> <div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" /> <AiPane :aiContent="aiContentBox5" />
</div> </div>
</template>
</div> </div>
</AnalysisBox> </AnalysisBox>
</div> </div>
...@@ -255,7 +259,8 @@ const props = defineProps({ ...@@ -255,7 +259,8 @@ const props = defineProps({
}); });
const REPORT_ANALYSIS_TIP_BOX5 = const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网"; "智库报告关键词云,数据来源:美国兰德公司官网";
const isShowAiContentBox5 = ref(false); // 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
...@@ -468,6 +473,7 @@ const box2Data = ref([ ...@@ -468,6 +473,7 @@ const box2Data = ref([
]); ]);
// 报告关键词云 // 报告关键词云
const box5Data = ref([]); const box5Data = ref([]);
const hasBox5ChartData = computed(() => Array.isArray(box5Data.value) && box5Data.value.length > 0);
/** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */ /** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */
const box5WordCloudKey = ref(0); const box5WordCloudKey = ref(0);
...@@ -490,6 +496,10 @@ const handleGetThinkTankReportIndustryCloud = async () => { ...@@ -490,6 +496,10 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) { if (data.length) {
box5WordCloudKey.value += 1; box5WordCloudKey.value += 1;
} }
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else { } else {
box5Data.value = []; box5Data.value = [];
} }
...@@ -582,11 +592,16 @@ const handleGetThinkTankReportViewpoint = async () => { ...@@ -582,11 +592,16 @@ const handleGetThinkTankReportViewpoint = async () => {
const res = await getThinkTankReportViewpoint(params); const res = await getThinkTankReportViewpoint(params);
console.log("核心论点", res.data); console.log("核心论点", res.data);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
majorOpinions.value = res.data.content || []; const nextOpinions = res.data.content || [];
majorOpinions.value = nextOpinions;
total.value = res.data.totalElements || 0; total.value = res.data.totalElements || 0;
// 重置展开状态 // 默认:第一条展开,其余关闭
expandedOpinionKeys.value = new Set(); const nextExpandedKeys = new Set();
if (Array.isArray(nextOpinions) && nextOpinions.length > 0) {
nextExpandedKeys.add(getOpinionExpandKey(nextOpinions[0], 0));
}
expandedOpinionKeys.value = nextExpandedKeys;
} }
} catch (error) { } catch (error) {
console.error("获取主要观点error", error); console.error("获取主要观点error", error);
......
...@@ -302,11 +302,46 @@ import AiButton from "@/components/base/Ai/AiButton/index.vue"; ...@@ -302,11 +302,46 @@ import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue"; import AiPane from "@/components/base/Ai/AiPane/index.vue";
import TipTab from "@/views/thinkTank/TipTab/index.vue"; import TipTab from "@/views/thinkTank/TipTab/index.vue";
import defaultNewsIcon from "@/assets/icons/default-icon-news.png"; import defaultNewsIcon from "@/assets/icons/default-icon-news.png";
import AreaTag from "@/components/base/AreaTag/index.vue";
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
const router = useRouter(); const router = useRouter();
/** 与 AreaTag 一致的领域色(取 tag 的文字色) */
const AREA_TAG_COLOR_BY_NAME = {
"人工智能": "rgba(245, 34, 45, 1)", // tag1
"生物科技": "rgba(19, 168, 168, 1)", // tag2
"新一代通信网络": "rgba(5, 95, 194, 1)", // tag3
// 兼容常见写法
"通信网络": "rgba(5, 95, 194, 1)",
"量子科技": "rgba(114, 46, 209, 1)", // tag4
"新能源": "rgba(82, 196, 26, 1)", // tag5
"集成电路": "rgba(22, 119, 255, 1)", // tag6
"海洋": "rgba(15, 120, 199, 1)", // tag7
"先进制造": "rgba(250, 173, 20, 1)", // tag8
"新材料": "rgba(250, 140, 22, 1)", // tag9
"航空航天": "rgba(47, 84, 235, 1)", // tag10
"太空": "rgba(47, 84, 235, 1)", // tag11
"深海": "rgba(73, 104, 161, 1)", // tag12
"极地": "rgba(133, 165, 255, 1)", // tag13
"核": "rgba(250, 84, 28, 1)", // tag14
"其他": "rgba(82, 196, 26, 1)" // tag15
};
const AREA_TAG_FALLBACK_COLORS = [
"rgba(5, 95, 194, 1)",
"rgba(245, 34, 45, 1)",
"rgba(19, 168, 168, 1)",
"rgba(250, 140, 22, 1)",
"rgba(114, 46, 209, 1)",
"rgba(82, 196, 26, 1)",
"rgba(22, 119, 255, 1)",
"rgba(250, 84, 28, 1)",
"rgba(47, 84, 235, 1)"
];
const getAreaTagColor = (name, idx = 0) =>
AREA_TAG_COLOR_BY_NAME[name] || AREA_TAG_FALLBACK_COLORS[idx % AREA_TAG_FALLBACK_COLORS.length];
/** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */ /** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */
const POLICY_TRACKING_TIP_BOX1 = const POLICY_TRACKING_TIP_BOX1 =
"智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网"; "智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网";
...@@ -320,7 +355,8 @@ const POLICY_FILTER_ALL_AREA = "全部领域"; ...@@ -320,7 +355,8 @@ const POLICY_FILTER_ALL_AREA = "全部领域";
const POLICY_FILTER_ALL_TIME = "全部时间"; const POLICY_FILTER_ALL_TIME = "全部时间";
const POLICY_FILTER_ALL_DEPT = "全部部门"; const POLICY_FILTER_ALL_DEPT = "全部部门";
const isShowAiContentPolicyPt1 = ref(false); // 刷新后默认展示 3 个图表 AI 总结
const isShowAiContentPolicyPt1 = ref(true);
const aiContentPolicyPt1 = ref(""); const aiContentPolicyPt1 = ref("");
const isPolicyPt1InterpretLoading = ref(false); const isPolicyPt1InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt1 = (val) => { const handleSwitchAiContentShowPolicyPt1 = (val) => {
...@@ -330,7 +366,7 @@ const handleSwitchAiContentShowPolicyPt1 = (val) => { ...@@ -330,7 +366,7 @@ const handleSwitchAiContentShowPolicyPt1 = (val) => {
} }
}; };
const isShowAiContentPolicyPt2 = ref(false); const isShowAiContentPolicyPt2 = ref(true);
const aiContentPolicyPt2 = ref(""); const aiContentPolicyPt2 = ref("");
const isPolicyPt2InterpretLoading = ref(false); const isPolicyPt2InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt2 = (val) => { const handleSwitchAiContentShowPolicyPt2 = (val) => {
...@@ -340,7 +376,7 @@ const handleSwitchAiContentShowPolicyPt2 = (val) => { ...@@ -340,7 +376,7 @@ const handleSwitchAiContentShowPolicyPt2 = (val) => {
} }
}; };
const isShowAiContentPolicyPt3 = ref(false); const isShowAiContentPolicyPt3 = ref(true);
const aiContentPolicyPt3 = ref(""); const aiContentPolicyPt3 = ref("");
const isPolicyPt3InterpretLoading = ref(false); const isPolicyPt3InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt3 = (val) => { const handleSwitchAiContentShowPolicyPt3 = (val) => {
...@@ -439,16 +475,20 @@ const handleGetThinkPolicyIndustry = async () => { ...@@ -439,16 +475,20 @@ const handleGetThinkPolicyIndustry = async () => {
box1Data.value = []; box1Data.value = [];
return; return;
} }
const data = list.map(item => ({ const data = list.map((item, idx) => ({
name: item.industry, name: item.industry,
value: item.amount, value: item.amount,
percent: item.percent percent: item.percent,
color: getAreaTagColor(item.industry, idx)
})); }));
box1Data.value = data; box1Data.value = data;
/* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */ /* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */
await nextTick(); await nextTick();
const box1Chart = getPieChart(box1Data.value); const box1Chart = getPieChart(box1Data.value);
setChart(box1Chart, "box1Chart"); setChart(box1Chart, "box1Chart");
if (isShowAiContentPolicyPt1.value) {
fetchPolicyPtBox1ChartInterpretation();
}
} else { } else {
box1Data.value = []; box1Data.value = [];
} }
...@@ -518,6 +558,9 @@ const handleGetPolicyAdviceDeptDistribution = async () => { ...@@ -518,6 +558,9 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
await nextTick(); await nextTick();
const box2Chart = getPieChart(box2Data.value); const box2Chart = getPieChart(box2Data.value);
setChart(box2Chart, "box2Chart"); setChart(box2Chart, "box2Chart");
if (isShowAiContentPolicyPt2.value) {
fetchPolicyPtBox2ChartInterpretation();
}
} else { } else {
box2Data.value = []; box2Data.value = [];
} }
...@@ -670,7 +713,8 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -670,7 +713,8 @@ const handleGetThinkPolicyIndustryChange = async () => {
const industryAmount = const industryAmount =
quarterData?.industryList?.find(i => i.industry === industry)?.amount || 0; quarterData?.industryList?.find(i => i.industry === industry)?.amount || 0;
return industryAmount; return industryAmount;
}) }),
color: getAreaTagColor(industry, frontendData.data.length)
}; };
frontendData.data.push(industryData); frontendData.data.push(industryData);
}); });
...@@ -680,6 +724,9 @@ const handleGetThinkPolicyIndustryChange = async () => { ...@@ -680,6 +724,9 @@ const handleGetThinkPolicyIndustryChange = async () => {
} }
box3Data.value = frontendData; box3Data.value = frontendData;
await renderBox3Chart(); await renderBox3Chart();
if (isShowAiContentPolicyPt3.value) {
fetchPolicyPtBox3ChartInterpretation();
}
} else { } else {
box3Data.value = { title: [], data: [] }; box3Data.value = { title: [], data: [] };
} }
...@@ -1255,14 +1302,14 @@ watch( ...@@ -1255,14 +1302,14 @@ watch(
} }
); );
onMounted(() => { onMounted(async () => {
handleGetThinkPolicyIndustry(); await handleGetThinkPolicyIndustry();
handleGetThinkPolicyIndustryTotal(); handleGetThinkPolicyIndustryTotal();
handleGetThinkPolicyIndustryChange(); await handleGetThinkPolicyIndustryChange();
handleGetHylyList(); handleGetHylyList();
handleGetGovAgencyList(); handleGetGovAgencyList();
handleGetThinkPolicy(); handleGetThinkPolicy();
handleGetPolicyAdviceDeptDistribution(); await handleGetPolicyAdviceDeptDistribution();
}); });
</script> </script>
...@@ -1476,7 +1523,7 @@ onMounted(() => { ...@@ -1476,7 +1523,7 @@ onMounted(() => {
width: 520px; width: 520px;
height: 372px; height: 372px;
box-sizing: border-box; box-sizing: border-box;
padding: 24px 24px 64px 24px; padding: 0px 24px 64px 24px;
position: relative; position: relative;
overflow: hidden; overflow: hidden;
...@@ -1504,7 +1551,7 @@ onMounted(() => { ...@@ -1504,7 +1551,7 @@ onMounted(() => {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
width: 472px; width: 472px;
height: 284px; height: 308px;
} }
.source { .source {
......
import * as echarts from 'echarts' import * as echarts from 'echarts'
const colorList = [ // 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色)
const AREA_TAG_COLOR_BY_NAME = {
'人工智能': 'rgba(245, 34, 45, 1)', // tag1
'生物科技': 'rgba(19, 168, 168, 1)', // tag2
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
const fallbackColorList = [
'rgba(5, 95, 194, 1)', 'rgba(5, 95, 194, 1)',
'rgba(245, 34, 45, 1)',
'rgba(19, 168, 168, 1)', 'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)', 'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)', 'rgba(114, 46, 209, 1)',
'rgba(115, 209, 61, 1)', 'rgba(82, 196, 26, 1)',
'rgba(206, 79, 81, 1)', 'rgba(22, 119, 255, 1)',
'rgba(145, 202, 255, 1)',
'rgba(95, 101, 108, 1)',
'rgba(250, 84, 28, 1)', 'rgba(250, 84, 28, 1)',
'rgba(47, 84, 235, 1)', 'rgba(47, 84, 235, 1)',
'rgba(64, 150, 255, 1)', 'rgba(133, 165, 255, 1)'
'rgba(34, 41, 52, 1)',
'rgba(173, 198, 255, 1)',
'rgba(255, 169, 64, 1)'
] ]
const parseRgba = (colorStr) => { const parseRgba = (colorStr) => {
...@@ -50,7 +67,8 @@ const getMultiLineChart = (chartInput) => { ...@@ -50,7 +67,8 @@ const getMultiLineChart = (chartInput) => {
const echartsSeries = series.map((item, index) => { const echartsSeries = series.map((item, index) => {
const baseColor = const baseColor =
item.color || item.color ||
colorList[index % colorList.length] || AREA_TAG_COLOR_BY_NAME[item.name] ||
fallbackColorList[index % fallbackColorList.length] ||
`rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)` `rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)`
const { r, g, b } = parseRgba(baseColor) const { r, g, b } = parseRgba(baseColor)
...@@ -58,9 +76,11 @@ const getMultiLineChart = (chartInput) => { ...@@ -58,9 +76,11 @@ const getMultiLineChart = (chartInput) => {
name: item.name, name: item.name,
type: 'line', type: 'line',
smooth: true, smooth: true,
lineStyle: { color: baseColor },
itemStyle: { color: baseColor },
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.3)` }, { offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.1)` },
{ offset: 1, color: `rgba(${r}, ${g}, ${b}, 0)` } { offset: 1, color: `rgba(${r}, ${g}, ${b}, 0)` }
]) ])
}, },
...@@ -147,7 +167,7 @@ const getMultiLineChart = (chartInput) => { ...@@ -147,7 +167,7 @@ const getMultiLineChart = (chartInput) => {
itemHeight: 12 itemHeight: 12
} }
], ],
color: colorList, // 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
xAxis: [ xAxis: [
{ {
type: 'category', type: 'category',
......
const getPieChart = (data) => { const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => {
const color = d?.color
if (!color) return d
return {
...d,
itemStyle: { ...(d.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色
labelLine: {
...(d.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color }
}
}
})
let option = { let option = {
series: [ series: [
{ {
...@@ -57,7 +71,7 @@ const getPieChart = (data) => { ...@@ -57,7 +71,7 @@ const getPieChart = (data) => {
labelLinePoints: points labelLinePoints: points
}; };
}, },
data: data data: seriesData
}] }]
} }
return option return option
......
...@@ -17,12 +17,12 @@ ...@@ -17,12 +17,12 @@
<AreaTag v-for="(tag, index) in thinkTank.tags" :key="index" :tagName="tag.industryName"></AreaTag> <AreaTag v-for="(tag, index) in thinkTank.tags" :key="index" :tagName="tag.industryName"></AreaTag>
</div> </div>
</div> </div>
<div class="header-top-right"> <!-- <div class="header-top-right">
<button class="blue-btn" @click="handleOpenThinkTankSite"> <button class="blue-btn" @click="handleOpenThinkTankSite">
<img class="btn-img" src="./images/image1.png" alt="" /> <img class="btn-img" src="./images/image1.png" alt="" />
<span class="text">{{ '查看智库官网' }}</span> <span class="text">{{ '查看智库官网' }}</span>
</button> </button>
</div> </div> -->
</div> </div>
<div class="header-footer"> <div class="header-footer">
<div class="tab" :class="{ tabActive: tabActiveName === '智库动态' }" @click="switchTab('智库动态')"> <div class="tab" :class="{ tabActive: tabActiveName === '智库动态' }" @click="switchTab('智库动态')">
......
...@@ -455,7 +455,7 @@ const handleGetThinkTankFundsSource = async () => { ...@@ -455,7 +455,7 @@ const handleGetThinkTankFundsSource = async () => {
formatter(params) { formatter(params) {
const valueYi = (params.data.value || 0) / 10000 const valueYi = (params.data.value || 0) / 10000
const percent = params.percent || 0 const percent = params.percent || 0
const valueStr = `${valueYi}${percent}%` const valueStr = `${valueYi.toFixed(2)}${percent}%`
let cumulative = 0 let cumulative = 0
for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0 for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0
const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360 const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360
......
...@@ -163,8 +163,8 @@ ...@@ -163,8 +163,8 @@
</div> </div>
<DivideHeader id="position2" class="divide-header" :titleText="'资讯要闻'"></DivideHeader> <DivideHeader id="position2" class="divide-header" :titleText="'资讯要闻'"></DivideHeader>
<div class="center-center"> <div class="center-center">
<NewsList :newsList="newsList" @item-click="handleToNewsAnalysis" @more-click="handleToMoreNews" <NewsList :newsList="newsList" @item-click="item => gotoNewsDetail(item.newsId)"
img="newsImage" title="newsTitle" content="newsContent" from="from" /> @more-click="handleToMoreNews" img="newsImage" title="newsTitle" content="newsContent" from="from" />
<MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail" <MessageBubble :messageList="messageList" imageUrl="personImage" @more-click="handleToSocialDetail"
@person-click="handleClickPerson" name="personName" content="remarks" source="orgName" /> @person-click="handleClickPerson" name="personName" content="remarks" source="orgName" />
</div> </div>
...@@ -449,11 +449,13 @@ import Box1Logo from "./assets/images/box1-logo.png"; ...@@ -449,11 +449,13 @@ import Box1Logo from "./assets/images/box1-logo.png";
import { setCanvasCreator } from "echarts/core"; import { setCanvasCreator } from "echarts/core";
import { ElMessage } from "element-plus"; import { ElMessage } from "element-plus";
import { useRouter } from 'vue-router'; import { useRouter } from 'vue-router';
import { useGotoNewsDetail } from '@/router/modules/news';
const gotoNewsDetail = useGotoNewsDetail()
const containerRef = ref(null); const containerRef = ref(null);
const statCountInfo = ref([]); const statCountInfo = ref([]);
const pageSize = ref(15) const pageSize = ref(15)
const totalAllItem = ref(0) const totalAllItem = ref(0)
const isShowAiContentBox5 = ref(false); const isShowAiContentBox5 = ref(true);
const aiContentBox5 = ref(""); const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false); const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => { const handleSwitchAiContentShowBox5 = (val) => {
...@@ -462,7 +464,8 @@ const handleSwitchAiContentShowBox5 = (val) => { ...@@ -462,7 +464,8 @@ const handleSwitchAiContentShowBox5 = (val) => {
fetchBox5ChartInterpretation(); fetchBox5ChartInterpretation();
} }
}; };
const isShowAiContentBox6 = ref(false); // 刷新后默认展示「领域分布情况」AI 总结
const isShowAiContentBox6 = ref(true);
const aiContentBox6 = ref(""); const aiContentBox6 = ref("");
const isBox6InterpretLoading = ref(false); const isBox6InterpretLoading = ref(false);
const handleSwitchAiContentShowBox6 = (val) => { const handleSwitchAiContentShowBox6 = (val) => {
...@@ -471,7 +474,8 @@ const handleSwitchAiContentShowBox6 = (val) => { ...@@ -471,7 +474,8 @@ const handleSwitchAiContentShowBox6 = (val) => {
fetchBox6ChartInterpretation(); fetchBox6ChartInterpretation();
} }
}; };
const isShowAiContentBox7 = ref(false); // 刷新后默认展示「智库资金流向」AI 总结
const isShowAiContentBox7 = ref(true);
const aiContentBox7 = ref(""); const aiContentBox7 = ref("");
const isBox7InterpretLoading = ref(false); const isBox7InterpretLoading = ref(false);
const handleSwitchAiContentShowBox7 = (val) => { const handleSwitchAiContentShowBox7 = (val) => {
...@@ -1004,6 +1008,10 @@ const renderBox5Chart = () => { ...@@ -1004,6 +1008,10 @@ const renderBox5Chart = () => {
const handleBox5AreaChange = () => { const handleBox5AreaChange = () => {
aiContentBox5.value = ""; aiContentBox5.value = "";
renderBox5Chart(); renderBox5Chart();
// 切换领域后,若 AI 面板已打开则重新触发流式解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
}; };
const handleBox5 = async year => { const handleBox5 = async year => {
...@@ -1012,7 +1020,13 @@ const handleBox5 = async year => { ...@@ -1012,7 +1020,13 @@ const handleBox5 = async year => {
box5selectetedArea.value = "全部领域"; box5selectetedArea.value = "全部领域";
await handleGetThinkTankPolicyIndustryChange(getBox5YearDateRange(y)); await handleGetThinkTankPolicyIndustryChange(getBox5YearDateRange(y));
renderBox5Chart(); renderBox5Chart();
// 若 AI 面板已打开,让解读在首次加载时自动生成;否则仅清空缓存
if (isShowAiContentBox5.value) {
aiContentBox5.value = "";
fetchBox5ChartInterpretation();
} else {
aiContentBox5.value = ""; aiContentBox5.value = "";
}
}; };
/** 兼容 getChartAnalysis 返回对象:从 data[0] 提取「解读」文本 */ /** 兼容 getChartAnalysis 返回对象:从 data[0] 提取「解读」文本 */
...@@ -1234,14 +1248,45 @@ const box6TankList = ref([ ...@@ -1234,14 +1248,45 @@ const box6TankList = ref([
} }
]); ]);
function transformToChartFormat(data) { function transformToChartFormat(data) {
// 预设颜色池(可按需修改或扩展) // 按 AreaTag 的颜色规则映射到饼图配色(取 tag 的文字色)
const colorPalette = ["#4096FF", "#FFA39E", "#ADC6FF", "#FFC069", "#B5F5EC", "#B37FEB", "#D6E4FF", "#FF8C8C", "#87E8DE"]; const areaTagColorByName = {
"人工智能": "rgba(245, 34, 45, 1)", // tag1
"生物科技": "rgba(19, 168, 168, 1)", // tag2
"新一代通信网络": "rgba(5, 95, 194, 1)", // tag3
// 兼容常见写法
"通信网络": "rgba(5, 95, 194, 1)",
"量子科技": "rgba(114, 46, 209, 1)", // tag4
"新能源": "rgba(82, 196, 26, 1)", // tag5
"集成电路": "rgba(22, 119, 255, 1)", // tag6
"海洋": "rgba(15, 120, 199, 1)", // tag7
"先进制造": "rgba(250, 173, 20, 1)", // tag8
"新材料": "rgba(250, 140, 22, 1)", // tag9
"航空航天": "rgba(47, 84, 235, 1)", // tag10
"太空": "rgba(47, 84, 235, 1)", // tag11
"深海": "rgba(73, 104, 161, 1)", // tag12
"极地": "rgba(133, 165, 255, 1)", // tag13
"核": "rgba(250, 84, 28, 1)", // tag14
"其他": "rgba(82, 196, 26, 1)" // tag15
};
// 未命中 AreaTag 映射时的兜底色板
const fallbackColorPalette = [
"rgba(5, 95, 194, 1)",
"rgba(245, 34, 45, 1)",
"rgba(19, 168, 168, 1)",
"rgba(250, 140, 22, 1)",
"rgba(114, 46, 209, 1)",
"rgba(82, 196, 26, 1)",
"rgba(22, 119, 255, 1)",
"rgba(250, 84, 28, 1)",
"rgba(47, 84, 235, 1)"
];
const list = Array.isArray(data) ? data.slice(0, 7) : []; const list = Array.isArray(data) ? data.slice(0, 7) : [];
return list.map((item, index) => ({ return list.map((item, index) => ({
name: item.industry, name: item.industry,
value: item.amount, value: item.amount,
color: colorPalette[index % colorPalette.length] color: areaTagColorByName[item.industry] || fallbackColorPalette[index % fallbackColorPalette.length]
})); }));
} }
// 政策建议领域分布 // 政策建议领域分布
...@@ -1287,6 +1332,10 @@ const handleBox6 = async () => { ...@@ -1287,6 +1332,10 @@ const handleBox6 = async () => {
aiContentBox6.value = ""; aiContentBox6.value = "";
await handleGetThinkTankPolicyIndustry(); await handleGetThinkTankPolicyIndustry();
renderBox6Chart(); renderBox6Chart();
// 若 AI 面板已打开,让解读在首次加载时自动生成
if (isShowAiContentBox6.value) {
fetchBox6ChartInterpretation();
}
}; };
// 智库资金流向 // 智库资金流向
...@@ -1398,6 +1447,10 @@ const handleBox7 = async () => { ...@@ -1398,6 +1447,10 @@ const handleBox7 = async () => {
const links = box7Data.value?.links ?? []; const links = box7Data.value?.links ?? [];
const box7Chart = getSankeyChart(nodes, links); const box7Chart = getSankeyChart(nodes, links);
setChart(box7Chart, "box7Chart"); setChart(box7Chart, "box7Chart");
// 若 AI 面板已打开,让解读在首次加载时自动生成
if (isShowAiContentBox7.value) {
fetchBox7ChartInterpretation();
}
}; };
/** 请求 box7 智库资金流向桑基图解读(入参:{ text: JSON.stringify({ type, name, nodes, links }) }) */ /** 请求 box7 智库资金流向桑基图解读(入参:{ text: JSON.stringify({ type, name, nodes, links }) }) */
...@@ -2044,9 +2097,12 @@ onMounted(async () => { ...@@ -2044,9 +2097,12 @@ onMounted(async () => {
handleGetNewReport(); handleGetNewReport();
handleGetThinkTankRiskSignal(); handleGetThinkTankRiskSignal();
handleBox5(box5selectetedYear.value); // 先拉到图表数据,再打开 AI 面板并触发解读,避免初始为空导致“无内容”
handleBox6(); await handleBox5(box5selectetedYear.value);
handleBox7(); handleSwitchAiContentShowBox5(true);
// 先把图表数据准备好,避免用户悬浮太快触发解读但数据未就绪
await handleBox6();
await handleBox7();
handleGetHylyList(); handleGetHylyList();
handleGetThinkTankHot(getDateYearsAgo(1)); handleGetThinkTankHot(getDateYearsAgo(1));
handleGetetThinkTankReport(); handleGetetThinkTankReport();
...@@ -3424,7 +3480,7 @@ onMounted(async () => { ...@@ -3424,7 +3480,7 @@ onMounted(async () => {
width: 1063px; width: 1063px;
box-sizing: border-box; box-sizing: border-box;
position: relative; position: relative;
padding: 24px 24px 64px 24px; padding: 0px 24px 64px 24px;
&.box5-main--empty { &.box5-main--empty {
display: flex; display: flex;
...@@ -3453,7 +3509,7 @@ onMounted(async () => { ...@@ -3453,7 +3509,7 @@ onMounted(async () => {
.box5-chart-canvas { .box5-chart-canvas {
flex: 1; flex: 1;
min-width: 0; min-width: 0;
height: 324px; height: 348px;
} }
.source { .source {
......
...@@ -24,11 +24,24 @@ ...@@ -24,11 +24,24 @@
</div> </div>
<div class="main"> <div class="main">
<div class="main-header"> <div class="main-header">
<div style=" margin-top: 17px;"> <div>
智库报告原文 智库报告原文
</div> </div>
<div class="btn-box"> <div class="btn-box">
<div class="translate"> <div class="translate">
<div class="search-input-wrap" v-if="showSearchInput">
<input v-model="searchKeywordText" class="search-input" placeholder="回车查询"
@keyup.enter="handleSearchInPdf" />
<div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div>
<button class="search-nav-btn" type="button" @click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1">
上一个
</button>
<button class="search-nav-btn" type="button" @click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total">
下一个
</button>
</div>
<div class="switch"> <div class="switch">
<el-switch v-model="valueSwitch" /> <el-switch v-model="valueSwitch" />
</div> </div>
...@@ -48,17 +61,24 @@ ...@@ -48,17 +61,24 @@
</div> </div>
</div> </div>
<div class="report-box"> <div class="report-box">
<pdf v-if="valueSwitch && reportUrlEnWithPage" ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" <div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
class="pdf-pane" /> <pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
<pdf v-if="reportUrlWithPage" ref="rightPdfRef" :pdfUrl="reportUrlWithPage" </div>
:class="['pdf-pane', { 'pdf-pane-full': !valueSwitch }]" /> <div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf
:key="`right-pdf-${valueSwitch ? 'split' : 'full'}`"
ref="rightPdfRef"
:pdfUrl="reportUrlWithPage"
class="pdf-pane-inner"
/>
</div>
</div> </div>
</div> </div>
</div> </div>
</template> </template>
<script setup> <script setup>
import { computed, ref, onMounted } from "vue"; import { computed, ref, onMounted, watch } from "vue";
import pdf from "./pdf.vue"; import pdf from "./pdf.vue";
import { import {
getThinkTankReportSummary, getThinkTankReportSummary,
...@@ -88,14 +108,59 @@ const buildPdfPageUrl = url => { ...@@ -88,14 +108,59 @@ const buildPdfPageUrl = url => {
const reportUrlWithPage = computed(() => buildPdfPageUrl(reportUrl.value)) const reportUrlWithPage = computed(() => buildPdfPageUrl(reportUrl.value))
const reportUrlEnWithPage = computed(() => buildPdfPageUrl(reportUrlEn.value)) const reportUrlEnWithPage = computed(() => buildPdfPageUrl(reportUrlEn.value))
const valueSwitch = ref(true) const valueSwitch = ref(true)
const showSearchInput = ref(false) const showSearchInput = ref(true)
const searchKeywordText = ref('') const searchKeywordText = ref('')
const leftPdfRef = ref(null) const leftPdfRef = ref(null)
const rightPdfRef = ref(null) const rightPdfRef = ref(null)
const matchInfo = ref({ current: 0, total: 0 })
const activePdfRef = ref(null)
const clearPdfSearchState = () => {
activePdfRef.value = null
matchInfo.value = { current: 0, total: 0 }
const leftPdf = leftPdfRef.value
const rightPdf = rightPdfRef.value
if (leftPdf && typeof leftPdf.clearSearch === 'function') {
leftPdf.clearSearch()
}
if (rightPdf && typeof rightPdf.clearSearch === 'function') {
rightPdf.clearSearch()
}
}
const updateMatchInfo = () => {
const pdf = activePdfRef.value
if (pdf && typeof pdf.getMatchInfo === 'function') {
matchInfo.value = pdf.getMatchInfo()
return
}
matchInfo.value = { current: 0, total: 0 }
}
watch(
() => searchKeywordText.value,
(val) => {
const keyword = String(val ?? '').trim()
if (!keyword) {
clearPdfSearchState()
}
}
)
watch(
() => valueSwitch.value,
() => {
// 切换「显示原文」会导致 PDF 重新挂载/布局变化:清空搜索与计数,回到初始状态
searchKeywordText.value = ''
clearPdfSearchState()
}
)
const handleSearchInPdf = async () => { const handleSearchInPdf = async () => {
const keyword = searchKeywordText.value?.trim() const keyword = searchKeywordText.value?.trim()
if (!keyword) return if (!keyword) return
activePdfRef.value = null
matchInfo.value = { current: 0, total: 0 }
const leftPdf = leftPdfRef.value const leftPdf = leftPdfRef.value
const rightPdf = rightPdfRef.value const rightPdf = rightPdfRef.value
let page = 0 let page = 0
...@@ -110,6 +175,8 @@ const handleSearchInPdf = async () => { ...@@ -110,6 +175,8 @@ const handleSearchInPdf = async () => {
} }
if (page && targetRef && typeof targetRef.goToPage === 'function') { if (page && targetRef && typeof targetRef.goToPage === 'function') {
targetRef.goToPage(page) targetRef.goToPage(page)
activePdfRef.value = targetRef
updateMatchInfo()
} else { } else {
try { try {
const { ElMessage } = await import('element-plus') const { ElMessage } = await import('element-plus')
...@@ -118,6 +185,20 @@ const handleSearchInPdf = async () => { ...@@ -118,6 +185,20 @@ const handleSearchInPdf = async () => {
} }
} }
const handlePrevMatch = () => {
const pdf = activePdfRef.value
if (!pdf || typeof pdf.prevMatch !== 'function') return
pdf.prevMatch()
updateMatchInfo()
}
const handleNextMatch = () => {
const pdf = activePdfRef.value
if (!pdf || typeof pdf.nextMatch !== 'function') return
pdf.nextMatch()
updateMatchInfo()
}
// 下载:中英文都下载,与政令原文页相同的 fetch → blob → a 标签触发下载 // 下载:中英文都下载,与政令原文页相同的 fetch → blob → a 标签触发下载
const downloadOnePdf = async (url, filename) => { const downloadOnePdf = async (url, filename) => {
const response = await fetch(url, { const response = await fetch(url, {
...@@ -410,11 +491,14 @@ onMounted(async () => { ...@@ -410,11 +491,14 @@ onMounted(async () => {
text-align: left; text-align: left;
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
align-items: center;
overflow: visible;
.btn-box { .btn-box {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 8px; gap: 8px;
flex-shrink: 0;
.translate { .translate {
display: flex; display: flex;
...@@ -422,6 +506,7 @@ onMounted(async () => { ...@@ -422,6 +506,7 @@ onMounted(async () => {
align-items: center; align-items: center;
height: 24px; height: 24px;
margin-right: 16px; margin-right: 16px;
flex-shrink: 0;
...@@ -522,6 +607,58 @@ onMounted(async () => { ...@@ -522,6 +607,58 @@ onMounted(async () => {
align-items: center; align-items: center;
gap: 8px; gap: 8px;
margin-left: 4px; margin-left: 4px;
flex-shrink: 0;
}
.search-input {
width: 160px;
height: 24px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
background: rgba(246, 250, 255, 1);
border-radius: 4px;
padding: 0 10px;
font-family: "Source Han Sans CN";
font-size: 14px;
line-height: 22px;
outline: none;
}
.search-match-count {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
min-width: 48px;
text-align: center;
flex-shrink: 0;
}
.search-nav-btn {
width: 68px;
height: 24px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
background: rgba(246, 250, 255, 1);
border-radius: 4px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
cursor: pointer;
padding: 0;
flex-shrink: 0;
white-space: nowrap;
}
.search-nav-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
} }
} }
} }
...@@ -536,13 +673,21 @@ onMounted(async () => { ...@@ -536,13 +673,21 @@ onMounted(async () => {
overflow-x: hidden; overflow-x: hidden;
} }
.pdf-pane { .pdf-pane-wrap {
width: 50%; flex: 0 0 50%;
max-width: 50%;
height: 100%; height: 100%;
min-width: 0;
} }
.pdf-pane-full { .pdf-pane-wrap.is-full {
flex: 0 0 100%;
max-width: 100%;
}
.pdf-pane-inner {
width: 100%; width: 100%;
height: 100%;
} }
} }
} }
......
<template> <template>
<div class="pdf-viewer"> <div class="pdf-viewer">
<canvas <!-- PDF 页面:canvas + textLayer 必须在同一容器内渲染 -->
v-for="page in pageCount" <div class="page-wrap" v-for="page in pageCount" :key="page">
:key="page" <canvas :ref="el => setCanvasRef(page, el)"></canvas>
:ref="el => setCanvasRef(page, el)" <div :ref="el => setOverlayRef(page, el)" class="textLayer"></div>
></canvas> </div>
<div v-if="loading" class="loading">加载中...</div> <div v-if="loading" class="loading">加载中...</div>
</div> </div>
</template> </template>
<script> <script>
import { ref, onMounted, nextTick } from 'vue'; import { ref, shallowRef, onMounted, nextTick, watch } from 'vue';
import * as pdfjsLib from 'pdfjs-dist'; import * as pdfjsLib from 'pdfjs-dist';
import { TextLayer } from 'pdfjs-dist';
pdfjsLib.GlobalWorkerOptions.workerSrc = new URL( pdfjsLib.GlobalWorkerOptions.workerSrc = new URL(
'pdfjs-dist/build/pdf.worker.min.mjs', 'pdfjs-dist/build/pdf.worker.min.mjs',
...@@ -28,98 +29,276 @@ export default { ...@@ -28,98 +29,276 @@ export default {
} }
}, },
setup(props) { setup(props) {
// 非响应式的 canvas 映射,避免触发布局递归更新
const canvasMap = {}; const canvasMap = {};
const overlayMap = {};
const pageCount = ref(0); const pageCount = ref(0);
const loading = ref(true); const loading = ref(true);
const pdfDocRef = ref(null); const renderedPageCount = ref(0);
let resolveRenderAll = null;
const waitAllPagesRendered = () => {
if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
return Promise.resolve();
}
return new Promise((resolve) => {
resolveRenderAll = resolve;
});
};
// pdfjs 的 document 对象内部使用 #private 字段,
// 若被 Vue 响应式深度代理会触发 "Cannot read from private field"。
// 因此用 shallowRef 保持为原始对象引用。
const pdfDocRef = shallowRef(null);
const searchKey = ref('');
const matchList = ref([]);
const matchIdx = ref(0);
// 保存 canvas
const setCanvasRef = (page, el) => { const setCanvasRef = (page, el) => {
if (!el) return; if (!el) return;
canvasMap[page] = el; canvasMap[page] = el;
}; };
// 保存 textLayer 容器(用于搜索高亮)
const setOverlayRef = (page, el) => {
if (!el) return;
overlayMap[page] = el;
};
// 清理 URL
const parsePdfUrl = (pdfUrl) => { const parsePdfUrl = (pdfUrl) => {
if (!pdfUrl || typeof pdfUrl !== 'string') return ''; if (!pdfUrl || typeof pdfUrl !== 'string') return '';
const [urlPart] = pdfUrl.split('#'); const [urlPart] = pdfUrl.split('#');
return urlPart; return urlPart;
};
// 清空所有高亮(不销毁 textLayer)
const clearHighlights = () => {
Object.values(overlayMap).forEach(layer => {
if (!layer) return;
const rects = layer.querySelectorAll('.highlight-rect');
rects.forEach(n => n.remove());
});
};
// 重置搜索状态:清空关键词、匹配列表与高亮
const clearSearch = () => {
searchKey.value = '';
matchList.value = [];
matchIdx.value = 0;
clearHighlights();
};
// 渲染单页 PDF(canvas + textLayer)
const renderPage = async (pdf, pageNum) => {
const pdfPage = await pdf.getPage(pageNum);
const canvas = canvasMap[pageNum];
const textLayer = overlayMap[pageNum];
if (!canvas || !textLayer) return;
// 以画布的可视宽度为基准自适应缩放,避免 CSS 强行拉伸导致 textLayer/高亮错位
const baseViewport = pdfPage.getViewport({ scale: 1 });
const desiredWidth = canvas.clientWidth || 726;
const scale = desiredWidth / baseViewport.width;
const viewport = pdfPage.getViewport({ scale });
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
// 保证 canvas 不再被 CSS 拉伸,和 textLayer 共享同一坐标系
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
textLayer.style.width = canvas.width + 'px';
textLayer.style.height = canvas.height + 'px';
textLayer.innerHTML = '';
// pdf.js v5 text layer 依赖 scale-factor 参与定位计算
textLayer.style.setProperty('--scale-factor', String(viewport.scale || 1));
await pdfPage.render({ canvasContext: context, viewport }).promise;
// 渲染 textLayer:pdfjs-dist v5 推荐用 TextLayer,renderTextLayer 可能不存在
try {
const textContent = await pdfPage.getTextContent();
const layer = new TextLayer({
textContentSource: textContent,
container: textLayer,
viewport
});
await layer.render();
} catch (e) {
console.warn('textLayer 渲染失败', e);
} }
renderedPageCount.value += 1;
if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
if (typeof resolveRenderAll === 'function') {
const fn = resolveRenderAll;
resolveRenderAll = null;
fn();
}
}
};
// 渲染 PDF
const renderPdf = async (pdfUrl) => { const renderPdf = async (pdfUrl) => {
const url = parsePdfUrl(pdfUrl) const url = parsePdfUrl(pdfUrl);
if (!url) return if (!url) return;
loading.value = true;
pdfDocRef.value = null;
clearHighlights();
matchList.value = [];
searchKey.value = '';
renderedPageCount.value = 0;
resolveRenderAll = null;
loading.value = true
pdfDocRef.value = null
try { try {
const loadingTask = pdfjsLib.getDocument(url); const pdf = await pdfjsLib.getDocument(url).promise;
const pdf = await loadingTask.promise; pdfDocRef.value = pdf;
pdfDocRef.value = pdf
pageCount.value = pdf.numPages; pageCount.value = pdf.numPages;
// 等待 canvas 按 pageCount 渲染出来
await nextTick(); await nextTick();
for (let p = 1; p <= pdf.numPages; p++) { for (let p = 1; p <= pdf.numPages; p++) {
const pdfPage = await pdf.getPage(p); await renderPage(pdf, p);
const viewport = pdfPage.getViewport({ scale: 1.5 }); }
const canvas = canvasMap[p]; } catch (err) {
if (!canvas) continue; console.error('PDF 加载失败', err);
const context = canvas.getContext('2d'); } finally {
const renderContext = { loading.value = false;
canvasContext: context, }
viewport: viewport
}; };
canvas.width = viewport.width; // 搜索关键词 + 高亮(记录每个命中的子串范围)
canvas.height = viewport.height; const doSearch = async () => {
const doc = pdfDocRef.value;
const key = searchKey.value.trim();
clearHighlights();
matchList.value = [];
matchIdx.value = 0;
if (!doc || !key) return;
// 首次搜索时确保所有页的 textLayer 已渲染完成,避免“越搜越多”
await waitAllPagesRendered();
await pdfPage.render(renderContext).promise; for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
const layer = overlayMap[pageNum];
if (!layer) continue;
const nodes = Array.from(layer.querySelectorAll('span'));
for (const el of nodes) {
const t = (el.textContent || '');
if (!t) continue;
let start = 0;
while (true) {
const idx = t.indexOf(key, start);
if (idx === -1) break;
matchList.value.push({ pageNum, el, startIdx: idx, endIdx: idx + key.length });
start = idx + Math.max(1, key.length);
} }
} catch (error) {
console.error('加载 PDF 出错:', error);
} finally {
loading.value = false;
} }
} }
/** 在 PDF 中查找关键词,返回首次出现的页码(1-based),未找到返回 0 */ if (matchList.value.length > 0) jumpTo(0);
const searchKeyword = async (keyword) => { };
const doc = pdfDocRef.value
if (!doc || !keyword || !String(keyword).trim()) return 0 // 跳转到第 N 个匹配项
const k = String(keyword).trim() const jumpTo = (idx) => {
const num = doc.numPages if (idx < 0 || idx >= matchList.value.length) return;
for (let p = 1; p <= num; p++) { matchIdx.value = idx;
const page = await doc.getPage(p) const m = matchList.value[idx];
const content = await page.getTextContent() const el = m?.el;
const text = (content.items || []).map(it => it.str || '').join('') if (!el) return;
if (text.includes(k)) return p clearHighlights();
// 用 Range 精确计算“子串”在页面上的矩形位置,再画黄色块,避免把整段 span 都标黄
const textNode = el.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
try {
const range = document.createRange();
range.setStart(textNode, Math.max(0, m.startIdx ?? 0));
range.setEnd(textNode, Math.max(0, m.endIdx ?? 0));
const rectList = Array.from(range.getClientRects());
const pageWrap = el.closest('.page-wrap');
const layer = overlayMap[m.pageNum];
if (pageWrap && layer && rectList.length) {
const pageRect = pageWrap.getBoundingClientRect();
rectList.forEach(r => {
const mark = document.createElement('div');
mark.className = 'highlight-rect';
mark.style.left = (r.left - pageRect.left) + 'px';
mark.style.top = (r.top - pageRect.top) + 'px';
mark.style.width = r.width + 'px';
mark.style.height = r.height + 'px';
layer.appendChild(mark);
});
} }
return 0 range.detach?.();
} catch (e) {
// ignore
} }
/** 滚动到指定页码(1-based)对应的 canvas */
const goToPage = (pageNum) => {
const canvas = canvasMap[pageNum]
if (canvas && typeof canvas.scrollIntoView === 'function') {
canvas.scrollIntoView({ behavior: 'smooth', block: 'start' })
} }
// 优先只滚动右侧 report-box,避免触发整页滚动导致 header 遮挡
const container = el.closest('.report-box');
if (container) {
const TOP_OFFSET = 72;
const containerRect = container.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const targetTop = (elRect.top - containerRect.top) + container.scrollTop - TOP_OFFSET;
container.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
} else {
el.scrollIntoView({ behavior: 'smooth', block: 'center' });
} }
};
const prevMatch = () => jumpTo(matchIdx.value - 1);
const nextMatch = () => jumpTo(matchIdx.value + 1);
// 首次挂载后再根据当前 url 渲染,避免 canvas 还没准备好 const getMatchInfo = () => {
onMounted(() => { const total = matchList.value.length;
if (props.pdfUrl) { const current = total ? matchIdx.value + 1 : 0;
renderPdf(props.pdfUrl) return { current, total };
};
// 外部调用方法
const searchKeyword = async (keyword) => {
searchKey.value = keyword;
await doSearch();
return matchList.value.length > 0 ? matchList.value[0].pageNum : 0;
};
const goToPage = (pageNum) => {
const canvasEl = canvasMap[pageNum];
if (!canvasEl) return;
const container = canvasEl.closest('.report-box');
if (container) {
const containerRect = container.getBoundingClientRect();
const canvasRect = canvasEl.getBoundingClientRect();
const targetTop =
(canvasRect.top - containerRect.top) + container.scrollTop;
container.scrollTo({ top: Math.max(0, targetTop), behavior: 'smooth' });
} else {
canvasEl.scrollIntoView({ behavior: 'smooth', block: 'start' });
} }
}) };
watch(() => props.pdfUrl, (newVal) => {
if (newVal) renderPdf(newVal);
}, { immediate: true });
return { return {
pageCount, pageCount,
setCanvasRef, setCanvasRef,
setOverlayRef,
loading, loading,
searchKey,
doSearch,
prevMatch,
nextMatch,
getMatchInfo,
matchList,
matchIdx,
searchKeyword, searchKeyword,
clearSearch,
goToPage goToPage
} };
} }
}; };
</script> </script>
...@@ -128,7 +307,12 @@ export default { ...@@ -128,7 +307,12 @@ export default {
.pdf-viewer { .pdf-viewer {
position: relative; position: relative;
width: 100%; width: 100%;
/* 高度由内容决定,让外层容器控制滚动 */ }
.page-wrap {
position: relative;
margin-bottom: 16px;
width: 100%;
} }
canvas { canvas {
...@@ -137,6 +321,44 @@ canvas { ...@@ -137,6 +321,44 @@ canvas {
display: block; display: block;
} }
.textLayer {
position: absolute;
left: 0;
top: 0;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 2;
line-height: 1;
}
/* 不展示整页“文字层”,只在命中时显示黄色背景 */
.textLayer :deep(span) {
position: absolute;
transform-origin: 0% 0%;
white-space: pre;
line-height: 1;
/* pdf.js v5 TextLayer:用变量计算真实字形盒子尺寸,否则背景宽高会不准 */
font-size: calc(var(--font-height, 0px) * var(--scale-factor, 1));
transform: scaleX(var(--scale-x, 1));
color: transparent;
}
.textLayer :deep(.highlight-text) {
background: #ff0;
opacity: 0.6;
padding: 0 1px;
border-radius: 2px;
}
.textLayer :deep(.highlight-rect) {
position: absolute;
background: #ff0;
opacity: 0.6;
border-radius: 2px;
pointer-events: none;
}
.loading { .loading {
position: absolute; position: absolute;
top: 50%; top: 50%;
......
...@@ -13,23 +13,40 @@ const getMultiLineChart = (data) => { ...@@ -13,23 +13,40 @@ const getMultiLineChart = (data) => {
const legendFirstLine = allNames.slice(0, legendSplitAt) const legendFirstLine = allNames.slice(0, legendSplitAt)
const legendSecondLine = allNames.slice(legendSplitAt) const legendSecondLine = allNames.slice(legendSplitAt)
// 定义配色数组 // 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色)
const colorList = [ const AREA_TAG_COLOR_BY_NAME = {
'rgba(5, 95, 194, 1)', // #055fc2 '人工智能': 'rgba(245, 34, 45, 1)', // tag1
'rgba(19, 168, 168, 1)', // #13a8a8 '生物科技': 'rgba(19, 168, 168, 1)', // tag2
'rgba(250, 140, 22, 1)', // #fa8c16 '新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
'rgba(114, 46, 209, 1)', // #722ed1 // 兼容后端/页面常见写法
'rgba(115, 209, 61, 1)', // #73d13d '通信网络': 'rgba(5, 95, 194, 1)',
'rgba(206, 79, 81, 1)', // #ce4f51 '量子科技': 'rgba(114, 46, 209, 1)', // tag4
'rgba(145, 202, 255, 1)', // #91caff '新能源': 'rgba(82, 196, 26, 1)', // tag5
'rgba(95, 101, 108, 1)', // #5f656c '集成电路': 'rgba(22, 119, 255, 1)', // tag6
'rgba(250, 84, 28, 1)', // #fa541c '海洋': 'rgba(15, 120, 199, 1)', // tag7
'rgba(47, 84, 235, 1)', // #2f54eb '先进制造': 'rgba(250, 173, 20, 1)', // tag8
'rgba(64, 150, 255, 1)', // #4096ff '新材料': 'rgba(250, 140, 22, 1)', // tag9
'rgba(34, 41, 52, 1)', // #222934 '航空航天': 'rgba(47, 84, 235, 1)', // tag10
'rgba(173, 198, 255, 1)', // #adc6ff '太空': 'rgba(47, 84, 235, 1)', // tag11
'rgba(255, 169, 64, 1)' // #ffa940 '深海': 'rgba(73, 104, 161, 1)', // tag12
]; '极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
// 兜底颜色池(未命中 AreaTag 映射时使用)
const fallbackColorList = [
'rgba(5, 95, 194, 1)',
'rgba(19, 168, 168, 1)',
'rgba(250, 140, 22, 1)',
'rgba(114, 46, 209, 1)',
'rgba(82, 196, 26, 1)',
'rgba(250, 84, 28, 1)',
'rgba(22, 119, 255, 1)',
'rgba(95, 101, 108, 1)',
'rgba(47, 84, 235, 1)',
'rgba(133, 165, 255, 1)',
]
// 解析 RGBA 颜色的辅助函数 // 解析 RGBA 颜色的辅助函数
const parseRgba = (colorStr) => { const parseRgba = (colorStr) => {
...@@ -50,19 +67,29 @@ const getMultiLineChart = (data) => { ...@@ -50,19 +67,29 @@ const getMultiLineChart = (data) => {
// 动态生成 series 配置 // 动态生成 series 配置
const echartsSeries = series.map((item, index) => { const echartsSeries = series.map((item, index) => {
// 获取当前系列的颜色(优先使用item.color,否则用预设颜色,再否则随机) // 获取当前系列的颜色(优先使用item.color,否则用预设颜色,再否则随机)
const baseColor = item.color || colorList[index % colorList.length] || `rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)`; const baseColor =
item.color ||
AREA_TAG_COLOR_BY_NAME[item.name] ||
fallbackColorList[index % fallbackColorList.length] ||
`rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)`;
const { r, g, b } = parseRgba(baseColor); const { r, g, b } = parseRgba(baseColor);
return ({ return ({
name: item.name, name: item.name,
type: 'line', type: 'line',
smooth: true, smooth: true,
lineStyle: {
color: baseColor
},
itemStyle: {
color: baseColor
},
// 新增/优化:面积填充渐变效果 // 新增/优化:面积填充渐变效果
areaStyle: { areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [ color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ {
offset: 0, // 顶部 offset: 0, // 顶部
color: `rgba(${r}, ${g}, ${b}, 0.3)` // 0.3 透明度 color: `rgba(${r}, ${g}, ${b}, 0.1)` // 按需求:0.1 -> 0
}, },
{ {
offset: 1, // 底部 offset: 1, // 底部
...@@ -131,7 +158,7 @@ const getMultiLineChart = (data) => { ...@@ -131,7 +158,7 @@ const getMultiLineChart = (data) => {
} }
} }
], ],
color: colorList, // 使用预设的配色数组 // 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
xAxis: [ xAxis: [
{ {
type: 'category', type: 'category',
......
const getPieChart = (data) => { const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => {
const color = d?.color
if (!color) return d
return {
...d,
itemStyle: { ...(d.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色
labelLine: {
...(d.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color }
}
}
})
let option = { let option = {
series: [ series: [
{ {
...@@ -61,7 +75,7 @@ const getPieChart = (data) => { ...@@ -61,7 +75,7 @@ const getPieChart = (data) => {
labelLinePoints: points labelLinePoints: points
}; };
}, },
data: data data: seriesData
}] }]
} }
return option return option
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论