提交 44a75d31 authored 作者: 刘宇琪's avatar 刘宇琪

Merge remote-tracking branch 'origin/pre' into pre

--- ---
description:
alwaysApply: true alwaysApply: true
--- ---
# Overview # Overview
Insert overview text here. The agent will only see this should they choose to apply the rule. Insert overview text here. The agent will only see this should they choose to apply the rule.
......
...@@ -2,10 +2,13 @@ stages: ...@@ -2,10 +2,13 @@ stages:
- build - build
- deploy - deploy
# cache: cache:
# key: "$CI_COMMIT_REF_SLUG" # cache:key 这里使用字符串,兼容性更好(部分 linter 不支持 key: { files: [...] })
# paths: # 预分支 pre 需要快速构建并实时同步,因此让 .npm 下载缓存跨分支复用
# - .npm/ key: "npm-cache-global"
paths:
- .npm/
policy: pull-push
build_pre: build_pre:
stage: build stage: build
...@@ -17,8 +20,13 @@ build_pre: ...@@ -17,8 +20,13 @@ build_pre:
script: script:
- node -v - node -v
- npm -v - npm -v
- npm config set cache .npm --global - echo "cache dir sizes:"
- npm ci --prefer-offline --no-audit --no-fund - 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 --include=optional --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ==="
- date -Iseconds
- npm run build - npm run build
artifacts: artifacts:
paths: paths:
...@@ -35,5 +43,55 @@ deploy_pre: ...@@ -35,5 +43,55 @@ deploy_pre:
dependencies: dependencies:
- build_pre - build_pre
script: script:
- apk add --no-cache rsync - apk add --no-cache rsync curl jq
- rsync -av --delete dist/ /nas/kjb_service/zm/pre-project/html/ # 只允许“最新一次 pre pipeline”部署到 nginx(加二次确认,避免短时间多次推送导致重复 rsync)
\ No newline at end of file - >
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 --include=optional --loglevel=verbose --timing --foreground-scripts
- echo "=== npm ci end ==="
- date -Iseconds
- npm run build
\ No newline at end of file
差异被折叠。
...@@ -36,7 +36,7 @@ ...@@ -36,7 +36,7 @@
"json5": "^2.2.3", "json5": "^2.2.3",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"markdown-it": "^14.1.0", "markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449", "pdfjs-dist": "^5.5.207",
"pinia": "^3.0.4", "pinia": "^3.0.4",
"vue": "^3.4.0", "vue": "^3.4.0",
"vue-router": "^4.2.5" "vue-router": "^4.2.5"
......
...@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) { ...@@ -77,6 +77,58 @@ function extractInterpretationFromLooseText(text) {
return String(m[1]).replace(/\\n/g, "\n").trim(); 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 流式) * 图表解读(SSE 流式)
* @param {object} data - 请求体 * @param {object} data - 请求体
...@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) { ...@@ -94,6 +146,8 @@ export function getChartAnalysis(data, options = {}) {
return new Promise((resolve, reject) => { return new Promise((resolve, reject) => {
let buffer = ""; let buffer = "";
let latestInterpretation = ""; let latestInterpretation = "";
/** 已推给前端的「解读」正文长度,用于只增量回调 onChunk */
let lastStreamedInterpretationLen = 0;
let settled = false; let settled = false;
const abortController = new AbortController(); const abortController = new AbortController();
...@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) { ...@@ -175,9 +229,31 @@ export function getChartAnalysis(data, options = {}) {
return; return;
} }
// 每收到一条消息即回调,用于流式渲染 // 流式渲染:不把 ```json、[、{ 等 markdown/JSON 外壳拼到界面
if (chunk && onDelta) { 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,则提前解析并中断连接 // 如果 buffer 已经拼完 markdown code fence,则提前解析并中断连接
......
...@@ -60,6 +60,17 @@ export function getDecreeMainContent(params) { ...@@ -60,6 +60,17 @@ export function getDecreeMainContent(params) {
}) })
} }
// 思维导图
/**
* @param { id }
*/
export function getDecreeMindMap(params) {
return request({
method: 'GET',
url: `/api/administrativeOrderInfo/mindMap/${params.id}`,
})
}
// 相关实体 // 相关实体
/** /**
* @param { id } * @param { id }
......
...@@ -3,9 +3,9 @@ import request from "@/api/request.js"; ...@@ -3,9 +3,9 @@ import request from "@/api/request.js";
// 最新科技政令 // 最新科技政令
export function getDepartmentList(params) { export function getDepartmentList(params) {
return request({ return request({
method: 'GET', method: 'POST',
url: `/api/administrativeDict/department`, url: `/api/administrativeDict/department`,
params data: params
}) })
} }
...@@ -91,10 +91,11 @@ export function getDecreeTypeList() { ...@@ -91,10 +91,11 @@ export function getDecreeTypeList() {
} }
// 关键机构 // 关键机构
export function getKeyOrganization() { export function getKeyOrganization(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/commonFeature/keyOrganization`, url: `/api/commonFeature/keyOrganization`,
params
}) })
} }
......
...@@ -33,7 +33,7 @@ export function getDecreeEntities(params) { ...@@ -33,7 +33,7 @@ export function getDecreeEntities(params) {
export function getDecreeRelatedChain(params) { export function getDecreeRelatedChain(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeOrderInfo/relatedChain/${params.id}`, url: `/api/chain/relatedChain/${params.id}`,
}) })
} }
...@@ -41,7 +41,7 @@ export function getDecreeRelatedChain(params) { ...@@ -41,7 +41,7 @@ export function getDecreeRelatedChain(params) {
export function getDecreeChainNodes(params) { export function getDecreeChainNodes(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeOrderInfo/relatedChainNodes/${params.id}`, url: `/api/chain/relatedChainNodes/${params.id}`,
}) })
} }
...@@ -49,7 +49,8 @@ export function getDecreeChainNodes(params) { ...@@ -49,7 +49,8 @@ export function getDecreeChainNodes(params) {
export function getDecreeRelatedEntitie(params) { export function getDecreeRelatedEntitie(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/administrativeOrderInfo/listRelatedEntitie/${params.id}`, url: `/api/organization/shareholding`,
params
}) })
} }
......
...@@ -80,10 +80,10 @@ export function getDecreeReport(params) { ...@@ -80,10 +80,10 @@ export function getDecreeReport(params) {
} }
// 政令关键词云 // 政令关键词云
export function getKeyWordUp() { export function getKeyWordUp(params) {
return request({ return request({
method: 'GET', method: 'GET',
url: `/api/element/getKeyWordUp/2025-01-01`, url: `/api/administrativeOrderInfo/wordCloud/${params.id}`,
}) })
} }
......
...@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) { ...@@ -46,7 +46,10 @@ export function getThinkTankPolicyIndustryChange(params) {
params: { params: {
startDate: params.startDate, startDate: params.startDate,
endDate: params.endDate endDate: params.endDate
} },
// 无数据年份(如 2026)后端可能返回 HTTP 400/500,避免走全局错误提示
validateStatus: (status) =>
(status >= 200 && status < 300) || status === 400 || status === 500
}); });
} }
...@@ -286,8 +289,7 @@ export function getThinkPolicyIndustryChange(params) { ...@@ -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) { export function getThinkPolicy(params) {
return request({ return request({
......
...@@ -9,7 +9,7 @@ ...@@ -9,7 +9,7 @@
<div class="switch-label switch-label-left">高亮实体</div> <div class="switch-label switch-label-left">高亮实体</div>
<el-switch v-model="isTranslate" /> <el-switch v-model="isTranslate" />
<div class="switch-label">文显示</div> <div class="switch-label">文显示</div>
<div <div
v-for="action in headerActions" v-for="action in headerActions"
...@@ -53,8 +53,8 @@ ...@@ -53,8 +53,8 @@
class="content-row" class="content-row"
:class="{ 'high-light': isHighlight }" :class="{ 'high-light': isHighlight }"
> >
<div class="content-cn" :class="{ 'translate-cn': !isTranslate }" v-html="item.content" /> <div class="content-en" v-html="item.contentEn" :class="{ 'translate-cn': !isTranslate }"></div>
<div v-if="isTranslate" class="content-en" v-html="item.contentEn" /> <div class="content-cn" v-html="item.content" v-if="isTranslate"></div>
</div> </div>
</el-scrollbar> </el-scrollbar>
</div> </div>
...@@ -150,11 +150,9 @@ const doUpdateWord = () => { ...@@ -150,11 +150,9 @@ const doUpdateWord = () => {
} }
displayReportData.value = originReportData.value.map((item) => { displayReportData.value = originReportData.value.map((item) => {
const cn = applyHighlightToText(item.content, term); const en = applyHighlightToText(item.contentEn, term);
const en = isTranslate.value const cn = isTranslate.value ? applyHighlightToText(item.content, term) : { html: item.content, count: 0 };
? applyHighlightToText(item.contentEn, term) findWordMax.value += en.count + cn.count;
: { html: item.contentEn, count: 0 };
findWordMax.value += cn.count + en.count;
return { return {
...item, ...item,
content: cn.html, content: cn.html,
......
...@@ -35,7 +35,7 @@ const tipText = computed(() => props.text || `数据来源:${props.dataSource} ...@@ -35,7 +35,7 @@ const tipText = computed(() => props.text || `数据来源:${props.dataSource}
width: 100%; width: 100%;
display: flex; display: flex;
gap: 8px; gap: 8px;
justify-content: center; justify-content: flex-start;
align-items: center; align-items: center;
height: 22px; height: 22px;
......
...@@ -14,13 +14,13 @@ ...@@ -14,13 +14,13 @@
</div> </div>
<div class="header-right"> <div class="header-right">
<div class="header-right-btn" @click="handleSave" v-if="showAllBtn"> <div class="header-right-btn" @click="handleSave" v-if="showAllBtn">
<img src="@/assets/icons/box-header-icon1.png" alt=""> <img src="@/assets/icons/box-header-icon1.png" alt="" />
</div> </div>
<div class="header-right-btn" @click="handleDownload"> <div class="header-right-btn" @click="handleDownload">
<img src="@/assets/icons/box-header-icon2.png" alt=""> <img src="@/assets/icons/box-header-icon2.png" alt="" />
</div> </div>
<div class="header-right-btn" @click="handleCollect"> <div class="header-right-btn" @click="handleCollect">
<img src="@/assets/icons/box-header-icon3.png" alt=""> <img src="@/assets/icons/box-header-icon3.png" alt="" />
</div> </div>
</div> </div>
</div> </div>
...@@ -31,21 +31,21 @@ ...@@ -31,21 +31,21 @@
</template> </template>
<script setup> <script setup>
import { ElMessage } from 'element-plus' import { ElMessage } from "element-plus";
import { ref, computed } from 'vue' import { ref, computed } from "vue";
const props = defineProps({ const props = defineProps({
title: { title: {
type: String, type: String,
default: '' default: ""
}, },
width: { width: {
type: String, type: String,
default: '' default: ""
}, },
height: { height: {
type: String, type: String,
default: '' default: ""
}, },
showAllBtn: { showAllBtn: {
type: Boolean, type: Boolean,
...@@ -56,39 +56,36 @@ const props = defineProps({ ...@@ -56,39 +56,36 @@ const props = defineProps({
type: Boolean, type: Boolean,
default: false default: false
} }
}) });
const handleSave = () => { const handleSave = () => {
if (props.devTip) { if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!') ElMessage.warning("当前功能正在开发中,敬请期待!");
return return;
} }
ElMessage.success('保存当前内容') ElMessage.success("保存当前内容");
// emit('save') // emit('save')
} };
const handleDownload = () => { const handleDownload = () => {
if (props.devTip) { if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!') ElMessage.warning("当前功能正在开发中,敬请期待!");
return return;
} }
ElMessage.success('下载当前内容') ElMessage.success("下载当前内容");
// emit('download') // emit('download')
};
}
const handleCollect = () => { const handleCollect = () => {
if (props.devTip) { if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!') ElMessage.warning("当前功能正在开发中,敬请期待!");
return return;
} }
ElMessage.success('收藏当前内容') ElMessage.success("收藏当前内容");
// emit('collect') // emit('collect')
};
} const emit = defineEmits(["save", "download", "collect"]);
const emit = defineEmits(['save', 'download', 'collect'])
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
...@@ -97,7 +94,7 @@ const emit = defineEmits(['save', 'download', 'collect']) ...@@ -97,7 +94,7 @@ const emit = defineEmits(['save', 'download', 'collect'])
border-radius: 10px; border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1); box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1); background: rgba(255, 255, 255, 1);
position: relative;
.wrapper-header { .wrapper-header {
height: 45px; height: 45px;
display: flex; display: flex;
...@@ -123,7 +120,7 @@ const emit = defineEmits(['save', 'download', 'collect']) ...@@ -123,7 +120,7 @@ const emit = defineEmits(['save', 'download', 'collect'])
// line-height: 26px; // line-height: 26px;
// letter-spacing: 0px; // letter-spacing: 0px;
height: 100%; height: 100%;
&>div { & > div {
height: 100%; height: 100%;
color: var(--color-main-active); color: var(--color-main-active);
font-family: Microsoft YaHei; font-family: Microsoft YaHei;
......
...@@ -9,6 +9,7 @@ const DecreeDeepDig = () => import('@/views/decree/decreeLayout/deepdig/index.vu ...@@ -9,6 +9,7 @@ const DecreeDeepDig = () => import('@/views/decree/decreeLayout/deepdig/index.vu
const DecreeInfluence = () => import('@/views/decree/decreeLayout/influence/index.vue') const DecreeInfluence = () => import('@/views/decree/decreeLayout/influence/index.vue')
const Institution = () => import('@/views/decree/institution/index.vue') const Institution = () => import('@/views/decree/institution/index.vue')
const DecreeOriginal = () => import('@/views/decree/decreeOriginal/index.vue') const DecreeOriginal = () => import('@/views/decree/decreeOriginal/index.vue')
const allOrganization = () => import('@/views/decree/allOrganization/index.vue')
const decreeRoutes = [ const decreeRoutes = [
...@@ -93,11 +94,12 @@ const decreeRoutes = [ ...@@ -93,11 +94,12 @@ const decreeRoutes = [
path: "/decree/decreeOriginal", path: "/decree/decreeOriginal",
name: "DecreeOriginal", name: "DecreeOriginal",
component: DecreeOriginal, component: DecreeOriginal,
// meta: {
// title: "政令原文"
// }
}, },
{
path: "/decree/allOrganization",
name: "allOrganization",
component: allOrganization,
},
] ]
export default decreeRoutes export default decreeRoutes
\ No newline at end of file
...@@ -15,22 +15,29 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -15,22 +15,29 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chart.on('click', function (params) { chart.on('click', function (params) {
switch (selectParam.moduleType) { switch (selectParam.moduleType) {
case '国会法案': case '国会法案':
if(selectParam.selectedDate && selectParam.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate+'-01-01',selectParam.selectedDate+'-12-31'])
}
// 判断点击的是否为饼图的数据项 // 判断点击的是否为饼图的数据项
if (params.componentType === 'series' && params.seriesType === 'pie') { if (params.componentType === 'series' && params.seriesType === 'pie') {
console.log('点击的扇形名称:', params.name); console.log('点击的扇形名称:', params.name);
if (selectParam.key === '领域') { if (selectParam.key === '领域') {
selectParam.domains = params.name selectParam.domains = params.name
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else if (selectParam.key === '议院委员会') { } else if (selectParam.key === '议院委员会') {
if (params.name === '众议院' || params.name === '参议院') { if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name selectParam.selectedCongress = params.name
selectParam.selectedOrg = '' selectParam.selectedOrg = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else { } else {
selectParam.selectedOrg = params.name selectParam.selectedOrg = params.name
selectParam.selectedCongress = '' selectParam.selectedCongress = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} }
} }
const route = router.resolve({ const route = router.resolve({
...@@ -40,9 +47,12 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -40,9 +47,12 @@ const setChart = (option, chartId, allowClick, selectParam) => {
window.open(route.href, "_blank"); window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') { } else if (params.componentType === 'series' && params.seriesType === 'bar') {
if (params.name === '已立法') { if (params.name === '已立法') {
selectParam.selectedStauts = 1 selectParam.selectedStatus = 1
} else { } else {
selectParam.selectedStauts = 0 selectParam.selectedStatus = 0
}
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
} }
const route = router.resolve({ const route = router.resolve({
path: "/dataLibrary/countryBill", path: "/dataLibrary/countryBill",
...@@ -52,11 +62,13 @@ const setChart = (option, chartId, allowClick, selectParam) => { ...@@ -52,11 +62,13 @@ const setChart = (option, chartId, allowClick, selectParam) => {
} else { } else {
console.log('当前点击', selectParam, params.seriesName, params.name); console.log('当前点击', selectParam, params.seriesName, params.name);
if (params.seriesName !== '通过率') { if (params.seriesName !== '通过率') {
selectParam = { selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
selectedDate: JSON.stringify(getMonthRange(params.name)), if (params.seriesName === '通过法案') {
status: params.seriesName === '通过法案' ? 1 : 0, selectParam.selectedStatus = 1
...selectParam } else {
selectParam.selectedStatus = null
} }
const route = router.resolve({ const route = router.resolve({
path: "/dataLibrary/countryBill", path: "/dataLibrary/countryBill",
query: selectParam query: selectParam
......
...@@ -1006,7 +1006,7 @@ const handleBox9Data = async () => { ...@@ -1006,7 +1006,7 @@ const handleBox9Data = async () => {
moduleType: '国会法案', moduleType: '国会法案',
key: '领域', key: '领域',
selectedDate: box9selectetedTime.value, selectedDate: box9selectetedTime.value,
status: box9LegislativeStatus.value === '提出法案' ? 0 : 1, selectedStatus: box9LegislativeStatus.value === '提出法案' ? 0 : 1,
isInvolveCn: 1 isInvolveCn: 1
} }
box9ChartInstance = setChart(box9Chart, "box9Chart", true, selectParam); box9ChartInstance = setChart(box9Chart, "box9Chart", true, selectParam);
......
...@@ -3,20 +3,20 @@ import * as echarts from 'echarts' ...@@ -3,20 +3,20 @@ import * as echarts from 'echarts'
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => { const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
return { return {
tooltip: { tooltip: {
trigger: 'axis', trigger: 'item',
axisPointer: { axisPointer: {
type: 'cross', type: 'cross',
label: { label: {
backgroundColor: '#6a7985' backgroundColor: '#6a7985'
} }
}, },
formatter: function (params) { // formatter: function (params) {
let res = params[0].name + '<br/>'; // let res = params[0].name + '<br/>';
params.forEach(item => { // params.forEach(item => {
res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>'; // res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>';
}); // });
return res; // return res;
} // }
}, },
grid: { grid: {
width: '96%', width: '96%',
......
...@@ -384,7 +384,6 @@ ...@@ -384,7 +384,6 @@
</div> </div>
</div> --> </div> -->
<AnalysisBox title="投票分析"> <AnalysisBox title="投票分析">
<div class="analysis-ai-wrapper analysis-ai-wrapper--box3">
<div class="vote-legend"> <div class="vote-legend">
<div class="vote-legend-item"> <div class="vote-legend-item">
<span class="vote-legend-dot agree"></span> <span class="vote-legend-dot agree"></span>
...@@ -395,6 +394,7 @@ ...@@ -395,6 +394,7 @@
<span>反对票</span> <span>反对票</span>
</div> </div>
</div> </div>
<div class="analysis-ai-wrapper analysis-ai-wrapper--box3">
<div class="box3-main" :class="{ 'box3-main--full': !voteFooterText }"> <div class="box3-main" :class="{ 'box3-main--full': !voteFooterText }">
<div class="box3-main-center"> <div class="box3-main-center">
<div class="box3-main-center-header"> <div class="box3-main-center-header">
...@@ -405,14 +405,15 @@ ...@@ -405,14 +405,15 @@
<div class="box3-main-center-header-box5"> <div class="box3-main-center-header-box5">
倒戈人数<span style="font-weight: normal; display: inline-block">(平均区间)</span> 倒戈人数<span style="font-weight: normal; display: inline-block">(平均区间)</span>
</div> </div>
<div class="box3-main-center-header-box6">关键议员</div>
</div> </div>
<div class="box3-main-center-content"> <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">
<div class="item-box1"> <div class="item-box1">
<div class="box1-left"> <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=" <div class="name nameBlod" :title="item.actionTitle" style="
white-space: nowrap; white-space: nowrap;
overflow: hidden; overflow: hidden;
...@@ -428,33 +429,34 @@ ...@@ -428,33 +429,34 @@
</div> </div>
<div class="box1-right"> <div class="box1-right">
<div class="box1-right-top"> <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> </el-progress>
</div> </div>
<div class="box1-right-bottom"> <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> </el-progress>
</div> </div>
</div> </div>
</div> </div>
<div class="item-box2"> <div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.agreeCount + "票" }}</div> <div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.agreeCount +
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.againstCount + "票" }}</div> "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.againstCount
+ "票" }}</div>
</div> </div>
<div class="item-box3"> <div class="item-box3">
<div class="box3-1"></div> <div class="box3-1"></div>
<div class="box3-2"></div> <div class="box3-2"></div>
</div> </div>
<div class="item-box4"> <div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.agreePercent + "%" }}</div> <div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.agreePercent
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.againstPercent + "%" }}</div> + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{
item.againstPercent + "%" }}</div>
</div> </div>
<div class="item-box5"></div> <div class="item-box5"></div>
<div class="item-box6">
<el-icon size="20" color="#555">
<ArrowDownBold />
</el-icon>
</div>
</div> </div>
<div class="item"> <div class="item">
<div class="item-box1"> <div class="item-box1">
...@@ -466,31 +468,33 @@ ...@@ -466,31 +468,33 @@
</div> </div>
<div class="box1-right"> <div class="box1-right">
<div class="box1-right-top"> <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> </el-progress>
</div> </div>
<div class="box1-right-bottom"> <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> </el-progress>
</div> </div>
</div> </div>
</div> </div>
<div class="item-box2"> <div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.dagreeCount + "票" }}</div> <div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.dagreeCount
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.dagainstCount + "票" }}</div> + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{
item.dagainstCount + "票" }}</div>
</div> </div>
<div class="item-box3"></div> <div class="item-box3"></div>
<div class="item-box4"> <div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.dagreePercent + "%" }}</div> <div class="box4-1" style="color: rgb(33, 129, 57)">{{
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.dagainstPercent + "%" }}</div> item.dagreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{
item.dagainstPercent + "%" }}</div>
</div> </div>
<div class="item-box5"> <div class="item-box5">
<div class="box5-1" style="color: #ce4f51">{{ item.dreverseCount + "人" }}</div> <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> </div>
</div> </div>
<div class="item"> <div class="item">
...@@ -503,31 +507,33 @@ ...@@ -503,31 +507,33 @@
</div> </div>
<div class="box1-right"> <div class="box1-right">
<div class="box1-right-top"> <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> </el-progress>
</div> </div>
<div class="box1-right-bottom"> <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> </el-progress>
</div> </div>
</div> </div>
</div> </div>
<div class="item-box2"> <div class="item-box2">
<div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.ragreeCount + "票" }}</div> <div class="box2-1" style="color: rgb(33, 129, 57)">{{ item.ragreeCount
<div class="box2-2" style="color: rgb(206, 79, 81)">{{ item.ragainstCount + "票" }}</div> + "票" }}</div>
<div class="box2-2" style="color: rgb(206, 79, 81)">{{
item.ragainstCount + "票" }}</div>
</div> </div>
<div class="item-box3"></div> <div class="item-box3"></div>
<div class="item-box4"> <div class="item-box4">
<div class="box4-1" style="color: rgb(33, 129, 57)">{{ item.ragreePercent + "%" }}</div> <div class="box4-1" style="color: rgb(33, 129, 57)">{{
<div class="box4-2" style="color: rgb(206, 79, 81)">{{ item.ragainstPercent + "%" }}</div> item.ragreePercent + "%" }}</div>
<div class="box4-2" style="color: rgb(206, 79, 81)">{{
item.ragainstPercent + "%" }}</div>
</div> </div>
<div class="item-box5"> <div class="item-box5">
<div class="box5-1" style="color: #ce4f51">{{ item.rreverseCount + "人" }}</div> <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> </div>
</div> </div>
</div> </div>
...@@ -701,7 +707,8 @@ ...@@ -701,7 +707,8 @@
<TipTab class="analysis-ai-tip" /> <TipTab class="analysis-ai-tip" />
<AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box3')" /> <AiButton class="analysis-ai-tip-action" @mouseenter="handleShowAiPane('box3')" />
</div> </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" /> <AiPane :aiContent="overviewAiContent.box3" />
</div> </div>
</div> </div>
...@@ -1248,6 +1255,7 @@ onMounted(async () => { ...@@ -1248,6 +1255,7 @@ onMounted(async () => {
.box1 { .box1 {
width: 792px; width: 792px;
height: 415px; height: 415px;
.box1-main { .box1-main {
height: 368px; height: 368px;
...@@ -1393,6 +1401,7 @@ onMounted(async () => { ...@@ -1393,6 +1401,7 @@ onMounted(async () => {
margin-top: 17px; margin-top: 17px;
width: 792px; width: 792px;
height: 415px; height: 415px;
.box2-main { .box2-main {
height: 359px; height: 359px;
...@@ -1640,6 +1649,7 @@ onMounted(async () => { ...@@ -1640,6 +1649,7 @@ onMounted(async () => {
margin-top: 16px; margin-top: 16px;
width: 792px; width: 792px;
height: 847px; height: 847px;
.box3 { .box3 {
width: 100%; width: 100%;
height: 100%; height: 100%;
...@@ -1647,7 +1657,7 @@ onMounted(async () => { ...@@ -1647,7 +1657,7 @@ onMounted(async () => {
.vote-legend { .vote-legend {
position: absolute; position: absolute;
top: 15px; top: 10px;
left: 50%; left: 50%;
transform: translateX(-50%); transform: translateX(-50%);
display: inline-flex; display: inline-flex;
...@@ -1735,12 +1745,6 @@ onMounted(async () => { ...@@ -1735,12 +1745,6 @@ onMounted(async () => {
min-width: 0; min-width: 0;
text-align: center; text-align: center;
} }
.box3-main-center-header-box6 {
flex: 90 1 0;
min-width: 0;
text-align: center;
}
} }
.box3-main-center-content { .box3-main-center-content {
...@@ -1976,23 +1980,6 @@ onMounted(async () => { ...@@ -1976,23 +1980,6 @@ onMounted(async () => {
line-height: 14px; 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(() => { ...@@ -417,11 +417,12 @@ onMounted(() => {
} }
.risk-signal-pane-top { .risk-signal-pane-top {
width: 1600px; // width: 1600px;
margin-top: 16px; margin-top: 16px;
margin-right: 18px; margin-right: 18px;
height: 116px; padding-bottom: 16px;
min-height: 116px; min-height: 116px;
height: auto;
flex-shrink: 0; flex-shrink: 0;
} }
......
...@@ -78,7 +78,7 @@ const props = defineProps({ ...@@ -78,7 +78,7 @@ const props = defineProps({
type: String, type: String,
default: '' default: ''
}, },
chartTypeList : { chartTypeList: {
type: Array, type: Array,
default: [] default: []
} }
...@@ -88,15 +88,19 @@ const chartItemList = computed(() => { ...@@ -88,15 +88,19 @@ const chartItemList = computed(() => {
let arr = [] let arr = []
props.chartTypeList.forEach(item => { props.chartTypeList.forEach(item => {
defaultChartTypeList.forEach(val => { defaultChartTypeList.forEach(val => {
if(val.name === item) { if (val.name === item) {
arr.push(val) arr.push(val)
} }
}) })
}) })
arr.forEach(item => { arr.forEach(item => {
item.active = false item.active = false
}) })
arr[0].active = true arr[0].active = true
// console.log('arr', arr);
return arr return arr
}) })
......
...@@ -478,12 +478,13 @@ const handleClickToolBox = () => { ...@@ -478,12 +478,13 @@ const handleClickToolBox = () => {
onMounted(() => { onMounted(() => {
const path = route.path const path = route.path
console.log(decodeURI(route.fullPath));
switch (path) { switch (path) {
case '/dataLibrary/countryBill': case '/dataLibrary/countryBill':
siderList.value[0].active = true siderList.value[0].active = true
siderList.value[0].isExpanded = true siderList.value[0].isExpanded = true
siderList.value[0].children[0].active = true siderList.value[0].children[0].active = true
break break
case '/dataLibrary/stateBill': case '/dataLibrary/stateBill':
siderList.value[0].active = true siderList.value[0].active = true
......
<template>
<div class="view-box">
<div class="container-box">
<div class="hard-box">
<div class="hard-name text-title-0-show">美国政府机构</div>
<div class="hard-num text-title-2-show">{{organizationInfo.total}}</div>
<div style="width: 0px; flex: auto;"></div>
<div class="hard-input">
<el-input v-model="organizationInfo.keyWord" @keyup.enter="onAllOrganization()" style="width:100%; height:100%;" :suffix-icon="Search" placeholder="搜索机构" />
</div>
<div class="hard-time">
<el-select v-model="organizationInfo.isSort" @change="onAllOrganization()" placeholder="发布时间" style="width:160px; margin-left:8px;">
<template #prefix>
<div class="icon1">
<img v-if="isSort" src="@/assets/icons/shengxu1.png" alt="" />
<img v-else src="@/assets/icons/jiangxu1.png" alt="" />
</div>
</template>
<el-option label="政令数量" :value="1" />
</el-select>
</div>
</div>
<div class="date-box">
<div class="date-icon">
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="handleDateChange" />
</div>
<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.orgImage || DefaultIcon2" alt="" />
</div>
<div class="item-right one-line-ellipsis">{{ item.orgName }}</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.totalRecent">+{{item.totalRecent}}</div>
</div>
</div>
<div class="pagination-box">
<el-pagination @current-change="onAllOrganization" :pageSize="organizationInfo.pageSize" :current-page="organizationInfo.pageNum" background layout="prev, pager, next" :total="organizationInfo.total" />
</div>
</div>
<div class="back-bnt" @click="router.back()">
<el-icon><Back /></el-icon>
<div style="margin-left: 6px;">返回</div>
</div>
</div>
</template>
<script setup name="index">
import {onMounted, reactive, ref} from "vue"
import { Search } from '@element-plus/icons-vue'
import router from "@/router";
import TimeTabPane from '@/components/base/TimeTabPane/index.vue';
import { getDepartmentList } from "@/api/decree/home";
import tipsTcon from "./assets/icons/tips-icon.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png";
const organizationInfo = reactive({
loading: false,
pageNum: 1,
pageSize: 8,
total: 0,
isSort: 1,
keyWord: "",
day: 7,
list: []
})
const onAllOrganization = async (num) => {
organizationInfo.pageNum = num || 1
organizationInfo.loading = true
try {
let {keyWord, pageNum, pageSize, day} = organizationInfo
const res = await getDepartmentList({day, pageNum:pageNum-1, pageSize, keyWord: keyWord||undefined});
console.log("机构列表", res);
if (res.code === 200) {
organizationInfo.list = res.data.orgList || [];
organizationInfo.total = res.data.total || 0;
}
} catch (error) {
console.error("获取机构列表数据失败", error);
organizationInfo.list = [];
organizationInfo.total = 0;
}
organizationInfo.loading = false
}
const handleDateChange = (event) => {
if (event?.time === '近一周') organizationInfo.day = 7
if (event?.time === '近一月') organizationInfo.day = 30
if (event?.time === '近一年') organizationInfo.day = 365
onAllOrganization()
}
// 跳转行政机构主页
const handleToInstitution = item => {
window.sessionStorage.setItem("curTabName", item.orgName);
const curRoute = router.resolve({
path: "/institution",
query: {
id: item.orgId
}
});
window.open(curRoute.href, "_blank");
};
const refOrganization = ref()
onMounted(() => {
// 根据元素的高度决定分页显示的机构数量
let height = 2;
if (refOrganization.value) {
height = Math.floor(refOrganization.value?.clientHeight/120)
}
organizationInfo.pageSize = height*4
onAllOrganization()
})
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
height: 100%;
background: url("./assets/images/background.png"), linear-gradient(180deg, rgba(229, 241, 254, 1) 0%, rgba(246, 251, 255, 0) 30%);
background-size: 100% 100%;
display: flex;
justify-content: center;
position: relative;
.back-bnt {
position: absolute;
top: 16px;
left: 30px;
width: 86px;
height: 38px;
background-color: white;
border-radius: 19px;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-primary-65-color);
font-family: Source Han Sans CN;
font-size: 16px;
cursor: pointer;
}
.container-box {
width: 1600px;
padding: 50px 0 20px;
display: flex;
flex-direction: column;
.hard-box {
display: flex;
align-items: center;
width: 100%;
.hard-name {
color: var(--text-primary-90-color);
height: 62px;
line-height: 62px !important;
}
.hard-num {
height: 36px;
background-color: var(--color-primary-100);
color: var(--bg-white-100);
border-radius: 18px;
line-height: 36px !important;
padding: 0 16px;
margin-left: 16px;
}
.hard-input {
background-color: var(--el-fill-color-blank);
border-radius: var(--el-border-radius-base);
box-shadow: 0 0 0 1px var(--el-border-color) inset;
box-sizing: border-box;
margin-left: 20px;
width: 160px;
height: 32px;
}
.hard-time {
height: 42px;
padding: 5px 0;
.icon1 {
width: 11px;
height: 14px;
font-size: 0px;
img {
width: 100%;
height: 100%;
}
}
}
}
.date-box {
margin-top: 6px;
display: flex;
align-items: center;
width: 100%;
.date-icon {
width: 16px;
height: 16px;
font-size: 0px;
margin-right: 6px;
img {
width: 100%;
height: 100%;
}
}
.date-text {
width: 20px;
flex: auto;
font-size: 18px;
line-height: 18px;
font-family: Source Han Sans CN;
color: var(--text-primary-80-color);
}
}
.organization-list {
width: 100%;
height: 20px;
padding: 16px 0;
margin-top: 10px;
flex: auto;
display: grid;
grid-template-columns: repeat(4, 1fr);
grid-auto-rows: 104px;
gap: 16px;
font-family: Microsoft YaHei;
.organization-item {
padding: 0 16px;
display: flex;
box-sizing: border-box;
background: rgba(255, 255, 255, 0.65);
border: 1px solid rgba(255, 255, 255, 1);
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
align-items: center;
justify-content: center;
cursor: pointer;
transition: transform 0.3s ease, box-shadow 0.3s ease;
position: relative;
&:hover {
transform: translateY(-3px);
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.15);
}
.item-left {
width: 48px;
height: 48px;
font-size: 0px;
img {
width: 100%;
height: 100%;
}
}
.item-right {
width: 20px;
flex: auto;
color: rgba(59, 65, 75, 1);
font-size: 20px;
font-weight: 700;
line-height: 20px;
margin: 0 16px;
}
.item-total {
font-size: 20px;
margin-right: 2px;
white-space: nowrap;
font-weight: 700;
line-height: 20px;
color: var(--color-primary-100);
}
.item-more {
font-size: 16px;
margin-right: 12px;
white-space: nowrap;
font-weight: 700;
line-height: 16px;
color: var(--color-primary-100);
}
.item-dot {
position: absolute;
right: -13px;
top: -10px;
padding: 0 8px;
height: 26px;
background-color: #FF4D4F;
color: white;
font-size: 16px;
line-height: 26px;
font-family: Source Han Sans CN;
border-radius: 14px;
letter-spacing: 1px;
}
}
}
.pagination-box {
display: flex;
justify-content: center;
}
}
}
</style>
\ No newline at end of file
<template>
<div class="view-box">
<div ref="graphContainer" style="height: 100%; width: 100%;"></div>
</div>
</template>
<script setup name="MindGraph">
import { ref, onBeforeUnmount } from "vue"
import * as G6 from '@antv/g6';
// 初始化画布
const graphContainer = ref(null);
let graph = null;
const onInitGraph = () => {
const container = graphContainer.value;
const width = container.clientWidth;
const height = container.clientHeight;
graph = new G6.Graph({
container: container,
width, height,
// fitView: true,
// fitViewPadding: 50,
defaultNode: {
type: "rect",
size: [250, 45],
style: {
fill: "#F6FAFF",
stroke: "#B9DCFF",
lineWidth: 1
},
labelCfg: {
style: {
fill: "#055FC2",
fontSize: 18,
lineHeight: 25,
fontWeight: "bold",
fontFamily: "Source Han Sans CN",
}
}
},
defaultEdge: {
type: "cubic-horizontal",
style: {
stroke: "#B9DCFF",
lineWidth: 2,
endArrow: true,
}
},
layout: {
type: 'dagre', // 层次布局
rankdir: 'LR', // 布局从左向右
controlPoints: true, // 节点间连线的控制点
nodesep: 10, // 同一层节点之间的距离
ranksep: 50, // 不同层节点之间的距离
},
modes: {
default: [
'drag-canvas', // 鼠标拖拽移动画布
'zoom-canvas', // 鼠标滚轮缩放
// 'drag-node' // 可选:允许拖拽节点
]
},
});
}
// 加载思维导图数据
const onMindGraphData = (nodes=[], edges=[]) => {
let data = { nodes:[], edges }
nodes.forEach(node => {
if (node.maxWidth) onFormatLineFeed(node);
data.nodes.push(node);
})
if (!graph) onInitGraph();
graph.data(data);
graph.render();
}
// 获取文本宽度
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const getLabelWidth = (label, size, family) => {
ctx.font = `${size}px ${family}`;
return ctx.measureText(label).width;
}
// 文本插入换行符
const onFormatLineFeed = (node) => {
let size = node?.labelCfg?.style?.fontSize || '16'
let family = node?.labelCfg?.style?.fontFamily || 'Source Han Sans CN'
const lines = [];
let line = '';
for (let char of node.label) {
const testLine = line + char;
const width = getLabelWidth(testLine, size, family);
if (width > node.maxWidth-40) {
lines.push(line);
line = char;
} else {
line = testLine;
}
}
if (line) lines.push(line);
node.label = lines.join("\n")
node.size = [node.maxWidth, 25*lines.length+20]
}
defineExpose({ onMindGraphData })
onBeforeUnmount(() => {
graph?.destroy()
})
</script>
<style scoped lang="scss">
.view-box {
width: 100%;
height: 100%;
}
</style>
\ No newline at end of file
...@@ -170,7 +170,6 @@ const onWordWrap = (word, num) => { ...@@ -170,7 +170,6 @@ const onWordWrap = (word, num) => {
} }
const handleClickDecree = decree => { const handleClickDecree = decree => {
window.sessionStorage.setItem("curTabName", decree.name);
const route = router.resolve({ const route = router.resolve({
path: "/decreeLayout", path: "/decreeLayout",
query: { query: {
......
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
</div> </div>
</template> </template>
<div class="box2-main"> <div class="box2-main">
<AiTips :tips="tips" /> <!-- <AiTips :tips="tips" /> -->
<div class="graph-box" v-if="contentType==1"> <div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" /> <ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div> </div>
...@@ -100,7 +100,7 @@ import { ...@@ -100,7 +100,7 @@ import {
getDecreeRelatedEntitie getDecreeRelatedEntitie
} from "@/api/decree/influence"; } from "@/api/decree/influence";
import ChartChain from "./com/ChartChain.vue"; 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 GraphChart from "@/components/base/GraphChart/index.vue";
import defaultIcon2 from "@/assets/icons/default-icon2.png"; import defaultIcon2 from "@/assets/icons/default-icon2.png";
import noticeIcon from "./assets/images/notice-icon.png"; import noticeIcon from "./assets/images/notice-icon.png";
...@@ -186,7 +186,7 @@ const headerChartData = (row) => { ...@@ -186,7 +186,7 @@ const headerChartData = (row) => {
onDecreeRelatedChain(row.id) onDecreeRelatedChain(row.id)
break; break;
case 2: case 2:
onDecreeRelatedEntitie(row.id) onDecreeRelatedEntitie(row.orgId)
break; break;
} }
} }
...@@ -247,14 +247,17 @@ const graphInfo = reactive({ ...@@ -247,14 +247,17 @@ const graphInfo = reactive({
nodes: [], nodes: [],
links: [], links: [],
}); });
const onDecreeRelatedEntitie = async (id) => { const onDecreeRelatedEntitie = async (orgId) => {
try { try {
const res = await getDecreeRelatedEntitie({ id }); const res = await getDecreeRelatedEntitie({ orgId, rule:false, withSanInfo:false });
console.log("实体关系:", res); console.log("实体关系:", res);
if (res.code === 200) { if (res.code === 200) {
graphInfo.links = res.data.map(onFormatLink) let arr1 = res.data.parentOrgList.map(item => ({ ...item, level: 1 }))
graphInfo.nodes = res.data.map(onFormatNode) let arr3 = res.data.childrenOrgList.map(item => ({ ...item, level: 3 }))
graphInfo.nodes.unshift(onFormatNode(entityInfo.node))
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) { } catch (error) {
console.log("获取实体关系失败", error); console.log("获取实体关系失败", error);
...@@ -262,17 +265,18 @@ const onDecreeRelatedEntitie = async (id) => { ...@@ -262,17 +265,18 @@ const onDecreeRelatedEntitie = async (id) => {
} }
const onFormatLink = (item, index) => { const onFormatLink = (item, index) => {
return { return {
id: `link-${index+1}`, id: `link-${index}-${item.id}`,
source: item.id+'', target: entityInfo.id+'', target: item.level==3 ? `${index}-${item.id}` : `-1-${entityInfo.node.orgId}`,
label: { show: true, color: "#055fc2", backgroundColor: "#eef7ff", borderWidth: 0, offset: [0, 15], formatter: item.relation }, 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 } lineStyle: { color: '#B9DCFF', type: "solid", opacity: 1 }
} }
} }
const onFormatNode = (item) => { const onFormatNode = (item, index) => {
let leader = item.id == entityInfo.id; let leader = item.id == entityInfo.node.orgId;
return { return {
id: item.id+'', id: `${index}-${item.id}`,
name: onWordWrap(item.companyName, 7), name: onWordWrap(item.name, 7),
label: { label: {
show: true, show: true,
color: "#3b414b", color: "#3b414b",
......
...@@ -46,7 +46,7 @@ ...@@ -46,7 +46,7 @@
<div class="custom-collapse-title"> <div class="custom-collapse-title">
<div class="custom-collapse-index">{{ index + 1 }}</div> <div class="custom-collapse-index">{{ index + 1 }}</div>
<div class="custom-collapse-name one-line-ellipsis"> <div class="custom-collapse-name one-line-ellipsis">
<span class="text-click-hover" @click.stop="handleClickDecree(item)">{{ item.title }}</span> <span class="text-click-hover" @click.stop="handleClickBull(item)">{{ item.title }}</span>
</div> </div>
</div> </div>
</template> </template>
...@@ -73,9 +73,9 @@ ...@@ -73,9 +73,9 @@
<div class="time-line-icon"> <div class="time-line-icon">
<img style="width: 100%; height: 100%;" :src="item.orgImage || DefaultIcon1" alt=""> <img style="width: 100%; height: 100%;" :src="item.orgImage || DefaultIcon1" alt="">
</div> </div>
<div class="time-line-name" @click="handleToInstitution(item)">{{ item.proposeOrgName }}</div> <div class="time-line-name text-click-hover" @click="handleToInstitution(item)">{{ item.proposeOrgName }}</div>
</div> </div>
<div class="timeline-content">{{ item.describe }}</div> <div class="timeline-content" @click="handleClickDecree(item)">{{ item.describe }}</div>
</div> </div>
</el-timeline-item> </el-timeline-item>
</el-timeline> </el-timeline>
...@@ -167,15 +167,24 @@ const handleGetPrev = async () => { ...@@ -167,15 +167,24 @@ const handleGetPrev = async () => {
}; };
// 跳转行政机构主页 // 跳转行政机构主页
const handleToInstitution = item => { const handleToInstitution = item => {
window.sessionStorage.setItem("curTabName", item.proposeOrgName);
const curRoute = router.resolve({ const curRoute = router.resolve({
path: "/institution", path: "/institution",
query: { query: {
id: item.id id: item.orgId
} }
}); });
window.open(curRoute.href, "_blank"); window.open(curRoute.href, "_blank");
}; };
// 跳转科技政令详情页
const handleClickDecree = item => {
const route = router.resolve({
path: "/decreeLayout",
query: {
id: item.id
}
});
window.open(route.href, "_blank");
};
// 法律依据 // 法律依据
const dependList = ref([]); const dependList = ref([]);
...@@ -186,7 +195,7 @@ const handleGetLaws = async () => { ...@@ -186,7 +195,7 @@ const handleGetLaws = async () => {
console.log("法律依据", res); console.log("法律依据", res);
if (res.code === 200 && res.data) { if (res.code === 200 && res.data) {
dependList.value = res.data; dependList.value = res.data;
dependActive.value = res.data.map(item => item.billId); // dependActive.value = res.data.map(item => item.billId);
} else { } else {
dependList.value = []; dependList.value = [];
} }
...@@ -196,7 +205,7 @@ const handleGetLaws = async () => { ...@@ -196,7 +205,7 @@ const handleGetLaws = async () => {
} }
}; };
// 跳转科技法案详情页 // 跳转科技法案详情页
const handleClickDecree = decree => { const handleClickBull = decree => {
window.sessionStorage.setItem("billId", decree.billId); window.sessionStorage.setItem("billId", decree.billId);
window.sessionStorage.setItem("curTabName", decree.title); window.sessionStorage.setItem("curTabName", decree.title);
const route = router.resolve({ const route = router.resolve({
...@@ -414,7 +423,6 @@ onMounted(() => { ...@@ -414,7 +423,6 @@ onMounted(() => {
.time-line-name { .time-line-name {
color: rgba(59, 65, 75, 1); color: rgba(59, 65, 75, 1);
font-size: 15px; font-size: 15px;
cursor: pointer;
} }
} }
...@@ -445,6 +453,7 @@ onMounted(() => { ...@@ -445,6 +453,7 @@ onMounted(() => {
font-size: 16px; font-size: 16px;
font-weight: 400; font-weight: 400;
line-height: 26px; line-height: 26px;
cursor: pointer;
} }
} }
} }
......
...@@ -83,7 +83,7 @@ ...@@ -83,7 +83,7 @@
<img :src="item.imageUrl || DefaultIconNews" alt="" /> <img :src="item.imageUrl || DefaultIconNews" alt="" />
</div> </div>
<div class="item-center"> <div class="item-center">
<div class="bubble-header" @click="handleClickToNewsDetail(item)"> <div class="bubble-header" @click="gotoNewsDetail(item.id)">
<span class="name">{{ item.sjbt }}</span> <span class="name">{{ item.sjbt }}</span>
<span class="meta">{{ item.sjsj }} · {{ item.source }}</span> <span class="meta">{{ item.sjsj }} · {{ item.source }}</span>
</div> </div>
...@@ -177,6 +177,9 @@ import DefaultIcon1 from "@/assets/icons/default-icon1.png"; ...@@ -177,6 +177,9 @@ import DefaultIcon1 from "@/assets/icons/default-icon1.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png"; import DefaultIcon2 from "@/assets/icons/default-icon2.png";
import DefaultIconNews from "@/assets/icons/default-icon-news.png"; import DefaultIconNews from "@/assets/icons/default-icon-news.png";
import { useGotoNewsDetail } from '@/router/modules/news';
const gotoNewsDetail = useGotoNewsDetail()
const route = useRoute(); const route = useRoute();
const decreeId = ref(route.query.id); const decreeId = ref(route.query.id);
...@@ -235,7 +238,7 @@ handleGetBasicInfo(); ...@@ -235,7 +238,7 @@ handleGetBasicInfo();
const wordCloudData = ref([]) const wordCloudData = ref([])
const onKeyWordUp = async () => { const onKeyWordUp = async () => {
try { try {
const res = await getKeyWordUp(); const res = await getKeyWordUp({id: decreeId.value});
console.log("政令关键词云", res); console.log("政令关键词云", res);
wordCloudData.value = res.data.slice(0, 10).map(item => ({name: item.name, value: item.count})); wordCloudData.value = res.data.slice(0, 10).map(item => ({name: item.name, value: item.count}));
} catch (error) { } catch (error) {
...@@ -271,15 +274,6 @@ const handleGetRelateEvents = async () => { ...@@ -271,15 +274,6 @@ const handleGetRelateEvents = async () => {
console.error("获取相关事件数据失败", error); console.error("获取相关事件数据失败", error);
} }
}; };
const handleClickToNewsDetail = news => {
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: news.id
}
});
window.open(route.href, "_blank");
};
// 发布机构 // 发布机构
const box3TopTopData = ref({ const box3TopTopData = ref({
...@@ -476,7 +470,6 @@ onMounted(() => { ...@@ -476,7 +470,6 @@ onMounted(() => {
font-weight: 400; font-weight: 400;
font-size: 16px; font-size: 16px;
line-height: 30px; line-height: 30px;
min-height: 300px;
} }
} }
......
...@@ -3,6 +3,14 @@ ...@@ -3,6 +3,14 @@
<div class="page-left"> <div class="page-left">
<div class="box1"> <div class="box1">
<AnalysisBox title="主要指令" :showAllBtn="false"> <AnalysisBox title="主要指令" :showAllBtn="false">
<template #header-btn>
<div class="mind-bnt" @click="onDecreeMindMap()">
<div class="mind-icon">
<img src="./assets/images/edit-line.png" alt="">
</div>
<div class="mind-text">思维导图</div>
</div>
</template>
<div class="analysis-box"> <div class="analysis-box">
<div class="analysis-top"> <div class="analysis-top">
<el-select v-model="areaType" :empty-values="[null, undefined]" @change="onMainContentData()" style="width: 200px;"> <el-select v-model="areaType" :empty-values="[null, undefined]" @change="onMainContentData()" style="width: 200px;">
...@@ -92,7 +100,7 @@ ...@@ -92,7 +100,7 @@
</div> </div>
</div> </div>
</div> </div>
<div class="organization-button"> <div class="organization-button" @click="handleClickToolBox()">
<div class="button-text">查看政令执行情况</div> <div class="button-text">查看政令执行情况</div>
<el-icon size="16"><Right /></el-icon> <el-icon size="16"><Right /></el-icon>
</div> </div>
...@@ -114,16 +122,29 @@ ...@@ -114,16 +122,29 @@
</AnalysisBox> </AnalysisBox>
</div> </div>
</div> </div>
<el-dialog v-model="isTreeDialog" width="1540px" top="8vh" class="viewpoint-dialog" destroy-on-close>
<template #header>
<div class="viewpoint-header">
<div class="viewpoint-title">政令举措思维导图</div>
</div>
</template>
<div class="viewpoint-body">
<MindGraph ref="refMindGraph"></MindGraph>
</div>
</el-dialog>
</div> </div>
</template> </template>
<script setup> <script setup>
import { ref, onMounted, reactive } from "vue"; import { ref, onMounted, reactive } from "vue";
import { useRoute } from "vue-router"; import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import router from "@/router"; import router from "@/router";
import { Search } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import MindGraph from "@/views/decree/com/MindGraph.vue"
import { getDecreeOrganization } from "@/api/decree/introduction"; import { getDecreeOrganization } from "@/api/decree/introduction";
import { getDecreeRelatedEntity, getDecreeMainContent } from "@/api/decree/background"; import { getDecreeRelatedEntity, getDecreeMainContent, getDecreeMindMap } from "@/api/decree/background";
import { getDecreehylyList } from "@/api/decree/home"; import { getDecreehylyList } from "@/api/decree/home";
import ActionButton from '@/components/base/ActionButton/index.vue' import ActionButton from '@/components/base/ActionButton/index.vue'
import DefaultIcon1 from "@/assets/icons/default-icon1.png"; import DefaultIcon1 from "@/assets/icons/default-icon1.png";
...@@ -132,6 +153,10 @@ import defaultCom from "@/views/coopRestriction/assets/images/default-icon2.png" ...@@ -132,6 +153,10 @@ import defaultCom from "@/views/coopRestriction/assets/images/default-icon2.png"
const route = useRoute(); const route = useRoute();
const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
// 科技领域 // 科技领域
const areaType = ref(""); const areaType = ref("");
const areaList = ref([]); const areaList = ref([]);
...@@ -272,6 +297,50 @@ const simpleNumToChinese = (num) => { ...@@ -272,6 +297,50 @@ const simpleNumToChinese = (num) => {
} }
} }
// 思维导图
const isTreeDialog = ref(false);
const refMindGraph = ref(null);
const onDecreeMindMap = async () => {
let labelCfg = {
position: 'left',
offset: -20,
style: {
fontWeight: "normal",
fontSize: 16,
textAlign: 'left',
autoWrap: true
}
}
isTreeDialog.value = true;
try {
let res = await getDecreeMindMap({id: route.query.id});
console.log("思维导图", res);
if (res.code === 200) {
let nodes = []
let edges = []
Object.keys(res.data||{}).forEach((label, count) => {
nodes.push({ id:`node-${count}`, label })
})
Object.values(res.data||{}).forEach((list, count) => {
list.forEach((item, index) => {
if (item.execAgent?.length) {
nodes.push({ id:`node-${count}-${index}`, label:item.textZh, maxWidth:600, labelCfg })
item.execAgent.forEach((label, num) => {
nodes.push({ id:`node-${count}-${index}-${num}`, label })
edges.push({ id:`edge1-${count}-${index}-${num}`, source:`node-${count}-${index}-${num}`, target:`node-${count}-${index}` })
edges.push({ id:`edge2-${count}-${index}-${num}`, source:`node-${count}`, target:`node-${count}-${index}-${num}` })
})
}
})
})
setTimeout(() => { refMindGraph.value.onMindGraphData(nodes, edges) }, 100)
}
} catch (error) {
console.error("获取思维导图数据失败:", error);
}
}
// 相关实体 // 相关实体
const entityList = ref([]); const entityList = ref([]);
const onRelatedEntityData = async () => { const onRelatedEntityData = async () => {
...@@ -359,6 +428,31 @@ onMounted(() => { ...@@ -359,6 +428,31 @@ onMounted(() => {
flex: auto; flex: auto;
.box1 { .box1 {
.mind-bnt {
background-color: var(--color-primary-10);
height: 28px;
border-radius: 14px;
display: flex;
align-items: center;
padding: 0 16px;
cursor: pointer;
.mind-icon {
width: 16px;
height: 13px;
font-size: 0px;
img {
width: 100%;
height: 100%;
}
}
.mind-text {
color: var(--color-primary-100);
font-family: Source Han Sans CN;
font-size: 16px;
line-height: 16px;
margin-left: 6px;
}
}
.analysis-box { .analysis-box {
display: flex; display: flex;
...@@ -776,5 +870,42 @@ onMounted(() => { ...@@ -776,5 +870,42 @@ onMounted(() => {
} }
} }
} }
// 修改element-plus弹出框样式
:deep(.viewpoint-dialog) {
padding: 0;
border-radius: 4px;
.el-dialog__body {
padding: 0;
}
.el-dialog__header {
padding: 0;
margin: 0;
position: relative;
height: 48px;
}
.el-dialog__headerbtn {
top: 50%;
transform: translateY(-50%);
right: 12px;
}
.viewpoint-header {
height: 48px;
display: flex;
align-items: center;
padding: 0 24px;
border-bottom: 1px solid rgb(234, 236, 238);
}
.viewpoint-title {
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
}
.viewpoint-body {
padding: 16px;
height: 77vh;
}
}
} }
</style> </style>
\ No newline at end of file
...@@ -196,10 +196,10 @@ const setActivePanelId = id => { ...@@ -196,10 +196,10 @@ const setActivePanelId = id => {
background-image: url("../../assets/images/activeDown.png"); background-image: url("../../assets/images/activeDown.png");
} }
.iconUp { .iconUp {
background-image: url("../../assets/images/UnActiveUp.png"); background-image: url("../../assets/images/unActiveUp.png");
} }
.iconDown { .iconDown {
background-image: url("../../assets/images/UnActiveDown.png"); background-image: url("../../assets/images/unActiveDown.png");
} }
} }
.activeItem { .activeItem {
......
...@@ -368,19 +368,29 @@ ...@@ -368,19 +368,29 @@
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 505px; margin-top: 16px"> <el-row :gutter="20" style="width: 1600px; margin: 0 auto; height: 540px; margin-top: 16px">
<el-col :span="8"> <el-col :span="8">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="480px"> <custom-container title="实体领域分布" :titleIcon="radarIcon" height="540px">
<template #header-right> <template #header-right>
<el-checkbox v-model="domainChecked" label="50%规则" size="large" /> <el-checkbox v-model="domainChecked" label="50%规则" size="large" />
</template> </template>
<template #default> <template #default>
<EChart :option="radarOption" autoresize :style="{ height: '460px' }" /> <EChart :option="radarOption" autoresize :style="{ height: '420px' }" />
<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.loading ? '解读生成中...' : radarChart.interpretation" />
</div>
</template> </template>
</custom-container> </custom-container>
</el-col> </el-col>
<el-col :span="16"> <el-col :span="16">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="480px"> <custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="540px">
<template #header-right> <template #header-right>
<div style="display: flex; align-items: center; gap: 16px"> <div style="display: flex; align-items: center; gap: 16px">
<el-checkbox v-model="trendChecked" label="50%规则" size="large" /> <el-checkbox v-model="trendChecked" label="50%规则" size="large" />
...@@ -390,13 +400,23 @@ ...@@ -390,13 +400,23 @@
</div> </div>
</template> </template>
<template #default> <template #default>
<EChart :option="trendOption" autoresize :style="{ height: '400px' }" /> <EChart :option="trendOption" autoresize :style="{ height: '420px' }" />
<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.loading ? '解读生成中...' : trendChart.interpretation" />
</div>
</template> </template>
</custom-container> </custom-container>
</el-col> </el-col>
</el-row> </el-row>
<el-row :gutter="20" style="width: 1600px; margin: 0 auto; margin-top: 39px"> <el-row :gutter="20" style="width: 1600px; margin: 0 auto; margin-top: 39px; padding-bottom: 60px">
<CustomTitle id="position4" title="资源库" style="margin-top: 0px" /> <CustomTitle id="position4" title="资源库" style="margin-top: 0px" />
<div class="resource-tabs"> <div class="resource-tabs">
<div <div
...@@ -714,11 +734,18 @@ import { ElMessage, ElMessageBox } from "element-plus"; ...@@ -714,11 +734,18 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { DArrowRight, Warning, Search } from "@element-plus/icons-vue"; import { DArrowRight, Warning, Search } from "@element-plus/icons-vue";
import EChart from "@/components/Chart/index.vue"; import EChart from "@/components/Chart/index.vue";
import tipsIcon from "./assets/icons/info-icon.png";
import AiButton from "@/components/base/Ai/AiButton/index.vue";
import AiPane from "@/components/base/Ai/AiPane/index.vue";
import { useChartInterpretation } from "@/views/exportControl/utils/common";
const sanctionCountChart = useChartInterpretation();
import { TAGTYPE } from "@/public/constant"; import { TAGTYPE } from "@/public/constant";
import { useGotoCompanyPages } from "@/router/modules/company"; import { useGotoCompanyPages } from "@/router/modules/company";
import { useGotoNewsDetail } from "@/router/modules/news"; import { useGotoNewsDetail } from "@/router/modules/news";
const gotoCompanyPages = useGotoCompanyPages(); const gotoCompanyPages = useGotoCompanyPages();
const gotoNewsDetail = useGotoNewsDetail(); const gotoNewsDetail = useGotoNewsDetail();
const trendChart = useChartInterpretation();
const radarChart = useChartInterpretation();
import { useRouter } from "vue-router"; import { useRouter } from "vue-router";
...@@ -1026,6 +1053,7 @@ const fetchTrendData = async () => { ...@@ -1026,6 +1053,7 @@ const fetchTrendData = async () => {
}); });
if (res && res[0] && res[0].yearDomainCount) { if (res && res[0] && res[0].yearDomainCount) {
trendOption.value = processYearDomainCountData(res[0].yearDomainCount); trendOption.value = processYearDomainCountData(res[0].yearDomainCount);
trendChart.interpret({ type: "柱状图", name: "实体清单数量增长趋势", data: res[0].yearDomainCount });
} }
} catch (error) { } catch (error) {
console.error("获取趋势图数据失败:", error); console.error("获取趋势图数据失败:", error);
...@@ -1297,6 +1325,7 @@ const fetchRadarData = async checked => { ...@@ -1297,6 +1325,7 @@ const fetchRadarData = async checked => {
} }
}; };
}); });
radarChart.interpret({ type: "雷达图", name: "实体领域分布", data: data });
} }
} catch (error) { } catch (error) {
console.error("获取雷达图数据失败:", error); console.error("获取雷达图数据失败:", error);
...@@ -3573,7 +3602,7 @@ const handleMediaClick = item => { ...@@ -3573,7 +3602,7 @@ const handleMediaClick = item => {
} }
.scroll-main { .scroll-main {
height: calc(100% - 144px) !important; // height: calc(100% - 144px) !important;
} }
.center-center { .center-center {
...@@ -3596,4 +3625,50 @@ const handleMediaClick = item => { ...@@ -3596,4 +3625,50 @@ const handleMediaClick = item => {
background: rgba(255, 255, 255, 1); 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> </style>
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
};
};
差异被折叠。
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论