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

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

---
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 --include=optional --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 --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 @@
"json5": "^2.2.3",
"lodash": "^4.17.21",
"markdown-it": "^14.1.0",
"pdfjs-dist": "^5.4.449",
"pdfjs-dist": "^5.5.207",
"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,则提前解析并中断连接
......
......@@ -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 }
......
......@@ -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
})
}
......@@ -91,10 +91,11 @@ export function getDecreeTypeList() {
}
// 关键机构
export function getKeyOrganization() {
export function getKeyOrganization(params) {
return request({
method: 'GET',
url: `/api/commonFeature/keyOrganization`,
params
})
}
......
......@@ -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
})
}
......
......@@ -80,10 +80,10 @@ export function getDecreeReport(params) {
}
// 政令关键词云
export function getKeyWordUp() {
export function getKeyWordUp(params) {
return request({
method: 'GET',
url: `/api/element/getKeyWordUp/2025-01-01`,
url: `/api/administrativeOrderInfo/wordCloud/${params.id}`,
})
}
......
......@@ -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({
......
......@@ -9,7 +9,7 @@
<div class="switch-label switch-label-left">高亮实体</div>
<el-switch v-model="isTranslate" />
<div class="switch-label">文显示</div>
<div class="switch-label">文显示</div>
<div
v-for="action in headerActions"
......@@ -53,8 +53,8 @@
class="content-row"
:class="{ 'high-light': isHighlight }"
>
<div class="content-cn" :class="{ 'translate-cn': !isTranslate }" v-html="item.content" />
<div v-if="isTranslate" class="content-en" v-html="item.contentEn" />
<div class="content-en" v-html="item.contentEn" :class="{ 'translate-cn': !isTranslate }"></div>
<div class="content-cn" v-html="item.content" v-if="isTranslate"></div>
</div>
</el-scrollbar>
</div>
......@@ -150,11 +150,9 @@ const doUpdateWord = () => {
}
displayReportData.value = originReportData.value.map((item) => {
const cn = applyHighlightToText(item.content, term);
const en = isTranslate.value
? applyHighlightToText(item.contentEn, term)
: { html: item.contentEn, count: 0 };
findWordMax.value += cn.count + en.count;
const en = applyHighlightToText(item.contentEn, term);
const cn = isTranslate.value ? applyHighlightToText(item.content, term) : { html: item.content, count: 0 };
findWordMax.value += en.count + cn.count;
return {
...item,
content: cn.html,
......
......@@ -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;
......
......@@ -14,13 +14,13 @@
</div>
<div class="header-right">
<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 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 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>
......@@ -31,21 +31,21 @@
</template>
<script setup>
import { ElMessage } from 'element-plus'
import { ref, computed } from 'vue'
import { ElMessage } from "element-plus";
import { ref, computed } from "vue";
const props = defineProps({
title: {
type: String,
default: ''
default: ""
},
width: {
type: String,
default: ''
default: ""
},
height: {
type: String,
default: ''
default: ""
},
showAllBtn: {
type: Boolean,
......@@ -56,39 +56,36 @@ const props = defineProps({
type: Boolean,
default: false
}
})
});
const handleSave = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
ElMessage.warning("当前功能正在开发中,敬请期待!");
return;
}
ElMessage.success('保存当前内容')
ElMessage.success("保存当前内容");
// emit('save')
}
};
const handleDownload = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
ElMessage.warning("当前功能正在开发中,敬请期待!");
return;
}
ElMessage.success('下载当前内容')
ElMessage.success("下载当前内容");
// emit('download')
}
};
const handleCollect = () => {
if (props.devTip) {
ElMessage.warning('当前功能正在开发中,敬请期待!')
return
ElMessage.warning("当前功能正在开发中,敬请期待!");
return;
}
ElMessage.success('收藏当前内容')
ElMessage.success("收藏当前内容");
// emit('collect')
};
}
const emit = defineEmits(['save', 'download', 'collect'])
const emit = defineEmits(["save", "download", "collect"]);
</script>
<style lang="scss" scoped>
......@@ -97,7 +94,7 @@ const emit = defineEmits(['save', 'download', 'collect'])
border-radius: 10px;
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
background: rgba(255, 255, 255, 1);
position: relative;
.wrapper-header {
height: 45px;
display: flex;
......@@ -123,7 +120,7 @@ const emit = defineEmits(['save', 'download', 'collect'])
// line-height: 26px;
// letter-spacing: 0px;
height: 100%;
&>div {
& > div {
height: 100%;
color: var(--color-main-active);
font-family: Microsoft YaHei;
......
......@@ -9,6 +9,7 @@ const DecreeDeepDig = () => import('@/views/decree/decreeLayout/deepdig/index.vu
const DecreeInfluence = () => import('@/views/decree/decreeLayout/influence/index.vue')
const Institution = () => import('@/views/decree/institution/index.vue')
const DecreeOriginal = () => import('@/views/decree/decreeOriginal/index.vue')
const allOrganization = () => import('@/views/decree/allOrganization/index.vue')
const decreeRoutes = [
......@@ -93,11 +94,12 @@ const decreeRoutes = [
path: "/decree/decreeOriginal",
name: "DecreeOriginal",
component: DecreeOriginal,
// meta: {
// title: "政令原文"
// }
},
{
path: "/decree/allOrganization",
name: "allOrganization",
component: allOrganization,
},
]
export default decreeRoutes
\ No newline at end of file
......@@ -15,22 +15,29 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chart.on('click', function (params) {
switch (selectParam.moduleType) {
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') {
console.log('点击的扇形名称:', params.name);
if (selectParam.key === '领域') {
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 === '议院委员会') {
if (params.name === '众议院' || params.name === '参议院') {
selectParam.selectedCongress = params.name
selectParam.selectedOrg = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
} else {
selectParam.selectedOrg = params.name
selectParam.selectedCongress = ''
if (selectParam.selectedDate.length === 4) {
selectParam.selectedDate = JSON.stringify([selectParam.selectedDate + '-01-01', selectParam.selectedDate + '-12-31'])
}
}
}
const route = router.resolve({
......@@ -40,9 +47,12 @@ const setChart = (option, chartId, allowClick, selectParam) => {
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
if (params.name === '已立法') {
selectParam.selectedStauts = 1
selectParam.selectedStatus = 1
} 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({
path: "/dataLibrary/countryBill",
......@@ -52,11 +62,13 @@ const setChart = (option, chartId, allowClick, selectParam) => {
} else {
console.log('当前点击', selectParam, params.seriesName, params.name);
if (params.seriesName !== '通过率') {
selectParam = {
selectedDate: JSON.stringify(getMonthRange(params.name)),
status: params.seriesName === '通过法案' ? 1 : 0,
...selectParam
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
if (params.seriesName === '通过法案') {
selectParam.selectedStatus = 1
} else {
selectParam.selectedStatus = null
}
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
......
......@@ -1006,7 +1006,7 @@ const handleBox9Data = async () => {
moduleType: '国会法案',
key: '领域',
selectedDate: box9selectetedTime.value,
status: box9LegislativeStatus.value === '提出法案' ? 0 : 1,
selectedStatus: box9LegislativeStatus.value === '提出法案' ? 0 : 1,
isInvolveCn: 1
}
box9ChartInstance = setChart(box9Chart, "box9Chart", true, selectParam);
......
......@@ -3,20 +3,20 @@ import * as echarts from 'echarts'
const getMultiLineChart = (dataX, dataY1, dataY2, dataY3) => {
return {
tooltip: {
trigger: 'axis',
trigger: 'item',
axisPointer: {
type: 'cross',
label: {
backgroundColor: '#6a7985'
}
},
formatter: function (params) {
let res = params[0].name + '<br/>';
params.forEach(item => {
res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>';
});
return res;
}
// formatter: function (params) {
// let res = params[0].name + '<br/>';
// params.forEach(item => {
// res += item.marker + item.seriesName + ': ' + item.value + (item.seriesName === '通过率' ? '%' : '') + '<br/>';
// });
// return res;
// }
},
grid: {
width: '96%',
......
......@@ -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;
}
......
......@@ -78,7 +78,7 @@ const props = defineProps({
type: String,
default: ''
},
chartTypeList : {
chartTypeList: {
type: Array,
default: []
}
......@@ -88,15 +88,19 @@ const chartItemList = computed(() => {
let arr = []
props.chartTypeList.forEach(item => {
defaultChartTypeList.forEach(val => {
if(val.name === item) {
if (val.name === item) {
arr.push(val)
}
})
})
arr.forEach(item => {
item.active = false
})
arr[0].active = true
// console.log('arr', arr);
return arr
})
......
......@@ -478,12 +478,13 @@ const handleClickToolBox = () => {
onMounted(() => {
const path = route.path
console.log(decodeURI(route.fullPath));
switch (path) {
case '/dataLibrary/countryBill':
siderList.value[0].active = true
siderList.value[0].isExpanded = true
siderList.value[0].children[0].active = true
break
case '/dataLibrary/stateBill':
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) => {
}
const handleClickDecree = decree => {
window.sessionStorage.setItem("curTabName", decree.name);
const route = router.resolve({
path: "/decreeLayout",
query: {
......
......@@ -75,7 +75,7 @@
</div>
</template>
<div class="box2-main">
<AiTips :tips="tips" />
<!-- <AiTips :tips="tips" /> -->
<div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div>
......@@ -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",
......
......@@ -46,7 +46,7 @@
<div class="custom-collapse-title">
<div class="custom-collapse-index">{{ index + 1 }}</div>
<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>
</template>
......@@ -73,9 +73,9 @@
<div class="time-line-icon">
<img style="width: 100%; height: 100%;" :src="item.orgImage || DefaultIcon1" alt="">
</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 class="timeline-content">{{ item.describe }}</div>
<div class="timeline-content" @click="handleClickDecree(item)">{{ item.describe }}</div>
</div>
</el-timeline-item>
</el-timeline>
......@@ -167,15 +167,24 @@ const handleGetPrev = async () => {
};
// 跳转行政机构主页
const handleToInstitution = item => {
window.sessionStorage.setItem("curTabName", item.proposeOrgName);
const curRoute = router.resolve({
path: "/institution",
query: {
id: item.id
id: item.orgId
}
});
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([]);
......@@ -186,7 +195,7 @@ const handleGetLaws = async () => {
console.log("法律依据", res);
if (res.code === 200 && res.data) {
dependList.value = res.data;
dependActive.value = res.data.map(item => item.billId);
// dependActive.value = res.data.map(item => item.billId);
} else {
dependList.value = [];
}
......@@ -196,7 +205,7 @@ const handleGetLaws = async () => {
}
};
// 跳转科技法案详情页
const handleClickDecree = decree => {
const handleClickBull = decree => {
window.sessionStorage.setItem("billId", decree.billId);
window.sessionStorage.setItem("curTabName", decree.title);
const route = router.resolve({
......@@ -414,7 +423,6 @@ onMounted(() => {
.time-line-name {
color: rgba(59, 65, 75, 1);
font-size: 15px;
cursor: pointer;
}
}
......@@ -445,6 +453,7 @@ onMounted(() => {
font-size: 16px;
font-weight: 400;
line-height: 26px;
cursor: pointer;
}
}
}
......
......@@ -83,7 +83,7 @@
<img :src="item.imageUrl || DefaultIconNews" alt="" />
</div>
<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="meta">{{ item.sjsj }} · {{ item.source }}</span>
</div>
......@@ -177,6 +177,9 @@ import DefaultIcon1 from "@/assets/icons/default-icon1.png";
import DefaultIcon2 from "@/assets/icons/default-icon2.png";
import DefaultIconNews from "@/assets/icons/default-icon-news.png";
import { useGotoNewsDetail } from '@/router/modules/news';
const gotoNewsDetail = useGotoNewsDetail()
const route = useRoute();
const decreeId = ref(route.query.id);
......@@ -235,7 +238,7 @@ handleGetBasicInfo();
const wordCloudData = ref([])
const onKeyWordUp = async () => {
try {
const res = await getKeyWordUp();
const res = await getKeyWordUp({id: decreeId.value});
console.log("政令关键词云", res);
wordCloudData.value = res.data.slice(0, 10).map(item => ({name: item.name, value: item.count}));
} catch (error) {
......@@ -271,15 +274,6 @@ const handleGetRelateEvents = async () => {
console.error("获取相关事件数据失败", error);
}
};
const handleClickToNewsDetail = news => {
const route = router.resolve({
path: "/newsAnalysis",
query: {
newsId: news.id
}
});
window.open(route.href, "_blank");
};
// 发布机构
const box3TopTopData = ref({
......@@ -476,7 +470,6 @@ onMounted(() => {
font-weight: 400;
font-size: 16px;
line-height: 30px;
min-height: 300px;
}
}
......
......@@ -3,6 +3,14 @@
<div class="page-left">
<div class="box1">
<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-top">
<el-select v-model="areaType" :empty-values="[null, undefined]" @change="onMainContentData()" style="width: 200px;">
......@@ -92,7 +100,7 @@
</div>
</div>
</div>
<div class="organization-button">
<div class="organization-button" @click="handleClickToolBox()">
<div class="button-text">查看政令执行情况</div>
<el-icon size="16"><Right /></el-icon>
</div>
......@@ -114,16 +122,29 @@
</AnalysisBox>
</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>
</template>
<script setup>
import { ref, onMounted, reactive } from "vue";
import { useRoute } from "vue-router";
import { ElMessage } from "element-plus";
import router from "@/router";
import { Search } from '@element-plus/icons-vue'
import MindGraph from "@/views/decree/com/MindGraph.vue"
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 ActionButton from '@/components/base/ActionButton/index.vue'
import DefaultIcon1 from "@/assets/icons/default-icon1.png";
......@@ -132,6 +153,10 @@ import defaultCom from "@/views/coopRestriction/assets/images/default-icon2.png"
const route = useRoute();
const handleClickToolBox = () => {
ElMessage.warning("当前功能正在开发中,敬请期待!");
};
// 科技领域
const areaType = ref("");
const areaList = ref([]);
......@@ -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 onRelatedEntityData = async () => {
......@@ -359,6 +428,31 @@ onMounted(() => {
flex: auto;
.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 {
display: flex;
......@@ -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>
\ No newline at end of file
......@@ -196,10 +196,10 @@ const setActivePanelId = id => {
background-image: url("../../assets/images/activeDown.png");
}
.iconUp {
background-image: url("../../assets/images/UnActiveUp.png");
background-image: url("../../assets/images/unActiveUp.png");
}
.iconDown {
background-image: url("../../assets/images/UnActiveDown.png");
background-image: url("../../assets/images/unActiveDown.png");
}
}
.activeItem {
......
......@@ -368,19 +368,29 @@
</el-col>
</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">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="480px">
<custom-container title="实体领域分布" :titleIcon="radarIcon" height="540px">
<template #header-right>
<el-checkbox v-model="domainChecked" label="50%规则" size="large" />
</template>
<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>
</custom-container>
</el-col>
<el-col :span="16">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="480px">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="540px">
<template #header-right>
<div style="display: flex; align-items: center; gap: 16px">
<el-checkbox v-model="trendChecked" label="50%规则" size="large" />
......@@ -390,13 +400,23 @@
</div>
</template>
<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>
</custom-container>
</el-col>
</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" />
<div class="resource-tabs">
<div
......@@ -714,11 +734,18 @@ import { ElMessage, ElMessageBox } from "element-plus";
import { DArrowRight, Warning, Search } from "@element-plus/icons-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 { useGotoCompanyPages } from "@/router/modules/company";
import { useGotoNewsDetail } from "@/router/modules/news";
const gotoCompanyPages = useGotoCompanyPages();
const gotoNewsDetail = useGotoNewsDetail();
const trendChart = useChartInterpretation();
const radarChart = useChartInterpretation();
import { useRouter } from "vue-router";
......@@ -1026,6 +1053,7 @@ 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);
......@@ -1297,6 +1325,7 @@ const fetchRadarData = async checked => {
}
};
});
radarChart.interpret({ type: "雷达图", name: "实体领域分布", data: data });
}
} catch (error) {
console.error("获取雷达图数据失败:", error);
......@@ -3573,7 +3602,7 @@ const handleMediaClick = item => {
}
.scroll-main {
height: calc(100% - 144px) !important;
// height: calc(100% - 144px) !important;
}
.center-center {
......@@ -3596,4 +3625,50 @@ 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 { 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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论