Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
risk-monitor
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
蔡建
risk-monitor
Commits
6714a4e9
提交
6714a4e9
authored
3月 22, 2026
作者:
朱政
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
feat:核心成员
上级
58537cbd
隐藏空白字符变更
内嵌
并排
正在显示
16 个修改的文件
包含
2502 行增加
和
553 行删除
+2502
-553
.env.development
.env.development
+4
-2
.env.production
.env.production
+4
-2
index.js
src/api/aiAnalysis/index.js
+282
-11
request.js
src/api/request.js
+16
-2
overview.js
src/api/thinkTank/overview.js
+29
-7
index.vue
src/components/base/TipTab/index.vue
+5
-3
setChart.js
src/utils/setChart.js
+4
-0
index.vue
src/views/thinkTank/ThinkTankDetail/PolicyTracking/index.vue
+995
-198
multiLineChart.js
...nk/ThinkTankDetail/PolicyTracking/utils/multiLineChart.js
+130
-49
index.vue
src/views/thinkTank/ThinkTankDetail/thinkInfo/index.vue
+66
-52
index.vue
src/views/thinkTank/TipTab/index.vue
+50
-0
tip-icon.svg
src/views/thinkTank/TipTab/tip-icon.svg
+6
-0
index.vue
src/views/thinkTank/allThinkTank/index.vue
+31
-3
index.vue
src/views/thinkTank/index.vue
+836
-219
multiLineChart.js
src/views/thinkTank/utils/multiLineChart.js
+42
-4
piechart.js
src/views/thinkTank/utils/piechart.js
+2
-1
没有找到文件。
.env.development
浏览文件 @
6714a4e9
VITE_BASE_API= '/api'
\ No newline at end of file
VITE_BASE_API= '/api'
# 图表解读等 /aiAnalysis 代理服务密钥(勿提交到公开仓库时可改为本地 .env.local)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
.env.production
浏览文件 @
6714a4e9
# 线上地址
VITE_BASE_API= '/api'
\ No newline at end of file
VITE_BASE_API= '/api'
# 图表解读等 AI 服务(与部署环境一致时填写)
VITE_AI_ANALYSIS_API_KEY=aircasKEY19491001
\ No newline at end of file
src/api/aiAnalysis/index.js
浏览文件 @
6714a4e9
import
request
from
"@/api/request.js"
;
import
{
getToken
}
from
"@/api/request.js"
;
const
CHART_INTERPRETATION_PATH
=
"/aiAnalysis/chart_interpretation"
;
/**
* 从接口各类返回结构中取出「解读」纯文本(含流式拼出的 ```json [...] ``` 字符串)
* @param {unknown} raw
* @returns {string}
*/
export
function
extractChartInterpretationText
(
raw
)
{
if
(
raw
==
null
||
raw
===
""
)
{
return
""
;
}
if
(
Array
.
isArray
(
raw
))
{
const
first
=
raw
[
0
];
if
(
first
&&
typeof
first
===
"object"
)
{
const
v
=
first
[
"解读"
]
??
first
.
interpretation
;
if
(
v
!=
null
&&
String
(
v
).
trim
())
{
return
String
(
v
).
trim
();
}
}
return
""
;
}
if
(
typeof
raw
===
"object"
)
{
if
(
Array
.
isArray
(
raw
.
data
))
{
return
extractChartInterpretationText
(
raw
.
data
);
}
if
(
typeof
raw
.
data
===
"string"
)
{
return
extractChartInterpretationText
(
raw
.
data
);
}
if
(
typeof
raw
.
text
===
"string"
)
{
return
extractChartInterpretationText
(
raw
.
text
);
}
const
v
=
raw
[
"解读"
]
??
raw
.
interpretation
;
if
(
v
!=
null
)
{
return
String
(
v
).
trim
();
}
for
(
const
key
of
[
"result"
,
"content"
,
"items"
,
"list"
])
{
if
(
Array
.
isArray
(
raw
[
key
]))
{
const
t
=
extractChartInterpretationText
(
raw
[
key
]);
if
(
t
)
{
return
t
;
}
}
}
return
""
;
}
if
(
typeof
raw
===
"string"
)
{
const
t
=
raw
.
trim
();
if
(
!
t
)
{
return
""
;
}
try
{
return
extractChartInterpretationText
(
JSON
.
parse
(
t
));
}
catch
{
/* 非纯 JSON */
}
const
unfenced
=
t
.
replace
(
/^```
(?:
json
)?\s
*/i
,
""
)
.
replace
(
/```
\s
*$/i
,
""
)
.
trim
();
try
{
return
extractChartInterpretationText
(
JSON
.
parse
(
unfenced
));
}
catch
{
/* 继续尝试截取数组 */
}
const
arrMatch
=
unfenced
.
match
(
/
\[\s
*
\{[\s\S]
*"解读"
[\s\S]
*
\}\s
*
\]
/
);
if
(
arrMatch
)
{
try
{
return
extractChartInterpretationText
(
JSON
.
parse
(
arrMatch
[
0
]));
}
catch
{
/* ignore */
}
}
const
byRegex
=
extractInterpretationByRegex
(
t
);
if
(
byRegex
)
{
return
byRegex
;
}
return
""
;
}
return
""
;
}
/**
* 从非严格 JSON 的文本中用正则提取「解读」字段(流式拼完后仍解析失败时的兜底)
* @param {string} s
* @returns {string}
*/
function
extractInterpretationByRegex
(
s
)
{
if
(
!
s
||
typeof
s
!==
"string"
)
{
return
""
;
}
const
m
=
s
.
match
(
/"解读"
\s
*:
\s
*"
((?:[^
"
\\]
|
\\
.
)
*
)
"/
);
if
(
!
m
||
!
m
[
1
])
{
return
""
;
}
return
m
[
1
].
replace
(
/
\\
n/g
,
"
\
n"
).
replace
(
/
\\
r/g
,
"
\
r"
).
replace
(
/
\\
"/g
,
'"'
).
replace
(
/
\\\\
/g
,
"
\
\"
);
}
/**
* 解析 SSE 文本行(流式或整包响应体)
* @param {string} fullText
* @returns {string}
*/
function parseSseChartInterpretationLines(fullText) {
let textFragments = "";
let lastFromArray = "";
/** 单行 JSON.parse 失败的 data 片段,多为跨多行的同一 JSON(如数组) */
const unparsedPayloads = [];
const lines = fullText.split(/
\
r?
\
n/);
for (const line of lines) {
const trimmed = line.trim();
if (!trimmed.startsWith("
data
:
")) {
continue;
}
const payload = trimmed.slice(5).trim();
if (!payload || payload === "
[
DONE
]
") {
continue;
}
let parsed;
try {
parsed = JSON.parse(payload);
} catch {
unparsedPayloads.push(payload);
continue;
}
if (Array.isArray(parsed)) {
const t = extractChartInterpretationText(parsed);
if (t) {
lastFromArray = t;
}
continue;
}
if (parsed && typeof parsed.text === "
string
") {
textFragments += parsed.text;
}
}
if (lastFromArray) {
return lastFromArray;
}
/* 合并跨多行 data: 的 JSON(例如 data: [ 、 data: { 、 data: "
图表标题
"…) */
if (unparsedPayloads.length > 0) {
const joinedNewline = unparsedPayloads.join("
\
n
").trim();
for (const candidate of [joinedNewline, unparsedPayloads.join("").trim()]) {
if (!candidate) {
continue;
}
try {
const t = extractChartInterpretationText(JSON.parse(candidate));
if (t) {
return t;
}
} catch {
/* ignore */
}
const t2 = extractChartInterpretationText(candidate);
if (t2) {
return t2;
}
const t3 = extractInterpretationByRegex(candidate);
if (t3) {
return t3;
}
}
}
const fromFragments = extractChartInterpretationText(textFragments);
if (fromFragments) {
return fromFragments;
}
const fragRegex = extractInterpretationByRegex(textFragments);
if (fragRegex) {
return fragRegex;
}
return extractChartInterpretationText(textFragments.trim());
}
/**
* @param {ReadableStream<Uint8Array> | null} body
* @param {(chunk: string) => void} [onDelta]
* @returns {Promise<string>}
*/
async function readSseChartInterpretationStream(body, onDelta) {
if (!body) {
return "";
}
const reader = body.getReader();
const decoder = new TextDecoder();
let all = "";
let lastPushed = "";
while (true) {
const { done, value } = await reader.read();
if (done) {
break;
}
all += decoder.decode(value, { stream: true });
const now = parseSseChartInterpretationLines(all);
if (now && now !== lastPushed) {
lastPushed = now;
onDelta?.(now);
}
}
all += decoder.decode();
const fin = parseSseChartInterpretationLines(all);
if (fin && fin !== lastPushed) {
onDelta?.(fin);
}
return fin;
}
/**
* POST 图表解读:支持 SSE(data: {"
text
":"
...
"} / data: [...])与普通 JSON
* @param {Record<string, unknown>} body
* @param {(interpretation: string) => void} [onInterpretationDelta] 流式过程中有可读解读时回调
* @returns {Promise<string>} 解读正文
*/
export async function fetchChartInterpretation(body, onInterpretationDelta) {
const headers = {
"
Content
-
Type
": "
application
/
json
",
Accept: "
text
/
event
-
stream
,
application
/
json
,
text
/
plain
,
*
/*"
};
const token = getToken();
if (token) {
headers.token = token;
}
const aiApiKey = import.meta.env.VITE_AI_ANALYSIS_API_KEY;
if (aiApiKey) {
headers["X-API-Key"] = aiApiKey;
}
const res = await fetch(CHART_INTERPRETATION_PATH, {
method: "POST",
headers,
body: JSON.stringify(body)
});
if (!res.ok) {
const errText = await res.text().catch(() => "");
throw new Error(errText || `Request failed with status ${res.status}`);
}
const contentType = res.headers.get("content-type") || "";
if (contentType.includes("text/event-stream") || contentType.includes("event-stream")) {
return readSseChartInterpretationStream(res.body, onInterpretationDelta);
}
const text = await res.text();
if (/^\s*data:/m.test(text)) {
const out = parseSseChartInterpretationLines(text);
if (out) {
onInterpretationDelta?.(out);
}
return out;
}
let parsed;
try {
parsed = JSON.parse(text);
} catch {
const extracted = extractChartInterpretationText(text);
return extracted || text.trim();
}
const extracted = extractChartInterpretationText(parsed);
if (extracted) {
return extracted;
}
if (typeof parsed === "string") {
return extractChartInterpretationText(parsed) || parsed;
}
return extractChartInterpretationText(text) || text.trim();
}
// 图表解读(流式)
/**
* @param {text}
* 图表解读(POST /chart_interpretation),兼容流式 SSE 与一次性 JSON
* @param {{ text: string }} data
* @param {{ onInterpretationDelta?: (s: string) => void }} [options]
* @returns {Promise<string>} 解读纯文本
*/
export
function
getChartAnalysis
(
data
)
{
return
request
({
method
:
'POST'
,
url
:
`/aiAnalysis/chart_interpretation`
,
data
,
})
}
\ No newline at end of file
export
async
function
getChartAnalysis
(
data
,
options
=
{})
{
const
{
onInterpretationDelta
}
=
options
;
return
fetchChartInterpretation
(
data
,
onInterpretationDelta
);
}
src/api/request.js
浏览文件 @
6714a4e9
...
...
@@ -51,6 +51,14 @@ service.interceptors.request.use(config => {
config
.
headers
[
'token'
]
=
token
// config.headers['Authorization'] = `Bearer ${token}` // 如果后端需要Bearer格式可以使用这个
}
// 图表解读等 AI 分析服务(Vite 代理 /aiAnalysis)需要 X-API-Key
const
reqUrl
=
String
(
config
.
url
||
''
)
if
(
reqUrl
.
includes
(
'aiAnalysis'
))
{
const
aiApiKey
=
import
.
meta
.
env
.
VITE_AI_ANALYSIS_API_KEY
if
(
aiApiKey
)
{
config
.
headers
[
'X-API-Key'
]
=
aiApiKey
}
}
return
config
},
error
=>
{
console
.
log
(
error
)
...
...
@@ -82,8 +90,14 @@ service.interceptors.response.use(
// 重复请求触发的取消不提示错误
if
(
isCanceledError
)
return
Promise
.
reject
(
error
)
// 处理token过期或无效的情况
if
(
error
.
response
&&
(
error
.
response
.
status
===
401
||
error
.
response
.
status
===
403
))
{
// 处理token过期或无效的情况(排除 AI 分析服务:其 401 多为 API Key 问题)
const
errUrl
=
String
(
error
.
config
?.
url
||
''
)
const
isAiAnalysisRequest
=
errUrl
.
includes
(
'aiAnalysis'
)
if
(
error
.
response
&&
(
error
.
response
.
status
===
401
||
error
.
response
.
status
===
403
)
&&
!
isAiAnalysisRequest
)
{
ElMessage
({
message
:
'Token已过期,请重新登录'
,
type
:
'error'
,
...
...
src/api/thinkTank/overview.js
浏览文件 @
6714a4e9
...
...
@@ -35,13 +35,19 @@ export function getThinkTankRiskSignal() {
})
}
// 政策建议趋势分布
/**
* 政策建议趋势分布(数量变化趋势)
* @param {{ startDate: string, endDate: string }} params - 如 2024-01-01 ~ 2024-12-31
*/
export
function
getThinkTankPolicyIndustryChange
(
params
)
{
return
request
({
method
:
'GET'
,
url
:
`/api/thinkTankOverview/policyIndustryChange/
${
params
}
`
,
})
method
:
"GET"
,
url
:
`/api/thinkTankOverview/policyIndustryChange`
,
params
:
{
startDate
:
params
.
startDate
,
endDate
:
params
.
endDate
}
});
}
// 政策建议领域分布
...
...
@@ -193,15 +199,31 @@ export function getThinkPolicyIndustryChange(params) {
})
}
//获取智库政策
/**
* 获取智库政策(政策追踪列表)
* GET /api/thinkTankInfo/policy
* Query: thinkTankId, startDate, endDate, orgIds, domainIds(科技领域/智库领域,逗号分隔 id), pageNum, pageSize, sortField, sortOrder, sortFun, reportId 等
*/
export
function
getThinkPolicy
(
params
)
{
return
request
({
method
:
'GET'
,
url
:
`/api/thinkTankInfo/policy/
${
params
.
id
}
/
${
params
.
startDate
}
`
,
url
:
'/api/thinkTankInfo/policy'
,
params
})
}
/**
* 政府机构字典(政策追踪-涉及部门筛选项)
* GET /api/commonDict/gov/agency
* @returns {Promise<{ code: number, data: Array<{ id: string, name: string }> }>}
*/
export
function
getGovAgencyDict
()
{
return
request
({
method
:
'GET'
,
url
:
'/api/commonDict/gov/agency'
})
}
//智库百科基本信息
export
function
getThinkTankInfoBasic
(
params
)
{
return
request
({
...
...
src/components/base/TipTab/index.vue
浏览文件 @
6714a4e9
...
...
@@ -24,17 +24,19 @@ const props = defineProps({
<
/script
>
<
style
lang
=
"scss"
scoped
>
.
tip
-
wrapper
{
.
tip
-
wrapper
{
width
:
100
%
;
display
:
flex
;
gap
:
8
px
;
justify
-
content
:
center
;
align
-
items
:
center
;
height
:
22
px
;
.
icon
{
.
icon
{
width
:
16
px
;
height
:
16
px
;
img
{
img
{
width
:
100
%
;
height
:
100
%
;
}
...
...
src/utils/setChart.js
浏览文件 @
6714a4e9
...
...
@@ -9,6 +9,10 @@ const setChart = (option, chartId) => {
chartDom
.
removeAttribute
(
"_echarts_instance_"
);
let
chart
=
echarts
.
init
(
chartDom
);
chart
.
setOption
(
option
);
// 容器可能受布局/异步渲染影响,强制一次 resize 保证 canvas 与容器一致
setTimeout
(()
=>
{
chart
.
resize
();
},
0
);
return
chart
;
};
...
...
src/views/thinkTank/ThinkTankDetail/PolicyTracking/index.vue
浏览文件 @
6714a4e9
...
...
@@ -6,21 +6,35 @@
<div
class=
"icon"
>
<img
src=
"./images/box-header-icon1.png"
alt=
""
/>
</div>
<div
class=
"title"
>
{{
"
政策建议领域分布
"
}}
</div>
<div
class=
"title"
>
{{
"
领域分布情况
"
}}
</div>
<!--
<div
class=
"box-header-right"
>
{{
"查看数据源 >"
}}
</div>
-->
<div
class=
"select-box"
>
<el-select
v-model=
"box1SelectYear"
placeholder=
"选择时间"
style=
"width: 100px"
>
<el-option
v-for=
"(item, index) in box1YearList"
:key=
"index"
:label=
"item.label + '年'"
:value=
"item.value"
@
click=
"handleGetThinkPolicyIndustry()
"
/>
<el-select
v-model=
"box1SelectYear"
placeholder=
"选择时间"
style=
"width: 100px"
@
change=
"handleGetThinkPolicyIndustry"
>
<el-option
v-for=
"item in box1YearList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value
"
/>
</el-select>
</div>
</div>
<div
class=
"box-main"
>
<div
id=
"box1Chart"
></div>
<div
class=
"source"
>
<div
class=
"info"
><img
src=
"./images/image-exclamation.png"
></div>
<div
class=
"text"
>
数据来源:美国国会官网,数据时间:2015.1至2025.12
</div>
</div>
<div
class=
"box-main"
:class=
"
{ 'chart-main--empty': !hasBox1ChartData }">
<template
v-if=
"!hasBox1ChartData"
>
<el-empty
class=
"policy-tracking-el-empty"
description=
"暂无数据"
:image-size=
"100"
/>
</
template
>
<
template
v-else
>
<div
id=
"box1Chart"
></div>
<div
class=
"source"
>
<TipTab
:text=
"POLICY_TRACKING_TIP_BOX1"
/>
</div>
<div
class=
"chart-box"
>
<div
class=
"btn-box"
v-if=
"!isShowAiContentPolicyPt1"
@
mouseenter=
"handleSwitchAiContentShowPolicyPt1(true)"
>
<AiButton
/>
</div>
<div
class=
"content-box"
v-if=
"isShowAiContentPolicyPt1"
@
mouseleave=
"handleSwitchAiContentShowPolicyPt1(false)"
>
<AiPane
:aiContent=
"aiContentPolicyPt1"
/>
</div>
</div>
</
template
>
</div>
</div>
<div
class=
"box1 box"
>
...
...
@@ -28,21 +42,35 @@
<div
class=
"icon"
>
<img
src=
"./images/box-header-icon2.png"
alt=
""
/>
</div>
<div
class=
"title"
>
{{
"
政策建议涉及部门分布
"
}}
</div>
<div
class=
"title"
>
{{ "
部门分布情况
" }}
</div>
<!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> -->
<div
class=
"select-box"
>
<el-select
v-model=
"box2SelectYear"
placeholder=
"选择时间"
style=
"width: 100px"
>
<el-option
v-for=
"(item, index) in box2YearList"
:key=
"index"
:label=
"item.label + '年'"
:value=
"item.value"
@
click=
"handleGetPolicyAdviceDeptDistribution()
"
/>
<el-select
v-model=
"box2SelectYear"
placeholder=
"选择时间"
style=
"width: 100px"
@
change=
"handleGetPolicyAdviceDeptDistribution"
>
<el-option
v-for=
"item in box2YearList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value
"
/>
</el-select>
</div>
</div>
<div
class=
"box-main"
>
<div
id=
"box2Chart"
></div>
<div
class=
"source"
>
<div
class=
"info"
><img
src=
"./images/image-exclamation.png"
></div>
<div
class=
"text"
>
数据来源:美国国会官网,数据时间:2015.1至2025.12
</div>
</div>
<div
class=
"box-main"
:class=
"{ 'chart-main--empty': !hasBox2ChartData }"
>
<
template
v-if=
"!hasBox2ChartData"
>
<el-empty
class=
"policy-tracking-el-empty"
description=
"暂无数据"
:image-size=
"100"
/>
</
template
>
<
template
v-else
>
<div
id=
"box2Chart"
></div>
<div
class=
"source"
>
<TipTab
:text=
"POLICY_TRACKING_TIP_BOX2"
/>
</div>
<div
class=
"chart-box"
>
<div
class=
"btn-box"
v-if=
"!isShowAiContentPolicyPt2"
@
mouseenter=
"handleSwitchAiContentShowPolicyPt2(true)"
>
<AiButton
/>
</div>
<div
class=
"content-box"
v-if=
"isShowAiContentPolicyPt2"
@
mouseleave=
"handleSwitchAiContentShowPolicyPt2(false)"
>
<AiPane
:aiContent=
"aiContentPolicyPt2"
/>
</div>
</div>
</
template
>
</div>
</div>
<!-- <div class="box2 box">
...
...
@@ -61,7 +89,7 @@
</el-select>
</div>
<div id="box2Chart">
<div
class=
"box2-item"
v-for=
"(item, index) in box2Data"
:key=
"index"
>
<div class="box2-item" v-for="(item, index) in box2
Total
Data" :key="index">
<div class="icon" :style="{ background: item.color }"></div>
<div class="name">{{ item.industry }}</div>
<el-progress :show-text="false" :color="box2ColorList[index]" style="width: 170px" stroke-width="8"
...
...
@@ -77,38 +105,60 @@
<div
class=
"icon"
>
<img
src=
"./images/box-header-icon2.png"
alt=
""
/>
</div>
<div
class=
"title"
>
{{
"
热门研究方向
变化趋势"
}}
</div>
<div
class=
"title"
>
{{ "
研究领域
变化趋势" }}
</div>
<!-- <div class="box-header-right">{{ "查看数据源 >" }}</div> -->
<div
class=
"select-box"
>
<el-select
v-model=
"box3SelectMonths"
placeholder=
"选择时间"
style=
"width: 100px"
>
<el-option
v-for=
"item in box3MonthsList"
:key=
"item.value"
:label=
"item.label + '年'"
:value=
"item.value"
@
click=
"handleGetThinkPolicyIndustryChange()
"
/>
<el-select
v-model=
"box3SelectMonths"
placeholder=
"选择时间"
style=
"width: 100px"
@
change=
"handleGetThinkPolicyIndustryChange"
>
<el-option
v-for=
"item in box3MonthsList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value
"
/>
</el-select>
</div>
</div>
<div
class=
"box3-main"
>
<div
id=
"box3Chart"
></div>
<div
class=
"source"
>
<div
class=
"info"
><img
src=
"./images/image-exclamation.png"
></div>
<div
class=
"text"
>
数据来源:美国国会官网,数据时间:2015.1至2025.12
</div>
</div>
<div
class=
"box3-main"
:class=
"{ 'chart-main--empty': !hasBox3ChartData }"
>
<
template
v-if=
"!hasBox3ChartData"
>
<el-empty
class=
"policy-tracking-el-empty"
description=
"暂无数据"
:image-size=
"100"
/>
</
template
>
<
template
v-else
>
<div
class=
"box3-chart-row"
>
<button
v-if=
"box3LegendShowNav"
type=
"button"
class=
"box3-legend-nav-btn"
:disabled=
"!box3LegendCanPrev"
aria-label=
"上一页图例"
@
click=
"handleBox3LegendPrev"
>
‹
</button>
<div
id=
"box3Chart"
class=
"box3-chart-canvas"
></div>
<button
v-if=
"box3LegendShowNav"
type=
"button"
class=
"box3-legend-nav-btn"
:disabled=
"!box3LegendCanNext"
aria-label=
"下一页图例"
@
click=
"handleBox3LegendNext"
>
›
</button>
</div>
<div
class=
"source"
>
<TipTab
:text=
"POLICY_TRACKING_TIP_BOX3"
/>
</div>
<div
class=
"chart-box"
>
<div
class=
"btn-box"
v-if=
"!isShowAiContentPolicyPt3"
@
mouseenter=
"handleSwitchAiContentShowPolicyPt3(true)"
>
<AiButton
/>
</div>
<div
class=
"content-box"
v-if=
"isShowAiContentPolicyPt3"
@
mouseleave=
"handleSwitchAiContentShowPolicyPt3(false)"
>
<AiPane
:aiContent=
"aiContentPolicyPt3"
/>
</div>
</div>
</
template
>
</div>
</div>
</div>
<div
class=
"bottom"
>
<div
class=
"bottom-header"
>
<div
class=
"search-box"
>
<div
class=
"search-box"
@
keyup
.
enter=
"handleGetThinkPolicy()"
>
<el-input
placeholder=
"搜索政策建议"
v-model=
"searchPolicy"
style=
"width: 270px"
/>
<div
class=
"icon"
>
<img
src=
"./images/search-icon.png"
alt=
""
/>
<img
src=
"./images/search-icon.png"
alt=
""
@
click=
"handleGetThinkPolicy()"
/>
</div>
</div>
<div
class=
"select-box"
>
<el-select
v-model=
"selectedYear"
placeholder=
"选择时间"
style=
"width: 120px"
@
click=
"handleGetThinkPolicy()"
>
<el-option
v-for=
"item in yearList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
<el-select
class=
"select-box-sort"
v-model=
"sort"
placeholder=
"倒序"
style=
"width: 120px"
:teleported=
"true"
<el-select
class=
"select-box-sort"
v-model=
"sort"
placeholder=
"发布时间"
style=
"width: 120px"
:teleported=
"true"
:placement=
"'bottom-start'"
:popper-options=
"{
modifiers: [
{
...
...
@@ -122,7 +172,7 @@
]
}"
>
<
template
#
prefix
>
<img
v-if=
"
!sort
"
src=
"../thinkDynamics/images/image down.png"
class=
"select-prefix-img"
alt=
""
/>
<img
v-if=
"
sort !== true
"
src=
"../thinkDynamics/images/image down.png"
class=
"select-prefix-img"
alt=
""
/>
<img
v-else
src=
"../thinkDynamics/images/image up.png"
class=
"select-prefix-img"
alt=
""
/>
</
template
>
<el-option
@
click=
"handleGetThinkPolicy()"
:key=
"true"
label=
"正序"
:value=
"true"
/>
...
...
@@ -138,17 +188,44 @@
<div
class=
"title"
>
{{ "科技领域" }}
</div>
</div>
<div
class=
"select-main"
>
<div
class=
"checkbox-group"
>
<!-- <el-checkbox v-for="(item, index) in areaList" :key="index" v-model="selectedAreaList" :label="item"
class="filter-checkbox">
{{ item }}
</el-checkbox> -->
<el-checkbox
class=
"filter-checkbox"
label=
"全部领域"
></el-checkbox>
<el-checkbox
class=
"filter-checkbox"
v-for=
"research in areaList"
:key=
"research.id"
v-model=
"selectedAreaList"
:label=
"research.id"
>
<el-checkbox-group
class=
"checkbox-group"
v-model=
"selectedAreaList"
@
change=
"handleChangePolicyAreaList"
>
<el-checkbox
class=
"filter-checkbox"
:label=
"POLICY_FILTER_ALL_AREA"
>
{{ POLICY_FILTER_ALL_AREA }}
</el-checkbox>
<el-checkbox
class=
"filter-checkbox"
v-for=
"research in areaList"
:key=
"research.id"
:label=
"research.id"
>
{{ research.name }}
</el-checkbox>
</div>
</el-checkbox-group>
</div>
</div>
<div
class=
"select-box-department"
>
<div
class=
"select-box-header"
>
<div
class=
"icon"
></div>
<div
class=
"title"
>
{{ "涉及部门" }}
</div>
</div>
<div
class=
"select-main"
>
<el-checkbox-group
class=
"checkbox-group"
v-model=
"selectedDepartmentList"
@
change=
"handleChangePolicyDepartmentList"
>
<el-checkbox
class=
"filter-checkbox"
:label=
"POLICY_FILTER_ALL_DEPT"
>
{{ POLICY_FILTER_ALL_DEPT }}
</el-checkbox>
<el-checkbox
class=
"filter-checkbox"
v-for=
"department in selectableDepartment"
:key=
"department.id"
:label=
"department.id"
>
{{ department.name }}
</el-checkbox>
</el-checkbox-group>
</div>
</div>
<div
class=
"select-box-publish"
>
...
...
@@ -157,49 +234,58 @@
<div
class=
"title"
>
{{ "发布时间" }}
</div>
</div>
<div
class=
"select-main"
>
<div
class=
"checkbox-group"
>
<!-- <el-checkbox v-for="(item, index) in areaList" :key="index" v-model="selectedAreaList" :label="item"
class="filter-checkbox">
{{ item }}
</el-checkbox> -->
<el-checkbox
class=
"filter-checkbox"
label=
"全部领域"
></el-checkbox>
<el-checkbox
class=
"filter-checkbox"
v-for=
"year in selectableYears"
:key=
"year"
v-model=
"selectedAreaList"
:label=
"year"
>
<el-checkbox-group
class=
"checkbox-group"
v-model=
"selectedYearList"
@
change=
"handleChangePolicyYearList"
>
<el-checkbox
class=
"filter-checkbox"
:label=
"POLICY_FILTER_ALL_TIME"
>
{{ POLICY_FILTER_ALL_TIME }}
</el-checkbox>
<el-checkbox
class=
"filter-checkbox"
v-for=
"year in selectableYears"
:key=
"year"
:label=
"year"
>
{{ year }}
</el-checkbox>
</
div
>
</
el-checkbox-group
>
</div>
</div>
</div>
<div
class=
"right"
>
<div
class=
"right-main"
>
<div
class=
"right-main-item"
v-for=
"
(item, index) in policyList"
:key=
"index
"
>
<div
class=
"right-main-item"
v-for=
"
item in policyList"
:key=
"item.id
"
>
<div
class=
"item-left"
>
<img
:src=
"item.imageUrl"
alt=
""
/>
</div>
<div
class=
"item-right"
>
<div>
<div
class=
"title"
>
{{ item.
content
}}
</div>
<div
class=
"title"
>
{{ item.
titleZh
}}
</div>
<div
class=
"info"
>
{{ item.times }} · {{ item.name }}
<div
class=
"text"
>
{{ item.reportDateDisplay }}·{{ item.contentZh }}
</div>
<div
class=
"more"
@
click=
"toDetail(item)"
>
<img
src=
"@/views/thinkTank/ThinkTankDetail/thinkDynamics/images/image open.png"
alt=
""
/>
</div>
</div>
<div
class=
"tag-box"
>
<AreaTag
v-for=
"(tag, idx) in item.tagList"
:key=
"idx"
:tagName=
"tag"
></AreaTag>
</div>
<div
class=
"file-box"
>
<div
class=
"file"
v-for=
"(file, idxx) in item.relat
ionBillsList"
:key=
"idxx
"
>
<div
class=
"file"
v-for=
"(file, idxx) in item.relat
edBills"
:key=
"`${file.id}-${idxx}`
"
>
<div
class=
"type"
>
法案
</div>
<div
class=
"title"
>
{{ file.
billN
ame }}
</div>
<div
class=
"title"
>
{{ file.
n
ame }}
</div>
<div
class=
"more"
>
<img
src=
"./images/arrow-right.png"
alt=
""
/>
</div>
</div>
<div
class=
"file"
v-for=
"(file, idxx) in item.relationAdList"
:key=
"idxx"
>
<div
class=
"file"
v-for=
"(file, idxx) in item.relatedAdministrativeOrders"
:key=
"file.bcId != null ? String(file.bcId) + '-' + idxx : idxx"
>
<div
class=
"type"
>
政令
</div>
<div
class=
"title"
>
{{ file.
adName
}}
</div>
<div
class=
"title"
>
{{ file.
content
}}
</div>
<div
class=
"more"
>
<img
src=
"./images/arrow-right.png"
alt=
""
/>
</div>
...
...
@@ -213,7 +299,7 @@
<div
class=
"right-footer"
>
<div
class=
"info"
>
共{{ total }}条政策建议
</div>
<div
class=
"page-box"
>
<el-pagination
:page-size=
"1
2
"
background
layout=
"prev, pager, next"
:total=
"total"
<el-pagination
:page-size=
"1
0
"
background
layout=
"prev, pager, next"
:total=
"total"
@
current-change=
"handleCurrentChange"
:current-page=
"currentPage"
/>
</div>
</div>
...
...
@@ -224,22 +310,71 @@
</template>
<
script
setup
>
import
{
ref
,
watch
,
onMounted
}
from
"vue"
;
import
{
ref
,
watch
,
onMounted
,
computed
,
nextTick
}
from
"vue"
;
import
setChart
from
"@/utils/setChart"
;
import
getPieChart
from
"./utils/piechart"
;
import
getMultiLineChart
from
"./utils/multiLineChart"
;
import
getMultiLineChart
,
{
POLICY_TRACKING_LEGEND_PAGE_SIZE
}
from
"./utils/multiLineChart"
;
import
{
getThinkPolicyIndustry
,
getThinkPolicyIndustryTotal
,
getThinkPolicyIndustryChange
,
getHylyList
,
getThinkPolicy
,
getPolicyAdviceDeptDistribution
getPolicyAdviceDeptDistribution
,
getGovAgencyDict
}
from
"@/api/thinkTank/overview"
;
import
{
getChartAnalysis
}
from
"@/api/aiAnalysis/index"
;
import
AiButton
from
"@/components/base/Ai/AiButton/index.vue"
;
import
AiPane
from
"@/components/base/Ai/AiPane/index.vue"
;
import
TipTab
from
"@/views/thinkTank/TipTab/index.vue"
;
import
defaultNewsIcon
from
"@/assets/icons/default-icon-news.png"
;
import
{
useRouter
}
from
"vue-router"
;
const
router
=
useRouter
();
/** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */
const
POLICY_TRACKING_TIP_BOX1
=
"智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网"
;
const
POLICY_TRACKING_TIP_BOX2
=
"智库报告中政策建议涉及领域分布情况,数据来源:美国兰德公司官网"
;
const
POLICY_TRACKING_TIP_BOX3
=
"智库报告热门研究领域变化趋势,数据来源:美国兰德公司官网"
;
/** 筛选「全部」项文案,与市场准入概览-资源库复选逻辑一致 */
const
POLICY_FILTER_ALL_AREA
=
"全部领域"
;
const
POLICY_FILTER_ALL_TIME
=
"全部时间"
;
const
POLICY_FILTER_ALL_DEPT
=
"全部部门"
;
const
isShowAiContentPolicyPt1
=
ref
(
false
);
const
aiContentPolicyPt1
=
ref
(
""
);
const
isPolicyPt1InterpretLoading
=
ref
(
false
);
const
handleSwitchAiContentShowPolicyPt1
=
(
val
)
=>
{
isShowAiContentPolicyPt1
.
value
=
val
;
if
(
val
)
{
fetchPolicyPtBox1ChartInterpretation
();
}
};
const
isShowAiContentPolicyPt2
=
ref
(
false
);
const
aiContentPolicyPt2
=
ref
(
""
);
const
isPolicyPt2InterpretLoading
=
ref
(
false
);
const
handleSwitchAiContentShowPolicyPt2
=
(
val
)
=>
{
isShowAiContentPolicyPt2
.
value
=
val
;
if
(
val
)
{
fetchPolicyPtBox2ChartInterpretation
();
}
};
const
isShowAiContentPolicyPt3
=
ref
(
false
);
const
aiContentPolicyPt3
=
ref
(
""
);
const
isPolicyPt3InterpretLoading
=
ref
(
false
);
const
handleSwitchAiContentShowPolicyPt3
=
(
val
)
=>
{
isShowAiContentPolicyPt3
.
value
=
val
;
if
(
val
)
{
fetchPolicyPtBox3ChartInterpretation
();
}
};
// import Img1 from "./images/img1.png";
// import Img2 from "./images/img2.png";
// import Img3 from "./images/img3.png";
...
...
@@ -284,11 +419,18 @@ const box1Data = ref([
// }
]);
const
relationBillsList
=
ref
([{
billName
:
"2025《人工智能安全与评估法案》"
}])
const
relationAdList
=
ref
([{
adName
:
"2025《人工智能安全与评估法案》"
}])
/** 与智库首页数量变化趋势一致:有数据才展示图表与数据来源 */
const
hasBox1ChartData
=
computed
(
()
=>
Array
.
isArray
(
box1Data
.
value
)
&&
box1Data
.
value
.
length
>
0
);
const
box1SelectYear
=
ref
(
"2025"
);
const
box1YearList
=
ref
([
{
label
:
"2026"
,
value
:
"2026"
},
{
label
:
"2025"
,
value
:
"2025"
...
...
@@ -300,10 +442,16 @@ const box1YearList = ref([
{
label
:
"2023"
,
value
:
"2023"
}
},
{
label
:
"2022"
,
value
:
"2022"
},
]);
const
selectableYears
=
ref
([
"2025年"
,
"2024年"
,
"2023年"
,
"2022年"
,
"2021年"
,
"更早"
]);
const
selectableDepartment
=
ref
([]);
const
handleGetThinkPolicyIndustry
=
async
()
=>
{
aiContentPolicyPt1
.
value
=
""
;
try
{
const
parmas
=
{
id
:
router
.
currentRoute
.
_value
.
params
.
id
,
...
...
@@ -313,21 +461,41 @@ const handleGetThinkPolicyIndustry = async () => {
console
.
log
(
"提出建议领域分布"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
const
list
=
Array
.
isArray
(
res
.
data
)
?
res
.
data
.
slice
(
0
,
7
)
:
[];
if
(
!
list
.
length
)
{
box1Data
.
value
=
[];
return
;
}
const
data
=
list
.
map
(
item
=>
({
name
:
item
.
industry
,
value
:
item
.
amount
,
percent
:
item
.
percent
}));
box1Data
.
value
=
data
;
/* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */
await
nextTick
();
const
box1Chart
=
getPieChart
(
box1Data
.
value
);
setChart
(
box1Chart
,
"box1Chart"
);
}
else
{
box1Data
.
value
=
[];
}
}
catch
(
error
)
{
console
.
error
(
"获取提出建议领域分布error"
,
error
);
box1Data
.
value
=
[];
}
};
// 政策建议涉及部门分布(饼图)
const
box2Data
=
ref
([]);
const
hasBox2ChartData
=
computed
(
()
=>
Array
.
isArray
(
box2Data
.
value
)
&&
box2Data
.
value
.
length
>
0
);
/** 仅「相关政策领域分布」注释区块用;勿写入 box2Data,否则与部门分布接口并行会覆盖饼图数据导致空白 */
const
box2TotalData
=
ref
([]);
const
handleGetPolicyAdviceDeptDistribution
=
async
()
=>
{
aiContentPolicyPt2
.
value
=
""
;
try
{
const
parmas
=
{
id
:
router
.
currentRoute
.
_value
.
params
.
id
,
...
...
@@ -335,7 +503,12 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
};
const
res
=
await
getPolicyAdviceDeptDistribution
(
parmas
);
console
.
log
(
"政策建议涉及部门分布"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
&&
Array
.
isArray
(
res
.
data
.
series
))
{
if
(
res
.
code
===
200
&&
res
.
data
&&
Array
.
isArray
(
res
.
data
.
series
)
&&
res
.
data
.
series
.
length
>
0
)
{
// 接口新结构:data.series 为 [{ name, value, percent, extra }]
const
list
=
res
.
data
.
series
.
slice
(
0
,
7
);
box2Data
.
value
=
list
.
map
(
item
=>
({
...
...
@@ -344,19 +517,26 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
percent
:
item
.
percent
}));
await
nextTick
();
const
box2Chart
=
getPieChart
(
box2Data
.
value
);
setChart
(
box2Chart
,
"box2Chart"
);
}
else
{
box2Data
.
value
=
[];
}
}
catch
(
error
)
{
console
.
error
(
"获取政策建议涉及部门分布 error"
,
error
);
box2Data
.
value
=
[];
}
};
// 相关政策领域分布
const
box2Data
=
ref
([]);
// 相关政策领域分布(旧模块用 box2Data,勿与部门分布混用)
const
box2ColorList
=
ref
([
"#CE4F51"
,
"#055FC2"
,
"#13A8A8"
,
"#722ED1"
,
"#FA8C16"
,
"#69B1FF"
]);
const
box2SelectYear
=
ref
(
"2025"
);
const
box2YearList
=
ref
([
{
label
:
"2026"
,
value
:
"2026"
},
{
label
:
"2025"
,
value
:
"2025"
...
...
@@ -368,7 +548,11 @@ const box2YearList = ref([
{
label
:
"2023"
,
value
:
"2023"
}
},
{
label
:
"2022"
,
value
:
"2022"
},
]);
const
handleGetThinkPolicyIndustryTotal
=
async
()
=>
{
...
...
@@ -380,30 +564,107 @@ const handleGetThinkPolicyIndustryTotal = async () => {
const
res
=
await
getThinkPolicyIndustryTotal
(
parmas
);
console
.
log
(
"相关政策领域分布"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
box2Data
.
value
=
res
.
data
;
box2TotalData
.
value
=
Array
.
isArray
(
res
.
data
)
?
res
.
data
:
[];
}
else
{
box2TotalData
.
value
=
[];
}
}
catch
(
error
)
{
console
.
error
(
"获取相关政策领域分布error"
,
error
);
box2TotalData
.
value
=
[];
}
};
// 热门研究方向变化趋势
const
box3Data
=
ref
({
// title: ["2024-Q1", "2024-Q2", "2024-Q3", "2024-Q4"],
// data: [
// {
// name: "新能源",
// value: [1,3,4,6]
// },
// {
// name: "人工智能",
// value: [1,3,4,2]
// },
// ...
// ]
title
:
[],
data
:
[]
});
const
hasBox3ChartData
=
computed
(()
=>
{
const
v
=
box3Data
.
value
;
if
(
!
v
||
typeof
v
!==
"object"
)
{
return
false
;
}
const
title
=
v
.
title
;
const
data
=
v
.
data
;
return
(
Array
.
isArray
(
title
)
&&
title
.
length
>
0
&&
Array
.
isArray
(
data
)
&&
data
.
length
>
0
);
});
const
box3LegendPageIndex
=
ref
(
0
);
const
box3LegendPageCount
=
computed
(()
=>
{
const
n
=
box3Data
.
value
?.
data
?.
length
??
0
;
return
Math
.
max
(
1
,
Math
.
ceil
(
n
/
POLICY_TRACKING_LEGEND_PAGE_SIZE
));
});
const
box3LegendShowNav
=
computed
(()
=>
{
const
n
=
box3Data
.
value
?.
data
?.
length
??
0
;
return
n
>
POLICY_TRACKING_LEGEND_PAGE_SIZE
;
});
const
box3LegendCanPrev
=
computed
(()
=>
box3LegendPageIndex
.
value
>
0
);
const
box3LegendCanNext
=
computed
(
()
=>
box3LegendPageIndex
.
value
<
box3LegendPageCount
.
value
-
1
);
const
handleBox3LegendPrev
=
()
=>
{
if
(
box3LegendPageIndex
.
value
<=
0
)
{
return
;
}
box3LegendPageIndex
.
value
-=
1
;
renderBox3Chart
();
};
const
handleBox3LegendNext
=
()
=>
{
if
(
box3LegendPageIndex
.
value
>=
box3LegendPageCount
.
value
-
1
)
{
return
;
}
box3LegendPageIndex
.
value
+=
1
;
renderBox3Chart
();
};
/** 研究领域变化趋势:图例分页与概览「数量变化趋势」一致 */
const
renderBox3Chart
=
async
()
=>
{
const
v
=
box3Data
.
value
;
if
(
!
v
||
!
Array
.
isArray
(
v
.
title
)
||
v
.
title
.
length
===
0
||
!
Array
.
isArray
(
v
.
data
)
||
v
.
data
.
length
===
0
)
{
return
;
}
const
totalNames
=
v
.
data
.
length
;
const
pages
=
Math
.
max
(
1
,
Math
.
ceil
(
totalNames
/
POLICY_TRACKING_LEGEND_PAGE_SIZE
));
if
(
box3LegendPageIndex
.
value
>=
pages
)
{
box3LegendPageIndex
.
value
=
pages
-
1
;
}
if
(
box3LegendPageIndex
.
value
<
0
)
{
box3LegendPageIndex
.
value
=
0
;
}
await
nextTick
();
const
box3Chart
=
getMultiLineChart
(
{
title
:
v
.
title
,
data
:
v
.
data
},
{
legendShowCount
:
POLICY_TRACKING_LEGEND_PAGE_SIZE
,
legendPageIndex
:
box3LegendPageIndex
.
value
}
);
setChart
(
box3Chart
,
"box3Chart"
);
};
const
box3SelectMonths
=
ref
(
"2025"
);
const
box3MonthsList
=
ref
([
{
label
:
"2026"
,
value
:
"2026"
},
{
label
:
"2025"
,
value
:
"2025"
...
...
@@ -415,10 +676,15 @@ const box3MonthsList = ref([
{
label
:
"2023"
,
value
:
"2023"
}
},
{
label
:
"2022"
,
value
:
"2022"
},
]);
const
handleGetThinkPolicyIndustryChange
=
async
()
=>
{
aiContentPolicyPt3
.
value
=
""
;
try
{
const
parmas
=
{
id
:
router
.
currentRoute
.
_value
.
params
.
id
,
...
...
@@ -426,7 +692,7 @@ const handleGetThinkPolicyIndustryChange = async () => {
};
const
res
=
await
getThinkPolicyIndustryChange
(
parmas
);
console
.
log
(
"热门研究方向变化趋势"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
if
(
res
.
code
===
200
&&
Array
.
isArray
(
res
.
data
)
&&
res
.
data
.
length
>
0
)
{
// 提取所有季度信息
const
quarters
=
res
.
data
.
map
(
item
=>
item
.
yearQuarter
);
quarters
.
sort
();
// 按时间排序
...
...
@@ -434,9 +700,11 @@ const handleGetThinkPolicyIndustryChange = async () => {
// 提取所有行业名称
const
industries
=
new
Set
();
res
.
data
.
forEach
(
item
=>
{
item
.
industryList
.
forEach
(
industry
=>
{
industries
.
add
(
industry
.
industry
);
});
if
(
Array
.
isArray
(
item
.
industryList
))
{
item
.
industryList
.
forEach
(
industry
=>
{
industries
.
add
(
industry
.
industry
);
});
}
});
// 构建前端数据结构
...
...
@@ -449,24 +717,175 @@ const handleGetThinkPolicyIndustryChange = async () => {
name
:
industry
,
value
:
quarters
.
map
(
quarter
=>
{
const
quarterData
=
res
.
data
.
find
(
q
=>
q
.
yearQuarter
===
quarter
);
const
industryAmount
=
quarterData
?.
industryList
.
find
(
i
=>
i
.
industry
===
industry
)?.
amount
||
0
;
const
industryAmount
=
quarterData
?.
industryList
?.
find
(
i
=>
i
.
industry
===
industry
)?.
amount
||
0
;
return
industryAmount
;
})
};
frontendData
.
data
.
push
(
industryData
);
});
if
(
!
frontendData
.
data
.
length
)
{
box3Data
.
value
=
{
title
:
[],
data
:
[]
};
return
;
}
box3Data
.
value
=
frontendData
;
// 传入全部行业数据,支持 13 条线同时展示
const
box3Chart
=
getMultiLineChart
(
box3Data
.
value
.
title
,
box3Data
.
value
.
data
);
setChart
(
box3Chart
,
"box3Chart"
);
box3LegendPageIndex
.
value
=
0
;
await
renderBox3Chart
();
}
else
{
box3Data
.
value
=
{
title
:
[],
data
:
[]
};
}
}
catch
(
error
)
{
console
.
error
(
"获取热门研究方向变化趋势error"
,
error
);
box3Data
.
value
=
{
title
:
[],
data
:
[]
};
}
};
/** 政策追踪-领域分布饼图解读(与概览页 box6 入参一致) */
const
fetchPolicyPtBox1ChartInterpretation
=
async
()
=>
{
const
list
=
Array
.
isArray
(
box1Data
.
value
)
?
box1Data
.
value
:
[];
if
(
!
list
.
length
)
{
aiContentPolicyPt1
.
value
=
"暂无图表数据"
;
return
;
}
const
hasValidContent
=
aiContentPolicyPt1
.
value
&&
aiContentPolicyPt1
.
value
!==
"解读生成中…"
&&
aiContentPolicyPt1
.
value
!==
"解读加载失败"
&&
aiContentPolicyPt1
.
value
!==
"暂无图表数据"
;
if
(
hasValidContent
||
isPolicyPt1InterpretLoading
.
value
)
{
return
;
}
isPolicyPt1InterpretLoading
.
value
=
true
;
aiContentPolicyPt1
.
value
=
"解读生成中…"
;
const
chartPayload
=
{
type
:
"饼图"
,
name
:
"领域分布情况"
,
year
:
box1SelectYear
.
value
,
data
:
list
.
map
((
item
)
=>
({
name
:
item
.
name
,
value
:
item
.
value
}))
};
try
{
const
text
=
await
getChartAnalysis
(
{
text
:
JSON
.
stringify
(
chartPayload
)
},
{
onInterpretationDelta
:
(
chunk
)
=>
{
if
(
chunk
)
{
aiContentPolicyPt1
.
value
=
chunk
;
}
}
}
);
aiContentPolicyPt1
.
value
=
text
||
"未返回有效解读内容"
;
}
catch
(
error
)
{
console
.
error
(
"政策追踪领域分布图表解读请求失败"
,
error
);
aiContentPolicyPt1
.
value
=
"解读加载失败"
;
}
finally
{
isPolicyPt1InterpretLoading
.
value
=
false
;
}
};
/** 政策追踪-部门分布饼图解读 */
const
fetchPolicyPtBox2ChartInterpretation
=
async
()
=>
{
const
list
=
Array
.
isArray
(
box2Data
.
value
)
?
box2Data
.
value
:
[];
if
(
!
list
.
length
)
{
aiContentPolicyPt2
.
value
=
"暂无图表数据"
;
return
;
}
const
hasValidContent
=
aiContentPolicyPt2
.
value
&&
aiContentPolicyPt2
.
value
!==
"解读生成中…"
&&
aiContentPolicyPt2
.
value
!==
"解读加载失败"
&&
aiContentPolicyPt2
.
value
!==
"暂无图表数据"
;
if
(
hasValidContent
||
isPolicyPt2InterpretLoading
.
value
)
{
return
;
}
isPolicyPt2InterpretLoading
.
value
=
true
;
aiContentPolicyPt2
.
value
=
"解读生成中…"
;
const
chartPayload
=
{
type
:
"饼图"
,
name
:
"部门分布情况"
,
year
:
box2SelectYear
.
value
,
data
:
list
.
map
((
item
)
=>
({
name
:
item
.
name
,
value
:
item
.
value
}))
};
try
{
const
text
=
await
getChartAnalysis
(
{
text
:
JSON
.
stringify
(
chartPayload
)
},
{
onInterpretationDelta
:
(
chunk
)
=>
{
if
(
chunk
)
{
aiContentPolicyPt2
.
value
=
chunk
;
}
}
}
);
aiContentPolicyPt2
.
value
=
text
||
"未返回有效解读内容"
;
}
catch
(
error
)
{
console
.
error
(
"政策追踪部门分布图表解读请求失败"
,
error
);
aiContentPolicyPt2
.
value
=
"解读加载失败"
;
}
finally
{
isPolicyPt2InterpretLoading
.
value
=
false
;
}
};
/** 政策追踪-研究领域变化趋势折线图解读(与概览页 box5 结构一致) */
const
fetchPolicyPtBox3ChartInterpretation
=
async
()
=>
{
const
v
=
box3Data
.
value
;
if
(
!
v
||
!
Array
.
isArray
(
v
.
title
)
||
v
.
title
.
length
===
0
||
!
Array
.
isArray
(
v
.
data
)
||
v
.
data
.
length
===
0
)
{
aiContentPolicyPt3
.
value
=
"暂无图表数据"
;
return
;
}
const
hasValidContent
=
aiContentPolicyPt3
.
value
&&
aiContentPolicyPt3
.
value
!==
"解读生成中…"
&&
aiContentPolicyPt3
.
value
!==
"解读加载失败"
&&
aiContentPolicyPt3
.
value
!==
"暂无图表数据"
;
if
(
hasValidContent
||
isPolicyPt3InterpretLoading
.
value
)
{
return
;
}
isPolicyPt3InterpretLoading
.
value
=
true
;
aiContentPolicyPt3
.
value
=
"解读生成中…"
;
const
chartPayload
=
{
type
:
"折线图"
,
name
:
"研究领域变化趋势"
,
data
:
v
.
title
.
map
((
label
,
i
)
=>
{
const
point
=
{
period
:
label
};
v
.
data
.
forEach
((
s
)
=>
{
point
[
s
.
name
]
=
s
.
value
[
i
]
??
0
;
});
return
point
;
})
};
try
{
const
text
=
await
getChartAnalysis
(
{
text
:
JSON
.
stringify
(
chartPayload
)
},
{
onInterpretationDelta
:
(
chunk
)
=>
{
if
(
chunk
)
{
aiContentPolicyPt3
.
value
=
chunk
;
}
}
}
);
aiContentPolicyPt3
.
value
=
text
||
"未返回有效解读内容"
;
}
catch
(
error
)
{
console
.
error
(
"政策追踪研究领域趋势图表解读请求失败"
,
error
);
aiContentPolicyPt3
.
value
=
"解读加载失败"
;
}
finally
{
isPolicyPt3InterpretLoading
.
value
=
false
;
}
};
// 搜索政策建议
const
searchPolicy
=
ref
(
""
);
...
...
@@ -487,9 +906,11 @@ const yearList = ref([
const
selectedYear
=
ref
(
1
);
// 科技领域
// 科技领域
/ 涉及部门 / 发布时间(含「全部」项,与市场准入概览资源库互斥逻辑一致)
const
areaList
=
ref
([]);
const
selectedAreaList
=
ref
([]);
const
selectedDepartmentList
=
ref
([
POLICY_FILTER_ALL_DEPT
]);
const
selectedAreaList
=
ref
([
POLICY_FILTER_ALL_AREA
]);
const
selectedYearList
=
ref
([
POLICY_FILTER_ALL_TIME
]);
//获取科技领域
const
handleGetHylyList
=
async
()
=>
{
...
...
@@ -498,11 +919,77 @@ const handleGetHylyList = async () => {
console
.
log
(
"智库研究类型信息"
,
res
.
data
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
areaList
.
value
=
res
.
data
;
console
.
log
(
areaList
.
value
)
}
}
catch
(
error
)
{
console
.
error
(
"获取研究类型error"
,
error
);
}
};
/** 涉及部门:GET /api/commonDict/gov/agency */
const
handleGetGovAgencyList
=
async
()
=>
{
try
{
const
res
=
await
getGovAgencyDict
();
console
.
log
(
res
,
" 涉及部门"
)
if
(
res
.
code
===
200
&&
Array
.
isArray
(
res
.
data
))
{
selectableDepartment
.
value
=
res
.
data
.
map
((
row
)
=>
({
id
:
String
(
row
.
id
),
name
:
row
.
name
}));
}
else
{
selectableDepartment
.
value
=
[];
}
}
catch
(
error
)
{
console
.
error
(
"获取政府机构字典 error"
,
error
);
selectableDepartment
.
value
=
[];
}
};
/** 科技领域:与「全部领域」互斥;空选时回到「全部领域」 */
const
handleChangePolicyAreaList
=
(
val
)
=>
{
if
(
val
.
includes
(
POLICY_FILTER_ALL_AREA
)
&&
val
.
length
>
1
)
{
if
(
val
[
val
.
length
-
1
]
===
POLICY_FILTER_ALL_AREA
)
{
selectedAreaList
.
value
=
[
POLICY_FILTER_ALL_AREA
];
}
else
{
selectedAreaList
.
value
=
val
.
filter
((
item
)
=>
item
!==
POLICY_FILTER_ALL_AREA
);
}
}
else
if
(
val
.
length
===
0
)
{
selectedAreaList
.
value
=
[
POLICY_FILTER_ALL_AREA
];
}
currentPage
.
value
=
1
;
handleGetThinkPolicy
();
};
/** 涉及部门:与「全部部门」互斥;空选时回到「全部部门」 */
const
handleChangePolicyDepartmentList
=
(
val
)
=>
{
if
(
val
.
includes
(
POLICY_FILTER_ALL_DEPT
)
&&
val
.
length
>
1
)
{
if
(
val
[
val
.
length
-
1
]
===
POLICY_FILTER_ALL_DEPT
)
{
selectedDepartmentList
.
value
=
[
POLICY_FILTER_ALL_DEPT
];
}
else
{
selectedDepartmentList
.
value
=
val
.
filter
((
item
)
=>
item
!==
POLICY_FILTER_ALL_DEPT
);
}
}
else
if
(
val
.
length
===
0
)
{
selectedDepartmentList
.
value
=
[
POLICY_FILTER_ALL_DEPT
];
}
currentPage
.
value
=
1
;
handleGetThinkPolicy
();
};
/** 发布时间:与「全部时间」互斥;空选时回到「全部时间」 */
const
handleChangePolicyYearList
=
(
val
)
=>
{
if
(
val
.
includes
(
POLICY_FILTER_ALL_TIME
)
&&
val
.
length
>
1
)
{
if
(
val
[
val
.
length
-
1
]
===
POLICY_FILTER_ALL_TIME
)
{
selectedYearList
.
value
=
[
POLICY_FILTER_ALL_TIME
];
}
else
{
selectedYearList
.
value
=
val
.
filter
((
item
)
=>
item
!==
POLICY_FILTER_ALL_TIME
);
}
}
else
if
(
val
.
length
===
0
)
{
selectedYearList
.
value
=
[
POLICY_FILTER_ALL_TIME
];
}
currentPage
.
value
=
1
;
handleGetThinkPolicy
();
};
const
policyList
=
ref
([
// {
// img: Img1,
...
...
@@ -583,8 +1070,119 @@ function getDateMonthsAgo(months) {
return
`
${
year
}
-
${
month
}
-
${
day
}
`
;
}
/** 当天 YYYY-MM-DD,作政策列表 endDate */
function
getTodayYmd
()
{
const
d
=
new
Date
();
const
y
=
d
.
getFullYear
();
const
m
=
String
(
d
.
getMonth
()
+
1
).
padStart
(
2
,
"0"
);
const
day
=
String
(
d
.
getDate
()).
padStart
(
2
,
"0"
);
return
`
${
y
}
-
${
m
}
-
${
day
}
`
;
}
/** 「更早」对应自然年区间下界(含) */
const
POLICY_YEAR_EARLIER_START
=
2000
;
/** 「更早」对应自然年上界(含) */
const
POLICY_YEAR_EARLIER_END
=
2020
;
const
POLICY_YEAR_LABEL_RE
=
/^
(\d{4})
年$/
;
/** 勾选「全部时间」时的固定起止(结束日按产品要求写死) */
const
POLICY_ALL_TIME_START_DATE
=
"2000-01-01"
;
const
POLICY_ALL_TIME_END_DATE
=
"2025-12-31"
;
/**
* 根据发布时间多选(如「2025年」「更早」)推导列表查询 startDate / endDate
* - 仅选「2025年」→ 2025-01-01 ~ 2025-12-31
* - 多选多个自然年 → 取最小年 01-01 与最大年 12-31 的包络
* - 「更早」→ 2000-01-01 ~ 2020-12-31,可与具体年份合并为并集区间
* - 仅「全部时间」→ 2000-01-01 ~ 2025-12-31(写死)
* - 未选任何项(无「全部时间」)→ 与顶部「近一年/两年/三年」一致
*/
function
getPolicyListDateRangeFromYearList
(
labels
,
relativeYearYears
)
{
const
raw
=
Array
.
isArray
(
labels
)
?
labels
.
filter
(
Boolean
)
:
[];
const
hasAllTime
=
raw
.
includes
(
POLICY_FILTER_ALL_TIME
);
const
set
=
new
Set
(
raw
);
set
.
delete
(
POLICY_FILTER_ALL_TIME
);
if
(
set
.
size
===
0
)
{
if
(
hasAllTime
)
{
return
{
startDate
:
POLICY_ALL_TIME_START_DATE
,
endDate
:
POLICY_ALL_TIME_END_DATE
};
}
const
years
=
Number
(
relativeYearYears
)
>
0
?
Number
(
relativeYearYears
)
:
1
;
return
{
startDate
:
getDateYearsAgo
(
years
),
endDate
:
getTodayYmd
()
};
}
let
minY
=
null
;
let
maxY
=
null
;
if
(
set
.
has
(
"更早"
))
{
minY
=
POLICY_YEAR_EARLIER_START
;
maxY
=
POLICY_YEAR_EARLIER_END
;
}
for
(
const
lab
of
set
)
{
const
m
=
String
(
lab
).
match
(
POLICY_YEAR_LABEL_RE
);
if
(
!
m
)
{
continue
;
}
const
y
=
Number
(
m
[
1
]);
minY
=
minY
==
null
?
y
:
Math
.
min
(
minY
,
y
);
maxY
=
maxY
==
null
?
y
:
Math
.
max
(
maxY
,
y
);
}
if
(
minY
==
null
||
maxY
==
null
)
{
const
years
=
Number
(
relativeYearYears
)
>
0
?
Number
(
relativeYearYears
)
:
1
;
return
{
startDate
:
getDateYearsAgo
(
years
),
endDate
:
getTodayYmd
()
};
}
return
{
startDate
:
`
${
minY
}
-01-01`
,
endDate
:
`
${
maxY
}
-12-31`
};
}
/** 将接口 reportDate(如 2025-12-28T08:00:00)格式化为 2025年12月28日(按字符串日期部分,避免时区偏移) */
function
formatPolicyReportDateToCn
(
isoStr
)
{
if
(
!
isoStr
)
{
return
""
;
}
const
m
=
String
(
isoStr
).
match
(
/^
(\d{4})
-
(\d{1,2})
-
(\d{1,2})
/
);
if
(
!
m
)
{
return
""
;
}
return
`
${
m
[
1
]}
年
${
Number
(
m
[
2
])}
月
${
Number
(
m
[
3
])}
日`
;
}
/** 接口 domains 转为标签文案列表 */
function
normalizePolicyDomains
(
domains
)
{
if
(
!
Array
.
isArray
(
domains
))
{
return
[];
}
return
domains
.
map
((
d
)
=>
{
if
(
typeof
d
===
"string"
)
{
return
d
;
}
return
d
?.
name
??
d
?.
industryName
??
d
?.
domainName
??
""
;
})
.
filter
(
Boolean
);
}
/** 列表行映射:左侧图为 organizations[0].logoUrl,文案与法案/政令用接口字段名 */
function
mapPolicyRowToView
(
row
)
{
const
orgs
=
Array
.
isArray
(
row
.
organizations
)
?
row
.
organizations
:
[];
const
logoUrl
=
orgs
[
0
]?.
logoUrl
||
defaultNewsIcon
;
const
reportDateDisplay
=
formatPolicyReportDateToCn
(
row
.
reportDate
);
return
{
...
row
,
titleZh
:
row
.
titleZh
??
row
.
title
??
""
,
contentZh
:
row
.
contentZh
??
row
.
content
??
""
,
reportDateDisplay
,
imageUrl
:
logoUrl
,
tagList
:
normalizePolicyDomains
(
row
.
domains
),
relatedBills
:
Array
.
isArray
(
row
.
relatedBills
)
?
row
.
relatedBills
:
[],
relatedAdministrativeOrders
:
Array
.
isArray
(
row
.
relatedAdministrativeOrders
)
?
row
.
relatedAdministrativeOrders
:
[],
reportId
:
row
.
reportId
??
row
.
id
};
}
const
toDetail
=
item
=>
{
window
.
sessionStorage
.
setItem
(
"curTabName"
,
item
.
content
);
window
.
sessionStorage
.
setItem
(
"curTabName"
,
item
.
content
Zh
??
item
.
content
??
""
);
const
route
=
router
.
resolve
({
name
:
"ReportDetail"
,
params
:
{
...
...
@@ -595,10 +1193,11 @@ const toDetail = item => {
};
const
total
=
ref
(
0
);
const
sort
=
ref
(
false
);
/** null:界面显示「发布时间」占位且默认按发布时间倒序;true 正序;false 倒序(显式选中) */
const
sort
=
ref
(
null
);
const
handleSwithSort
=
()
=>
{
sort
.
value
=
!
sort
.
val
ue
;
handleGetThinkPolicy
()
()
;
sort
.
value
=
sort
.
value
===
true
?
false
:
tr
ue
;
handleGetThinkPolicy
();
};
const
currentPage
=
ref
(
1
);
...
...
@@ -610,32 +1209,58 @@ const handleCurrentChange = page => {
const
handleGetThinkPolicy
=
async
()
=>
{
try
{
const
thinkTankId
=
router
.
currentRoute
.
_value
.
params
.
id
;
const
domainIds
=
selectedAreaList
.
value
.
filter
((
id
)
=>
id
!=
null
&&
id
!==
""
&&
id
!==
POLICY_FILTER_ALL_AREA
)
.
map
((
id
)
=>
String
(
id
))
.
join
(
","
);
const
orgIds
=
selectedDepartmentList
.
value
.
filter
((
id
)
=>
id
!=
null
&&
id
!==
""
&&
id
!==
POLICY_FILTER_ALL_DEPT
)
.
map
((
id
)
=>
String
(
id
))
.
join
(
","
);
const
{
startDate
,
endDate
}
=
getPolicyListDateRangeFromYearList
(
selectedYearList
.
value
,
selectedYear
.
value
);
const
parmas
=
{
id
:
router
.
currentRoute
.
_value
.
params
.
i
d
,
startDate
:
getDateYearsAgo
(
selectedYear
.
value
)
,
sortFun
:
sort
.
valu
e
,
currentPage
:
currentPage
.
value
,
thinkTankI
d
,
startDate
,
endDat
e
,
pageNum
:
currentPage
.
value
,
pageSize
:
10
,
researchTypeIds
:
selectedAreaList
.
value
.
join
(
","
)
sortOrder
:
sort
.
value
===
true
?
"asc"
:
"desc"
,
sortFun
:
sort
.
value
===
true
,
keyword
:
searchPolicy
.
value
};
if
(
domainIds
)
{
parmas
.
domainIds
=
domainIds
;
}
if
(
orgIds
)
{
parmas
.
orgIds
=
orgIds
;
}
const
res
=
await
getThinkPolicy
(
parmas
);
console
.
log
(
"智库政策"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
policyList
.
value
=
res
.
data
.
content
.
map
(
item
=>
({
...
item
,
relationBillsList
:
relationBillsList
.
value
,
relationAdList
:
relationAdList
.
value
}));
total
.
value
=
res
.
data
.
totalElements
;
const
list
=
Array
.
isArray
(
res
.
data
.
content
)
?
res
.
data
.
content
:
[];
policyList
.
value
=
list
.
map
((
row
)
=>
mapPolicyRowToView
(
row
));
total
.
value
=
res
.
data
.
totalElements
??
res
.
data
.
total
??
0
;
}
else
{
// 业务失败(如 code 500、data 为 null)时清空列表,避免仍展示上一次请求的数据
policyList
.
value
=
[];
total
.
value
=
0
;
}
}
catch
(
error
)
{
console
.
error
(
"获取智库政策error"
,
error
);
policyList
.
value
=
[];
total
.
value
=
0
;
}
};
watch
(
()
=>
selectedAreaList
.
value
,
val
=>
{
()
=>
selectedYear
.
value
,
()
=>
{
currentPage
.
value
=
1
;
handleGetThinkPolicy
();
}
);
...
...
@@ -645,6 +1270,7 @@ onMounted(() => {
handleGetThinkPolicyIndustryTotal
();
handleGetThinkPolicyIndustryChange
();
handleGetHylyList
();
handleGetGovAgencyList
();
handleGetThinkPolicy
();
handleGetPolicyAdviceDeptDistribution
();
});
...
...
@@ -655,6 +1281,11 @@ onMounted(() => {
width
:
100%
;
height
:
100%
;
.policy-tracking-el-empty
{
padding
:
0
;
margin
:
0
;
}
.top
{
height
:
420px
;
width
:
1600px
;
...
...
@@ -674,7 +1305,8 @@ onMounted(() => {
.box-header
{
width
:
520px
;
height
:
48px
;
border-bottom
:
1px
solid
rgba
(
234
,
236
,
238
,
1
);
box-sizing
:
border-box
;
border-bottom
:
1px
solid
rgba
(
240
,
242
,
244
,
1
);
display
:
flex
;
position
:
relative
;
...
...
@@ -694,8 +1326,8 @@ onMounted(() => {
margin-top
:
11px
;
margin-left
:
18px
;
height
:
26px
;
color
:
rgba
(
5
,
95
,
194
,
1
);
font-family
:
Microsoft
YaHei
;
color
:
rgba
(
20
,
89
,
187
,
1
);
font-family
:
"Source Han Sans CN"
;
font-size
:
20px
;
font-weight
:
700
;
line-height
:
26px
;
...
...
@@ -797,113 +1429,151 @@ onMounted(() => {
}
}
/* 与智库概览 box6-main 一致:主区高度 372、底边距留给 TipTab + AI */
.box-main
{
height
:
372px
;
width
:
520px
;
height
:
372px
;
box-sizing
:
border-box
;
padding
:
24px
24px
64px
24px
;
position
:
relative
;
padding
:
24px
24px
0
;
&
.chart-main--empty
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
24px
;
#box1Chart
{
height
:
309px
;
width
:
472px
;
:deep
(
.el-empty__image
)
{
margin-bottom
:
0
;
}
}
#box1Chart
,
#box2Chart
{
height
:
309px
;
width
:
472px
;
}
#box3Chart
{
height
:
309px
;
width
:
472px
;
height
:
284px
;
}
.source
{
margin
:
0
auto
;
margin-top
:
10
px
;
/* 上下0,左右自动居中 */
width
:
3
7
0px
;
position
:
absolute
;
bottom
:
21
px
;
left
:
50%
;
transform
:
translateX
(
-50%
);
width
:
3
5
0px
;
height
:
22px
;
display
:
flex
;
}
.info
{
width
:
16px
;
height
:
16px
;
margin-top
:
3px
;
img
{
width
:
100%
;
height
:
100%
;
}
.chart-box
{
position
:
absolute
;
right
:
0
;
bottom
:
18px
;
.btn-box
{
width
:
74px
;
height
:
28px
;
}
.text
{
font-family
:
"Source Han Sans CN"
;
font-weight
:
400
;
font-size
:
14px
;
line-height
:
22px
;
letter-spacing
:
0px
;
text-align
:
left
;
color
:
rgb
(
132
,
136
,
142
);
margin-left
:
8px
;
.content-box
{
width
:
520px
;
position
:
absolute
;
right
:
0
;
bottom
:
-18px
;
}
}
}
.box3-main
{
height
:
372px
;
width
:
520px
;
height
:
372px
;
box-sizing
:
border-box
;
padding
:
24px
24px
64px
24px
;
position
:
relative
;
overflow
:
hidden
;
box-sizing
:
border-box
;
padding-top
:
24px
;
&
.chart-main--empty
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
24px
;
overflow
:
visible
;
:deep
(
.el-empty__image
)
{
margin-bottom
:
0
;
}
}
.box3-chart-row
{
display
:
flex
;
flex-direction
:
row
;
align-items
:
flex-start
;
gap
:
8px
;
width
:
100%
;
box-sizing
:
border-box
;
}
#box3Chart
{
height
:
300px
;
width
:
520px
;
.box3-legend-nav-btn
{
flex-shrink
:
0
;
width
:
32px
;
height
:
32px
;
margin-top
:
6px
;
padding
:
0
;
box-sizing
:
border-box
;
border
:
1px
solid
rgba
(
220
,
223
,
230
,
1
);
border-radius
:
6px
;
background
:
rgba
(
255
,
255
,
255
,
1
);
color
:
rgba
(
59
,
65
,
75
,
1
);
font-size
:
20px
;
line-height
:
1
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
}
.source
{
margin
:
0
auto
;
margin-top
:
10px
;
.box3-legend-nav-btn
:hover:not
(
:disabled
)
{
border-color
:
var
(
--
color-main-primary
,
#055fc2
);
color
:
var
(
--
color-main-primary
,
#055fc2
);
}
/* 上下0,左右自动居中 */
width
:
370px
;
height
:
22px
;
display
:
flex
;
.box3-legend-nav-btn
:disabled
{
opacity
:
0
.35
;
cursor
:
not
-
allowed
;
}
.info
{
width
:
16px
;
height
:
16px
;
margin-top
:
3px
;
.box3-chart-canvas
{
flex
:
1
;
min-width
:
0
;
width
:
472px
;
height
:
284px
;
}
img
{
width
:
100%
;
height
:
100%
;
.source
{
position
:
absolute
;
bottom
:
21px
;
left
:
50%
;
transform
:
translateX
(
-50%
);
width
:
350px
;
height
:
22px
;
}
display
:
flex
;
}
.chart-box
{
position
:
absolute
;
right
:
0
;
bottom
:
18px
;
.btn-box
{
width
:
74px
;
height
:
28px
;
}
.text
{
font-family
:
"Source Han Sans CN"
;
font-weight
:
400
;
font-size
:
14px
;
line-height
:
22px
;
letter-spacing
:
0px
;
text-align
:
left
;
color
:
rgb
(
132
,
136
,
142
);
margin-left
:
8px
;
.content-box
{
width
:
520px
;
position
:
absolute
;
right
:
0
;
bottom
:
-18px
;
}
}
}
...
...
@@ -1133,6 +1803,120 @@ onMounted(() => {
}
}
.select-box-department
{
margin-top
:
16px
;
.select-box-header
{
display
:
flex
;
.title
{
font-family
:
"Source Han Sans CN"
;
font-weight
:
700
;
font-size
:
16px
;
line-height
:
24px
;
letter-spacing
:
1px
;
text-align
:
left
;
}
}
.select-main
{
margin-top
:
12px
;
}
.checkbox-group
{
display
:
grid
;
gap
:
8px
;
margin-left
:
24px
;
.filter-checkbox
{
height
:
24px
;
margin
:
0
;
font-family
:
"Source Han Sans CN"
,
sans-serif
;
font-weight
:
400
;
font-size
:
16px
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
justify
;
}
}
.paixu-btn
{
display
:
flex
;
width
:
120px
;
height
:
32px
;
box-sizing
:
border-box
;
border
:
1px
solid
rgba
(
230
,
231
,
232
,
1
);
border-radius
:
4px
;
background
:
rgba
(
255
,
255
,
255
,
1
);
&
:hover
{
background
:
var
(
--
color-bg-hover
);
}
cursor
:
pointer
;
.icon1
{
width
:
11px
;
height
:
14px
;
margin-top
:
10px
;
margin-left
:
9px
;
img
{
width
:
100%
;
height
:
100%
;
}
}
.text
{
height
:
19px
;
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
14px
;
font-weight
:
400
;
line-height
:
18px
;
letter-spacing
:
0px
;
text-align
:
left
;
margin-top
:
7px
;
margin-left
:
9px
;
}
.icon2
{
width
:
10px
;
height
:
5px
;
margin-top
:
5px
;
margin-left
:
13px
;
img
{
width
:
100%
;
height
:
100%
;
}
}
}
.icon
{
margin-top
:
4px
;
width
:
8px
;
height
:
16px
;
background
:
var
(
--
color-main-active
);
border-radius
:
0
4px
4px
0
;
}
.title
{
color
:
rgba
(
5
,
95
,
194
,
1
);
margin-left
:
17px
;
font-family
:
Microsoft
YaHei
;
font-style
:
Bold
;
font-size
:
20px
;
font-weight
:
700
;
line-height
:
26px
;
letter-spacing
:
0px
;
text-align
:
left
;
}
}
.select-box-publish
{
margin-top
:
16px
;
...
...
@@ -1250,7 +2034,7 @@ onMounted(() => {
.right
{
width
:
1224px
;
m
ax
-height
:
1670px
;
m
in
-height
:
1670px
;
margin-bottom
:
20px
;
box-sizing
:
border-box
;
border
:
1px
solid
rgba
(
234
,
236
,
238
,
1
);
...
...
@@ -1276,6 +2060,7 @@ onMounted(() => {
padding-left
:
37px
;
padding-right
:
36px
;
width
:
calc
(
100%
+
37px
-
36px
);
height
:
153px
;
border-bottom
:
1px
solid
rgba
(
234
,
236
,
238
,
1
);
display
:
flex
;
...
...
@@ -1286,6 +2071,7 @@ onMounted(() => {
img
{
width
:
100%
;
height
:
100%
;
display
:
block
;
}
}
...
...
@@ -1297,7 +2083,7 @@ onMounted(() => {
.title
{
// height: 24px;
color
:
rgb
a
(
59
,
65
,
75
,
1
);
color
:
rgb
(
59
,
65
,
75
);
font-family
:
"Source Han Sans CN"
;
font-size
:
18px
;
font-weight
:
700
;
...
...
@@ -1309,15 +2095,26 @@ onMounted(() => {
.info
{
margin-top
:
7px
;
height
:
22px
;
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
"Source Han Sans CN"
;
font-size
:
14px
;
font-weight
:
400
;
line-height
:
22px
;
letter-spacing
:
0px
;
text-align
:
left
;
display
:
flex
;
.text
{
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
"Source Han Sans CN"
;
font-size
:
14px
;
font-weight
:
400
;
line-height
:
22px
;
letter-spacing
:
0px
;
text-align
:
left
;
width
:
1020px
;
white-space
:
nowrap
;
/* 强制不换行,保持一行 */
overflow
:
hidden
;
/* 超出隐藏 */
text-overflow
:
ellipsis
;
/* 超出显示 ... */
}
.more
{
width
:
16px
;
height
:
16px
;
...
...
src/views/thinkTank/ThinkTankDetail/PolicyTracking/utils/multiLineChart.js
浏览文件 @
6714a4e9
import
*
as
echarts
from
'echarts'
import
{
size
,
split
}
from
'lodash'
// data: [{ name: string, value: number[] }, ...]
const
getMultiLineChart
=
(
dataX
,
seriesData
)
=>
{
/** 政策追踪「研究领域变化趋势」图例分页:每页条数(与概览数量变化趋势逻辑一致,条数按产品要求为 4) */
export
const
POLICY_TRACKING_LEGEND_PAGE_SIZE
=
4
const
colorList
=
[
'rgba(5, 95, 194, 1)'
,
'rgba(19, 168, 168, 1)'
,
'rgba(250, 140, 22, 1)'
,
'rgba(114, 46, 209, 1)'
,
'rgba(115, 209, 61, 1)'
,
'rgba(206, 79, 81, 1)'
,
'rgba(145, 202, 255, 1)'
,
'rgba(95, 101, 108, 1)'
,
'rgba(250, 84, 28, 1)'
,
'rgba(47, 84, 235, 1)'
,
'rgba(64, 150, 255, 1)'
,
'rgba(34, 41, 52, 1)'
,
'rgba(173, 198, 255, 1)'
,
'rgba(255, 169, 64, 1)'
]
const
parseRgba
=
(
colorStr
)
=>
{
const
match
=
colorStr
.
match
(
/rgba
\((\d
+
)
,
\s
*
(\d
+
)
,
\s
*
(\d
+
)
,
\s
*
(\d
+
(\.\d
+
)?)\)
/
)
if
(
match
)
{
return
{
r
:
parseInt
(
match
[
1
],
10
),
g
:
parseInt
(
match
[
2
],
10
),
b
:
parseInt
(
match
[
3
],
10
),
a
:
parseFloat
(
match
[
4
])
}
}
return
{
r
:
0
,
g
:
0
,
b
:
0
,
a
:
1
}
}
/**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} chartInput
* @param {{ legendShowCount?: number, legendPageIndex?: number }} [options]
*/
const
getMultiLineChart
=
(
chartInput
,
options
=
{})
=>
{
const
title
=
chartInput
.
title
const
series
=
chartInput
.
data
||
[]
const
legendShowCount
=
typeof
options
.
legendShowCount
===
'number'
&&
options
.
legendShowCount
>
0
?
options
.
legendShowCount
:
POLICY_TRACKING_LEGEND_PAGE_SIZE
const
rawPageIndex
=
Number
(
options
.
legendPageIndex
)
||
0
const
allNames
=
series
.
map
((
item
)
=>
item
.
name
)
const
pageCount
=
Math
.
max
(
1
,
Math
.
ceil
(
allNames
.
length
/
legendShowCount
))
const
legendPageIndex
=
Math
.
min
(
Math
.
max
(
0
,
rawPageIndex
),
pageCount
-
1
)
const
legendStart
=
legendPageIndex
*
legendShowCount
const
legendData
=
allNames
.
slice
(
legendStart
,
legendStart
+
legendShowCount
)
const
xCount
=
Array
.
isArray
(
title
)
?
title
.
length
:
0
const
labelFontSize
=
xCount
>
8
?
10
:
xCount
>
5
?
11
:
12
const
labelRotate
=
xCount
>
6
?
28
:
0
const
echartsSeries
=
series
.
map
((
item
,
index
)
=>
{
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
{
r
,
g
,
b
}
=
parseRgba
(
baseColor
)
return
{
name
:
item
.
name
,
type
:
'line'
,
smooth
:
true
,
areaStyle
:
{
color
:
new
echarts
.
graphic
.
LinearGradient
(
0
,
0
,
0
,
1
,
[
{
offset
:
0
,
color
:
`rgba(
${
r
}
,
${
g
}
,
${
b
}
, 0.3)`
},
{
offset
:
1
,
color
:
`rgba(
${
r
}
,
${
g
}
,
${
b
}
, 0)`
}
])
},
symbolSize
:
8
,
emphasis
:
{
focus
:
'series'
},
data
:
item
.
value
}
})
return
{
tooltip
:
{
trigger
:
'axis'
,
...
...
@@ -13,72 +89,78 @@ const getMultiLineChart = (dataX, seriesData) => {
}
}
},
/* 贴满 #box3Chart:四边 0,由 containLabel 在网格内为轴文字留位,避免左侧/底部大块留白 */
grid
:
{
top
:
'15%'
,
right
:
'5%'
,
bottom
:
'5%'
,
left
:
'5%'
,
top
:
50
,
right
:
10
,
bottom
:
0
,
left
:
20
,
containLabel
:
true
},
legend
:
{
icon
:
'circle'
,
show
:
true
,
top
:
10
,
left
:
'10%'
,
type
:
'plain'
,
data
:
legendData
,
top
:
4
,
left
:
'center'
,
icon
:
'circle'
,
textStyle
:
{
fontSize
:
16
,
fontFamily
:
'Source Han Sans CN'
,
fontWeight
:
400
,
lineHeight
:
24
,
letterSpacing
:
0
,
align
:
'left'
,
color
:
'rgb(95, 101, 108)'
fontFamily
:
'Source Han Sans CN'
,
fontWeight
:
400
,
fontSize
:
14
,
lineHeight
:
24
,
letterSpacing
:
0
,
align
:
'left'
,
color
:
'rgb(95, 101, 108)'
},
itemWidth
:
12
,
itemHeight
:
12
,
itemWidth
:
12
,
itemHeight
:
12
},
color
:
colorList
,
xAxis
:
[
{
type
:
'category'
,
boundaryGap
:
false
,
data
:
dataX
,
axisLine
:
{
lineStyle
:
{
color
:
'rgb(231, 243, 255)'
,
},
},
axisLabel
:
{
color
:
'rgb(132, 136, 142)'
,
fontFamily
:
'Microsoft YaHei'
,
fontWeight
:
400
,
fontSize
:
12
,
},
data
:
title
,
axisLine
:
{
lineStyle
:
{
color
:
'rgb(231, 243, 255)'
}
},
axisLabel
:
{
color
:
'rgb(132, 136, 142)'
,
fontFamily
:
'Source Han Sans CN'
,
fontWeight
:
400
,
fontSize
:
labelFontSize
,
interval
:
0
,
hideOverlap
:
false
,
rotate
:
labelRotate
,
margin
:
6
}
}
],
yAxis
:
[
{
type
:
'value'
,
splitLine
:{
show
:
true
,
lineStyle
:{
color
:
"rgb(231, 243, 255)"
,
type
:
'dashed'
splitNumber
:
4
,
axisLabel
:
{
color
:
'rgb(132, 136, 142)'
,
fontFamily
:
'Source Han Sans CN'
,
fontWeight
:
400
,
fontSize
:
11
,
margin
:
6
},
splitLine
:
{
show
:
true
,
lineStyle
:
{
color
:
'rgb(231, 243, 255)'
,
type
:
'dashed'
}
}
}
],
series
:
(
seriesData
||
[]).
map
(
item
=>
({
name
:
item
.
name
,
type
:
'line'
,
emphasis
:
{
focus
:
'series'
},
data
:
item
.
value
}))
series
:
echartsSeries
}
}
export
default
getMultiLineChart
\ No newline at end of file
export
default
getMultiLineChart
src/views/thinkTank/ThinkTankDetail/thinkInfo/index.vue
浏览文件 @
6714a4e9
...
...
@@ -180,7 +180,7 @@
</div>
</AnalysisBox>
</div>
<div
class=
"box"
>
<div
class=
"box
box-core-researchers
"
>
<!--
<div
class=
"box-header"
>
<div
class=
"header-left"
></div>
<div
class=
"title"
>
核心研究人员
</div>
...
...
@@ -213,16 +213,17 @@
</div>
</div>
</div>
-->
<AnalysisBox
title=
"核心研究人员"
>
<AnalysisBox
title=
"核心研究人员"
width=
"1104px"
height=
"900px"
>
<div
class=
"box3-main"
>
<div
class=
"box3-main-
left
"
>
<div
class=
"box3-main-
top
"
>
<div
id=
"box3Chart"
></div>
<div
class=
"source"
>
<div
class=
"info"
><img
src=
"./images/image-exclamation.png"
></div>
<div
class=
"text"
>
数据来源:美国国会官网,数据时间:2015.1至2025.12
</div>
</div>
</div>
<div
class=
"box3-main-right"
>
<div
class=
"source"
>
<div
class=
"info"
><img
src=
"./images/image-exclamation.png"
></div>
<div
class=
"text"
>
数据来源:美国国会官网,数据时间:2015.1至2025.12
</div>
</div>
<div
class=
"box3-main-bottom"
>
<div
class=
"box3-right-item"
v-for=
"(item, index) in box3RightData"
:key=
"index"
>
<div
class=
"icon"
@
click=
"handleClickPerson(item)"
>
<img
:src=
"item.imageUrl ? item.imageUrl : DefaultIcon1"
alt=
""
/>
...
...
@@ -244,7 +245,7 @@
</
template
>
<
script
setup
>
import
{
ref
,
onMounted
}
from
"vue"
;
import
{
ref
,
onMounted
,
nextTick
}
from
"vue"
;
import
setChart
from
"@/utils/setChart"
;
import
getPieChart
from
"./utils/piechart"
;
import
getTreeMapChart
from
"./utils/treeMapChart"
;
...
...
@@ -602,7 +603,7 @@ const handleBox3Chart = () => {
const
box3Chart
=
getTreeMapChart
(
treemapData
);
delete
box3Chart
.
series
[
0
].
itemStyle
;
box3Chart
.
series
[
0
].
sort
=
false
;
// 图表充满 box3-main-
left
,左右各留 12px
// 图表充满 box3-main-
top
,左右各留 12px
box3Chart
.
series
[
0
].
left
=
12
;
box3Chart
.
series
[
0
].
right
=
12
;
box3Chart
.
series
[
0
].
top
=
0
;
...
...
@@ -610,11 +611,10 @@ const handleBox3Chart = () => {
// 方块内文字与区域标题统一为黑色,方块内文字加 1px 白色描边(等效 CSS text-stroke: 1px #fff)
box3Chart
.
series
[
0
].
label
=
{
...
box3Chart
.
series
[
0
].
label
,
color
:
'rgb(51,51,51)'
,
textBorderColor
:
'#fff'
,
textBorderWidth
:
0.7
,
// 可选:白色阴影兜底
textShadowColor
:
'#fff'
,
color
:
'white'
,
fontSize
:
16
,
textShadowBlur
:
0
,
textShadowOffsetX
:
0
,
textShadowOffsetY
:
0
...
...
@@ -739,7 +739,9 @@ onMounted(() => {
handleGetThinkTankResearchAreae
()
handleGetThinkPerson
()
handleBox3Chart
();
nextTick
(()
=>
{
handleBox3Chart
();
});
});
</
script
>
...
...
@@ -876,7 +878,8 @@ onMounted(() => {
.right
{
width
:
1104px
;
height
:
1245px
;
/* 三栏:390 + 390 + 900,间距 16×2 */
height
:
1712px
;
margin-top
:
16px
;
display
:
flex
;
flex-direction
:
column
;
...
...
@@ -885,6 +888,7 @@ onMounted(() => {
.box
{
width
:
1104px
;
height
:
390px
;
flex-shrink
:
0
;
// box-sizing: border-box;
// border: 1px solid rgba(234, 236, 238, 1);
// border-radius: 10px;
...
...
@@ -1230,59 +1234,62 @@ onMounted(() => {
.box3-main
{
display
:
flex
;
flex-direction
:
column
;
.box3-main-left
{
width
:
536px
;
height
:
326px
;
margin-left
:
9px
;
box-sizing
:
border-box
;
.box3-main-top
{
width
:
1100px
;
height
:
372px
;
box-sizing
:
border-box
;
padding-left
:
35px
;
#box3Chart
{
width
:
536
px
;
height
:
290
px
;
width
:
1035
px
;
height
:
372
px
;
}
.source
{
margin
:
0
auto
;
margin-top
:
10px
;
/* 上下0,左右自动居中 */
width
:
370px
;
height
:
22px
;
display
:
flex
;
.info
{
width
:
16px
;
height
:
16px
;
margin-top
:
3px
;
}
img
{
width
:
100%
;
height
:
100%
;
.source
{
margin
:
0
auto
;
margin-top
:
10px
;
}
/* 上下0,左右自动居中 */
width
:
370px
;
height
:
22px
;
display
:
flex
;
.info
{
width
:
16px
;
height
:
16px
;
margin-top
:
3px
;
}
img
{
width
:
100%
;
height
:
100%
;
.text
{
font-family
:
"Source Han Sans CN"
;
font-weight
:
400
;
font-size
:
14px
;
line-height
:
22px
;
letter-spacing
:
0px
;
text-align
:
left
;
color
:
rgb
(
132
,
136
,
142
);
margin-left
:
8px
;
}
}
.text
{
font-family
:
"Source Han Sans CN"
;
font-weight
:
400
;
font-size
:
14px
;
line-height
:
22px
;
letter-spacing
:
0px
;
text-align
:
left
;
color
:
rgb
(
132
,
136
,
142
);
margin-left
:
8px
;
}
}
.box3-main-
right
{
.box3-main-
bottom
{
margin-left
:
28px
;
margin-top
:
12
px
;
margin-top
:
24
px
;
width
:
536px
;
height
:
326px
;
...
...
@@ -1356,9 +1363,15 @@ onMounted(() => {
}
}
}
}
.box.box-core-researchers
{
width
:
1104px
;
height
:
900px
;
flex-shrink
:
0
;
}
}
}
</
style
>
\ No newline at end of file
src/views/thinkTank/TipTab/index.vue
0 → 100644
浏览文件 @
6714a4e9
<
template
>
<div
class=
"tip-wrapper"
>
<div
class=
"icon"
>
<img
src=
"./tip-icon.svg"
alt=
""
>
</div>
<div
class=
"text text-tip-2 text-primary-50-clor"
>
{{
`${text
}
`
}}
<
/div
>
<
/div
>
<
/template
>
<
script
setup
>
const
props
=
defineProps
({
dataSource
:
{
type
:
String
,
default
:
'美国国会官网'
}
,
dataTime
:
{
type
:
String
,
default
:
'2023.1至2025.12'
}
,
text
:
{
type
:
String
,
default
:
''
}
,
}
)
<
/script
>
<
style
lang
=
"scss"
scoped
>
.
tip
-
wrapper
{
width
:
100
%
;
display
:
flex
;
gap
:
8
px
;
justify
-
content
:
center
;
align
-
items
:
center
;
height
:
22
px
;
.
icon
{
width
:
16
px
;
height
:
16
px
;
img
{
width
:
100
%
;
height
:
100
%
;
}
}
}
<
/style>
\ No newline at end of file
src/views/thinkTank/TipTab/tip-icon.svg
0 → 100644
浏览文件 @
6714a4e9
<svg
viewBox=
"0 0 16 16"
xmlns=
"http://www.w3.org/2000/svg"
xmlns:xlink=
"http://www.w3.org/1999/xlink"
width=
"16.000000"
height=
"16.000000"
fill=
"none"
customFrame=
"#000000"
>
<rect
id=
"容器 704"
width=
"16.000000"
height=
"16.000000"
x=
"0.000000"
y=
"0.000000"
/>
<circle
id=
"椭圆 96"
cx=
"8"
cy=
"8"
r=
"7"
fill=
"rgb(230,231,232)"
/>
<circle
id=
"椭圆 97"
cx=
"8"
cy=
"4"
r=
"1"
fill=
"rgb(132,136,142)"
/>
<path
id=
"矩形 241"
d=
"M6.49996 6L8.00028 6.0004C8.55256 6.0004 9.00028 6.44811 9.00028 7.00039L9.00028 10.4992C9.00028 10.7754 9.22408 10.9989 9.50033 10.9992L9.50033 10.9997C9.77657 10.9998 10.0005 11.2236 10.0005 11.4998L10.0003 11.5001C10.0002 11.7765 9.77622 12.0006 9.49978 12.0006L8.00028 12.0004L6.50033 12.0004C6.22423 12.0004 6.00064 11.7767 6.00049 11.5006L6.00021 11.5005C6.00021 11.2243 6.22418 11.0003 6.50037 11.0003L6.50037 11.0006C6.77649 11.0007 7.00042 10.7766 7.00042 10.5005L7.00017 7.50005C7.00017 7.22376 6.77644 7.00047 6.50015 7.00002L6.49946 6.99922C6.22357 6.999 6 6.77565 6 6.49976C6.00011 6.22373 6.22393 6 6.49996 6Z"
fill=
"rgb(132,136,142)"
fill-rule=
"evenodd"
/>
</svg>
src/views/thinkTank/allThinkTank/index.vue
浏览文件 @
6714a4e9
...
...
@@ -24,7 +24,7 @@
</div>
<div
class=
"select-box"
>
<div
class=
"search-box"
>
<el-input
placeholder=
"搜索智库"
v-model=
"searchPolicy"
>
<el-input
placeholder=
"搜索智库"
v-model=
"searchPolicy"
@
keyup
.
enter=
"handleGetThinkTankList()"
>
<template
#
suffix
>
<img
src=
"../assets/images/Line_Search.png"
class=
"search-icon"
alt=
"搜索"
@
click=
"handleGetThinkTankList()"
>
...
...
@@ -70,6 +70,9 @@
{{ "近期美国智库机构发布涉华报告数量汇总" }}
</div>
</div>
<div
class=
"time-tab-pane"
>
<TimeTabPane
@
time-click=
"handleTimeClick"
/>
</div>
</div>
<div
class=
"all-item"
>
<div
class=
"item-card"
v-for=
"(item, index) in sortedCardList"
:key=
"item.id || index"
...
...
@@ -111,6 +114,7 @@ import { ref, reactive, computed, onMounted } from "vue";
import
{
getAllThinkTankList
}
from
"@/api/thinkTank/overview"
import
{
useRouter
}
from
'vue-router'
;
import
router
from
"@/router"
;
import
TimeTabPane
from
'@/components/base/TimeTabPane/index.vue'
const
cardList
=
ref
([]);
// 按 reportNumber 从大到小排序,保证从左到右从上到下排列
const
sortedCardList
=
computed
(()
=>
{
...
...
@@ -125,6 +129,22 @@ const sortedCardList = computed(() => {
const
currentPage
=
ref
(
1
)
const
pageSize
=
ref
(
15
)
const
total
=
ref
(
0
)
const
timePeriod
=
ref
(
"WEEK"
)
const
handleTimeClick
=
item
=>
{
const
time
=
item
?.
time
if
(
time
===
"近一周"
)
{
timePeriod
.
value
=
"WEEK"
}
else
if
(
time
===
"近一月"
)
{
timePeriod
.
value
=
"MONTH"
}
else
if
(
time
===
"近一年"
)
{
timePeriod
.
value
=
"YEAR"
}
// 切换时间范围后重新拉取(从第一页开始)
currentPage
.
value
=
1
handleGetThinkTankList
()
}
const
handleCurrentChange
=
page
=>
{
currentPage
.
value
=
page
;
handleGetThinkTankList
()
...
...
@@ -142,7 +162,8 @@ const handleGetThinkTankList = async () => {
// 后端通常是 0-based,这里做一次转换
currentPage
:
currentPage
.
value
-
1
,
pageSize
:
pageSize
.
value
,
keyword
:
searchPolicy
.
value
keyword
:
searchPolicy
.
value
,
timePeriod
:
timePeriod
.
value
});
console
.
log
(
"智库列表"
,
res
);
cardList
.
value
=
[];
...
...
@@ -391,6 +412,7 @@ onMounted(async () => {
height
:
36px
;
margin-top
:
14px
;
display
:
flex
;
justify-content
:
space-between
;
.title-info
{
height
:
24px
;
...
...
@@ -419,6 +441,11 @@ onMounted(async () => {
color
:
rgb
(
59
,
65
,
75
);
}
}
.time-tab-pane
{
width
:
248px
;
height
:
36px
;
}
}
.all-item
{
...
...
@@ -463,7 +490,8 @@ onMounted(async () => {
padding
:
2px
8px
;
/* 左右留空隙,更美观 */
white-space
:
nowrap
white-space
:
nowrap
;
z-index
:
9999
;
}
.item-header
{
...
...
src/views/thinkTank/index.vue
浏览文件 @
6714a4e9
...
...
@@ -51,6 +51,9 @@
{{
"近期美国智库机构发布涉华报告数量汇总"
}}
</div>
</div>
<div
class=
"time-tab-pane"
>
<TimeTabPane
@
time-click=
"handleTimeClick"
/>
</div>
</div>
<div
class=
"home-main-header-card-box"
>
<div
class=
"card"
v-for=
"(item, index) in sortedCardList"
:key=
"index"
@
click=
"handleClick(item)"
>
...
...
@@ -173,22 +176,50 @@
<div
class=
"box5-header-icon"
>
<img
src=
"./assets/images/box3-header-icon.png"
alt=
""
/>
</div>
<div
class=
"box5-header-title"
>
{{ "
政策建议趋势分布
" }}
</div>
<div
class=
"box5-header-title"
>
{{ "
数量变化趋势
" }}
</div>
</div>
<div
class=
"box5-select-box"
>
<el-select
v-model=
"box5selectetedMonths"
placeholder=
"选择时间"
style=
"width: 120px"
>
<el-option
v-for=
"item in box5MonthsList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
@
click=
"changeBox5Data(item.value)"
/>
<el-select
v-model=
"box5selectetedArea"
placeholder=
"选择领域"
style=
"width: 120px"
@
change=
"handleBox5AreaChange"
>
<el-option
label=
"全部领域"
value=
"全部领域"
/>
<el-option
v-for=
"item in box5RawData.data"
:key=
"item.name"
:label=
"item.name"
:value=
"item.name"
/>
</el-select>
<el-select
v-model=
"box5selectetedYear"
placeholder=
"选择年份"
style=
"width: 120px"
@
change=
"changeBox5Data"
>
<el-option
v-for=
"item in box5YearList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
</div>
</div>
<div
class=
"box5-main"
>
<el-empty
v-if=
"!hasBox5ChartData"
description=
"暂无数据"
:image-size=
"100"
/>
<div
id=
"box5Chart"
v-else
></div>
<div
class=
"source"
>
<div
class=
"info"
><img
src=
"./assets/images/image-exclamation.png"
></div>
<div
class=
"text"
>
数据来源:美国国会官网,数据时间:2015.1至2025.12
</div>
</div>
<div
class=
"box5-main"
:class=
"{ 'box5-main--empty': !hasBox5ChartData }"
>
<
template
v-if=
"!hasBox5ChartData"
>
<el-empty
class=
"box5-el-empty"
description=
"暂无数据"
:image-size=
"100"
/>
</
template
>
<
template
v-else
>
<div
class=
"box5-chart-row"
>
<button
v-if=
"box5LegendShowNav"
type=
"button"
class=
"box5-legend-nav-btn"
:disabled=
"!box5LegendCanPrev"
aria-label=
"上一页图例"
@
click=
"handleBox5LegendPrev"
>
‹
</button>
<div
id=
"box5Chart"
class=
"box5-chart-canvas"
></div>
<button
v-if=
"box5LegendShowNav"
type=
"button"
class=
"box5-legend-nav-btn"
:disabled=
"!box5LegendCanNext"
aria-label=
"下一页图例"
@
click=
"handleBox5LegendNext"
>
›
</button>
</div>
<div
class=
"source"
>
<TipTab
:text=
"'智库报告数量变化趋势,数据来源:美国各智库官网'"
/>
</div>
<div
class=
"chart-box"
>
<div
class=
"btn-box"
v-if=
"!isShowAiContentBox5"
@
mouseenter=
"handleSwitchAiContentShowBox5(true)"
>
<AiButton
/>
</div>
<div
class=
"content-box"
v-if=
"isShowAiContentBox5"
@
mouseleave=
"handleSwitchAiContentShowBox5(false)"
>
<AiPane
:aiContent=
"aiContentBox5"
/>
</div>
</div>
</
template
>
</div>
</div>
<div
class=
"box6"
>
...
...
@@ -196,20 +227,39 @@
<div
class=
"header-icon"
>
<img
src=
"./assets/images/box6-header-icon.png"
alt=
""
/>
</div>
<div
class=
"header-title"
>
{{ "
政策建议领域分布
" }}
</div>
<div
class=
"header-title"
>
{{ "
领域分布情况
" }}
</div>
<div
class=
"box6-select-box"
>
<el-select
v-model=
"box6selectetedTank"
placeholder=
"选择智库"
style=
"width: 120px"
>
<el-option
label=
"全部智库"
value=
"全部智库"
/>
<el-option
v-for=
"item in box6TankList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
/>
</el-select>
<el-select
v-model=
"box6selectetedYear"
placeholder=
"选择时间"
style=
"width: 120px"
>
<el-option
v-for=
"item in box6YearList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
@
click=
"handleBox6()"
/>
</el-select>
</div>
</div>
<div
class=
"box6-main"
>
<div
id=
"box6Chart"
></div>
<div
class=
"source"
>
<div
class=
"info"
><img
src=
"./assets/images/image-exclamation.png"
></div>
<div
class=
"text"
>
数据来源:美国国会官网,数据时间:2015.1至2025.12
</div>
</div>
<div
class=
"box6-main"
:class=
"{ 'box6-main--empty': !hasBox6ChartData }"
>
<
template
v-if=
"!hasBox6ChartData"
>
<el-empty
class=
"box6-el-empty"
description=
"暂无数据"
:image-size=
"100"
/>
</
template
>
<
template
v-else
>
<div
id=
"box6Chart"
></div>
<div
class=
"source"
>
<TipTab
:text=
"'智库报告领域分布情况,数据来源:美国各智库官网'"
/>
</div>
<div
class=
"chart-box"
>
<div
class=
"btn-box"
v-if=
"!isShowAiContentBox6"
@
mouseenter=
"handleSwitchAiContentShowBox6(true)"
>
<AiButton
/>
</div>
<div
class=
"content-box"
v-if=
"isShowAiContentBox6"
@
mouseleave=
"handleSwitchAiContentShowBox6(false)"
>
<AiPane
:aiContent=
"aiContentBox6"
/>
</div>
</div>
</
template
>
</div>
</div>
</div>
...
...
@@ -223,7 +273,26 @@
<div
class=
"box7-header-title"
>
{{ "智库资金流向" }}
</div>
</div>
</div>
<div
class=
"box7-main"
id=
"box7Chart"
></div>
<div
class=
"box7-main"
:class=
"{ 'box7-main--empty': !hasBox7ChartData }"
>
<
template
v-if=
"!hasBox7ChartData"
>
<el-empty
class=
"box7-el-empty"
description=
"暂无数据"
:image-size=
"100"
/>
</
template
>
<
template
v-else
>
<div
id=
"box7Chart"
></div>
<div
class=
"source"
>
<TipTab
:text=
"'美国科技智库与主要政府机构之间的资金往来,数据来源:美国各智库官网'"
/>
</div>
<div
class=
"chart-box"
>
<div
class=
"btn-box"
v-if=
"!isShowAiContentBox7"
@
mouseenter=
"handleSwitchAiContentShowBox7(true)"
>
<AiButton
/>
</div>
<div
class=
"content-box"
v-if=
"isShowAiContentBox7"
@
mouseleave=
"handleSwitchAiContentShowBox7(false)"
>
<AiPane
:aiContent=
"aiContentBox7"
/>
</div>
</div>
</
template
>
</div>
</div>
<div
class=
"box8"
>
<div
class=
"box8-header"
>
...
...
@@ -233,31 +302,30 @@
</div>
<div
class=
"box8-header-title"
>
{{ "智库研究热点" }}
</div>
</div>
<div
class=
"box8-select-box"
>
<el-select
v-model=
"box8selectetedYear"
placeholder=
"选择时间"
style=
"width: 120px"
>
<el-option
v-for=
"item in box8YearList"
:key=
"item.value"
:label=
"item.label"
:value=
"item.value"
@
click=
"changeBox8Data(item.value)"
/>
</el-select>
</div>
</div>
<div
class=
"box8-main"
>
<div
class=
"box8-item"
v-for=
"(item, index) in box8Data"
:key=
"index"
>
<div
class=
"item-left"
:class=
"{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }"
>
{{ index + 1 }}
</div>
<!-- <el-popover effect="dark" :content="item.clause" placement="top-start">
<div
class=
"box8-main-item"
>
<div
class=
"box8-item"
v-for=
"(item, index) in box8Data"
:key=
"index"
>
<div
class=
"item-left"
:class=
"{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }"
>
{{ index + 1 }}
</div>
<!-- <el-popover effect="dark" :content="item.clause" placement="top-start">
<template #reference> -->
<div
class=
"item-center"
:class=
"{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }"
>
{{ item.clause }}
</div>
<!-- </template>
<div
class=
"item-center"
:class=
"{ itemBold1: index === 0, itemBold2: index === 1, itemBold3: index === 2 }"
>
{{ item.clause }}
</div>
<!-- </template>
</el-popover> -->
<!-- <div class="item-right">{{ `${item.count}份报告 >` }}</div> -->
<div
class=
"item-count"
>
{{ item.count + "份报告 >" }}
</div>
<!-- <div class="item-right">{{ `${item.count}份报告 >` }}</div> -->
<div
class=
"item-count"
>
{{ item.count + "份报告 >" }}
</div>
</div>
</div>
</div>
</div>
</div>
...
...
@@ -355,9 +423,10 @@ import {
getThinkTankReportRemarks
}
from
"@/api/thinkTank/overview"
;
import
{
getPersonSummaryInfo
}
from
"@/api/common/index"
;
import
getMultiLineChart
from
"./utils/multiLineChart"
;
import
getMultiLineChart
,
{
MULTILINE_LEGEND_SHOW_COUNT
}
from
"./utils/multiLineChart"
;
import
getPieChart
from
"./utils/piechart"
;
import
getSankeyChart
from
"./utils/sankey"
;
import
{
getChartAnalysis
}
from
"@/api/aiAnalysis/index"
;
import
defaultNewsIcon
from
"@/assets/icons/default-icon-news.png"
;
import
defaultHeaderIcin
from
"@/assets/icons/default-icon1.png"
;
import
News1
from
"./assets/images/news1.png"
;
...
...
@@ -365,6 +434,9 @@ import News2 from "./assets/images/news2.png";
import
News3
from
"./assets/images/news3.png"
;
import
News4
from
"./assets/images/news4.png"
;
import
News5
from
"./assets/images/news5.png"
;
import
AiButton
from
'@/components/base/Ai/AiButton/index.vue'
import
AiPane
from
'@/components/base/Ai/AiPane/index.vue'
import
TipTab
from
"./TipTab/index.vue"
;
import
Message1
from
"./assets/images/message-icon1.png"
;
import
Message2
from
"./assets/images/message-icon2.png"
;
...
...
@@ -398,12 +470,40 @@ const containerRef = ref(null);
const
statCountInfo
=
ref
([]);
const
pageSize
=
ref
(
15
)
const
totalAllItem
=
ref
(
0
)
const
isShowAiContentBox5
=
ref
(
false
);
const
aiContentBox5
=
ref
(
""
);
const
isBox5InterpretLoading
=
ref
(
false
);
const
handleSwitchAiContentShowBox5
=
(
val
)
=>
{
isShowAiContentBox5
.
value
=
val
;
if
(
val
)
{
fetchBox5ChartInterpretation
();
}
};
const
isShowAiContentBox6
=
ref
(
false
);
const
aiContentBox6
=
ref
(
""
);
const
isBox6InterpretLoading
=
ref
(
false
);
const
handleSwitchAiContentShowBox6
=
(
val
)
=>
{
isShowAiContentBox6
.
value
=
val
;
if
(
val
)
{
fetchBox6ChartInterpretation
();
}
};
const
isShowAiContentBox7
=
ref
(
false
);
const
aiContentBox7
=
ref
(
""
);
const
isBox7InterpretLoading
=
ref
(
false
);
const
handleSwitchAiContentShowBox7
=
(
val
)
=>
{
isShowAiContentBox7
.
value
=
val
;
if
(
val
)
{
fetchBox7ChartInterpretation
();
}
};
const
handleGetAllThinkTankList
=
async
()
=>
{
try
{
const
res
=
await
getAllThinkTankList
({
// 后端通常是 0-based,这里做一次转换
currentPage
:
currentPage
.
value
-
1
,
pageSize
:
pageSize
.
value
pageSize
:
pageSize
.
value
,
timePeriod
:
timePeriod
.
value
});
console
.
log
(
"智库列表"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
...
...
@@ -421,6 +521,23 @@ const goToAllThinkTank = () => {
// 替换为你的实际路由路径
routerTo
.
push
(
'/thinkTank/allThinkTank'
);
};
const
timePeriod
=
ref
(
"WEEK"
)
const
handleTimeClick
=
item
=>
{
const
time
=
item
?.
time
if
(
time
===
"近一周"
)
{
timePeriod
.
value
=
"WEEK"
}
else
if
(
time
===
"近一月"
)
{
timePeriod
.
value
=
"MONTH"
}
else
if
(
time
===
"近一年"
)
{
timePeriod
.
value
=
"YEAR"
}
// 切换时间范围后重新拉取(从第一页开始)
currentPage
.
value
=
1
handleGetAllThinkTankList
()
// 同步刷新首页卡片数据(包含 increaseReportNumber)
handleGetThinkTankList
()
}
const
getStatCountInfo
=
async
()
=>
{
...
...
@@ -497,21 +614,31 @@ const sortedCardList = computed(() => {
// 获取智库列表
const
handleGetThinkTankList
=
async
()
=>
{
try
{
const
res
=
await
getThinkTankList
();
const
res
=
await
getAllThinkTankList
({
// 后端通常是 0-based,这里做一次转换
currentPage
:
currentPage
.
value
-
1
,
pageSize
:
pageSize
.
value
,
timePeriod
:
timePeriod
.
value
});
console
.
log
(
"智库列表"
,
res
);
cardList
.
value
=
[];
if
(
res
.
code
===
200
&&
res
.
data
)
{
cardList
.
value
=
res
.
data
.
map
(
item
=>
{
return
{
id
:
item
.
id
,
logo
:
item
.
imageUrl
,
name
:
item
.
name
,
tagList
:
item
.
tags
,
country
:
item
.
country
,
desc
:
item
.
describe
,
rank
:
item
.
id
,
reportNumber
:
item
.
reportNumber
};
});
// 兼容两种返回结构:
// 1) data.content(分页)
// 2) data 直接是数组(非分页)
const
list
=
Array
.
isArray
(
res
.
data
)
?
res
.
data
:
(
res
.
data
?.
content
||
[]);
cardList
.
value
=
list
.
map
(
item
=>
({
id
:
item
.
id
,
logo
:
item
.
imageUrl
,
name
:
item
.
name
,
tagList
:
item
.
tags
,
country
:
item
.
country
,
desc
:
item
.
describe
,
rank
:
item
.
id
,
reportNumber
:
item
.
reportNumber
,
increaseReportNumber
:
item
.
increaseReportNumber
}));
}
}
catch
(
error
)
{
console
.
error
(
"获取智库列表error"
,
error
);
...
...
@@ -759,26 +886,13 @@ const handleGetThinkTankReportRemarks = async () => {
}
};
// 政策建议趋势分布
const
box5Data
=
ref
({
// title: ["2014", "2015", "2016", "2017", "2018", "2019", "2020", "2021", "2022", "2023", "2024", "2025"],
// data: [
// {
// name: "人工智能",
// value: [73, 32, 42, 48, 38, 49, 63, 75, 70, 86, 95, 87]
// },
// {
// name: "集成电路",
// value: [8, 3, 2, 8, 9, 10, 12, 18, 16, 18, 20, 22]
// },
// {
// name: "量子科技",
// value: [1, 2, 1, 1, 1, 1, 1, 2, 1, 1, 2, 3]
// }
// ]
// 政策建议趋势分布(接口全量,按领域筛选后用于绘图)
const
box5RawData
=
ref
({
title
:
[],
data
:
[]
});
const
hasBox5ChartData
=
computed
(()
=>
{
const
v
=
box5Data
.
value
;
const
v
=
box5
Raw
Data
.
value
;
if
(
!
v
||
typeof
v
!==
"object"
)
return
false
;
return
Array
.
isArray
(
v
.
title
)
&&
v
.
title
.
length
>
0
&&
Array
.
isArray
(
v
.
data
)
&&
v
.
data
.
length
>
0
;
});
...
...
@@ -807,33 +921,34 @@ function getDateMonthsAgo(months) {
const
day
=
String
(
pastDate
.
getDate
()).
padStart
(
2
,
"0"
);
return
`
${
year
}
-
${
month
}
-
${
day
}
`
;
}
const
box5selectetedMonths
=
ref
(
24
);
const
box5MonthsList
=
ref
([
{
label
:
"近3个月"
,
value
:
3
},
{
label
:
"近6个月"
,
value
:
6
},
{
label
:
"近12个月"
,
value
:
12
},
{
label
:
"近24个月"
,
value
:
24
}
/** 自然年日期范围(传给 policyIndustryChange) */
const
getBox5YearDateRange
=
year
=>
{
const
y
=
Number
(
year
);
return
{
startDate
:
`
${
y
}
-01-01`
,
endDate
:
`
${
y
}
-12-31`
};
};
const
box5selectetedYear
=
ref
(
2025
);
const
box5YearList
=
ref
([
{
label
:
"2026"
,
value
:
2026
},
{
label
:
"2025"
,
value
:
2025
},
{
label
:
"2024"
,
value
:
2024
},
{
label
:
"2023"
,
value
:
2023
},
{
label
:
"2022"
,
value
:
2022
}
]);
function
changeBox5Data
(
value
)
{
handleBox5
(
getDateMonthsAgo
(
value
));
}
const
box5selectetedArea
=
ref
(
"全部领域"
);
const
changeBox5Data
=
year
=>
{
handleBox5
(
year
);
};
// 政策建议趋势分布
const
handleGetThinkTankPolicyIndustryChange
=
async
dat
e
=>
{
const
handleGetThinkTankPolicyIndustryChange
=
async
rang
e
=>
{
try
{
const
res
=
await
getThinkTankPolicyIndustryChange
(
dat
e
);
const
res
=
await
getThinkTankPolicyIndustryChange
(
rang
e
);
console
.
log
(
"政策建议趋势分布"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
const
originalData
=
res
.
data
;
...
...
@@ -865,21 +980,153 @@ const handleGetThinkTankPolicyIndustryChange = async date => {
});
result
.
data
.
push
(
industryData
);
});
box5Data
.
value
=
result
;
box5RawData
.
value
=
result
;
console
.
log
(
box5RawData
.
value
.
data
,
"box5RawData"
);
box5RawData
.
value
.
data
.
forEach
(
item
=>
{
console
.
log
(
item
.
name
);
});
}
else
{
box5Data
.
value
=
{
title
:
[],
data
:
[]
};
box5
Raw
Data
.
value
=
{
title
:
[],
data
:
[]
};
}
}
catch
(
error
)
{
console
.
error
(
"获取政策建议趋势分布error"
,
error
);
box5RawData
.
value
=
{
title
:
[],
data
:
[]
};
}
};
/** 按所选领域得到折线图数据(全部领域 = 全量序列) */
const
box5ChartDisplayData
=
computed
(()
=>
{
const
raw
=
box5RawData
.
value
;
if
(
!
raw
||
!
Array
.
isArray
(
raw
.
title
)
||
!
Array
.
isArray
(
raw
.
data
))
{
return
{
title
:
[],
data
:
[]
};
}
if
(
box5selectetedArea
.
value
===
"全部领域"
)
{
return
{
title
:
raw
.
title
,
data
:
[...
raw
.
data
]
};
}
const
series
=
raw
.
data
.
filter
(
d
=>
d
.
name
===
box5selectetedArea
.
value
);
return
{
title
:
raw
.
title
,
data
:
series
.
length
?
series
:
[...
raw
.
data
]
};
});
const
box5LegendPageIndex
=
ref
(
0
);
const
box5LegendPageCount
=
computed
(()
=>
{
const
n
=
box5ChartDisplayData
.
value
?.
data
?.
length
??
0
;
return
Math
.
max
(
1
,
Math
.
ceil
(
n
/
MULTILINE_LEGEND_SHOW_COUNT
));
});
const
box5LegendShowNav
=
computed
(()
=>
{
const
n
=
box5ChartDisplayData
.
value
?.
data
?.
length
??
0
;
return
n
>
MULTILINE_LEGEND_SHOW_COUNT
;
});
const
box5LegendCanPrev
=
computed
(()
=>
box5LegendPageIndex
.
value
>
0
);
const
box5LegendCanNext
=
computed
(
()
=>
box5LegendPageIndex
.
value
<
box5LegendPageCount
.
value
-
1
);
const
handleBox5LegendPrev
=
()
=>
{
if
(
box5LegendPageIndex
.
value
<=
0
)
{
return
;
}
box5LegendPageIndex
.
value
-=
1
;
renderBox5Chart
();
};
const
handleBox5LegendNext
=
()
=>
{
if
(
box5LegendPageIndex
.
value
>=
box5LegendPageCount
.
value
-
1
)
{
return
;
}
box5LegendPageIndex
.
value
+=
1
;
renderBox5Chart
();
};
const
handleBox5
=
async
date
=>
{
await
handleGetThinkTankPolicyIndustryChange
(
date
);
let
box5Chart
=
box5Data
.
value
?
getMultiLineChart
(
box5Data
.
value
)
:
""
;
const
renderBox5Chart
=
()
=>
{
const
chartInput
=
box5ChartDisplayData
.
value
;
if
(
!
chartInput
.
title
.
length
||
!
chartInput
.
data
.
length
)
{
return
;
}
const
totalNames
=
chartInput
.
data
.
length
;
const
pages
=
Math
.
max
(
1
,
Math
.
ceil
(
totalNames
/
MULTILINE_LEGEND_SHOW_COUNT
));
if
(
box5LegendPageIndex
.
value
>=
pages
)
{
box5LegendPageIndex
.
value
=
pages
-
1
;
}
if
(
box5LegendPageIndex
.
value
<
0
)
{
box5LegendPageIndex
.
value
=
0
;
}
const
box5Chart
=
getMultiLineChart
(
chartInput
,
{
legendShowCount
:
MULTILINE_LEGEND_SHOW_COUNT
,
legendPageIndex
:
box5LegendPageIndex
.
value
});
setChart
(
box5Chart
,
"box5Chart"
);
};
const
handleBox5AreaChange
=
()
=>
{
aiContentBox5
.
value
=
""
;
box5LegendPageIndex
.
value
=
0
;
renderBox5Chart
();
};
const
handleBox5
=
async
year
=>
{
const
y
=
year
!=
null
?
Number
(
year
)
:
box5selectetedYear
.
value
;
box5selectetedYear
.
value
=
y
;
box5selectetedArea
.
value
=
"全部领域"
;
box5LegendPageIndex
.
value
=
0
;
await
handleGetThinkTankPolicyIndustryChange
(
getBox5YearDateRange
(
y
));
renderBox5Chart
();
aiContentBox5
.
value
=
""
;
};
/** 请求 box5 折线图解读并更新 aiContentBox5(支持流式 SSE) */
const
fetchBox5ChartInterpretation
=
async
()
=>
{
const
v
=
box5ChartDisplayData
.
value
;
if
(
!
v
||
!
Array
.
isArray
(
v
.
title
)
||
v
.
title
.
length
===
0
||
!
Array
.
isArray
(
v
.
data
)
||
v
.
data
.
length
===
0
)
{
aiContentBox5
.
value
=
"暂无图表数据"
;
return
;
}
const
hasValidContent
=
aiContentBox5
.
value
&&
aiContentBox5
.
value
!==
"解读生成中…"
&&
aiContentBox5
.
value
!==
"解读加载失败"
&&
aiContentBox5
.
value
!==
"暂无图表数据"
;
if
(
hasValidContent
||
isBox5InterpretLoading
.
value
)
{
return
;
}
isBox5InterpretLoading
.
value
=
true
;
aiContentBox5
.
value
=
"解读生成中…"
;
const
chartPayload
=
{
type
:
"折线图"
,
name
:
"数量变化趋势"
,
data
:
v
.
title
.
map
((
label
,
i
)
=>
{
const
point
=
{
period
:
label
};
v
.
data
.
forEach
((
s
)
=>
{
point
[
s
.
name
]
=
s
.
value
[
i
]
??
0
;
});
return
point
;
})
};
try
{
const
text
=
await
getChartAnalysis
(
{
text
:
JSON
.
stringify
(
chartPayload
)
},
{
onInterpretationDelta
:
(
chunk
)
=>
{
if
(
chunk
)
{
aiContentBox5
.
value
=
chunk
;
}
}
}
);
aiContentBox5
.
value
=
text
||
"未返回有效解读内容"
;
}
catch
(
error
)
{
console
.
error
(
"图表解读请求失败"
,
error
);
aiContentBox5
.
value
=
"解读加载失败"
;
}
finally
{
isBox5InterpretLoading
.
value
=
false
;
}
};
// 政策建议领域分布
const
box6Data
=
ref
([
{
...
...
@@ -920,6 +1167,10 @@ const box6Data = ref([
]);
const
box6selectetedYear
=
ref
(
"2025"
);
const
box6YearList
=
ref
([
{
label
:
"2026"
,
value
:
"2026"
},
{
label
:
"2025"
,
value
:
"2025"
...
...
@@ -937,6 +1188,94 @@ const box6YearList = ref([
value
:
"2022"
}
]);
const
box6selectetedArea
=
ref
(
"全部领域"
);
/** 领域分布:有接口返回的有效分项时才展示图表(与 box5 空态一致) */
const
hasBox6ChartData
=
computed
(()
=>
{
const
list
=
Array
.
isArray
(
box6Data
.
value
)
?
box6Data
.
value
:
[];
return
list
.
length
>
0
;
});
/** 按所选领域得到饼图数据(全部领域 = 全部分块) */
const
box6PieDisplayData
=
computed
(()
=>
{
const
list
=
Array
.
isArray
(
box6Data
.
value
)
?
box6Data
.
value
:
[];
if
(
!
list
.
length
)
{
return
[];
}
if
(
box6selectetedArea
.
value
===
"全部领域"
)
{
return
list
;
}
const
filtered
=
list
.
filter
(
item
=>
item
.
name
===
box6selectetedArea
.
value
);
if
(
filtered
.
length
)
{
return
filtered
;
}
return
[{
name
:
"暂无该领域数据"
,
value
:
1
,
color
:
"#E0E0E0"
}];
});
/** 饼图解读用数据:排除占位项 */
const
getBox6InterpretationSeries
=
()
=>
{
const
pie
=
box6PieDisplayData
.
value
;
return
pie
.
filter
(
item
=>
item
.
name
!==
"暂无数据"
&&
item
.
name
!==
"暂无该领域数据"
);
};
/** 请求 box6 领域分布饼图解读(入参:{ text: JSON.stringify({ type, name, data }) },与柱状图示例同结构) */
const
fetchBox6ChartInterpretation
=
async
()
=>
{
const
series
=
getBox6InterpretationSeries
();
if
(
!
series
.
length
)
{
aiContentBox6
.
value
=
"暂无图表数据"
;
return
;
}
const
hasValidContent
=
aiContentBox6
.
value
&&
aiContentBox6
.
value
!==
"解读生成中…"
&&
aiContentBox6
.
value
!==
"解读加载失败"
&&
aiContentBox6
.
value
!==
"暂无图表数据"
;
if
(
hasValidContent
||
isBox6InterpretLoading
.
value
)
{
return
;
}
isBox6InterpretLoading
.
value
=
true
;
aiContentBox6
.
value
=
"解读生成中…"
;
const
chartPayload
=
{
type
:
"饼图"
,
name
:
"领域分布情况"
,
year
:
box6selectetedYear
.
value
,
data
:
series
.
map
(
item
=>
({
name
:
item
.
name
,
value
:
item
.
value
}))
};
try
{
const
text
=
await
getChartAnalysis
(
{
text
:
JSON
.
stringify
(
chartPayload
)
},
{
onInterpretationDelta
:
(
chunk
)
=>
{
if
(
chunk
)
{
aiContentBox6
.
value
=
chunk
;
}
}
}
);
aiContentBox6
.
value
=
text
||
"未返回有效解读内容"
;
}
catch
(
error
)
{
console
.
error
(
"领域分布图表解读请求失败"
,
error
);
aiContentBox6
.
value
=
"解读加载失败"
;
}
finally
{
isBox6InterpretLoading
.
value
=
false
;
}
};
const
box6AreaList
=
ref
([
]);
const
box6selectetedTank
=
ref
(
"全部智库"
);
const
box6TankList
=
ref
([
{
label
:
"兰德公司"
,
value
:
"兰德公司"
}
]);
function
transformToChartFormat
(
data
)
{
// 预设颜色池(可按需修改或扩展)
const
colorPalette
=
[
"#4096FF"
,
"#FFA39E"
,
"#ADC6FF"
,
"#FFC069"
,
"#B5F5EC"
,
"#B37FEB"
,
"#D6E4FF"
,
"#FF8C8C"
,
"#87E8DE"
];
...
...
@@ -965,52 +1304,49 @@ const handleGetThinkTankPolicyIndustry = async () => {
}
}
catch
(
error
)
{
console
.
error
(
"获取政策建议领域分布error"
,
error
);
box6Data
.
value
=
[];
}
};
const
renderBox6Chart
=
()
=>
{
if
(
!
hasBox6ChartData
.
value
)
{
return
;
}
const
pieData
=
box6PieDisplayData
.
value
;
if
(
!
pieData
.
length
)
{
return
;
}
const
box6Chart
=
getPieChart
(
pieData
);
setChart
(
box6Chart
,
"box6Chart"
);
};
const
handleBox6AreaChange
=
()
=>
{
aiContentBox6
.
value
=
""
;
renderBox6Chart
();
};
const
handleBox6
=
async
()
=>
{
box6selectetedArea
.
value
=
"全部领域"
;
aiContentBox6
.
value
=
""
;
await
handleGetThinkTankPolicyIndustry
();
const
box6Chart
=
getPieChart
(
box6Data
.
value
);
setChart
(
box6Chart
,
"box6Chart"
);
renderBox6Chart
();
};
// 智库资金流向
const
box7Data
=
ref
({
nodes
:
[
{
name
:
"美国"
,
itemStyle
:
{
color
:
"#1f5cb8"
}
},
{
name
:
"英国"
,
itemStyle
:
{
color
:
"#6f42c1"
}
},
{
name
:
"加拿大"
,
itemStyle
:
{
color
:
"#d9534f"
}
},
// 中间层
{
name
:
"美国能源部"
,
itemStyle
:
{
color
:
"#a4c639"
}
},
{
name
:
"美国财政部"
,
itemStyle
:
{
color
:
"#f06292"
}
},
{
name
:
"美国国务院"
,
itemStyle
:
{
color
:
"#b08d55"
}
},
{
name
:
"美国国际开发署"
,
itemStyle
:
{
color
:
"#4285f4"
}
},
{
name
:
"美国内政部"
,
itemStyle
:
{
color
:
"#8cae9e"
}
},
{
name
:
"美国国家民主基金会"
,
itemStyle
:
{
color
:
"#a1887f"
}
},
// 右侧层 (部分)
{
name
:
"麻省理工学院科技评论"
,
itemStyle
:
{
color
:
"#7cb342"
}
},
{
name
:
"麦肯锡全球研究院"
,
itemStyle
:
{
color
:
"#cddc39"
}
},
{
name
:
"卡内基国际和平研究院"
,
itemStyle
:
{
color
:
"#afb42b"
}
},
{
name
:
"战略与国际研究中心"
,
itemStyle
:
{
color
:
"#827717"
}
},
{
name
:
"兰德公司"
,
itemStyle
:
{
color
:
"#689f38"
}
}
],
links
:
[
{
source
:
"美国"
,
target
:
"美国能源部"
,
value
:
50
},
{
source
:
"美国"
,
target
:
"美国财政部"
,
value
:
40
},
{
source
:
"美国"
,
target
:
"美国国务院"
,
value
:
60
},
{
source
:
"美国"
,
target
:
"美国国际开发署"
,
value
:
20
},
{
source
:
"美国"
,
target
:
"美国内政部"
,
value
:
15
},
{
source
:
"英国"
,
target
:
"美国国务院"
,
value
:
10
},
{
source
:
"加拿大"
,
target
:
"美国能源部"
,
value
:
5
},
// Flow to right
{
source
:
"美国能源部"
,
target
:
"麻省理工学院科技评论"
,
value
:
20
},
{
source
:
"美国能源部"
,
target
:
"麦肯锡全球研究院"
,
value
:
25
},
{
source
:
"美国财政部"
,
target
:
"卡内基国际和平研究院"
,
value
:
30
},
{
source
:
"美国国务院"
,
target
:
"战略与国际研究中心"
,
value
:
40
},
{
source
:
"美国国务院"
,
target
:
"兰德公司"
,
value
:
15
}
]
nodes
:
[],
links
:
[]
});
/** 资金流向:有节点与连线时才展示桑基图 */
const
hasBox7ChartData
=
computed
(()
=>
{
const
raw
=
box7Data
.
value
;
if
(
!
raw
||
typeof
raw
!==
"object"
)
{
return
false
;
}
const
nodes
=
Array
.
isArray
(
raw
.
nodes
)
?
raw
.
nodes
:
[];
const
links
=
Array
.
isArray
(
raw
.
links
)
?
raw
.
links
:
[];
return
nodes
.
length
>
0
&&
links
.
length
>
0
;
});
// 遍历每个智库
...
...
@@ -1085,19 +1421,77 @@ const handleGetThinkTankDonation = async () => {
try
{
const
res
=
await
getThinkTankDonation
();
console
.
log
(
"智库资金流向"
,
res
.
data
,
transformDataToSankey
(
res
.
data
));
if
(
res
.
code
===
200
&&
res
.
data
)
{
if
(
res
.
code
===
200
&&
Array
.
isArray
(
res
.
data
)
)
{
box7Data
.
value
=
transformDataToSankey
(
res
.
data
);
}
else
{
box7Data
.
value
=
{
nodes
:
[],
links
:
[]
};
}
}
catch
(
error
)
{
console
.
error
(
"获取智库资金流向error"
,
error
);
box7Data
.
value
=
{
nodes
:
[],
links
:
[]
};
}
};
const
handleBox7
=
async
()
=>
{
aiContentBox7
.
value
=
""
;
await
handleGetThinkTankDonation
();
const
box7Chart
=
getSankeyChart
(
box7Data
.
value
.
nodes
,
box7Data
.
value
.
links
);
if
(
!
hasBox7ChartData
.
value
)
{
return
;
}
const
nodes
=
box7Data
.
value
?.
nodes
??
[];
const
links
=
box7Data
.
value
?.
links
??
[];
const
box7Chart
=
getSankeyChart
(
nodes
,
links
);
setChart
(
box7Chart
,
"box7Chart"
);
};
/** 请求 box7 智库资金流向桑基图解读(入参:{ text: JSON.stringify({ type, name, nodes, links }) }) */
const
fetchBox7ChartInterpretation
=
async
()
=>
{
const
raw
=
box7Data
.
value
;
const
nodes
=
Array
.
isArray
(
raw
?.
nodes
)
?
raw
.
nodes
:
[];
const
links
=
Array
.
isArray
(
raw
?.
links
)
?
raw
.
links
:
[];
if
(
!
nodes
.
length
&&
!
links
.
length
)
{
aiContentBox7
.
value
=
"暂无图表数据"
;
return
;
}
const
hasValidContent
=
aiContentBox7
.
value
&&
aiContentBox7
.
value
!==
"解读生成中…"
&&
aiContentBox7
.
value
!==
"解读加载失败"
&&
aiContentBox7
.
value
!==
"暂无图表数据"
;
if
(
hasValidContent
||
isBox7InterpretLoading
.
value
)
{
return
;
}
isBox7InterpretLoading
.
value
=
true
;
aiContentBox7
.
value
=
"解读生成中…"
;
const
chartPayload
=
{
type
:
"桑基图"
,
name
:
"智库资金流向"
,
nodes
:
nodes
.
map
(
n
=>
({
name
:
n
.
name
})),
links
:
links
.
map
(
l
=>
({
source
:
l
.
source
,
target
:
l
.
target
,
value
:
l
.
value
}))
};
try
{
const
text
=
await
getChartAnalysis
(
{
text
:
JSON
.
stringify
(
chartPayload
)
},
{
onInterpretationDelta
:
(
chunk
)
=>
{
if
(
chunk
)
{
aiContentBox7
.
value
=
chunk
;
}
}
}
);
aiContentBox7
.
value
=
text
||
"未返回有效解读内容"
;
}
catch
(
error
)
{
console
.
error
(
"智库资金流向图表解读请求失败"
,
error
);
aiContentBox7
.
value
=
"解读加载失败"
;
}
finally
{
isBox7InterpretLoading
.
value
=
false
;
}
};
// 智库研究热点
const
box8Data
=
ref
([
// {
...
...
@@ -1806,7 +2200,7 @@ onMounted(async () => {
handleGetNewReport
();
handleGetThinkTankRiskSignal
();
handleBox5
(
getDateMonthsAgo
(
24
)
);
handleBox5
(
box5selectetedYear
.
value
);
handleBox6
();
handleBox7
();
handleGetHylyList
();
...
...
@@ -2068,6 +2462,12 @@ onMounted(async () => {
height
:
36px
;
margin-top
:
64px
;
display
:
flex
;
justify-content
:
space-between
;
.time-tab-pane
{
width
:
248px
;
height
:
36px
;
}
.title-info
{
height
:
24px
;
...
...
@@ -2104,6 +2504,9 @@ onMounted(async () => {
display
:
flex
;
justify-content
:
space-between
;
gap
:
16px
;
position
:
relative
;
z-index
:
9999999
;
overflow
:
visible
;
.card
{
width
:
253px
;
...
...
@@ -2116,12 +2519,12 @@ onMounted(async () => {
transition
:
all
0
.3s
;
cursor
:
pointer
;
position
:
relative
;
// 让 red-info 按当前 card 自身定位
z-index
:
1
;
z-index
:
9999998
;
&
:hover
{
transform
:
translateY
(
-3px
);
box-shadow
:
0
4px
16px
rgba
(
0
,
0
,
0
,
0
.15
);
z-index
:
2
;
z-index
:
9999999
;
}
.red-info
{
...
...
@@ -2145,7 +2548,8 @@ onMounted(async () => {
padding
:
2px
8px
;
/* 左右留空隙,更美观 */
white-space
:
nowrap
white-space
:
nowrap
;
z-index
:
10000000
;
}
.card-header
{
...
...
@@ -3161,11 +3565,13 @@ onMounted(async () => {
}
.box5-select-box
{
width
:
12
0px
;
width
:
24
0px
;
height
:
28px
;
position
:
absolute
;
top
:
11px
;
right
:
31px
;
display
:
flex
;
gap
:
2px
;
}
}
...
...
@@ -3173,15 +3579,76 @@ onMounted(async () => {
height
:
412px
;
width
:
1063px
;
box-sizing
:
border-box
;
position
:
relative
;
padding
:
24px
24px
64px
24px
;
&
.box5-main--empty
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
24px
;
:deep
(
.el-empty__image
)
{
margin-bottom
:
0
;
}
}
.box5-el-empty
{
padding
:
0
;
margin
:
0
;
}
.box5-chart-row
{
display
:
flex
;
flex-direction
:
row
;
align-items
:
flex-start
;
gap
:
8px
;
width
:
100%
;
box-sizing
:
border-box
;
}
#box5Chart
{
height
:
340px
;
width
:
1063px
;
.box5-legend-nav-btn
{
flex-shrink
:
0
;
width
:
32px
;
height
:
32px
;
margin-top
:
6px
;
padding
:
0
;
box-sizing
:
border-box
;
border
:
1px
solid
rgba
(
220
,
223
,
230
,
1
);
border-radius
:
6px
;
background
:
rgba
(
255
,
255
,
255
,
1
);
color
:
rgba
(
59
,
65
,
75
,
1
);
font-size
:
20px
;
line-height
:
1
;
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
}
.box5-legend-nav-btn
:hover:not
(
:disabled
)
{
border-color
:
var
(
--
color-main-primary
,
#055fc2
);
color
:
var
(
--
color-main-primary
,
#055fc2
);
}
.box5-legend-nav-btn
:disabled
{
opacity
:
0
.35
;
cursor
:
not
-
allowed
;
}
.box5-chart-canvas
{
flex
:
1
;
min-width
:
0
;
height
:
324px
;
}
.source
{
margin
:
0
auto
;
margin-top
:
23px
;
position
:
absolute
;
bottom
:
21px
;
left
:
50%
;
/* 让元素左边对齐父级中线 */
transform
:
translateX
(
-50%
);
/* 上下0,左右自动居中 */
width
:
370px
;
...
...
@@ -3213,6 +3680,25 @@ onMounted(async () => {
margin-left
:
8px
;
}
}
.chart-box
{
position
:
absolute
;
right
:
0
;
bottom
:
18px
;
.btn-box
{
width
:
74px
;
height
:
28px
;
}
.content-box
{
width
:
1063px
;
position
:
absolute
;
right
:
0
;
bottom
:
-18px
;
}
}
}
}
...
...
@@ -3258,11 +3744,13 @@ onMounted(async () => {
}
.box6-select-box
{
width
:
12
0px
;
width
:
24
0px
;
height
:
28px
;
position
:
absolute
;
top
:
12px
;
right
:
25px
;
display
:
flex
;
gap
:
2px
}
}
...
...
@@ -3270,16 +3758,38 @@ onMounted(async () => {
width
:
520px
;
height
:
412px
;
box-sizing
:
border-box
;
padding
:
24px
24px
0
;
padding
:
24px
24px
64px
24px
;
position
:
relative
;
&
.box6-main--empty
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
24px
;
:deep
(
.el-empty__image
)
{
margin-bottom
:
0
;
}
}
.box6-el-empty
{
padding
:
0
;
margin
:
0
;
}
#box6Chart
{
width
:
4
46
px
;
height
:
3
09
px
;
width
:
4
72
px
;
height
:
3
24
px
;
}
.source
{
margin
:
0
auto
;
margin-top
:
23px
;
position
:
absolute
;
bottom
:
21px
;
left
:
50%
;
/* 让元素左边对齐父级中线 */
transform
:
translateX
(
-50%
);
/* 让元素自身向左回退一半宽度 */
/* 上下0,左右自动居中 */
width
:
370px
;
...
...
@@ -3311,6 +3821,25 @@ onMounted(async () => {
margin-left
:
8px
;
}
}
.chart-box
{
position
:
absolute
;
right
:
0
;
bottom
:
18px
;
.btn-box
{
width
:
74px
;
height
:
28px
;
}
.content-box
{
width
:
520px
;
position
:
absolute
;
right
:
0
;
bottom
:
-18px
;
}
}
}
...
...
@@ -3403,9 +3932,92 @@ onMounted(async () => {
}
.box7-main
{
width
:
1064px
;
height
:
412px
;
box-sizing
:
border-box
;
padding
:
24px
24px
0
;
padding
:
24px
24px
64px
24px
;
position
:
relative
;
&
.box7-main--empty
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
padding
:
24px
;
:deep
(
.el-empty__image
)
{
margin-bottom
:
0
;
}
}
.box7-el-empty
{
padding
:
0
;
margin
:
0
;
}
#box7Chart
{
width
:
1016px
;
/* 填满宽度 */
height
:
324px
;
/* 填满高度 */
}
.source
{
position
:
absolute
;
bottom
:
21px
;
left
:
50%
;
/* 让元素左边对齐父级中线 */
transform
:
translateX
(
-50%
);
/* 上下0,左右自动居中 */
height
:
22px
;
display
:
flex
;
.info
{
width
:
16px
;
height
:
16px
;
margin-top
:
3px
;
img
{
width
:
100%
;
height
:
100%
;
}
}
.text
{
font-family
:
"Source Han Sans CN"
;
font-weight
:
400
;
font-size
:
14px
;
line-height
:
22px
;
letter-spacing
:
0px
;
text-align
:
left
;
color
:
rgb
(
132
,
136
,
142
);
margin-left
:
8px
;
}
}
.chart-box
{
position
:
absolute
;
right
:
0
;
bottom
:
18px
;
.btn-box
{
width
:
74px
;
height
:
28px
;
}
.content-box
{
width
:
1063px
;
position
:
absolute
;
right
:
0
;
bottom
:
-18px
;
}
}
}
}
...
...
@@ -3464,80 +4076,85 @@ onMounted(async () => {
}
.box8-main
{
margin
:
0
auto
;
margin-top
:
5px
;
width
:
452px
;
height
:
382px
;
overflow
:
hidden
;
.box8-item
{
margin-top
:
14px
;
height
:
412px
;
.box8-main-item
{
margin
:
0
auto
;
margin-top
:
5px
;
width
:
452px
;
height
:
24
px
;
display
:
flex
;
height
:
382
px
;
overflow
:
hidden
;
.item-left
{
width
:
20px
;
.box8-item
{
margin-top
:
14px
;
width
:
452px
;
height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
font-family
:
"Source Han Sans CN"
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
center
;
}
display
:
flex
;
.item-center
{
margin-left
:
20px
;
width
:
425px
;
height
:
24px
;
color
:
rgb
(
59
,
65
,
75
);
font-family
:
"Source Han Sans CN"
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
left
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.item-left
{
width
:
20px
;
height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
font-family
:
"Source Han Sans CN"
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
center
;
}
.item-count
{
height
:
24px
;
width
:
92px
;
text-align
:
right
;
font-family
:
"Source Han Sans CN"
;
color
:
rgb
(
95
,
101
,
108
)
}
.item-center
{
margin-left
:
20px
;
width
:
425px
;
height
:
24px
;
color
:
rgb
(
59
,
65
,
75
);
font-family
:
"Source Han Sans CN"
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
left
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
white-space
:
nowrap
;
}
.itemBold1
{
color
:
rgba
(
206
,
79
,
81
,
1
);
font-weight
:
700
;
}
.item-count
{
height
:
24px
;
width
:
92px
;
text-align
:
right
;
font-family
:
"Source Han Sans CN"
;
color
:
rgb
(
95
,
101
,
108
)
}
.itemBold2
{
color
:
rgba
(
255
,
169
,
64
,
1
);
font-weight
:
700
;
}
.itemBold1
{
color
:
rgba
(
206
,
79
,
81
,
1
);
font-weight
:
700
;
}
.itemBold3
{
color
:
rgba
(
255
,
197
,
61
,
1
);
font-weight
:
700
;
}
.itemBold2
{
color
:
rgba
(
255
,
169
,
64
,
1
);
font-weight
:
700
;
}
.item-right
{
width
:
92px
;
height
:
24px
;
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
right
;
cursor
:
pointer
;
.itemBold3
{
color
:
rgba
(
255
,
197
,
61
,
1
);
font-weight
:
700
;
}
.item-right
{
width
:
92px
;
height
:
24px
;
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
right
;
cursor
:
pointer
;
}
}
}
}
...
...
src/views/thinkTank/utils/multiLineChart.js
浏览文件 @
6714a4e9
import
*
as
echarts
from
'echarts'
;
const
getMultiLineChart
=
(
data
)
=>
{
/** 图例分页:每页展示的图例项数量(box5 数量变化趋势) */
export
const
MULTILINE_LEGEND_SHOW_COUNT
=
11
;
/**
* @param {{ title: unknown[], data: Array<{ name: string, value: unknown[], color?: string }> }} data
* @param {{ legendShowCount?: number, legendPageIndex?: number }} [options]
*/
const
getMultiLineChart
=
(
data
,
options
=
{})
=>
{
// 提取标题和系列数据
const
title
=
data
.
title
const
series
=
data
.
data
const
legendShowCount
=
typeof
options
.
legendShowCount
===
'number'
&&
options
.
legendShowCount
>
0
?
options
.
legendShowCount
:
MULTILINE_LEGEND_SHOW_COUNT
const
rawPageIndex
=
Number
(
options
.
legendPageIndex
)
||
0
const
allNames
=
series
.
map
((
item
)
=>
item
.
name
)
const
pageCount
=
Math
.
max
(
1
,
Math
.
ceil
(
allNames
.
length
/
legendShowCount
))
const
legendPageIndex
=
Math
.
min
(
Math
.
max
(
0
,
rawPageIndex
),
pageCount
-
1
)
const
legendStart
=
legendPageIndex
*
legendShowCount
const
legendData
=
allNames
.
slice
(
legendStart
,
legendStart
+
legendShowCount
)
// 定义配色数组
const
colorList
=
[
'rgba(5, 95, 194, 1)'
,
// #055fc2
...
...
@@ -48,6 +66,7 @@ const getMultiLineChart = (data) => {
return
({
name
:
item
.
name
,
type
:
'line'
,
smooth
:
true
,
// 新增/优化:面积填充渐变效果
areaStyle
:
{
color
:
new
echarts
.
graphic
.
LinearGradient
(
0
,
0
,
0
,
1
,
[
...
...
@@ -80,8 +99,9 @@ const getMultiLineChart = (data) => {
}
}
},
/* 顶部预留足够空间:多行图例 + Y 轴标题「数量」在纵轴顶端,避免与图例重合 */
grid
:
{
top
:
'
15
%'
,
top
:
'
28
%'
,
right
:
'5%'
,
bottom
:
'5%'
,
left
:
'5%'
,
...
...
@@ -89,10 +109,12 @@ const getMultiLineChart = (data) => {
},
legend
:
{
show
:
true
,
top
:
10
,
type
:
'plain'
,
data
:
legendData
,
top
:
8
,
left
:
'center'
,
icon
:
'circle'
,
textStyle
:
{
textStyle
:
{
fontFamily
:
'Source Han Sans CN'
,
// 字体
fontWeight
:
400
,
// 字重值(Regular对应400)
fontSize
:
14
,
// 字号
...
...
@@ -112,6 +134,22 @@ const getMultiLineChart = (data) => {
yAxis
:
[
{
type
:
'value'
,
name
:
'数量'
,
/* 纵轴:end = 坐标轴最大值一端,即竖轴上方 */
nameLocation
:
'end'
,
/* 在默认基础上再向左约 20px:nameGap 为轴标题与轴线间距(左侧 Y 轴顶端时增大则标题更靠左) */
nameGap
:
20
,
nameRotate
:
0
,
nameTextStyle
:
{
color
:
'rgba(170, 173, 177, 1)'
,
fontFamily
:
'Source Han Sans CN'
,
fontWeight
:
400
,
fontSize
:
14
,
lineHeight
:
22
,
letterSpacing
:
0
,
align
:
'right'
,
verticalAlign
:
'bottom'
},
splitLine
:
{
show
:
true
,
// 显示网格线
lineStyle
:
{
...
...
src/views/thinkTank/utils/piechart.js
浏览文件 @
6714a4e9
...
...
@@ -10,7 +10,8 @@ const getPieChart = (data) => {
// left: '15%',
// containLabel: true
// },
radius
:
[
70
,
100
],
// 使用百分比半径,避免固定像素导致饼图“看起来没铺满容器”
radius
:
[
'45%'
,
'60%'
],
height
:
'100%'
,
left
:
'center'
,
width
:
'100%'
,
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论