提交 b708ffdd authored 作者: 朱政's avatar 朱政

Merge branch 'pre' into zz-dev

流水线 #277 已通过 于阶段
in 1 分 24 秒
......@@ -143,6 +143,7 @@ export function getChartAnalysis(data, options = {}) {
: typeof options?.onInterpretationDelta === "function"
? options.onInterpretationDelta
: null;
const externalSignal = options?.signal;
return new Promise((resolve, reject) => {
let buffer = "";
let latestInterpretation = "";
......@@ -150,16 +151,32 @@ export function getChartAnalysis(data, options = {}) {
let lastStreamedInterpretationLen = 0;
let settled = false;
const abortController = new AbortController();
const externalAbortHandler = () => {
abortController.abort();
};
if (externalSignal) {
if (externalSignal.aborted) {
abortController.abort();
} else {
externalSignal.addEventListener("abort", externalAbortHandler, { once: true });
}
}
const safeResolve = value => {
if (settled) return;
settled = true;
if (externalSignal) {
externalSignal.removeEventListener("abort", externalAbortHandler);
}
resolve(value);
};
const safeReject = err => {
if (settled) return;
settled = true;
if (externalSignal) {
externalSignal.removeEventListener("abort", externalAbortHandler);
}
reject(err);
};
......
......@@ -56,11 +56,12 @@ export function getBillDyqk(params) {
* @param {id,cRelated,currentPage,pageSize}
* @header token
*/
export function getBillBackground(params) {
export function getBillBackground(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/background/${params.id}`,
params,
signal: config.signal
})
}
// 相关事件-根据法案ID获取相关事件信息
......@@ -107,11 +108,12 @@ export function getBillPersonAnalyzeDy(params) {
* @param {id}
* @header token
*/
export function getBillContentId(params) {
export function getBillContentId(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/contentId/${params.id}`,
params,
signal: config.signal
})
}
......@@ -120,11 +122,12 @@ export function getBillContentId(params) {
* @param {billId,id,cRelated,currentPage,pageSize,domainNameList,measuresNameList,content}
* @header token
*/
export function getBillContentTk(params) {
export function getBillContentTk(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/tk/${params.billId}/${params.id}`,
params,
signal: config.signal
})
}
......@@ -133,11 +136,12 @@ export function getBillContentTk(params) {
* @param {billId,versionId,cRelated}
* @header token
*/
export function getBillContentXzfs(params) {
export function getBillContentXzfs(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/xzfs/${params.billId}/${params.versionId}`,
params,
signal: config.signal
})
}
......@@ -146,11 +150,12 @@ export function getBillContentXzfs(params) {
* @param {billId,versionId,cRelated}
* @header token
*/
export function getBillHyly(params) {
export function getBillHyly(params, config = {}) {
return request({
method: 'GET',
url: `/api/billInfoBean/content/hyly/${params.billId}/${params.versionId}`,
params,
signal: config.signal
})
}
......
......@@ -3,8 +3,14 @@ import request from "@/api/request.js";
// 涉华法案领域分布
/**
* @param {Object} params
* @param {string} params.year - 年份
* @param {string} [params.status] - 状态:提出法案/通过法案
* @param {string} params.year - 年份(2022-2026)
* @param {string} [params.status] - 法案状态:
* - 提案
* - 众议院通过
* - 参议院通过
* - 分歧已解决
* - 呈交总统
* - 完成立法
*/
export function getBillIndustry(params) {
return request({
......
......@@ -11,3 +11,10 @@ export function search(data) {
data:data
})
}
export function getThinkTankList() {
return request({
method: 'GET',
url: `/temporarySearch/search-info/all-organization-names`,
})
}
\ No newline at end of file
......@@ -292,10 +292,10 @@ export function getSingleSanctionOverviewList(data) {
* @param {string} params.sanRecordId - 制裁记录ID
* @header token
*/
export function getSingleSanctionTotalCount(id) {
export function getSingleSanctionTotalCount(sanTypeId, recordId) {
return request({
method: "GET",
url: `/api/sanctionList/statistics/total?sanTypeId=${id}`,
url: `/api/sanctionList/statistics/total?sanTypeId=${sanTypeId}&sanRecordId=${recordId}`,
});
}
......
import request from "@/api/request.js";
\ No newline at end of file
......@@ -82,6 +82,7 @@ const getGraphChart = (nodes, links, layoutType) => {
}
},
force: {
layoutAnimation: false, // 关闭初始化晃来晃去的动画
repulsion: 300,
gravity: 0,
edgeLength: 300
......
<template>
<div class="graph-chart-wrapper" id="graph">
</div>
<div class="graph-chart-wrapper" id="graph"></div>
</template>
<script setup>
import { onMounted, onBeforeUnmount } from 'vue';
import setChart from '@/utils/setChart';
import getGraphChart from './graphChart';
import { onMounted, onBeforeUnmount, watch } from "vue";
import setChart from "@/utils/setChart";
import getGraphChart from "./graphChart";
const emits = defineEmits(["handleClickNode"])
const emits = defineEmits(["handleClickNode"]);
const props = defineProps({
nodes: {
type: Array,
......@@ -21,33 +19,44 @@ const props = defineProps({
},
layoutType: {
type: String,
default: 'force'
default: "force"
},
width: {
type: String,
default: 'force'
default: "force"
},
height: {
type: String,
default: 'force'
default: "force"
}
})
});
let chart = null
let chart = null;
onMounted(() => {
const graph = getGraphChart(props.nodes, props.links, props.layoutType)
chart = setChart(graph, 'graph')
chart.on("click", (event) => { emits("handleClickNode", event) })
})
const graph = getGraphChart(props.nodes, props.links, props.layoutType);
chart = setChart(graph, "graph");
chart.on("click", event => {
emits("handleClickNode", event);
});
});
onBeforeUnmount(() => {
chart.off("click")
chart.dispose()
})
chart.off("click");
chart.dispose();
});
watch(
() => [props.nodes, props.links],
() => {
if (chart) {
const graph = getGraphChart(props.nodes, props.links, props.layoutType);
chart.setOption(graph, true);
}
},
{ deep: true }
);
</script>
<style lang="scss" scoped>
.graph-chart-wrapper {
width: 100%;
......
......@@ -17,7 +17,14 @@
</template>
<script setup>
import {ref} from 'vue'
import {onMounted, ref} from 'vue'
const props = defineProps({
activeTime: {
typeof: String,
default: '近一周'
}
})
const timeList = ref([
{
......@@ -34,6 +41,10 @@ const timeList = ref([
},
])
onMounted(() => {
timeList.value.forEach(item => { item.active = item.time === props.activeTime })
})
const handleTimeClick = (item, index) => {
timeList.value.forEach(time => {
time.active = false
......
......@@ -18,13 +18,13 @@ const getWordCloudChart = data => {
// 其他形状你可以使用形状路径
// shape: 'circle', // 示例
// 或者自定义路径
gridSize: 15, // 网格大小,影响词间距。
gridSize: 5, // 网格大小,影响词间距。
sizeRange: [16, 36], // 定义词云中文字大小的范围
rotationRange: [0, 0],
// rotationRange: [-90, 90],
// rotationStep: 10,
drawOutOfBound: false, // 是否超出画布
shrinkToFit: true, // 是否自动缩小以适应容器
shrinkToFit: false, // 是否自动缩小以适应容器
// 字体
textStyle: {
color: function (params) {
......
......@@ -128,7 +128,19 @@ const homeTitleList = ref([
}
]);
const homeActiveTitleIndex = ref(0);
const homeActiveTitleIndex = computed(() => {
let activeIndex = 1
if (route.fullPath.includes('/ZMOverView')) {
activeIndex = 0
} else if (route.fullPath.includes('/dataLibrary')) {
activeIndex = 3
} else if (route.fullPath.includes('/chat') || route.fullPath.includes('/writtingAsstaint')) {
activeIndex = 2
} else {
activeIndex = 1
}
return activeIndex
})
const isShowMenu = ref(false);
const handleShowMenu = (index, isShow) => {
......@@ -148,7 +160,8 @@ const handleHoverMenu = isShow => {
isShowMenu.value = isShow;
};
const menuList = ref([
const menuList = computed(() => {
let menu = [
// {
// title: "中美科技博弈概览",
// icon: Menu1,
......@@ -220,7 +233,77 @@ const menuList = ref([
path: "/scientificFunding",
active: false
}
]);
]
switch (route.fullPath) {
case '/billHome':
menu.forEach(item => {
item.active = false
})
menu[0].active = true
break
case '/decree':
menu.forEach(item => {
item.active = false
})
menu[1].active = true
break
case '/thinkTank':
menu.forEach(item => {
item.active = false
})
menu[2].active = true
break
case '/exportControl':
menu.forEach(item => {
item.active = false
})
menu[3].active = true
break
case '/cooperationRestrictions':
menu.forEach(item => {
item.active = false
})
menu[4].active = true
break
case '/finance':
menu.forEach(item => {
item.active = false
})
menu[5].active = true
break
case '/marketAccessRestrictions':
menu.forEach(item => {
item.active = false
})
menu[6].active = true
break
case '/ruleRestrictions':
menu.forEach(item => {
item.active = false
})
menu[7].active = true
break
case '/technologyFigures':
menu.forEach(item => {
item.active = false
})
menu[8].active = true
break
case '/innovationSubject':
menu.forEach(item => {
item.active = false
})
menu[9].active = true
break
case '/scientificFunding':
menu.forEach(item => {
item.active = false
})
menu[10].active = true
break
}
return menu
})
const isShowTool = ref(false);
......@@ -243,10 +326,7 @@ const toolList = ref([
])
const handleToModule = (item, index) => {
window.sessionStorage.setItem('homeActiveTitleIndex', index)
if (index === 1) {
homeActiveTitleIndex.value = index
item.active = true
router.push({
path: item.path
})
......@@ -265,10 +345,7 @@ const handleToModule = (item, index) => {
};
const handleClickTitle = (item, index) => {
if (index === 0 || index === 3) {
window.sessionStorage.setItem('homeActiveTitleIndex', index)
homeActiveTitleIndex.value = index
router.push(item.path)
}
};
......@@ -279,17 +356,8 @@ const handleClickToolBox = () => {
onMounted(() => {
handleGetPersonType();
if (route.query.titleIndex) {
homeActiveTitleIndex.value = Number(route.query.titleIndex)
} else {
homeActiveTitleIndex.value = Number(window.sessionStorage.getItem('homeActiveTitleIndex'))
}
});
onUnmounted(() => {
window.sessionStorage.removeItem('homeActiveTitleIndex')
})
</script>
<style lang="scss" scoped>
......
......@@ -11,7 +11,7 @@
`}}
</pre>
<div class="chart-box">
<GraphChart :nodes="nodes" :links="links" layoutType="force">
<GraphChart :nodes="nodes" :links="links" layoutType="none">
</GraphChart>
</div>
</el-col>
......
const getQuarterRange = (quatarNum) => {
const getQuarterRange = (year, quatarNum) => {
const quarters = {
1: ['2025-01-01', '2025-03-31'],
2: ['2025-04-01', '2025-06-30'],
3: ['2025-07-01', '2025-09-30'],
4: ['2025-10-01', '2025-12-31']
1: [year+ '-01-01', year+ '-03-31'],
2: [year+ '-04-01', year+ '-06-30'],
3: [year+ '-07-01', year+ '-09-30'],
4: [year+ '-10-01', year+ '-12-31']
};
return quarters[quatarNum];
......
......@@ -16,68 +16,52 @@ const setChart = (option, chartId, allowClick, selectParam) => {
chart.on('click', function (params) {
switch (selectParam.moduleType) {
case '国会法案':
// 判断点击的是否为饼图的数据项
if (params.componentType === 'series' && params.seriesType === 'pie') {
console.log('点击的扇形名称:', params.name);
if (selectParam.key === '领域') {
if (selectParam.key === 1) {
// console.log('当前点击', selectParam, params.seriesName, params.name);
selectParam.selectedStatus = params.seriesName
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
return
} else if (selectParam.key === 2) {
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 === '议院委员会') {
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
return
} else if (selectParam.key === 3) {
if (params.name === '众议院' || 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 {
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({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
if (params.name === '已立法') {
selectParam.selectedStatus = 1
} else {
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",
query: selectParam
});
window.open(route.href, "_blank");
return
} else {
console.log('当前点击', selectParam, params.seriesName, params.name);
if (params.seriesName !== '通过率') {
selectParam.selectedDate = JSON.stringify(getMonthRange(params.name))
if (params.seriesName === '通过法案') {
selectParam.selectedStatus = 1
} else {
selectParam.selectedStatus = null
}
selectParam.selectedStatus = params.name
const route = router.resolve({
path: "/dataLibrary/countryBill",
query: selectParam
});
window.open(route.href, "_blank");
}
}
break
case '政令':
......@@ -89,16 +73,38 @@ const setChart = (option, chartId, allowClick, selectParam) => {
});
window.open(route.href, "_blank");
} else if (params.componentType === 'series' && params.seriesType === 'bar') {
const year = params.name.slice(0, 4)
const quatarNum = Number(params.name[params.name.length - 1])
selectParam.selectedDate = JSON.stringify(getQuarterRange(quatarNum))
selectParam.selectedDate = JSON.stringify(getQuarterRange(year, quatarNum))
const route = router.resolve({
path: "/dataLibrary/dataDecree",
query: selectParam
});
window.open(route.href, "_blank");
}
break
case '科技智库报告':
if (selectParam.key === 1) {
selectParam.domains = params.seriesName
const year = params.name.slice(0, 4)
const quatarNum = Number(params.name[params.name.length - 1])
selectParam.selectedDate = JSON.stringify(getQuarterRange(year, quatarNum))
const route = router.resolve({
path: "/dataLibrary/dataThinkTank",
query: selectParam
});
window.open(route.href, "_blank");
return
} else if (selectParam.key === 2) {
selectParam.domains = params.name
const route = router.resolve({
path: "/dataLibrary/dataThinkTank",
query: selectParam
});
window.open(route.href, "_blank");
return
}
}
});
......
......@@ -8,7 +8,7 @@
<el-button :type="box1Btn2Type" plain @click="handleClickBox1Btn(2)">全部背景</el-button>
</div>
</template>
<div class="box1-main">
<div class="box1-main" v-loading="backgroundLoading">
<div class="box1-main-center">
<div class="box1-main-item" v-for="item in backgroundDisplayList" :key="item.id">
<div class="id">{{ item.displayIndex }}</div>
......@@ -198,6 +198,8 @@ const handleClickBox2Btn = index => {
const aboutUserList = ref([]);
const backgroundList = ref([]);
const backgroundLoading = ref(false);
let backgroundAbortController = null;
const eventList = ref([]);
......@@ -244,6 +246,12 @@ const nextIconColor = computed(() => (currentIndex.value < personList.value.leng
// 获取立法背景内容
const handleGetBillBackground = async () => {
if (backgroundAbortController) {
backgroundAbortController.abort();
}
const controller = new AbortController();
backgroundAbortController = controller;
const cRelated = box1BtnActive.value === 1 ? "Y" : "N";
const params = {
cRelated: cRelated,
......@@ -251,11 +259,21 @@ const handleGetBillBackground = async () => {
currentPage: currentPage.value - 1,
pageSize: 10
};
backgroundLoading.value = true;
try {
const res = await getBillBackground(params);
const res = await getBillBackground(params, { signal: controller.signal });
backgroundList.value = res.data.content;
total.value = res.data.totalElements; // 假设API返回totalElements
} catch (error) { }
} catch (error) {
if (error?.name !== "AbortError" && error?.code !== "ERR_CANCELED") {
console.error(error);
}
} finally {
if (backgroundAbortController === controller) {
backgroundLoading.value = false;
backgroundAbortController = null;
}
}
};
// 获取相关事件
......
......@@ -49,7 +49,8 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
containLabel: true
},
legend: {
data: ['提出法案', '通过法案', '众议院通过', '参议院通过', '双院通过'],
// 图例顺序:提出法案、众议院通过、参议院通过、解决分歧、完成立法
data: ['提出法案', '众议院通过', '参议院通过', '解决分歧', '完成立法'],
show: true,
top: 10,
icon: 'circle',
......@@ -126,7 +127,8 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
data: proposedData
},
{
name: '通过法案',
// 众议院通过
name: '众议院通过',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -137,10 +139,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[1]
},
data: passData
data: houseData
},
{
name: '众议院通过',
// 参议院通过
name: '参议院通过',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -151,10 +154,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[2]
},
data: houseData
data: senateData
},
{
name: '参议院通过',
// 解决分歧
name: '解决分歧',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -165,10 +169,11 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[3]
},
data: senateData
data: hsData
},
{
name: '双院通过',
// 完成立法
name: '完成立法',
type: 'line',
smooth: true,
symbol: 'emptyCircle',
......@@ -179,7 +184,7 @@ const getMultiLineChart = (dataX, proposedData, passData, houseData, senateData,
itemStyle: {
color: lineColors[4]
},
data: hsData
data: passData
}
]
}
......
......@@ -27,17 +27,6 @@
</div>
<div class="left-box-bottom" v-if="showTabs">
<template v-if="isLoading">
<div class="left-box-bottom-item is-skeleton" v-for="n in 4" :key="n">
<div class="icon">
<el-skeleton-item class="skeleton-tab-icon" variant="text" />
</div>
<div class="name">
<el-skeleton-item class="skeleton-tab-text" variant="text" />
</div>
</div>
</template>
<template v-else>
<div class="left-box-bottom-item"
:class="{ leftBoxBottomItemActive: activeTitle === item.name }" v-for="item in tabs"
:key="item.path" @click="emit('tab-click', item)">
......@@ -49,7 +38,6 @@
{{ item.name }}
</div>
</div>
</template>
</div>
</div>
......@@ -73,17 +61,6 @@
</div>
<div class="right-box-bottom" v-if="showActions">
<template v-if="isLoading">
<div class="btn3 is-skeleton">
<div class="icon">
<el-skeleton-item class="skeleton-action-icon" variant="text" />
</div>
<div class="text">
<el-skeleton-item class="skeleton-action-text" variant="text" />
</div>
</div>
</template>
<template v-else>
<div class="btn2" @click="emit('open-analysis', 'forsee')">
<div class="icon">
<img :src="btnIconForsee" alt="" />
......@@ -96,7 +73,6 @@
</div>
<div class="text">{{ "分析报告" }}</div>
</div>
</template>
</div>
</div>
</div>
......
......@@ -834,6 +834,7 @@ onMounted(() => {
flex-shrink: 1;
max-width: 170px;
text-align: center;
margin-bottom: 10px;
}
.nameItemActive {
......
......@@ -46,7 +46,7 @@
<div class="chart-ai-wrap">
<div :class="['right-box2-main', { 'right-box-main--full': !domainFooterText }]" id="chart2"></div>
<div class="overview-tip-row">
<TipTab class="overview-tip" />
<TipTab class="overview-tip" :text="'涉华科技法案数量及通过率变化趋势,数据来源:美国国会官网'"/>
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('domain')" />
</div>
<div v-if="aiPaneVisible.domain" class="overview-ai-pane"
......@@ -72,7 +72,7 @@
<div class="chart-ai-wrap">
<div :class="['right-box1-main', { 'right-box-main--full': !limitFooterText }]" id="chart1"></div>
<div class="overview-tip-row">
<TipTab class="overview-tip" />
<TipTab class="overview-tip" :text="'涉华科技法案数量及通过率变化趋势,数据来源:美国国会官网'"/>
<AiButton class="overview-tip-action" @mouseenter="handleShowAiPane('limit')" />
</div>
<div v-if="aiPaneVisible.limit" class="overview-ai-pane"
......@@ -150,8 +150,8 @@
</template>
<script setup>
import { ref, onMounted, computed, watch } from "vue";
import { useRoute } from "vue-router";
import { ref, onMounted, onBeforeUnmount, computed, watch } from "vue";
import { useRoute, onBeforeRouteLeave } from "vue-router";
import * as echarts from "echarts";
import { Search } from "@element-plus/icons-vue";
import getPieChart from "./utils/piechart";
......@@ -165,6 +165,31 @@ import { extractTextEntity } from "@/api/intelligent/index";
const route = useRoute();
const pageAbortController = new AbortController();
const isRequestCanceled = error => {
return (
error?.code === "ERR_CANCELED" ||
error?.name === "CanceledError" ||
error?.name === "AbortError" ||
(typeof error?.message === "string" && /canceled|aborted/i.test(error.message))
);
};
const getPageSignal = () => pageAbortController.signal;
const stopCurrentPageRequests = () => {
pageAbortController.abort();
// 让旧请求回调全部失效,避免离开页面后回写状态
tkRequestToken.value += 1;
xzfsRequestToken.value += 1;
hylyRequestToken.value += 1;
entityRequestToken.value += 1;
termsLoading.value = false;
limitLoading.value = false;
domainLoading.value = false;
aiPaneLoading.value = { domain: false, limit: false };
};
const curBill = ref("");
const curBillId = ref(null);
......@@ -294,7 +319,7 @@ const ensureEntitiesForTerms = async terms => {
try {
const results = await Promise.all(
tasks.map(async item => {
const res = await extractTextEntity(item.text);
const res = await extractTextEntity(item.text, { signal: getPageSignal() });
const entities = normalizeEntities(res?.result ?? res?.data?.result ?? res?.data ?? res);
return { key: item.key, entities };
})
......@@ -434,6 +459,7 @@ const requestAiPaneContent = async key => {
const res = await getChartAnalysis(
{ text: JSON.stringify(payload) },
{
signal: getPageSignal(),
onChunk: chunk => {
const current = overviewAiContent.value[key];
const base = current === "智能总结生成中..." ? "" : current;
......@@ -455,6 +481,7 @@ const requestAiPaneContent = async key => {
}
aiPaneFetched.value = { ...aiPaneFetched.value, [key]: true };
} catch (error) {
if (isRequestCanceled(error)) return;
console.error("获取图表解读失败", error);
overviewAiContent.value = { ...overviewAiContent.value, [key]: "智能总结生成失败" };
} finally {
......@@ -518,7 +545,7 @@ const handleGetBillList = async () => {
id: route.query.billId
};
try {
const res = await getBillContentId(params);
const res = await getBillContentId(params, { signal: getPageSignal() });
console.log("法案id列表", res);
const rawList = Array.isArray(res?.data) ? res.data : [];
const seen = new Set();
......@@ -579,7 +606,7 @@ const handleGetBillContentTk = async cRelated => {
params.content = searchKeyword.value.trim();
}
try {
const res = await getBillContentTk(params);
const res = await getBillContentTk(params, { signal: getPageSignal() });
if (currentToken !== tkRequestToken.value) {
return;
}
......@@ -627,6 +654,9 @@ const handleGetBillContentTk = async cRelated => {
total.value = 0;
}
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== tkRequestToken.value) {
return;
}
......@@ -652,7 +682,7 @@ const handleGetBillContentXzfs = async () => {
};
try {
const res = await getBillContentXzfs(params);
const res = await getBillContentXzfs(params, { signal: getPageSignal() });
if (currentToken !== xzfsRequestToken.value) {
return;
}
......@@ -679,6 +709,9 @@ const handleGetBillContentXzfs = async () => {
let chart1 = getPieChart(chart1Data.value, chart1ColorList.value);
setChart(chart1, "chart1");
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== xzfsRequestToken.value) {
return;
}
......@@ -701,7 +734,7 @@ const handleGetBillHyly = async () => {
};
try {
const res = await getBillHyly(params);
const res = await getBillHyly(params, { signal: getPageSignal() });
if (currentToken !== hylyRequestToken.value) {
return;
}
......@@ -729,6 +762,9 @@ const handleGetBillHyly = async () => {
let chart2 = getPieChart(chart2Data.value, chart2ColorList.value);
setChart(chart2, "chart2");
} catch (error) {
if (isRequestCanceled(error)) {
return;
}
if (currentToken !== hylyRequestToken.value) {
return;
}
......@@ -745,6 +781,14 @@ onMounted(async () => {
await handleGetBillContentXzfs();
await handleGetBillHyly();
});
onBeforeRouteLeave(() => {
stopCurrentPageRequests();
});
onBeforeUnmount(() => {
stopCurrentPageRequests();
});
</script>
<style lang="scss" scoped>
......
......@@ -316,7 +316,7 @@ const handleSearch = async () => {
console.log("综合搜索结果", res);
if (res.code === 200 && res.data) {
if (!selectedDomains.value.length) {
domains.value = Object.entries(res.data.aggregations).map(([name, id]) => ({
domains.value = Object.entries(res.data.aggregationsDomain).map(([name, id]) => ({
name,
id,
selected: false
......
......@@ -94,11 +94,11 @@
</div>
</div>
<div class="header-right">
<div class="header-right-item item1">
<div class="header-right-item item1" @click="handleExport">
<div class="icon">
<img src="../../assets/icons/download.svg" alt="">
</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
<div class="text text-tip-1">{{ '导出' }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
......@@ -693,10 +693,6 @@ const statusList = ref([
name: '提出法案',
id: '提出法案'
},
{
name: '通过法案',
id: '通过法案'
},
{
name: '众议院通过',
id: '众议院通过'
......@@ -706,8 +702,12 @@ const statusList = ref([
id: '参议院通过'
},
{
name: '分歧已解决',
id: '分歧已解决'
name: '解决分歧',
id: '解决分歧'
},
{
name: '完成立法',
id: '完成立法'
},
])
......@@ -833,13 +833,12 @@ const fetchTableData = async () => {
}))
}
const curDemensionItem = staticsDemensionList.value.filter(item => {
return item.name === curDemension.value
})[0]
activeChart.value = ''
timer3.value = setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
......@@ -1040,7 +1039,7 @@ const initParam = () => {
}
isInvolveCn.value = route.query.isInvolveCn ? true : false
if (route.query.selectedStatus) {
selectedStatus.value = route.query.selectedStatus === '1' ? '通过' : '提出'
selectedStatus.value = route.query.selectedStatus
} else {
selectedStatus.value = '全部阶段'
}
......@@ -1061,7 +1060,7 @@ const initParam = () => {
}
isInvolveCn.value = savedQuery.isInvolveCn ? true : false
if (savedQuery.selectedStatus) {
selectedStatus.value = savedQuery.selectedStatus === '1' ? '通过' : '提出'
selectedStatus.value = savedQuery.selectedStatus
} else {
selectedStatus.value = '全部阶段'
}
......@@ -1102,6 +1101,10 @@ const handlePerClick = item => {
// 导出
const handleExport = () => {
if (!selectedCount.value) {
ElMessage.warning('请至少选择一项!')
return
}
console.log(selectedMap.value);
const arr = Array.from(selectedMap.value);
......@@ -1111,7 +1114,7 @@ const handleExport = () => {
const link = document.createElement('a');
link.href = url;
link.download = 'export.json';
link.download = 'bill.json';
link.click();
URL.revokeObjectURL(url);
......
<template>
<div class="input-wrapper">
<div class="input-left text-tip-1">{{ inputTitle + ':' }}</div>
<div class="input-right">
<el-input v-model="inputValue" :placeholder="placeholderName" style="width: 240px" />
</div>
</div>
</template>
<script setup>
import { computed } from 'vue';
const props = defineProps({
inputTitle: {
type: String,
default: ''
},
placeholderName: {
type: String,
default: ''
},
inputName: {
type: String,
default: ''
},
})
const emit = defineEmits(['update:inputText', 'update:customTime'])
const inputValue = computed({
get: () => props.inputName,
set: (value) => emit('update:inputText', value)
})
</script>
<style lang="scss" scoped>
.input-wrapper {
width: 348px;
height: 28px;
display: flex;
justify-content: space-between;
align-items: center;
gap: 8px;
.input-left {
width: 100px;
height: 24px;
color: var(--text-primary-65-color);
}
.input-right {
width: 240px;
display: flex;
gap: 8px;
justify-content: space-between;
:deep(.el-input__wrapper) {
border-radius: 4px;
border: 1px solid var(--bg-black-10);
}
}
}
</style>
\ No newline at end of file
......@@ -101,11 +101,11 @@
</div>
</div>
<div class="header-right">
<div class="header-right-item item1">
<div class="header-right-item item1" @click="handleExport">
<div class="icon">
<img src="../assets/icons/download.svg" alt="">
</div>
<div class="text text-tip-1" @click="handleExport">{{ '导出' }}</div>
<div class="text text-tip-1">{{ '导出' }}</div>
</div>
<div class="header-right-item2 item2">
<el-select v-model="curOperation" placeholder="批量操作" style="width: 120px">
......@@ -137,7 +137,7 @@
<el-table-column label="发布时间" width="120" class-name="date-column">
<template #default="scope">{{ scope.row.date }}</template>
</el-table-column>
<el-table-column label="发布机构" width="180">
<el-table-column label="发布机构">
<template #default="scope">
<span class="person-item text-compact" @click="handlePerClick(scope.row)">{{ scope.row.organizationName
}}</span>
......@@ -396,6 +396,7 @@ const handleCloseCurTag = (tag, index) => {
break
case '发布时间':
selectedDate.value = ''
customTime.value = []
break
case '发布机构':
selectedIns.value = '全部机构'
......@@ -765,6 +766,8 @@ const fetchTableData = async () => {
return item.name === curDemension.value
})[0]
activeChart.value = ''
setTimeout(() => {
activeChart.value = curDemensionItem.chartTypeList[0]
curChartData.value = curDemensionItem.data
......@@ -989,12 +992,12 @@ const initParam = () => {
const handleClickToDetail = (curDecree) => {
console.log('curDecree', curDecree);
window.sessionStorage.setItem("billId", curDecree.id);
window.sessionStorage.setItem("decreeId", curDecree.id);
window.sessionStorage.setItem("curTabName", curDecree.title);
const route = router.resolve({
path: "/decreeLayout",
query: {
billId: curDecree.id
id: curDecree.id
}
});
window.open(route.href, "_blank");
......@@ -1340,4 +1343,6 @@ onMounted(async () => {
// :deep(.el-table__header th:first-child) {
// background-color: #e6f7ff;
// color: #1890ff;
// }</style>
\ No newline at end of file
// }
</style>
\ No newline at end of file
......@@ -27,7 +27,7 @@
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="handleDateChange" />
<TimeTabPane @time-click="handleDateChange" activeTime="近一年" />
</div>
<div class="organization-list" ref="refOrganization" v-loading="organizationInfo.loading">
<div class="organization-item" v-for="(item, index) in organizationInfo.list" :key="index"
......@@ -77,7 +77,7 @@ const organizationInfo = reactive({
total: 0,
isSort: 1,
keyWord: "",
day: 7,
day: 365,
list: []
})
......
......@@ -34,7 +34,7 @@
<img :src="tipsTcon" alt="">
</div>
<div class="date-text">近期美国各联邦政府机构发布涉华政令数量汇总</div>
<TimeTabPane @time-click="onKeyOrganization" />
<TimeTabPane @time-click="onKeyOrganization" activeTime="近一年" />
</div>
<div class="home-main-header-item-box" v-if="keyOrganizationList.length">
<div class="organization-item" v-for="(item, index) in keyOrganizationList" :key="index" @click="handleToInstitution(item)">
......@@ -767,7 +767,7 @@ const handleGetDecreeYearOrder = async () => {
chart1Data.value.dataY = res.data.map(item => {
return item.count;
});
summarize1.value = await onChartInterpretation({type:"柱状图",name:"数量变化趋势",data:res.data})
onChartInterpretation({type:"柱状图",name:"数量变化趋势",data:res.data}, summarize1)
}
} catch (error) {
console.error("行政令发布频度error", error);
......@@ -775,15 +775,26 @@ const handleGetDecreeYearOrder = async () => {
box5Params.loading = false
};
// AI智能总结
const onChartInterpretation = async (text) => {
const onChartInterpretation = async (text, param) => {
param.value = "正在生成..."
// 👇 新增:超时 + 终止请求(只加这一段)
const controller = new AbortController();
const timeout = setTimeout(() => controller.abort(), 10000); // 10秒超时
try {
const response = await fetch('/aiAnalysis/chart_interpretation', {
method: 'POST',
headers: {
"X-API-Key": "aircasKEY19491001",
'Content-Type': 'application/json',
},
body: JSON.stringify({text}) // 把参数转为JSON字符串
body: JSON.stringify({text}),
signal: controller.signal // 👇 新增:绑定中断信号
});
clearTimeout(timeout); // 👇 新增:请求成功清除定时器
if (!response.ok) throw new Error(`HTTP 错误 ${response.status}`);
const reader = response.body.getReader();
const decoder = new TextDecoder();
......@@ -806,7 +817,10 @@ const onChartInterpretation = async (text) => {
}
}
}
return summarize
param.value = summarize
} catch (err) {
param.value = "系统异常,生成失败";
}
}
const handleBox5 = async () => {
......@@ -870,7 +884,7 @@ const handleGetDecreeArea = async () => {
value: item.count
};
});
summarize2.value = await onChartInterpretation({type:"环形图",name:"领域分布情况",data:res.data})
onChartInterpretation({type:"环形图",name:"领域分布情况",data:res.data}, summarize2)
}
} catch (error) {
console.error("政令科技领域error", error);
......@@ -1194,7 +1208,7 @@ const handleSearch = () => {
// 关键机构
const keyOrganizationList = ref([]);
const onKeyOrganization = async (event) => {
let day = 7
let day = 365
if (event?.time === '近一周') day = 7
if (event?.time === '近一月') day = 30
if (event?.time === '近一年') day = 365
......
......@@ -79,8 +79,9 @@
<div class="graph-box" v-if="contentType==1">
<ChartChain :listData="fishbone.list" :baseData="fishbone.base" />
</div>
<div class="graph-box" v-if="contentType==2 && graphInfo.nodes.length">
<GraphChart :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
<div class="graph-box" v-if="contentType==2">
<GraphChart v-if="graphInfo.nodes?.length" :nodes="graphInfo.nodes" :links="graphInfo.links" layoutType="force" />
<el-empty v-else style="padding: 60px 0" description="暂无数据" :image-size="100" />
</div>
</div>
</AnalysisBox>
......@@ -168,6 +169,7 @@ const onDecreeEntities = async (page=1) => {
const contentType = ref(1);
const headerContentType = (type) => {
contentType.value = type;
if (!entityInfo.total) return;
headerChartData(entityInfo.node)
};
const headerChartData = (row) => {
......
......@@ -77,7 +77,7 @@
<el-col :span="16">
<custom-container titleType="primary" title="最新出口管制政策" :titleIcon="houseIcon" height="450px">
<template #header-right>
<el-button type="primary" @click="handleToEntityList()" link>
<el-button type="primary" @click="handleToEntityList" link>
{{ "查看详情 >" }}
</el-button>
</template>
......@@ -390,7 +390,7 @@
</custom-container>
</el-col>
<el-col :span="16">
<custom-container title="实体清单数量增长趋势" :titleIcon="qushiIcon" height="540px">
<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" />
......@@ -995,8 +995,6 @@ onMounted(async () => {
const maxCountItem1 = _.maxBy(cclList1, "count");
const maxCountForList1 = maxCountItem1 ? maxCountItem1.count : 0;
console.log("shuju list", list);
console.log("shuju total", total);
tableData1.value = _.map(list, item => {
return {
year: item.year,
......@@ -1053,7 +1051,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 });
trendChart.interpret({ type: "柱状图", name: "制裁清单数量增长趋势", data: res[0].yearDomainCount });
}
} catch (error) {
console.error("获取趋势图数据失败:", error);
......@@ -1147,10 +1145,13 @@ const handleCarouselChange = index => {
// 跳转到V2.0单次制裁
const handleToEntityList = item => {
console.log("这是什么数据1 =>", item);
let id = item?.id;
let sanTypeId = item?.sanTypeId || 1;
if (!id) {
const currentItem = entitiesDataInfoList.value[currentCarouselIndex.value];
id = currentItem?.id;
sanTypeId = currentItem?.sanTypeId || 1;
}
window.sessionStorage.setItem(
"curTabName",
......@@ -1159,7 +1160,8 @@ const handleToEntityList = item => {
const routeData = router.resolve({
path: "/exportControl/singleSanction",
query: {
id: id
id,
sanTypeId
}
});
// 打开一个新页面
......@@ -2363,7 +2365,8 @@ const handleMediaClick = item => {
overflow-y: auto;
.home-top-bg {
background: url("./assets/images/background.png"),
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%;
position: absolute;
......@@ -3631,7 +3634,7 @@ const handleMediaClick = item => {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-start;
padding: 22px 0;
.data-origin-icon {
width: 16px;
......
import * as echarts from "echarts";
import chinaJson from "./China.json";
import _ from "lodash";
import { name } from "dayjs/locale/zh-cn";
//饼图
export function getPieOption(data, title) {
let option = {
......@@ -1006,7 +1007,7 @@ export const getMultipleBarChart_m = object => {
margin: 20
},
axisLabel: {
formatter: "{value}",
formatter: "{value}",
color: "rgba(95, 101, 108, 1)",
margin: 20
},
......@@ -1022,6 +1023,21 @@ export const getMultipleBarChart_m = object => {
},
yAxis: {
type: "value",
name: "数量",
nameLocation: "end",
nameGap: 20,
nameRotate: 0,
nameTextStyle: {
color: "#666",
fontFamily: "Microsoft YaHei",
fontWeight: 400,
fontSize: 14,
lineHeight: 14,
letterSpacing: 0,
align: "right",
verticalAlign: "bottom",
padding: [0, 8, 0, 0]
},
splitNumber: 5,
alignTicks: false,
axisLabel: {
......
......@@ -105,7 +105,7 @@
</div>
</template>
<!-- <div class="echarts" ref="sanctionCountChartRef"></div> -->
<EChart :option="sanctionCountChartOption" autoresize :style="{ height: '300px' }" />
<EChart :option="sanctionCountChartOption" autoresize :style="{ height: '300px', padding: '0 20px' }" />
<!-- <div class="bottom">
<div class="ai">
<div class="left">
......@@ -186,7 +186,7 @@
</el-select>
</template>
<!-- <div class="echarts" ref="domainChartRef"></div> -->
<EChart :option="domainChartOption" autoresize :style="{ height: '300px' }" />
<EChart :option="domainChartOption" autoresize :style="{ height: '300px', padding: '0 20px' }" />
<!-- <div class="bottom">
<div class="ai">
<div class="left">
......@@ -218,7 +218,7 @@
</el-select>
</template>
<!-- <div class="echarts" ref="typeChartRef"></div> -->
<EChart :option="typeChartOption" autoresize :style="{ height: '300px' }" />
<EChart :option="typeChartOption" autoresize :style="{ height: '300px', padding: '0 20px' }" />
<!-- <div class="bottom">
<div class="ai">
<div class="left">
......@@ -717,8 +717,21 @@ const domainChartOption = ref({
width: 1.1
}
},
labelLayout: {
hideOverlap: true
labelLayout: function (params) {
// hideOverlap: true
const points = params.labelLinePoints;
const isLeft = params.labelRect.x < params.rect.x + params.rect.width / 2;
// 调整指示线终点到 label 垂直中心
const labelCenterY = params.labelRect.y + params.labelRect.height / 2;
points[2][1] = labelCenterY;
// 调整指示线终点到 label 水平边缘
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
},
itemStyle: {
borderWidth: 0
......@@ -926,10 +939,23 @@ const typeChartOption = ref({
}
},
labelLayout: function (params) {
const isLeft = params.labelRect.x < chart.getWidth() / 2;
// const isLeft = params.labelRect.x < chart.getWidth() / 2;
// const points = params.labelLinePoints;
// points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
// return {
// labelLinePoints: points
// };
const points = params.labelLinePoints;
// Update the end point.
const isLeft = params.labelRect.x < params.rect.x + params.rect.width / 2;
// 调整指示线终点到 label 垂直中心
const labelCenterY = params.labelRect.y + params.labelRect.height / 2;
points[2][1] = labelCenterY;
// 调整指示线终点到 label 水平边缘
points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
return {
labelLinePoints: points
};
......@@ -1508,8 +1534,8 @@ onMounted(() => {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 22px 0;
justify-content: flex-start;
padding: 22px;
.data-origin-icon {
width: 16px;
height: 16px;
......
......@@ -241,11 +241,11 @@ const handleClick = item => {
const route = router.resolve({
path: "/exportControl/singleSanction",
query: {
id: item.id
id: item.id,
sanTypeId: item.sanTypeId || 1
}
});
window.open(route.href, "_blank");
};
const selectedDomain = ref(0);
......
......@@ -54,7 +54,7 @@
</div>
</div>
</div> -->
<EChart :option="domainChartOption" autoresize :style="{ height: '300px' }" />
<EChart :option="domainChartOption" autoresize :style="{ height: '300px', padding: '0 20px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
......@@ -104,7 +104,7 @@
</div>
</div>
</div> -->
<EChart :option="typeChartOption" autoresize :style="{ height: '300px' }" />
<EChart :option="typeChartOption" autoresize :style="{ height: '300px', padding: '0 20px' }" />
<div class="data-origin-box">
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
......@@ -348,7 +348,7 @@ const totalCount = ref({});
const getTotalCount = async () => {
if (!sanRecordId.value) return;
try {
const res = await getSingleSanctionTotalCount(route.query.sanTypeId);
const res = await getSingleSanctionTotalCount(route.query.sanTypeId, sanRecordId.value);
if (res.code === 200) {
totalCount.value = res.data || {};
}
......@@ -1264,8 +1264,8 @@ onMounted(() => {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
padding: 22px 0;
justify-content: flex-start;
padding: 22px;
.data-origin-icon {
width: 16px;
height: 16px;
......
......@@ -107,22 +107,185 @@ const initGraph = (layoutType = 1) => {
});
};
// const initNormalGraph = (layoutType, width, height) => {
// const data = processGraphData(props.graphData);
// console.log("初始数据", props.graphData);
// if (!data.nodes || data.nodes.length === 0) return;
// const layout = {
// type: "none",
// center: [width / 2, height / 2],
// preventOverlap: true,
// nodeSpacing: 80,
// linkDistance: 250,
// nodeStrength: -800,
// edgeStrength: 0.1,
// collideStrength: 0.8,
// alphaDecay: 0.01,
// alphaMin: 0.001
// };
// graphInstance.value = new G6.Graph({
// container: containerRef.value,
// width,
// height,
// fitView: true,
// fitViewPadding: 100,
// fitCenter: true,
// animate: true,
// animateCfg: {
// duration: 300,
// easing: "easeLinear"
// },
// minZoom: 0.1,
// maxZoom: 10,
// modes: {
// default: [
// "drag-canvas",
// "zoom-canvas",
// "drag-node",
// {
// type: "activate-relations",
// trigger: "mouseenter",
// resetSelected: true
// }
// ]
// },
// layout,
// defaultNode: {
// type: "image",
// size: 40,
// clipCfg: {
// show: true,
// type: "circle",
// r: 20
// },
// labelCfg: {
// position: "bottom",
// offset: 10,
// style: {
// fill: "#333",
// fontSize: 11,
// fontFamily: "Microsoft YaHei",
// textAlign: "center",
// background: {
// fill: "rgba(255, 255, 255, 0.95)",
// padding: [4, 6, 4, 6],
// radius: 4
// }
// }
// }
// // 注意:节点边框样式在 processGraphData 中单独设置,不在这里设置
// },
// defaultEdge: {
// type: "quadratic",
// style: {
// stroke: "red",
// lineWidth: 3,
// opacity: 0.9,
// shadowColor: "rgba(231, 243, 255, 1)",
// shadowBlur: 4,
// endArrow: {
// path: "M 0,0 L 12,6 L 12,-6 Z",
// fill: "#5B8FF9"
// }
// },
// labelCfg: {
// autoRotate: true,
// style: {
// fill: "rgba(137, 193, 255, 1)",
// fontSize: 10,
// fontFamily: "Microsoft YaHei",
// background: {
// fill: "rgba(231, 243, 255, 1)",
// padding: [2, 4, 2, 4],
// radius: 5
// }
// }
// }
// },
// nodeStateStyles: {
// active: {
// shadowColor: "#1459BB",
// shadowBlur: 15,
// stroke: "#1459BB",
// lineWidth: 3
// },
// inactive: {
// opacity: 0.3
// }
// },
// edgeStateStyles: {
// active: {
// stroke: "#1459BB",
// lineWidth: 4
// },
// inactive: {
// opacity: 0.15
// }
// }
// });
// graphInstance.value.data(data);
// graphInstance.value.render();
// bindGraphEvents();
// };
const initNormalGraph = (layoutType, width, height) => {
const data = processGraphData(props.graphData);
console.log("初始数据", props.graphData);
if (!data.nodes || data.nodes.length === 0) return;
// 中心节点坐标
const centerX = width / 2;
const centerY = height / 2;
const upperY = centerY - 240; // 上方节点 Y 坐标
const lowerY = centerY + 240; // 下方节点 Y 坐标
const nodeSpacing = 100; // 节点水平间距
// 分离中心节点和其他节点
const centerNode = data.nodes.find(n => n.isCenter);
const otherNodes = data.nodes.filter(n => !n.isCenter);
const totalNodes = otherNodes.length;
const upperCount = Math.ceil(totalNodes / 2); // 向上取整,上方多一个
const lowerCount = totalNodes - upperCount;
// 为上方节点分配坐标
const upperNodes = otherNodes.slice(0, upperCount);
upperNodes.forEach((node, index) => {
const totalWidth = (upperCount - 1) * nodeSpacing;
const startX = centerX - totalWidth / 2;
node.x = startX + index * nodeSpacing;
node.y = upperY;
// 固定位置,防止力导向布局移动
node.fx = node.x;
node.fy = node.y;
});
// 为下方节点分配坐标
const lowerNodes = otherNodes.slice(upperCount);
lowerNodes.forEach((node, index) => {
const totalWidth = (lowerCount - 1) * nodeSpacing;
const startX = centerX - totalWidth / 2;
node.x = startX + index * nodeSpacing;
node.y = lowerY;
// 固定位置,防止力导向布局移动
node.fx = node.x;
node.fy = node.y;
});
// 设置中心节点坐标
if (centerNode) {
centerNode.x = centerX;
centerNode.y = centerY;
// 固定中心节点位置
centerNode.fx = centerX;
centerNode.fy = centerY;
}
const layout = {
type: "force",
center: [width / 2, height / 2],
preventOverlap: true,
nodeSpacing: 80,
linkDistance: 250,
nodeStrength: -800,
edgeStrength: 0.1,
collideStrength: 0.8,
alphaDecay: 0.01,
alphaMin: 0.001
type: "none", // 使用预设坐标,不进行力导向布局
center: [centerX, centerY]
};
graphInstance.value = new G6.Graph({
......@@ -175,10 +338,9 @@ const initNormalGraph = (layoutType, width, height) => {
}
}
}
// 注意:节点边框样式在 processGraphData 中单独设置,不在这里设置
},
defaultEdge: {
type: "quadratic",
type: "line",
style: {
stroke: "red",
lineWidth: 3,
......@@ -230,7 +392,6 @@ const initNormalGraph = (layoutType, width, height) => {
graphInstance.value.render();
bindGraphEvents();
};
const initCircularGraph = (width, height) => {
const data = processGraphData(props.graphData);
......@@ -307,7 +468,7 @@ const initCircularGraph = (width, height) => {
}
},
defaultEdge: {
type: "quadratic",
type: "line",
style: {
stroke: "#5B8FF9",
lineWidth: 3,
......
......@@ -110,6 +110,7 @@
@node-click="handleNodeClick"
@layout-change="handleLayoutChange"
/>
<!-- <GraphChart :nodes="nodes" :links="links" layoutType="none" /> -->
</div>
</div>
</div>
......@@ -134,6 +135,7 @@ import {
} from "@/api/exportControlV2.0";
import RelationGraph from "./components/RelationGraph.vue";
import AnalysisBox from "@/components/base/boxBackground/analysisBox.vue";
import GraphChart from "@/components/base/GraphChart/index.vue";
const sanRecordId = ref("");
const activeTab = ref(["实体穿透分析"]);
......@@ -157,6 +159,9 @@ const graphData = ref({ nodes: [], links: [] });
const treeData = ref(null);
const selectedNode = ref(null);
const nodes = ref([]);
const links = ref([]);
const singleSanctionEntityEquityData = ref(null);
const singleSanctionEntitySupplyChainData = ref(null);
const singleSanctionEntityList = ref([]);
......@@ -204,6 +209,7 @@ const updateGraphData = () => {
const data =
rightActiveTab.value === "supplyChain" ? singleSanctionEntitySupplyChainData.value : singleSanctionEntityEquityData.value;
console.log("图谱数据 =>", data);
if (!data) return;
const nodes = [];
......@@ -252,8 +258,172 @@ const updateGraphData = () => {
});
graphData.value = { nodes, links };
// nodes.value = nodes;
// links.value = links;
};
const links1 = [
{ source: 1, target: 7, label: { show: true, formatter: "合作" } },
{ source: 2, target: 7, label: { show: true, formatter: "持股" } },
{ source: 3, target: 7, label: { show: true, formatter: "合作" } },
{ source: 4, target: 7, lineStyle: { type: "dashed", color: "#d32f2f" }, label: { show: true, formatter: "从属" } },
{ source: 5, target: 7, label: { show: true, formatter: "合作" } },
{ source: 6, target: 7, label: { show: true, formatter: "持股" } },
{ source: 0, target: 7, label: { show: true, formatter: "持股" } },
{ source: 8, target: 7, label: { show: true, formatter: "合作" } },
{ source: 9, target: 7, lineStyle: { type: "dashed", color: "#d32f2f" }, label: { show: true, formatter: "从属" } },
{ source: 10, target: 7, lineStyle: { type: "dashed", color: "#d32f2f" }, label: { show: true, formatter: "合作" } },
{ source: 11, target: 7, label: { show: true, formatter: "合作" } },
{ source: 12, target: 7, label: { show: true, formatter: "合作" } },
{ source: 13, target: 7, label: { show: true, formatter: "合作" } },
{ source: 14, target: 7, label: { show: true, formatter: "合作" } },
{ source: 15, target: 7, label: { show: true, formatter: "合作", color: "red", borderColor: "red" } }
];
// const updateGraphData = () => {
// const data =
// rightActiveTab.value === "supplyChain" ? singleSanctionEntitySupplyChainData.value : singleSanctionEntityEquityData.value;
// console.log("图谱数据 =>", data);
// if (!data) return;
// const newNodes = [];
// const newLinks = [];
// // 容器尺寸(根据 .right-echarts 的高度 calc(100% - 56px) ≈ 772px)
// const containerWidth = 1000;
// const containerHeight = 700;
// // 中心节点坐标(居中)
// const centerX = containerWidth / 2;
// const centerY = containerHeight / 2;
// // 上下节点分布参数
// const upperY = centerY - 200; // 上方节点 Y 坐标
// const lowerY = centerY + 200; // 下方节点 Y 坐标
// const nodeSpacing = 100; // 节点水平间距
// // 合并所有节点列表(上游 + 下游)
// const allItems = [];
// const parentList = data.parentOrgList || [];
// const childList = data.childrenOrgList || [];
// // 添加上游节点
// parentList.forEach((item, index) => {
// allItems.push({
// ...item,
// linkType: rightActiveTab.value === "supplyChain" ? "供应商" : item.type || "持股",
// direction: "source" // 链接方向:指向中心节点
// });
// });
// // 添加下游节点
// childList.forEach((item, index) => {
// allItems.push({
// ...item,
// linkType: rightActiveTab.value === "supplyChain" ? "客户" : item.description || "投资",
// direction: "target" // 链接方向:从中心节点指出
// });
// });
// // 中心节点
// newNodes.push({
// id: 0,
// name: data.orgName || "中心节点",
// symbol: `image://${companyActive}`,
// symbolSize: 60,
// value: 10,
// isSanctioned: true,
// x: centerX,
// y: centerY
// });
// // 计算上下分配
// const totalNodes = allItems.length;
// const upperCount = Math.ceil(totalNodes / 2); // 向上取整,上方多一个
// const lowerCount = totalNodes - upperCount;
// // 上方节点(前一半)
// const upperItems = allItems.slice(0, upperCount);
// upperItems.forEach((item, index) => {
// const totalWidth = (upperCount - 1) * nodeSpacing;
// const startX = centerX - totalWidth / 2;
// const x = startX + index * nodeSpacing;
// const y = upperY;
// const nodeId = `n-${index}`;
// newNodes.push({
// id: nodeId,
// name: item.name || `节点${index}`,
// symbol: `image://${item.isSanctioned ? companyActive : company}`,
// symbolSize: 40,
// value: 5,
// isSanctioned: item.isSanctioned,
// x,
// y
// });
// // 根据 direction 决定链接方向
// if (item.direction === "source") {
// newLinks.push({
// source: nodeId,
// target: 0,
// name: item.linkType,
// label: { show: true, formatter: item.description }
// });
// } else {
// newLinks.push({
// source: 0,
// target: nodeId,
// name: item.linkType,
// label: { show: true, formatter: item.description }
// });
// }
// });
// // 下方节点(后一半)
// const lowerItems = allItems.slice(upperCount);
// lowerItems.forEach((item, index) => {
// const totalWidth = (lowerCount - 1) * nodeSpacing;
// const startX = centerX - totalWidth / 2;
// const x = startX + index * nodeSpacing;
// const y = lowerY;
// const nodeId = `n-${upperCount + index}`;
// newNodes.push({
// id: nodeId,
// name: item.name || `节点${upperCount + index}`,
// symbol: `image://${item.isSanctioned ? companyActive : company}`,
// symbolSize: 40,
// value: 5,
// isSanctioned: item.isSanctioned,
// x,
// y
// });
// // 根据 direction 决定链接方向
// if (item.direction === "source") {
// newLinks.push({
// source: nodeId,
// target: 0,
// name: item.linkType,
// label: { show: true, formatter: "{c}" }
// });
// } else {
// newLinks.push({
// source: 0,
// target: nodeId,
// name: item.linkType,
// label: { show: true, formatter: "{c}" }
// });
// }
// });
// console.log("最终节点数:", newNodes, "最终链接数:", newLinks, "上方节点:", upperCount, "下方节点:", lowerCount);
// // 更新响应式数据
// nodes.value = newNodes;
// links.value = newLinks;
// };
const updateTreeData = data => {
if (!data) return;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论