提交 eb3bba82 authored 作者: yanpeng's avatar yanpeng

Merge branch 'pre' into yp-dev

流水线 #106 已失败 于阶段
in 37 分 40 秒
---
description:
alwaysApply: true
---
# Overview
Insert overview text here. The agent will only see this should they choose to apply the rule.
......
......@@ -2,10 +2,13 @@ stages:
- build
- deploy
# cache:
# key: "$CI_COMMIT_REF_SLUG"
# paths:
# - .npm/
cache:
# cache:key 这里使用字符串,兼容性更好(部分 linter 不支持 key: { files: [...] })
# 预分支 pre 需要快速构建并实时同步,因此让 .npm 下载缓存跨分支复用
key: "npm-cache-global"
paths:
- .npm/
policy: pull-push
build_pre:
stage: build
......@@ -17,8 +20,13 @@ build_pre:
script:
- node -v
- npm -v
- npm config set cache .npm --global
- npm ci --prefer-offline --no-audit --no-fund
- echo "cache dir sizes:"
- du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true
- echo "=== npm ci start ==="
- date -Iseconds
- npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ==="
- date -Iseconds
- npm run build
artifacts:
paths:
......@@ -35,5 +43,55 @@ deploy_pre:
dependencies:
- build_pre
script:
- apk add --no-cache rsync
- rsync -av --delete dist/ /nas/kjb_service/zm/pre-project/html/
\ No newline at end of file
- apk add --no-cache rsync curl jq
# 只允许“最新一次 pre pipeline”部署到 nginx(加二次确认,避免短时间多次推送导致重复 rsync)
- >
LATEST_PIPELINE_ID="$(
curl --silent --show-error --fail
--header "JOB-TOKEN: $CI_JOB_TOKEN"
"$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/pipelines?ref=pre&order_by=id&sort=desc&per_page=1"
| jq -r '.[0].id'
)"
- >
if [ -z "$LATEST_PIPELINE_ID" ] || [ "$LATEST_PIPELINE_ID" != "$CI_PIPELINE_ID" ]; then
echo "skip deploy: not latest pipeline (latest=$LATEST_PIPELINE_ID current=$CI_PIPELINE_ID)";
exit 0;
fi
- sleep 20
- >
LATEST_PIPELINE_ID="$(
curl --silent --show-error --fail
--header "JOB-TOKEN: $CI_JOB_TOKEN"
"$CI_SERVER_URL/api/v4/projects/$CI_PROJECT_ID/pipelines?ref=pre&order_by=id&sort=desc&per_page=1"
| jq -r '.[0].id'
)"
- >
if [ -z "$LATEST_PIPELINE_ID" ] || [ "$LATEST_PIPELINE_ID" != "$CI_PIPELINE_ID" ]; then
echo "skip deploy: not latest pipeline after debounce (latest=$LATEST_PIPELINE_ID current=$CI_PIPELINE_ID)";
exit 0;
fi
- rsync -avz --delete dist/ /nas/kjb_service/zm/pre-project/html/
# 非 protected 分支:push 时先做 build 校验(避免合并 pre 时出现 build 报错)
build_check:
stage: build
image: node:20-bullseye
tags:
- risk-monitor-frontend
# 只在 push 时做构建校验,且排除 protected 分支与目标分支 pre
only:
- pushes
except:
- pre
- protected_branches
script:
- node -v
- npm -v
- echo "cache dir sizes:"
- du -sh "$CI_PROJECT_DIR/.npm" "$CI_PROJECT_DIR/.npm/_cacache" 2>/dev/null || true
- echo "=== npm ci start ==="
- date -Iseconds
- npm ci --cache "$CI_PROJECT_DIR/.npm" --no-audit --no-fund --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ==="
- date -Iseconds
- npm run build
\ No newline at end of file
差异被折叠。
......@@ -36,7 +36,7 @@
"json5": "^2.2.3",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449",
"pdfjs-dist": "^3.11.174",
"pinia": "^3.0.4",
"vue": "^3.4.0",
"vue-router": "^4.2.5"
......
......@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) {
return String(m[1]).replace(/\\n/g, "\n").trim();
}
/**
* 从流式累积 buffer 中提取「解读」字符串的已生成部分(滤掉 ```json、[、{ 等外壳,避免界面出现 json\n[\n)
* 支持未闭合的字符串(流式进行中)
* @param {string} buffer
* @returns {string}
*/
function extractStreamingInterpretationFromBuffer(buffer) {
const s = String(buffer || "");
let rest = s.replace(/^\uFEFF/, "");
const fence = rest.match(/^```(?:json)?\s*/i);
if (fence) {
rest = rest.slice(fence[0].length);
}
const keyRe =
/["'](?:解读|interpretation|analysis|content)["']\s*:\s*"/i;
const m = rest.match(keyRe);
if (!m) {
return "";
}
let pos = m.index + m[0].length;
let out = "";
while (pos < rest.length) {
const ch = rest[pos];
if (ch === '"') {
break;
}
if (ch === "\\") {
pos += 1;
if (pos >= rest.length) {
break;
}
const esc = rest[pos];
if (esc === "n") {
out += "\n";
} else if (esc === "r") {
out += "\r";
} else if (esc === "t") {
out += "\t";
} else if (esc === '"' || esc === "\\") {
out += esc;
} else {
out += esc;
}
pos += 1;
continue;
}
out += ch;
pos += 1;
}
return out;
}
/**
* 图表解读(SSE 流式)
* @param {object} data - 请求体
......@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) {
return new Promise((resolve, reject) => {
let buffer = "";
let latestInterpretation = "";
/** 已推给前端的「解读」正文长度,用于只增量回调 onChunk */
let lastStreamedInterpretationLen = 0;
let settled = false;
const abortController = new AbortController();
......@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) {
return;
}
// 每收到一条消息即回调,用于流式渲染
// 流式渲染:不把 ```json、[、{ 等 markdown/JSON 外壳拼到界面
if (chunk && onDelta) {
onDelta(chunk);
let parsedMsg = null;
try {
parsedMsg = JSON.parse(raw);
} catch (_) {
parsedMsg = null;
}
const isReasoningChunk =
parsedMsg &&
typeof parsedMsg === "object" &&
parsedMsg.type === "reasoning" &&
typeof parsedMsg.chunk === "string";
if (isReasoningChunk) {
onDelta(parsedMsg.chunk);
} else {
const visible = extractStreamingInterpretationFromBuffer(buffer);
if (visible.length > lastStreamedInterpretationLen) {
onDelta(
visible.slice(lastStreamedInterpretationLen)
);
lastStreamedInterpretationLen = visible.length;
}
}
}
// 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
......
......@@ -3,9 +3,9 @@ import request from "@/api/request.js";
// 最新科技政令
export function getDepartmentList(params) {
return request({
method: 'GET',
method: 'POST',
url: `/api/administrativeDict/department`,
params
data: params
})
}
......@@ -99,15 +99,6 @@ export function getKeyOrganization(params) {
})
}
// 所有机构
export function getAllOrganization(params) {
return request({
method: 'POST',
url: `/api/administrativeOrderOverview/orderCount`,
data: params
})
}
// AI智能总结
export function getChartInterpretation(params) {
return request({
......
......@@ -33,7 +33,7 @@ export function getDecreeEntities(params) {
export function getDecreeRelatedChain(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChain/${params.id}`,
url: `/api/chain/relatedChain/${params.id}`,
})
}
......@@ -41,7 +41,7 @@ export function getDecreeRelatedChain(params) {
export function getDecreeChainNodes(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/relatedChainNodes/${params.id}`,
url: `/api/chain/relatedChainNodes/${params.id}`,
})
}
......@@ -49,7 +49,8 @@ export function getDecreeChainNodes(params) {
export function getDecreeRelatedEntitie(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/listRelatedEntitie/${params.id}`,
url: `/api/organization/shareholding`,
params
})
}
......
......@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) {
params: {
startDate: params.startDate,
endDate: params.endDate
}
},
// 无数据年份(如 2026)后端可能返回 HTTP 400/500,避免走全局错误提示
validateStatus: (status) =>
(status >= 200 && status < 300) || status === 400 || status === 500
});
}
......@@ -286,8 +289,7 @@ 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({
......
......@@ -3,65 +3,66 @@ import request from "@/api/request.js";
// 中美博弈概览V2:最新风险动态统计
export function getLatestRiskUpdates(params) {
return request({
method: "GET",
method: 'GET',
url: `/api/rivalryIndexV2/LatestRiskUpdates`,
params: params
});
})
}
// 中美博弈概览V2:最新风险信号
export function getLatestRisks() {
return request({
method: "GET",
url: `/api/rivalryIndexV2/LatestRisks`
});
method: 'GET',
url: `/api/rivalryIndexV2/LatestRisks`,
})
}
// 中美博弈概览V2:美对华制裁措施数量趋势
export function geDomainContainmentTrend(params) {
return request({
method: "GET",
method: 'GET',
url: `/api/rivalryIndexV2/DomainContainmentTrend`,
params: params
});
})
}
// 中美博弈概况:获取榜单字典
export function getChartDict() {
return request({
method: "GET",
url: `/api/union/summary/chartDict`
});
method: 'GET',
url: `/api/union/summary/chartDict`,
})
}
// 根据字典信息,获取年份信息
export function getYearDict(id) {
return request({
method: "GET",
url: `/api/union/summary/chartYear/${id}`
});
}
// 中美博弈概况:中美科技实力对比
export function getCompare(id, year) {
export function getCompare(id) {
return request({
method: "GET",
url: `/api/union/summary/compare/${id}/${year}`
});
method: 'GET',
url: `/api/union/summary/compare/${id}`,
})
}
// 中美博弈分析
export function getTechnologyGameAnalysis() {
return request({
method: "GET",
url: `/api/rivalryIndexV2/TechnologyGameAnalysis`
});
method: 'GET',
url: `/api/rivalryIndexV2/TechnologyGameAnalysis`,
})
}
//中美博弈概览V7:美国政府部门对华制裁最新动态
export function getGovernmentSanctionsDynamics() {
return request({
method: "GET",
url: `/api/rivalryIndex/governmentSanctionsDynamics`
});
method: 'GET',
url: `/api/rivalryIndex/governmentSanctionsDynamics`,
})
}
\ No newline at end of file
......@@ -35,7 +35,7 @@ const tipText = computed(() => props.text || `数据来源:${props.dataSource}
width: 100%;
display: flex;
gap: 8px;
justify-content: center;
justify-content: flex-start;
align-items: center;
height: 22px;
......
......@@ -18,15 +18,6 @@
>
<el-option :value="value.id" :label="value.name" v-for="(value, index) in originList" :key="index" />
</el-select>
<el-select
class="select-item"
size="default"
style="margin-left: 15px; width: 200px; height: 32px"
v-model="year"
@change="handleGetCompare()"
>
<el-option :value="value" :label="value" v-for="(value, index) in yearList" :key="index" />
</el-select>
</div>
</div>
<div style="display: flex; height: 650px; width: 100%; padding-top: 12px">
......@@ -246,7 +237,7 @@ import Echarts from "@/components/Chart/index.vue";
import mockData from "./mock.json";
import radarChart from "./radarChart3.js";
import { getCompare, getChartDict, getYearDict, getTechnologyGameAnalysis } from "@/api/zmOverview/risk/index.js";
import { getCompare, getChartDict, getTechnologyGameAnalysis } from "@/api/zmOverview/risk/index.js";
import icon1 from "./icon/btn-icon-0.png";
import icon2 from "./icon/btn-icon-1.png";
import icon3 from "./icon/btn-icon-2.png";
......@@ -360,27 +351,10 @@ const handleGetChartDict = async () => {
console.error("获取数据来源error", error);
}
};
const yearList = ref([]);
const year = ref("");
//年份
const handleGetYearDict = async () => {
try {
const res = await getYearDict(origin.value);
console.log("年份", res);
if (res.code === 200 && res.data) {
yearList.value = res.data;
year.value = res.data[0];
}
} catch (error) {
console.error("获取年份error", error);
}
};
//中美科技实力对比
const handleGetCompare = async () => {
try {
const res = await getCompare(origin.value, year.value);
const res = await getCompare(origin.value);
console.log("中美科技实力对比", res);
if (res.code === 200 && res.data) {
tableData.value = res.data[0].children;
......@@ -418,7 +392,6 @@ const handlegetTechnologyGameAnalysis = async () => {
};
onMounted(async () => {
await handleGetChartDict();
await handleGetYearDict();
await handleGetCompare();
await handlegetTechnologyGameAnalysis();
// const dom = document.getElementById("char");
......@@ -593,7 +566,6 @@ const lineOption = ref({
},
yAxis: {
type: "value",
min: 77,
// name: "指数",
nameLocation: "top",
nameGap: 35,
......
......@@ -37,7 +37,7 @@
<fourSuppress></fourSuppress>
<!-- 中美博弈概况 -->
<commonTitle id="zm-overview" title="中美博弈概况" style="margin-top: 64px; margin-bottom: 36px"></commonTitle>
<gameProfile />
<gameProfile></gameProfile>
<div class="bottom-info">
<div class="info-item">
<div class="info-item-left">
......
......@@ -384,7 +384,6 @@
</div>
</div> -->
<AnalysisBox title="投票分析">
<div class="analysis-ai-wrapper analysis-ai-wrapper--box3">
<div class="vote-legend">
<div class="vote-legend-item">
<span class="vote-legend-dot agree"></span>
......@@ -395,6 +394,7 @@
<span>反对票</span>
</div>
</div>
<div class="analysis-ai-wrapper analysis-ai-wrapper--box3">
<div class="box3-main" :class="{ 'box3-main--full': !voteFooterText }">
<div class="box3-main-center">
<div class="box3-main-center-header">
......@@ -405,14 +405,15 @@
<div class="box3-main-center-header-box5">
倒戈人数<span style="font-weight: normal; display: inline-block">(平均区间)</span>
</div>
<div class="box3-main-center-header-box6">关键议员</div>
</div>
<div class="box3-main-center-content">
<div class="box3-main-center-content-box" v-for="item in voteAnalysisList" :key="item.actionId">
<div class="box3-main-center-content-box" v-for="item in voteAnalysisList"
:key="item.actionId">
<div class="item">
<div class="item-box1">
<div class="box1-left">
<div style="width: 100%; display: flex; flex-direction: column; align-items: flex-end">
<div
style="width: 100%; display: flex; flex-direction: column; align-items: flex-end">
<div class="name nameBlod" :title="item.actionTitle" style="
white-space: nowrap;
overflow: hidden;
......@@ -428,33 +429,34 @@
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="Number(item.agreePercent)" :show-text="false" color="rgb(33, 129, 57)">
<el-progress :percentage="Number(item.agreePercent)"
:show-text="false" color="rgb(33, 129, 57)">
</el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="Number(item.againstPercent)" :show-text="false" color="rgb(206, 79, 81)">
<el-progress :percentage="Number(item.againstPercent)"
:show-text="false" color="rgb(206, 79, 81)">
</el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.agreeCount + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.againstCount + "票" }}</div>
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.agreeCount +
"票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.againstCount
+ "票" }}</div>
</div>
<div class="item-box3">
<div class="box3-1"></div>
<div class="box3-2"></div>
</div>
<div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.agreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.againstPercent + "%" }}</div>
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.agreePercent
+ "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{
item.againstPercent + "%" }}</div>
</div>
<div class="item-box5"></div>
<div class="item-box6">
<el-icon size="20" color="#555">
<ArrowDownBold />
</el-icon>
</div>
</div>
<div class="item">
<div class="item-box1">
......@@ -466,31 +468,33 @@
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="Number(item.dagreePercent)" :show-text="false" color="rgb(33, 129, 57)">
<el-progress :percentage="Number(item.dagreePercent)"
:show-text="false" color="rgb(33, 129, 57)">
</el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="Number(item.dagainstPercent)" :show-text="false" color="rgb(206, 79, 81)">
<el-progress :percentage="Number(item.dagainstPercent)"
:show-text="false" color="rgb(206, 79, 81)">
</el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.dagreeCount + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.dagainstCount + "票" }}</div>
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.dagreeCount
+ "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{
item.dagainstCount + "票" }}</div>
</div>
<div class="item-box3"></div>
<div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.dagreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.dagainstPercent + "%" }}</div>
<div class="box4-1" style="color: rgb(33, 129, 57)">{{
item.dagreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{
item.dagainstPercent + "%" }}</div>
</div>
<div class="item-box5">
<div class="box5-1" style="color: #ce4f51">{{ item.dreverseCount + "人" }}</div>
</div>
<div class="item-box6">
<div class="img-box" v-if="item.dpersonImageUrl">
<img :src="item.dpersonImageUrl" alt="" />
</div>
<div class="box5-1" style="color: #ce4f51">{{ item.dreverseCount + "人"
}}</div>
</div>
</div>
<div class="item">
......@@ -503,31 +507,33 @@
</div>
<div class="box1-right">
<div class="box1-right-top">
<el-progress :percentage="Number(item.ragreePercent)" :show-text="false" color="rgb(33, 129, 57)">
<el-progress :percentage="Number(item.ragreePercent)"
:show-text="false" color="rgb(33, 129, 57)">
</el-progress>
</div>
<div class="box1-right-bottom">
<el-progress :percentage="Number(item.ragainstPercent)" :show-text="false" color="rgb(206, 79, 81)">
<el-progress :percentage="Number(item.ragainstPercent)"
:show-text="false" color="rgb(206, 79, 81)">
</el-progress>
</div>
</div>
</div>
<div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.ragreeCount + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.ragainstCount + "票" }}</div>
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.ragreeCount
+ "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{
item.ragainstCount + "票" }}</div>
</div>
<div class="item-box3"></div>
<div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.ragreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.ragainstPercent + "%" }}</div>
<div class="box4-1" style="color: rgb(33, 129, 57)">{{
item.ragreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{
item.ragainstPercent + "%" }}</div>
</div>
<div class="item-box5">
<div class="box5-1" style="color: #ce4f51">{{ item.rreverseCount + "人" }}</div>
</div>
<div class="item-box6">
<div class="img-box" v-if="item.rpersonImageUrl">
<img :src="item.rpersonImageUrl" alt="" />
</div>
<div class="box5-1" style="color: #ce4f51">{{ item.rreverseCount + "人"
}}</div>
</div>
</div>
</div>
......@@ -701,7 +707,8 @@
<TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box3')" />
</div>
<div v-if="aiPaneVisible.box3" class="analysis-ai-pane" @mouseleave="handleHideAiPane('box3')">
<div v-if="aiPaneVisible.box3" class="analysis-ai-pane"
@mouseleave="handleHideAiPane('box3')">
<AiPane :aiContent="overviewAiContent.box3" />
</div>
</div>
......@@ -1248,6 +1255,7 @@ onMounted(async () => {
.box1 {
width: 792px;
height: 415px;
.box1-main {
height: 368px;
......@@ -1393,6 +1401,7 @@ onMounted(async () => {
margin-top: 17px;
width: 792px;
height: 415px;
.box2-main {
height: 359px;
......@@ -1640,6 +1649,7 @@ onMounted(async () => {
margin-top: 16px;
width: 792px;
height: 847px;
.box3 {
width: 100%;
height: 100%;
......@@ -1647,7 +1657,7 @@ onMounted(async () => {
.vote-legend {
position: absolute;
top: 15px;
top: 10px;
left: 50%;
transform: translateX(-50%);
display: inline-flex;
......@@ -1735,12 +1745,6 @@ onMounted(async () => {
min-width: 0;
text-align: center;
}
.box3-main-center-header-box6 {
flex: 90 1 0;
min-width: 0;
text-align: center;
}
}
.box3-main-center-content {
......@@ -1976,23 +1980,6 @@ onMounted(async () => {
line-height: 14px;
}
}
.item-box6 {
flex: 90 1 0;
min-width: 0;
text-align: center;
.img-box {
width: 30px;
height: 30px;
margin-left: 30px;
img {
width: 100%;
height: 100%;
}
}
}
}
}
}
......
......@@ -417,11 +417,12 @@ onMounted(() => {
}
.risk-signal-pane-top {
width: 1600px;
// width: 1600px;
margin-top: 16px;
margin-right: 18px;
height: 116px;
padding-bottom: 16px;
min-height: 116px;
height: auto;
flex-shrink: 0;
}
......
......@@ -65,8 +65,8 @@
@clickChartItem="handleSwitchActiveChart">
<template #chart-box>
<LineChart v-if="activeChart === '折线图'" :lineChartData="curChartData" />
<PieChart v-if="activeChart === '饼状图'" :pieChartData="curChartData" />
<BarChart v-if="activeChart === '柱状图'" :barChartData="curChartData" />
<PieChart v-if="activeChart === '饼状图'" :pieChartData="curChartData" />
<RaderChart v-if="activeChart === '雷达图'" :radarChartData="curChartData" />
</template>
</ChartContainer>
......@@ -200,6 +200,12 @@ const staticsDemensionList = ref([
data: {
dataX: [],
dataY: []
},
quatarData: {
},
yearData: {
}
},
{
......
......@@ -113,7 +113,7 @@
<el-select v-model="isSort" placeholder="发布时间" style="width: 166px" @change="handlePxChange">
<template #prefix>
<div style="display: flex; align-items: center; height: 100%">
<img src="../../assets/icons/desc-icon.svg" style="width: 14px; height: 14px" />
<img src="../assets/icons/desc-icon.svg" style="width: 14px; height: 14px" />
</div>
</template>
<el-option v-for="item in releaseTimeList" :key="item.value" :label="item.label" :value="item.value" />
......@@ -204,6 +204,12 @@ const staticsDemensionList = ref([
data: {
dataX: [],
dataY: []
},
quatarData:{
},
yearData: {
}
},
{
......
......@@ -30,12 +30,12 @@
<div class="organization-list" ref="refOrganization" v-loading="organizationInfo.loading">
<div class="organization-item" v-for="(item, index) in organizationInfo.list" :key="index" @click="handleToInstitution(item)">
<div class="item-left">
<img :src="item.imgUrl || DefaultIcon2" alt="" />
<img :src="item.orgImage || DefaultIcon2" alt="" />
</div>
<div class="item-right one-line-ellipsis">{{ item.orgName }}</div>
<div class="item-total">{{ item.totalOrderNum }}项</div>
<div class="item-total">{{ item.total }}项</div>
<el-icon color="var(--color-primary-100)"><ArrowRightBold /></el-icon>
<div class="item-dot" v-if="item.recentOrderNum">+{{item.recentOrderNum}}</div>
<div class="item-dot" v-if="item.totalRecent">+{{item.totalRecent}}</div>
</div>
</div>
<div class="pagination-box">
......@@ -56,7 +56,7 @@ import { Search } from '@element-plus/icons-vue'
import router from "@/router";
import TimeTabPane from '@/components/base/TimeTabPane/index.vue';
import { getAllOrganization } from "@/api/decree/home";
import { getDepartmentList } from "@/api/decree/home";
import tipsTcon from "./assets/icons/tips-icon.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png";
......@@ -77,7 +77,7 @@ const onAllOrganization = async (num) => {
organizationInfo.loading = true
try {
let {keyWord, pageNum, pageSize, day} = organizationInfo
const res = await getAllOrganization({day, pageNum:pageNum-1, pageSize, keyWord: keyWord||undefined});
const res = await getDepartmentList({day, pageNum:pageNum-1, pageSize, keyWord: keyWord||undefined});
console.log("机构列表", res);
if (res.code === 200) {
organizationInfo.list = res.data.orgList || [];
......
......@@ -100,7 +100,7 @@ import {
getDecreeRelatedEntitie
} from "@/api/decree/influence";
import ChartChain from "./com/ChartChain.vue";
import AiTips from "./com/AiTips.vue";
// import AiTips from "./com/AiTips.vue";
import GraphChart from "@/components/base/GraphChart/index.vue";
import defaultIcon2 from "@/assets/icons/default-icon2.png";
import noticeIcon from "./assets/images/notice-icon.png";
......@@ -186,7 +186,7 @@ const headerChartData = (row) => {
onDecreeRelatedChain(row.id)
break;
case 2:
onDecreeRelatedEntitie(row.id)
onDecreeRelatedEntitie(row.orgId)
break;
}
}
......@@ -247,14 +247,17 @@ const graphInfo = reactive({
nodes: [],
links: [],
});
const onDecreeRelatedEntitie = async (id) => {
const onDecreeRelatedEntitie = async (orgId) => {
try {
const res = await getDecreeRelatedEntitie({ id });
const res = await getDecreeRelatedEntitie({ orgId, rule:false, withSanInfo:false });
console.log("实体关系:", res);
if (res.code === 200) {
graphInfo.links = res.data.map(onFormatLink)
graphInfo.nodes = res.data.map(onFormatNode)
graphInfo.nodes.unshift(onFormatNode(entityInfo.node))
let arr1 = res.data.parentOrgList.map(item => ({ ...item, level: 1 }))
let arr3 = res.data.childrenOrgList.map(item => ({ ...item, level: 3 }))
graphInfo.links = [...arr1,...arr3].map(onFormatLink)
graphInfo.nodes = [...arr1,...arr3].map(onFormatNode)
graphInfo.nodes.unshift(onFormatNode({name:res.data.orgName, id:res.data.orgId}, -1))
}
} catch (error) {
console.log("获取实体关系失败", error);
......@@ -262,17 +265,18 @@ const onDecreeRelatedEntitie = async (id) => {
}
const onFormatLink = (item, index) => {
return {
id: `link-${index+1}`,
source: item.id+'', target: entityInfo.id+'',
label: { show: true, color: "#055fc2", backgroundColor: "#eef7ff", borderWidth: 0, offset: [0, 15], formatter: item.relation },
id: `link-${index}-${item.id}`,
target: item.level==3 ? `${index}-${item.id}` : `-1-${entityInfo.node.orgId}`,
source: item.level==3 ? `-1-${entityInfo.node.orgId}` : `${index}-${item.id}`,
label: { show: true, color: "#055fc2", backgroundColor: "#eef7ff", borderWidth: 0, offset: [0, 15], formatter: item.description },
lineStyle: { color: '#B9DCFF', type: "solid", opacity: 1 }
}
}
const onFormatNode = (item) => {
let leader = item.id == entityInfo.id;
const onFormatNode = (item, index) => {
let leader = item.id == entityInfo.node.orgId;
return {
id: item.id+'',
name: onWordWrap(item.companyName, 7),
id: `${index}-${item.id}`,
name: onWordWrap(item.name, 7),
label: {
show: true,
color: "#3b414b",
......
......@@ -368,29 +368,19 @@
</el-col>
</el-row>
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 540px; margin-top: 16px">
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 505px; margin-top: 16px">
<el-col :span="8">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="520px">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="480px">
<template #header-right>
<el-checkbox v-model="domainChecked" label="50%规则" size="large" />
</template>
<template #default>
<EChart :option="radarOption" autoresize :style="{ height: '400px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">出口管制实体领域分布情况,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="radarChart.interpretation" />
</div>
<EChart :option="radarOption" autoresize :style="{ height: '460px' }" />
</template>
</custom-container>
</el-col>
<el-col :span="16">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="520px">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="480px">
<template #header-right>
<div style="display: flex; align-items: center; gap: 16px">
<el-checkbox v-model="trendChecked" label="50%规则" size="large" />
......@@ -401,16 +391,6 @@
</template>
<template #default>
<EChart :option="trendOption" autoresize :style="{ height: '400px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">出口管制实体清单数量增长趋势,数据来源:美国各行政机构官网</div>
</div>
<div class="ai-pane">
<AiButton />
<AiPane :aiContent="trendChart.interpretation" />
</div>
</template>
</custom-container>
</el-col>
......@@ -732,10 +712,9 @@ import * as echarts from "echarts";
import setChart from "@/utils/setChart";
import { ElMessage, ElMessageBox } from "element-plus";
import { DArrowRight, Warning, Search, TrendCharts } from "@element-plus/icons-vue";
import { DArrowRight, Warning, Search } from "@element-plus/icons-vue";
import EChart from "@/components/Chart/index.vue";
import { TAGTYPE } from "@/public/constant";
import { useChartInterpretation } from "@/views/exportControl/utils/common";
import { useGotoCompanyPages } from "@/router/modules/company";
import { useGotoNewsDetail } from "@/router/modules/news";
const gotoCompanyPages = useGotoCompanyPages();
......@@ -747,8 +726,6 @@ const router = useRouter();
import CustomContainer from "@/components/Container/index.vue";
import ClickableCard from "./components/link.vue";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import InfoCard from "./components/info.vue";
import CustomTitle from "./components/title.vue";
import CommonPrompt from "./commonPrompt/index.vue";
......@@ -769,7 +746,6 @@ import entityIcon from "./assets/images/icon-entity.png";
import comTitle from "./assets/images/panel1_1.png";
import getMultiLineChart from "./utils/multiLineChart";
import icon01 from "./assets/images/jianzhu.png";
import tipsIcon from "./assets/icons/info-icon.png";
import {
getEntitiesDataCount,
getEntitiesDataInfo,
......@@ -1039,7 +1015,6 @@ const handleToSocialDetail = item => {
});
window.open(route.href, "_blank");
};
const trendChart = useChartInterpretation();
// 获取趋势图数据
const fetchTrendData = async () => {
try {
......@@ -1051,7 +1026,6 @@ const fetchTrendData = async () => {
});
if (res && res[0] && res[0].yearDomainCount) {
trendOption.value = processYearDomainCountData(res[0].yearDomainCount);
trendChart.interpret({ type: "柱状图", name: "实体清单数量增长趋势", data: res[0].yearDomainCount });
}
} catch (error) {
console.error("获取趋势图数据失败:", error);
......@@ -1231,7 +1205,7 @@ const radarOption = ref({
},
radar: {
radius: "60%",
center: ["52%", "45%"],
center: ["50%", "50%"],
// shape: 'circle',
indicator: [],
axisName: {
......@@ -1251,14 +1225,11 @@ const radarOption = ref({
]
});
const radarChart = useChartInterpretation();
// 获取雷达图数据
const fetchRadarData = async checked => {
try {
const data = await getSanDomainCount(checked, "export");
console.log("雷达数据data", data);
if (data && Array.isArray(data) && data.length > 0) {
radarChart.interpret({ type: "雷达图", name: "实体领域分布", data: data });
// 收集所有可能的领域名称
const allDomains = new Set();
data.forEach(item => {
......@@ -1660,11 +1631,10 @@ const fetchNewsInfo = async () => {
const handlePerClick = item => {
// console.log("点击了社交媒体消息:", item);
window.sessionStorage.setItem("curTabName", item.name);
console.log("人物点击 =>", item);
const route = router.resolve({
path: "/characterPage",
query: {
type: item.personType || [1, 2, 3][Math.floor(Math.random() * 3)],
type: item.type || [1, 2, 3][Math.floor(Math.random() * 3)],
personId: item.personId
}
});
......@@ -3626,50 +3596,4 @@ const handleMediaClick = item => {
background: rgba(255, 255, 255, 1);
}
}
.data-origin-box {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 22px 0;
.data-origin-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 8px;
img {
width: 100%;
height: 100%;
}
}
.data-origin-text {
font-family: Source Han Sans CN;
font-size: 14px;
color: var(--text-primary-50-color);
}
}
.ai-pane {
position: absolute;
right: 0px;
bottom: 15px;
z-index: 2;
:deep(.ai-pane-wrapper) {
display: none;
}
:deep(.ai-button-wrapper) {
display: flex;
}
&:hover {
width: 100%;
bottom: 0px;
:deep(.ai-pane-wrapper) {
display: block;
}
:deep(.ai-button-wrapper) {
display: none;
}
}
}
</style>
import * as echarts from "echarts";
import chinaJson from "./China.json";
import _ from "lodash";
import { name } from "dayjs/locale/zh-cn";
//饼图
export function getPieOption(data, title) {
let option = {
......@@ -1023,12 +1022,6 @@ export const getMultipleBarChart_m = object => {
},
yAxis: {
type: "value",
name: "数量",
nameLocation: "end",
nameGap: 5,
nameTextStyle: {
padding: [0, 40, 10, 0] // [上, 右, 下, 左] —— 减少右侧 padding 相当于左移
},
splitNumber: 5,
alignTicks: false,
axisLabel: {
......
import { ref } from "vue";
export const useChartInterpretation = () => {
const loading = ref(false);
const interpretation = ref("");
const error = ref(null);
const interpret = async text => {
loading.value = true;
error.value = null;
interpretation.value = "";
try {
const response = await fetch("/aiAnalysis/chart_interpretation", {
method: "POST",
headers: {
"X-API-Key": "aircasKEY19491001",
"Content-Type": "application/json"
},
body: JSON.stringify({ text })
});
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split("\n");
buffer = lines.pop() || "";
for (const line of lines) {
if (line.startsWith("data: ")) {
const content = line.substring(6);
const textMatch = content.match(/"解读":\s*"([^"]*)"/);
if (textMatch && textMatch[1]) {
interpretation.value = textMatch[1];
}
}
}
}
} catch (err) {
error.value = err.message || "AI 解读失败";
console.error("AI Chart Interpretation Error:", err);
} finally {
loading.value = false;
}
};
return {
loading,
interpretation,
error,
interpret
};
};
......@@ -31,7 +31,7 @@
<div class="info-row">
<div class="label">发布人:</div>
<div class="value link">
<img :src="formattedData.avartarUrl" alt="" class="icon avatar" />
<img :src="defaultTitle" alt="" class="icon avatar" />
<span @click="handleClick">{{ formattedData.postPersonName }} ></span>
</div>
</div>
......@@ -46,7 +46,7 @@
<div class="content-title">制裁实体分布:</div>
<div class="distribution-list">
<div class="list-item" v-for="(item, index) in entityDistribution" :key="index">
<img :src="item.imageUrl || flag" alt="" class="flag" />
<img :src="flag" alt="" class="flag" />
<div class="country-name">{{ item.name }}</div>
<div class="progress-bar-container">
<div
......@@ -152,7 +152,6 @@
</div>
<div class="right-content">
<div class="sanction-group-list">
<el-empty v-if="sanctionList.length === 0" description="暂无制裁清单数据" />
<div class="sanction-group" v-for="(group, index) in sanctionList" :key="index">
<el-table :data="group.entities" style="width: 100%">
<el-table-column label="实体名称" min-width="280">
......@@ -303,7 +302,7 @@ const getSanctionOverviewList = async () => {
removeCount.value = data.removeCount || 0;
removeRuleCount.value = data.removeRuleCount || 0;
const list = searchType.value == "add" ? data.sanList : data.removeList || [];
const list = data.sanList || [];
sanctionList.value = list.map(item => ({
reason: item.sanReason,
entities: (item.orgList || []).map(org => ({
......@@ -449,8 +448,7 @@ const formattedData = computed(() => {
fileCode: info.fileCode ? `${info.fileCode} ` : "",
administrativeOrderId: info.administrativeOrderId ? `No. ${info.administrativeOrderId}` : "",
postPersonName: info.postPersonName,
domains: info.domainNames,
avartarUrl: info.postPersonAvatarUrl
domains: info.domainNames
};
});
......
......@@ -160,7 +160,7 @@
</AnalysisBox>
</div>
<div class="box2">
<AnalysisBox title="共识观点列表" :showAllBtn="true" v-if="isBox2 || isAnalysisLoading">
<AnalysisBox title="核心观点分析" :showAllBtn="true" v-if="isBox2 || isAnalysisLoading">
<div class="box2-main">
<div class="empty-image" v-if="isBox2 && !isAnalysisLoading">
<img src="../assets/images/empty-image.png" alt="" />
......@@ -228,7 +228,11 @@
<div v-for="(sv, svIdx) in item.sourceViewDetails" :key="`${sv.report_id}-${sv.view_id}-${svIdx}`"
class="source-view-detail">
<div class="source-view-detail-title">
<span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx) }}</span>
<span
class="source-view-detail-title-text"
:class="{ 'is-clickable-report': hasReportLinkForSourceView(sv) }"
@click.stop="handleOpenReportOriginalFromSource(sv)"
>{{ getSourceViewDisplayTitle(sv) }}</span>
<span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
......@@ -274,8 +278,11 @@
<div v-for="(sv, svIdx) in item.sourceViewDetails"
:key="`${sv.report_id}-${sv.view_id}-${svIdx}`" class="source-view-detail">
<div class="source-view-detail-title">
<span class="source-view-detail-title-text">{{ getSourceViewDisplayTitle(sv, svIdx)
}}</span>
<span
class="source-view-detail-title-text"
:class="{ 'is-clickable-report': hasReportLinkForSourceView(sv) }"
@click.stop="handleOpenReportOriginalFromSource(sv)"
>{{ getSourceViewDisplayTitle(sv) }}</span>
<span class="source-view-detail-org" v-if="sv.thinktankName || sv.thinktankLogoUrl">
<img v-if="sv.thinktankLogoUrl" :src="sv.thinktankLogoUrl" alt="" />
<span class="source-view-detail-org-text">{{ sv.thinktankName }}</span>
......@@ -323,7 +330,7 @@ const sort = ref("");
const searchPolicy = ref("");
const isBox2 = ref(true)
const isAnalysisLoading = ref(false)
const isBeingAnalysisExpanded = ref(false)
const isBeingAnalysisExpanded = ref(true)
const beingAnalysisContent = ref("")
const beingAnalysisContentRef = ref(null)
const activeOpinionTab = ref('consensus')
......@@ -435,11 +442,23 @@ const getViewpointDetailForSource = (reportId, viewId) => {
thinktankLogoUrl: ""
}
}
/** 展开区标题:优先中文标题,否则英文;无标题则返回空串(由上游过滤) */
const getSourceViewDisplayTitle = (sv, idx) => {
const zh = String(sv.titleZh ?? "").trim()
/** 根据 report_id 找到报告名(用于展开区标题展示) */
const getReportNameById = (reportId) => {
const id = String(reportId ?? "")
if (!id) return ""
const list = selectedReportList.value || []
const hit = Array.isArray(list) ? list.find((r) => String(r?.id ?? "") === id) : null
return String(hit?.name ?? "").trim()
}
/** 展开区标题:显示报告名《xxx》(优先);否则回退中文标题/英文标题;无标题则返回空串(由上游过滤) */
const getSourceViewDisplayTitle = (sv) => {
const reportName = String(sv?.reportName ?? "").trim()
if (reportName) return `《${reportName}》`
const fromId = getReportNameById(sv?.report_id)
if (fromId) return `《${fromId}》`
const zh = String(sv?.titleZh ?? "").trim()
if (zh) return zh
const en = String(sv.title ?? "").trim()
const en = String(sv?.title ?? "").trim()
if (en) return en
return ""
}
......@@ -449,6 +468,20 @@ const getSourceViewDisplayContent = (sv) => {
if (zh) return zh
return String(sv.content ?? "").trim()
}
/** 是否存在可跳转的报告 id(source_views 的 report_id) */
const hasReportLinkForSourceView = (sv) => Boolean(String(sv?.report_id ?? "").trim())
/** 点击报告标题:新标签打开该报告原文页 */
const handleOpenReportOriginalFromSource = (sv) => {
const id = String(sv?.report_id ?? "").trim()
if (!id) return
const route = router.resolve({
name: "ReportOriginal",
params: { id }
})
window.open(route.href, "_blank")
}
const tryParseAnswerFromStreamText = (text) => {
const lines = String(text || "")
.split(/\r?\n/)
......@@ -492,9 +525,11 @@ const consensusList = computed(() => {
const sourceViewDetails = sourceViews
.map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id)
const reportName = getReportNameById(v.report_id)
return {
report_id: v.report_id,
view_id: v.view_id,
reportName,
titleZh: detail.titleZh,
contentZh: detail.contentZh,
title: detail.title,
......@@ -504,14 +539,15 @@ const consensusList = computed(() => {
}
})
.filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0)
const title = getSourceViewDisplayTitle(sv)
return Boolean(title)
})
const uniqueReportCount = new Set(sourceViewDetails.map((sv) => String(sv.report_id ?? ""))).size
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return {
id: `consensus-${index + 1}`,
consensusContent: item?.consensus_content || "",
reportCount: sourceViewDetails.length,
reportCount: uniqueReportCount,
sourceViewText,
sourceViewDetails
}
......@@ -525,9 +561,11 @@ const differenceList = computed(() => {
const sourceViewDetails = sourceViews
.map((v) => {
const detail = getViewpointDetailForSource(v.report_id, v.view_id)
const reportName = getReportNameById(v.report_id)
return {
report_id: v.report_id,
view_id: v.view_id,
reportName,
titleZh: detail.titleZh,
contentZh: detail.contentZh,
title: detail.title,
......@@ -537,19 +575,43 @@ const differenceList = computed(() => {
}
})
.filter((sv) => {
const title = getSourceViewDisplayTitle(sv, 0)
const title = getSourceViewDisplayTitle(sv)
return Boolean(title)
})
const uniqueReportCount = new Set(sourceViewDetails.map((sv) => String(sv.report_id ?? ""))).size
const sourceViewText = sourceViews.map((v) => `report_id: ${v.report_id}, view_id: ${v.view_id}`).join(";")
return {
id: `difference-${index + 1}`,
disagreementContent: item?.disagreement_content || "",
reportCount: sourceViewDetails.length,
reportCount: uniqueReportCount,
sourceViewText,
sourceViewDetails
}
})
})
// 默认展开:每次分析结果就绪后,共识/分歧列表第一条展开,其余关闭
watch(
consensusList,
(list) => {
if (!Array.isArray(list) || list.length === 0) return
if (openConsensusIds.value.size > 0) return
if (!list[0]?.id) return
openConsensusIds.value = new Set([list[0].id])
},
{ immediate: true }
)
watch(
differenceList,
(list) => {
if (!Array.isArray(list) || list.length === 0) return
if (openDifferencesIds.value.size > 0) return
if (!list[0]?.id) return
openDifferencesIds.value = new Set([list[0].id])
},
{ immediate: true }
)
// 近N年发布(用于 startDate)
const selectedYears = ref(5);
const yearsOptions = [
......@@ -585,9 +647,12 @@ const handleAnalysis = async () => {
if (!canProceed.value) return
isBox2.value = false
isAnalysisLoading.value = true
isBeingAnalysisExpanded.value = false
isBeingAnalysisExpanded.value = true
beingAnalysisContent.value = ""
domainViewAnalysisRes.value = null
// 默认:共识/分歧第一条展开,其余关闭(每次开始分析先清空旧展开状态)
openConsensusIds.value = new Set()
openDifferencesIds.value = new Set()
// 每次进入“开始分析”默认回到共识观点,避免上次状态残留导致内容错位
activeOpinionTab.value = 'consensus'
await handlePostReportDomainViewAnalysis()
......@@ -601,6 +666,8 @@ const handleBack = () => {
beingAnalysisContent.value = ""
// 返回选择时也重置,确保下次进入分析展示一致
activeOpinionTab.value = 'consensus'
openConsensusIds.value = new Set()
openDifferencesIds.value = new Set()
}
const pageSize = 10;
const total = ref(0);
......@@ -1029,7 +1096,7 @@ onMounted(async () => {
.being-analysis-detail-box {
width: 1063px;
height: 160px;
height: 260px;
background-color: rgb(246, 250, 255);
border-radius: 10px;
......@@ -1087,7 +1154,7 @@ onMounted(async () => {
.being-analysis-box-content {
width: 983px;
height: 104px;
height: 204px;
margin-left: 40px;
overflow-y: auto;
overflow-x: hidden;
......
......@@ -50,24 +50,24 @@
</div>
<div class="text">{{ "查看官网" }}</div>
</div> -->
<div class="btn">
<!-- <div class="btn">
<div class="icon">
<img src="./images/btn-icon2.png" alt="" />
</div>
<div class="text" @click="goToOfficialWebsite()">{{ "查看官网" }}</div>
</div>
</div> -->
<div class="btn">
<div class="icon">
<img src="./images/btn-icon2.png" alt="" />
</div>
<div class="text" @click="toReport()">{{ "报告原文" }}</div>
</div>
<div class="btn" @click="handleDownloadDocument">
<!-- <div class="btn" @click="handleDownloadDocument">
<div class="icon">
<img src="./images/btn-icon3.png" alt="" />
</div>
<div class="text">{{ "文档下载" }}</div>
</div>
</div> -->
<div class="btn btn1" @click="handleAnalysisClick">
<div class="icon">
<img src="./images/btn-icon4.png" alt="" />
......
......@@ -103,17 +103,19 @@
</div>
<div class="right-footer-box">
<div class="relatedBills" v-for="(item, index) in box1DataItem.relatedBills" :key="index" v-show="item">
<div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "法案" }}</div>
<div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedBills-content">{{ item.name }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div>
<div class="footer-image" @click.stop="handleBillMoreClick(item)"> <img src="../images/image-right.png"
alt="" /></div>
</div>
<div class="relatedAdministrativeOrders" v-for="(item, index) in box1DataItem.relatedAdministrativeOrders"
v-show="item" :key="index">
<div class="tag">{{ "政令" }}</div>
<div class="tag">{{ "科技领域相似" }}</div>
<div class="relatedAdministrativeOrders-content">{{ item.content }}</div>
<div class="footer-image"> <img src="../images/image-right.png" alt="" /></div>
<div class="footer-image" @click.stop="handleAdministrativeMoreClick(item)"> <img
src="../images/image-right.png" alt="" /></div>
</div>
</div>
</div>
......@@ -217,6 +219,29 @@ const handleTagClick = tag => {
activeItemIndex.value = 0;
updatePageData();
};
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
const handleSearchOpinions = () => {
// 搜索时默认切回“全部”标签
activeTag.value = "";
......
......@@ -43,7 +43,7 @@
<!-- 多个作者:显示第一个 + 等 -->
<span v-else>
{{ reportAuthors[0].name }}
{{ reportAuthors[0].name }}{{ reportAuthors.length }}
</span>
</template>
......@@ -57,7 +57,7 @@
@error="() => { if (author.avatar) author.avatar = null; }" /></div>
<div class="author-text">
<div class="author-name">{{ author.name }}</div>
<div class="author-position">{{ author.job }}</div>
</div>
</div>
</div>
......@@ -67,6 +67,10 @@
<div class="box5">
<AnalysisBox title="报告关键词云" :showAllBtn="true">
<div class="box5-main">
<template v-if="!hasBox5ChartData">
<el-empty class="box5-el-empty" description="暂无数据" :image-size="100" />
</template>
<template v-else>
<div class="box5Chart">
<!-- 有数据后再挂载子组件:子组件仅在 onMounted 初始化,异步数据到达后需 v-if + key 强制重新挂载 -->
<WordCloudChart v-if="box5Data.length" :key="box5WordCloudKey" :data="box5Data" width="100%"
......@@ -74,14 +78,15 @@
</div>
<div class="box5-footer">
<TipTab :text="REPORT_ANALYSIS_TIP_BOX5" />
</div>
<div class="ai-wrap" @mouseenter="handleSwitchAiContentShowBox5(true)">
<AiButton />
</div>
</div>
<div class="ai-content" v-if="isShowAiContentBox5" @mouseleave="handleSwitchAiContentShowBox5(false)">
<AiPane :aiContent="aiContentBox5" />
</div>
</template>
</div>
</AnalysisBox>
</div>
......@@ -254,7 +259,8 @@ const props = defineProps({
});
const REPORT_ANALYSIS_TIP_BOX5 =
"智库报告关键词云,数据来源:美国兰德公司官网";
const isShowAiContentBox5 = ref(false);
// 刷新后默认展示「报告关键词云」AI 总结
const isShowAiContentBox5 = ref(true);
const aiContentBox5 = ref("");
const isBox5InterpretLoading = ref(false);
const handleSwitchAiContentShowBox5 = (val) => {
......@@ -467,6 +473,7 @@ const box2Data = ref([
]);
// 报告关键词云
const box5Data = ref([]);
const hasBox5ChartData = computed(() => Array.isArray(box5Data.value) && box5Data.value.length > 0);
/** 词云子组件不 watch 数据,每次接口成功有数据时递增 key,强制重新挂载以触发 onMounted */
const box5WordCloudKey = ref(0);
......@@ -489,6 +496,10 @@ const handleGetThinkTankReportIndustryCloud = async () => {
if (data.length) {
box5WordCloudKey.value += 1;
}
// 刷新后默认展开 AI:数据就绪即触发解读
if (isShowAiContentBox5.value) {
fetchBox5ChartInterpretation();
}
} else {
box5Data.value = [];
}
......@@ -581,11 +592,16 @@ const handleGetThinkTankReportViewpoint = async () => {
const res = await getThinkTankReportViewpoint(params);
console.log("核心论点", res.data);
if (res.code === 200 && res.data) {
majorOpinions.value = res.data.content || [];
const nextOpinions = res.data.content || [];
majorOpinions.value = nextOpinions;
total.value = res.data.totalElements || 0;
// 重置展开状态
expandedOpinionKeys.value = new Set();
// 默认:第一条展开,其余关闭
const nextExpandedKeys = new Set();
if (Array.isArray(nextOpinions) && nextOpinions.length > 0) {
nextExpandedKeys.add(getOpinionExpandKey(nextOpinions[0], 0));
}
expandedOpinionKeys.value = nextExpandedKeys;
}
} catch (error) {
console.error("获取主要观点error", error);
......@@ -927,9 +943,18 @@ onMounted(() => {
display: flex;
justify-content: space-between;
align-items: center;
position: absolute;
bottom: 20px;
left: 32px;
}
.ai-wrap {
position: absolute;
bottom: 18px;
right: 0;
cursor: pointer;
}
......@@ -942,12 +967,7 @@ onMounted(() => {
}
.ai-wrap {
position: relative;
cursor: pointer;
}
}
}
......
......@@ -252,7 +252,7 @@
<div class="file" v-for="(file, idxx) in item.relatedBills" :key="`${file.id}-${idxx}`">
<div class="type">法案</div>
<div class="title">{{ file.name }}</div>
<div class="more">
<div class="more" @click.stop="handleBillMoreClick(file)">
<img src="./images/arrow-right.png" alt="" />
</div>
</div>
......@@ -260,7 +260,7 @@
:key="file.bcId != null ? String(file.bcId) + '-' + idxx : idxx">
<div class="type">政令</div>
<div class="title">{{ file.content }}</div>
<div class="more">
<div class="more" @click.stop="handleAdministrativeMoreClick(file)">
<img src="./images/arrow-right.png" alt="" />
</div>
</div>
......@@ -302,11 +302,46 @@ 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 AreaTag from "@/components/base/AreaTag/index.vue";
import { useRouter } from "vue-router";
const router = useRouter();
/** 与 AreaTag 一致的领域色(取 tag 的文字色) */
const AREA_TAG_COLOR_BY_NAME = {
"人工智能": "rgba(245, 34, 45, 1)", // tag1
"生物科技": "rgba(19, 168, 168, 1)", // tag2
"新一代通信网络": "rgba(5, 95, 194, 1)", // tag3
// 兼容常见写法
"通信网络": "rgba(5, 95, 194, 1)",
"量子科技": "rgba(114, 46, 209, 1)", // tag4
"新能源": "rgba(82, 196, 26, 1)", // tag5
"集成电路": "rgba(22, 119, 255, 1)", // tag6
"海洋": "rgba(15, 120, 199, 1)", // tag7
"先进制造": "rgba(250, 173, 20, 1)", // tag8
"新材料": "rgba(250, 140, 22, 1)", // tag9
"航空航天": "rgba(47, 84, 235, 1)", // tag10
"太空": "rgba(47, 84, 235, 1)", // tag11
"深海": "rgba(73, 104, 161, 1)", // tag12
"极地": "rgba(133, 165, 255, 1)", // tag13
"核": "rgba(250, 84, 28, 1)", // tag14
"其他": "rgba(82, 196, 26, 1)" // tag15
};
const AREA_TAG_FALLBACK_COLORS = [
"rgba(5, 95, 194, 1)",
"rgba(245, 34, 45, 1)",
"rgba(19, 168, 168, 1)",
"rgba(250, 140, 22, 1)",
"rgba(114, 46, 209, 1)",
"rgba(82, 196, 26, 1)",
"rgba(22, 119, 255, 1)",
"rgba(250, 84, 28, 1)",
"rgba(47, 84, 235, 1)"
];
const getAreaTagColor = (name, idx = 0) =>
AREA_TAG_COLOR_BY_NAME[name] || AREA_TAG_FALLBACK_COLORS[idx % AREA_TAG_FALLBACK_COLORS.length];
/** 与智库概览 TipTab 文案格式一致(政策追踪-美国国会) */
const POLICY_TRACKING_TIP_BOX1 =
"智库报告中政策建议的领域分布情况,数据来源:美国兰德公司官网";
......@@ -320,7 +355,8 @@ const POLICY_FILTER_ALL_AREA = "全部领域";
const POLICY_FILTER_ALL_TIME = "全部时间";
const POLICY_FILTER_ALL_DEPT = "全部部门";
const isShowAiContentPolicyPt1 = ref(false);
// 刷新后默认展示 3 个图表 AI 总结
const isShowAiContentPolicyPt1 = ref(true);
const aiContentPolicyPt1 = ref("");
const isPolicyPt1InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt1 = (val) => {
......@@ -330,7 +366,7 @@ const handleSwitchAiContentShowPolicyPt1 = (val) => {
}
};
const isShowAiContentPolicyPt2 = ref(false);
const isShowAiContentPolicyPt2 = ref(true);
const aiContentPolicyPt2 = ref("");
const isPolicyPt2InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt2 = (val) => {
......@@ -340,7 +376,7 @@ const handleSwitchAiContentShowPolicyPt2 = (val) => {
}
};
const isShowAiContentPolicyPt3 = ref(false);
const isShowAiContentPolicyPt3 = ref(true);
const aiContentPolicyPt3 = ref("");
const isPolicyPt3InterpretLoading = ref(false);
const handleSwitchAiContentShowPolicyPt3 = (val) => {
......@@ -439,16 +475,20 @@ const handleGetThinkPolicyIndustry = async () => {
box1Data.value = [];
return;
}
const data = list.map(item => ({
const data = list.map((item, idx) => ({
name: item.industry,
value: item.amount,
percent: item.percent
percent: item.percent,
color: getAreaTagColor(item.industry, idx)
}));
box1Data.value = data;
/* v-if 有数据后才挂载 #box1Chart,须等 DOM 更新后再 init echarts */
await nextTick();
const box1Chart = getPieChart(box1Data.value);
setChart(box1Chart, "box1Chart");
if (isShowAiContentPolicyPt1.value) {
fetchPolicyPtBox1ChartInterpretation();
}
} else {
box1Data.value = [];
}
......@@ -458,6 +498,30 @@ const handleGetThinkPolicyIndustry = async () => {
}
};
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
// 政策建议涉及部门分布(饼图)
const box2Data = ref([]);
......@@ -494,6 +558,9 @@ const handleGetPolicyAdviceDeptDistribution = async () => {
await nextTick();
const box2Chart = getPieChart(box2Data.value);
setChart(box2Chart, "box2Chart");
if (isShowAiContentPolicyPt2.value) {
fetchPolicyPtBox2ChartInterpretation();
}
} else {
box2Data.value = [];
}
......@@ -646,7 +713,8 @@ const handleGetThinkPolicyIndustryChange = async () => {
const industryAmount =
quarterData?.industryList?.find(i => i.industry === industry)?.amount || 0;
return industryAmount;
})
}),
color: getAreaTagColor(industry, frontendData.data.length)
};
frontendData.data.push(industryData);
});
......@@ -656,6 +724,9 @@ const handleGetThinkPolicyIndustryChange = async () => {
}
box3Data.value = frontendData;
await renderBox3Chart();
if (isShowAiContentPolicyPt3.value) {
fetchPolicyPtBox3ChartInterpretation();
}
} else {
box3Data.value = { title: [], data: [] };
}
......@@ -1231,14 +1302,14 @@ watch(
}
);
onMounted(() => {
handleGetThinkPolicyIndustry();
onMounted(async () => {
await handleGetThinkPolicyIndustry();
handleGetThinkPolicyIndustryTotal();
handleGetThinkPolicyIndustryChange();
await handleGetThinkPolicyIndustryChange();
handleGetHylyList();
handleGetGovAgencyList();
handleGetThinkPolicy();
handleGetPolicyAdviceDeptDistribution();
await handleGetPolicyAdviceDeptDistribution();
});
</script>
......@@ -1423,9 +1494,8 @@ onMounted(() => {
.source {
position: absolute;
bottom: 21px;
left: 50%;
transform: translateX(-50%);
width: 350px;
left: 24px;
width: 420px;
height: 22px;
display: flex;
}
......@@ -1453,7 +1523,7 @@ onMounted(() => {
width: 520px;
height: 372px;
box-sizing: border-box;
padding: 24px 24px 64px 24px;
padding: 0px 24px 64px 24px;
position: relative;
overflow: hidden;
......@@ -1481,15 +1551,13 @@ onMounted(() => {
flex: 1;
min-width: 0;
width: 472px;
height: 284px;
height: 308px;
}
.source {
position: absolute;
bottom: 21px;
left: 50%;
transform: translateX(-50%);
width: 350px;
bottom: 24px;
width: 420px;
height: 22px;
display: flex;
......
import * as echarts from 'echarts'
const colorList = [
// 按 AreaTag 的颜色规则映射到折线图配色(取 tag 的文字色)
const AREA_TAG_COLOR_BY_NAME = {
'人工智能': 'rgba(245, 34, 45, 1)', // tag1
'生物科技': 'rgba(19, 168, 168, 1)', // tag2
'新一代通信网络': 'rgba(5, 95, 194, 1)', // tag3
// 兼容常见写法
'通信网络': 'rgba(5, 95, 194, 1)',
'量子科技': 'rgba(114, 46, 209, 1)', // tag4
'新能源': 'rgba(82, 196, 26, 1)', // tag5
'集成电路': 'rgba(22, 119, 255, 1)', // tag6
'海洋': 'rgba(15, 120, 199, 1)', // tag7
'先进制造': 'rgba(250, 173, 20, 1)', // tag8
'新材料': 'rgba(250, 140, 22, 1)', // tag9
'航空航天': 'rgba(47, 84, 235, 1)', // tag10
'太空': 'rgba(47, 84, 235, 1)', // tag11
'深海': 'rgba(73, 104, 161, 1)', // tag12
'极地': 'rgba(133, 165, 255, 1)', // tag13
'核': 'rgba(250, 84, 28, 1)', // tag14
'其他': 'rgba(82, 196, 26, 1)' // tag15
}
const fallbackColorList = [
'rgba(5, 95, 194, 1)',
'rgba(245, 34, 45, 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(82, 196, 26, 1)',
'rgba(22, 119, 255, 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)'
'rgba(133, 165, 255, 1)'
]
const parseRgba = (colorStr) => {
......@@ -50,7 +67,8 @@ const getMultiLineChart = (chartInput) => {
const echartsSeries = series.map((item, index) => {
const baseColor =
item.color ||
colorList[index % colorList.length] ||
AREA_TAG_COLOR_BY_NAME[item.name] ||
fallbackColorList[index % fallbackColorList.length] ||
`rgba(${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, ${Math.floor(Math.random() * 256)}, 1)`
const { r, g, b } = parseRgba(baseColor)
......@@ -58,9 +76,11 @@ const getMultiLineChart = (chartInput) => {
name: item.name,
type: 'line',
smooth: true,
lineStyle: { color: baseColor },
itemStyle: { color: baseColor },
areaStyle: {
color: new echarts.graphic.LinearGradient(0, 0, 0, 1, [
{ offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.3)` },
{ offset: 0, color: `rgba(${r}, ${g}, ${b}, 0.1)` },
{ offset: 1, color: `rgba(${r}, ${g}, ${b}, 0)` }
])
},
......@@ -147,7 +167,7 @@ const getMultiLineChart = (chartInput) => {
itemHeight: 12
}
],
color: colorList,
// 不使用全局 color,改为每条 series 自己定色(与 AreaTag 一致)
xAxis: [
{
type: 'category',
......
const getPieChart = (data) => {
const seriesData = (Array.isArray(data) ? data : []).map((d) => {
const color = d?.color
if (!color) return d
return {
...d,
itemStyle: { ...(d.itemStyle || {}), color },
// “飞线”(labelLine)跟随领域色
labelLine: {
...(d.labelLine || {}),
lineStyle: { ...(d.labelLine?.lineStyle || {}), color }
}
}
})
let option = {
series: [
{
......@@ -57,7 +71,7 @@ const getPieChart = (data) => {
labelLinePoints: points
};
},
data: data
data: seriesData
}]
}
return option
......
......@@ -17,12 +17,12 @@
<AreaTag v-for="(tag, index) in thinkTank.tags" :key="index" :tagName="tag.industryName"></AreaTag>
</div>
</div>
<div class="header-top-right">
<!-- <div class="header-top-right">
<button class="blue-btn" @click="handleOpenThinkTankSite">
<img class="btn-img" src="./images/image1.png" alt="" />
<span class="text">{{ '查看智库官网' }}</span>
</button>
</div>
</div> -->
</div>
<div class="header-footer">
<div class="tab" :class="{ tabActive: tabActiveName === '智库动态' }" @click="switchTab('智库动态')">
......
......@@ -402,80 +402,8 @@ const handleSelectedFiltersUpdate = val => {
const author = ref('') // 作者
const curFooterList = ref([
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img1
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img2
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img3
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img4
// },
// {
// title: "中美经济竞争:复杂经济和地缘政治关系中的...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img5
// },
// {
// title: "中国、智慧城市和中东:留给地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img6
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img7
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "",
// img: Img8
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img9
// },
// {
// title: "中国对AI的转型产业政策",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img10
// },
// {
// title: "中美对抗、竞争和合作跨越人工智能通用领域...",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img11
// },
// {
// title: "中国、智慧城市和中东:地区和美国的选择",
// time: "2025年6月26日",
// from: "兰德科技智库",
// img: Img12
// }
]);
/** 智库报告 / 调查项目共用列表:与资源库一致,初始为空,接口非成功时清空 */
const curFooterList = ref([]);
......@@ -579,11 +507,10 @@ const handleGetThinkDynamicsReport = async (payload) => {
params.keyword = keyword;
}
const res = await getThinkTankReport(params);
console.log("智库动态报告", res);
if (res.code === 200 && res.data) {
curFooterList.value = res.data.content;
total.value = res.data.totalElements;
} else if (res.code === 500 && res.message === "未找到对应筛选的报告") {
} else {
curFooterList.value = [];
total.value = 0;
}
......
......@@ -453,9 +453,9 @@ const handleGetThinkTankFundsSource = async () => {
alignTo: 'edge',
offset: [0, -LABEL_OFFSET_UP],
formatter(params) {
const valueYi = (params.data.value || 0) / 100000000
const valueYi = (params.data.value || 0) / 10000
const percent = params.percent || 0
const valueStr = `${valueYi.toFixed(3)}亿 ${percent}%`
const valueStr = `${valueYi.toFixed(2)} ${percent}%`
let cumulative = 0
for (let i = 0; i < params.dataIndex; i++) cumulative += dataList[i].value || 0
const centerAngle = 90 + ((cumulative + (params.data.value || 0) / 2) / total) * 360
......@@ -1096,10 +1096,10 @@ onMounted(() => {
}
.source {
margin: 0 auto;
margin-top: 10px;
margin-left: 240px;
/* 上下0,左右自动居中 */
width: 370px;
height: 22px;
display: flex;
......@@ -1285,7 +1285,7 @@ onMounted(() => {
}
.source {
margin: 0 auto;
margin-left: 35px;
margin-top: 10px;
/* 上下0,左右自动居中 */
......
......@@ -7,18 +7,11 @@
<div class="title">{{ "科技领域" }}</div>
</div>
<div class="select-main">
<el-checkbox-group
class="checkbox-group"
:model-value="selectedTypeIds"
@change="handleAreaGroupChange">
<el-checkbox-group class="checkbox-group" :model-value="selectedTypeIds" @change="handleAreaGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_AREA">
{{ RESOURCE_FILTER_ALL_AREA }}
</el-checkbox>
<el-checkbox
class="filter-checkbox"
v-for="t in (researchTypeList || [])"
:key="t.id"
:label="t.id">
<el-checkbox class="filter-checkbox" v-for="t in (researchTypeList || [])" :key="t.id" :label="t.id">
{{ t.name }}
</el-checkbox>
</el-checkbox-group>
......@@ -31,18 +24,11 @@
<div class="title">{{ "发布时间" }}</div>
</div>
<div class="select-main">
<el-checkbox-group
class="checkbox-group"
:model-value="selectedYearIds"
@change="handleYearGroupChange">
<el-checkbox-group class="checkbox-group" :model-value="selectedYearIds" @change="handleYearGroupChange">
<el-checkbox class="filter-checkbox" :label="RESOURCE_FILTER_ALL_TIME">
{{ RESOURCE_FILTER_ALL_TIME }}
</el-checkbox>
<el-checkbox
class="filter-checkbox"
v-for="y in (researchTimeList || [])"
:key="y.id"
:label="y.id">
<el-checkbox class="filter-checkbox" v-for="y in (researchTimeList || [])" :key="y.id" :label="y.id">
{{ y.name }}
</el-checkbox>
</el-checkbox-group>
......@@ -71,17 +57,17 @@
</div>
<div class="file-box">
<div class="file">
<div class="file" v-for="sv in item.billInfoList" :key="sv.id || sv.name">
<div class="type">法案</div>
<div class="title">{{ item.billInfoList[0].name }}</div>
<div class="more">
<div class="title">{{ sv.name }}</div>
<div class="more" @click.stop="handleBillMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
<div class="file">
<div class="file" v-for="(sv, index) in item.administrativeOrderInfoVOList" :key="index">
<div class="type">政令</div>
<div class="title">{{ item.billInfoList[0].name }}</div>
<div class="more">
<div class="title">{{ sv.content }}</div>
<div class="more" @click.stop="handleAdministrativeMoreClick(sv)">
<img src="../assets/images/image-right.png" alt="" />
</div>
</div>
......@@ -106,6 +92,7 @@
<script setup>
import { ref } from "vue";
import { useRouter } from "vue-router";
import AreaTag from "@/components/base/AreaTag/index.vue";
import {
RESOURCE_FILTER_ALL_AREA,
......@@ -126,6 +113,32 @@ defineProps({
const emit = defineEmits(["filter-change", "page-change", "item-click"]);
const router = useRouter();
/** 政策建议关联法案:新标签页打开法案介绍页,billId 随接口 id 变化 */
const handleBillMoreClick = (bill) => {
const billId = bill?.id;
if (!billId) {
return;
}
const route = router.resolve({
path: "/billLayout/bill/introduction",
query: { billId: String(billId) }
});
window.open(route.href, "_blank");
};
/** 政策建议关联法案:新标签页打开政令介绍页*/
const handleAdministrativeMoreClick = (ad) => {
const id = ad?.bcId;
if (!id) {
return;
}
const route = router.resolve({
path: "/decreeLayout/overview/introduction",
query: { id: String(id) }
});
window.open(route.href, "_blank");
};
const selectedTypeIds = ref([RESOURCE_FILTER_ALL_AREA]);
const selectedYearIds = ref([RESOURCE_FILTER_ALL_TIME]);
......@@ -428,6 +441,7 @@ const handleYearGroupChange = (val) => {
height: 20px;
display: flex;
margin-top: 2px;
cursor: pointer;
.img {
width: 100%;
......
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论