提交 57a3db48 authored 作者: coderBryanFu's avatar coderBryanFu

Merge branch 'pre' of http://8.140.26.4:10003/caijian/risk-monitor into fk-dev

流水线 #294 已通过 于阶段
in 1 分 26 秒
......@@ -196,7 +196,8 @@ export function getChartAnalysis(data, options = {}) {
},
body: JSON.stringify(data),
signal: abortController.signal,
openWhenHidden: true,
openWhenHidden: false,
retryDelay: 1000,
maxRetries: 2,
onopen: response => {
......
......@@ -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获取相关事件信息
......
......@@ -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
......@@ -91,7 +91,7 @@ export function getHylyList() {
/**
* 智库概览/智库动态-智库报告、调查项目
* 智库概览/智库动态-智库报告
* GET /api/thinkTankOverview/report
* 常用 query:pageNum, pageSize, sortFun, domainIds, startDate, endDate, category(调查项目), thinkTankId(详情页), keyword(动态搜索)
*/
......@@ -103,6 +103,81 @@ export function getThinkTankReport(params) {
})
}
//智库概览调查项目
export function getThinkTankProjects(params) {
return request({
method: 'GET',
url: `/api/think-tank/projects`,
params
})
}
//智库概览页国会听证会
export function getThinkTankTestimonies(params) {
return request({
method: 'GET',
url: `/api/think-tank/testimonies`,
params
})
}
//智库调查项目详情主页
export function getThinkTankProjectsInfo(params) {
return request({
method: 'GET',
url: `/api/think-tank/projects/${params.id}`,
})
}
//智库国会听证会详情主页
export function getThinkTankHearingInfo(params) {
return request({
method: 'GET',
url: `/api/think-tank/testimonies/${params.id}`,
})
}
// 智库详情-调查项目(按智库 id)
export function getThinkTankProjectsByThinkTankId(params) {
return request({
method: 'GET',
url: `/api/think-tank/${params.thinkTankId}/projects`,
params: {
pageNum: params.pageNum,
pageSize: params.pageSize,
sortFun: params.sortFun,
sortField: params.sortField,
sortOrder: params.sortOrder,
domainIds: params.domainIds,
startDate: params.startDate,
endDate: params.endDate,
projectName: params.projectName,
}
})
}
// 智库动态-国会听证会(按智库 id)
export function getThinkTankTestimoniesByThinkTankId(params) {
return request({
method: 'GET',
url: `/api/think-tank/${params.thinkTankId}/testimonies`,
params: {
pageNum: params.pageNum,
pageSize: params.pageSize,
domainIds: params.domainIds,
startDate: params.startDate,
endDate: params.endDate,
title: params.title,
}
})
}
//智库调查项目详情作者
export function getThinkTankProjectsAuthors(params) {
return request({
method: 'GET',
url: `/api/think-tank/projects/${params.id}/team`,
})
}
// 智库概览:政策建议(资源库-政策建议)
export function getThinkTankOverviewPolicy(params) {
return request({
......@@ -378,6 +453,15 @@ export const getThinkTankReportRelated = (params) => {
}
);
}
//调查项目:获取项目报告
export const getThinkTankProjectRelated = (params) => {
return request(
{
method: 'GET',
url: `/api/think-tank/projects/${params}/reports`,
}
);
}
//获取报告原文
export const getThinkTankReportcontentUrl = (params) => {
......@@ -424,7 +508,21 @@ export function getThinkTankReportViewpoint(params) {
}
})
}
// 获取报告核心论点(支持关键字搜索)
export function getThinkTankHearingViewpoint(params) {
const { testimonyId, pageSize, keyword = '',pageNum } = params
return request({
method: 'GET',
url: `/api/think-tank/testimonies/qa`,
params: {
pageSize,
keyword,
pageNum,
testimonyId
}
})
}
//获取涉及科技领域
export function getThinkTankReportIndustry(params) {
return request({
......@@ -440,6 +538,20 @@ export function getThinkTankReportIndustryCloud(params) {
url: `/api/thinkTankReport/keyword/${params.id}`,
})
}
//获取调查项目词云
export function getThinkTankProjectCloud(params) {
return request({
method: 'GET',
url: `/api/think-tank/projects/${params.id}/word-cloud`,
})
}
//获取国会听证会词云
export function getThinkTankHearingCloud(params) {
return request({
method: 'GET',
url: `/api/think-tank/testimonies/${params.id}/wordcloud`,
})
}
//获取政策建议落实情况
export function getThinkTankReportPolicy(params) {
......
<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,
default: []
},
links: {
type: Array,
default: []
},
layoutType: {
type: String,
default: 'force'
},
width: {
type: String,
default: 'force'
},
height: {
type: String,
default: 'force'
}
})
nodes: {
type: Array,
default: []
},
links: {
type: Array,
default: []
},
layoutType: {
type: String,
default: "force"
},
width: {
type: String,
default: "force"
},
height: {
type: String,
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%;
height: 100%;
// width: 800px;
// height: 500px;
width: 100%;
height: 100%;
// width: 800px;
// height: 500px;
}
</style>
\ No newline at end of file
</style>
......@@ -121,7 +121,7 @@ const exportControlRoutes = [
name: "commercialControlList",
component: () => import("@/views/exportControl/v2.0CommercialControlList/index.vue"),
meta: {
title: "商业管制清单"
title: "商业管制清单概览"
}
}
]
......
......@@ -41,17 +41,31 @@ const thinktankRoutes = [
path: "/thinkTank/reportOriginal/:id",
name: "ReportOriginal",
component: ReportOriginal,
meta: {
title: "报告原文",
dynamicTitle: true,
titleStorageKey: "reportOriginalTabName"
}
},
{
path: "/thinkTank/SurveyProjectView/:id",
name: "SurveyProjectView",
component: SurveyProjectView,
meta: {
title: "调查项目",
dynamicTitle: true,
titleStorageKey: "surveyProjectTabName"
}
},
{
path: "/thinkTank/CongressHearingView/:id",
name: "CongressHearingView",
component: CongressHearingView,
meta: {
title: "国会听证会",
dynamicTitle: true,
titleStorageKey: "congressHearingTabName"
}
},
{
path: "/thinkTank/allThinkTank",
......
......@@ -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;
}
}
};
// 获取相关事件
......
......@@ -153,7 +153,7 @@
class="item-box"
v-for="item in sharedEvents"
:key="`shared-${item.id}`"
style="width: 280px; flex-shrink: 0;"
:style="{ width: TIMELINE_ITEM_WIDTH_PX + 'px', flexShrink: 0 }"
>
<div class="item-time">{{ item.actionDate }}</div>
<div class="item-box-dot"></div>
......@@ -211,7 +211,15 @@ const dialogPos = ref({ left: "0px", top: "0px" });
const ORG_SENATE = "参议院";
const ORG_HOUSE = "众议院";
const PRESIDENT_KEYWORD = "呈递给总统";
/** 卡片宽度;同一条时间线上相邻圆点间距 ≥ 此值,避免同轨卡片横向重叠 */
const TIMELINE_ITEM_WIDTH_PX = 280;
/** 时间线锚点为圆点;与样式 .item-box-dot { left: 10px } 一致 */
const DOT_LEFT_IN_ITEM_BOX_PX = 10;
/** 与 scoped 样式中 .top/.bottom .content-box 的 margin-left 一致,用于跨轨圆点与「两密点中点」对齐 */
const SENATE_CONTENT_BOX_MARGIN_LEFT_PX = 134;
const HOUSE_CONTENT_BOX_MARGIN_LEFT_PX = 30;
/** 双轨主线在最后一个圆点/卡片之后保留的空白(原先用整卡宽 W 导致尾部过长) */
const TIMELINE_LINE_TAIL_PADDING_PX = 48;
const ARROW_SEGMENT_TOTAL_PX = 16;
const DIAGONAL_LINE_WIDTH_PX = 127;
......@@ -281,50 +289,98 @@ const dualLaneTimeline = computed(() => {
.filter((item) => !isDualEvent(item));
});
const senateEventsPositioned = computed(() => {
const list = [];
let slotIndex = 0;
dualLaneTimeline.value.forEach((item) => {
if (!isSenateEvent(item)) return;
list.push({
key: item.id,
/**
* 双轨事件已按时间排好(dualLaneTimeline)。依次放置:与前一条同轨则绝对 x + W,
* 异轨则以前一条圆点绝对位置为锚 + 0.5W(方案 C)。单轨(灰线)仍用 sharedEvents 顺序定宽。
*/
const dualLaneLayout = computed(() => {
const U = dualLaneTimeline.value;
const W = TIMELINE_ITEM_WIDTH_PX;
const chamberRowSenate = (item) => {
const s = isSenateEvent(item);
const h = isHouseEvent(item);
if (s && !h) return true;
if (h && !s) return false;
return Boolean(s);
};
const idxInU = (item) => U.indexOf(item);
const toSlots = (list, posByItem) =>
list.map((item) => ({
key: `${idxInU(item)}-${item.id ?? "e"}`,
item,
left: slotIndex * TIMELINE_ITEM_WIDTH_PX
});
slotIndex += 1;
});
return list;
});
left: (posByItem.get(item) ?? 0) - DOT_LEFT_IN_ITEM_BOX_PX
}));
const houseEventsPositioned = computed(() => {
const list = [];
let slotIndex = 0;
dualLaneTimeline.value.forEach((item) => {
if (!isHouseEvent(item)) return;
list.push({
key: item.id,
item,
left: slotIndex * TIMELINE_ITEM_WIDTH_PX
});
slotIndex += 1;
});
return list;
if (!U.length) {
return { senateSlots: [], houseSlots: [], maxDualLaneRightPx: 0 };
}
const posByItem = new Map();
let prevAbs = null;
let prevSenate = null;
for (const item of U) {
const sen = chamberRowSenate(item);
const margin = sen ? SENATE_CONTENT_BOX_MARGIN_LEFT_PX : HOUSE_CONTENT_BOX_MARGIN_LEFT_PX;
const absDot =
prevAbs === null ? margin : prevAbs + (sen === prevSenate ? W : W / 2);
posByItem.set(item, absDot - margin);
prevAbs = absDot;
prevSenate = sen;
}
/** 相对 content-box 左缘:卡片右边界 = layoutDotX - DOT_LEFT + W */
let maxDualLaneRightPx = 0;
for (const item of U) {
const layoutDotX = posByItem.get(item) ?? 0;
const rightPx = layoutDotX - DOT_LEFT_IN_ITEM_BOX_PX + W;
maxDualLaneRightPx = Math.max(maxDualLaneRightPx, rightPx);
}
return {
senateSlots: toSlots(
U.filter((item) => isSenateEvent(item)),
posByItem
),
houseSlots: toSlots(
U.filter((item) => isHouseEvent(item)),
posByItem
),
maxDualLaneRightPx
};
});
const senateEventsPositioned = computed(() => dualLaneLayout.value.senateSlots);
const houseEventsPositioned = computed(() => dualLaneLayout.value.houseSlots);
const sharedEvents = computed(() => {
return sortedTimeline.value.filter((item, idx) => {
return isDualEvent(item) || idx >= mergeIndexExclusive.value;
});
});
const dualLaneCount = computed(() => {
return Math.max(senateEventsPositioned.value.length, houseEventsPositioned.value.length);
/** 双轨 content-box 内所需宽度:最后卡片右缘 + 尾部留白(与 maxLineWidth 中的 extent 一致) */
const dualLaneContentExtentPx = computed(() => {
if (!dualLaneTimeline.value.length) return 0;
return (
dualLaneLayout.value.maxDualLaneRightPx + TIMELINE_LINE_TAIL_PADDING_PX
);
});
/**
* 双轨横线(top-line / bottom-line 共用)宽度:起点 left:110,需画到与卡片区右缘对齐。
* 右缘 x = 120 + 参院 margin + extent = 254 + extent,故宽度 = 254 + extent - 110 = 144 + extent。
* 旧式 254+extent 多 110px,导致上下线都偏长。
*/
const maxLineWidth = computed(() => {
const senateWidth = 254 + dualLaneCount.value * TIMELINE_ITEM_WIDTH_PX;
const houseWidth = 150 + dualLaneCount.value * TIMELINE_ITEM_WIDTH_PX;
return Math.max(senateWidth, houseWidth);
if (!dualLaneTimeline.value.length) {
return Math.max(254, 150);
}
const extent = dualLaneContentExtentPx.value;
return 120 + SENATE_CONTENT_BOX_MARGIN_LEFT_PX + extent - 110;
});
const lineWidth = computed(() => `${maxLineWidth.value}px`);
......@@ -350,22 +406,41 @@ const rightArrowCount = computed(() => {
return Math.max(1, Math.ceil(sharedLineWidth.value / ARROW_SEGMENT_TOTAL_PX));
});
/** 参院 content 起点更靠右(margin 134 vs 30),众院 box 需多 104px 才能与参院右缘对齐 */
const CONTENT_BOX_WIDTH_DELTA_SENATE_HOUSE_PX =
SENATE_CONTENT_BOX_MARGIN_LEFT_PX - HOUSE_CONTENT_BOX_MARGIN_LEFT_PX;
const senateBoxStyle = computed(() => ({
width: `${maxLineWidth.value + 110 - 254}px`,
width: `${
dualLaneTimeline.value.length
? dualLaneContentExtentPx.value
: 110
}px`,
justifyContent: "flex-start"
}));
const houseBoxStyle = computed(() => ({
width: `${maxLineWidth.value + 110 - 150}px`,
width: `${
dualLaneTimeline.value.length
? dualLaneContentExtentPx.value + CONTENT_BOX_WIDTH_DELTA_SENATE_HOUSE_PX
: 214
}px`,
justifyContent: "flex-start"
}));
const rightPos = computed(() => `${maxLineWidth.value + 90}px`);
const sharedBoxStyle = computed(() => ({
left: `${maxLineWidth.value + 219}px`,
width: `${Math.max(sharedLineWidth.value, sharedEvents.value.length * TIMELINE_ITEM_WIDTH_PX)}px`
}));
const sharedBoxStyle = computed(() => {
const rightTopPx = Number.parseFloat(String(rightTop.value)) || 0;
const rightCenterY = rightTopPx + 12;
return {
left: `${maxLineWidth.value + 230}px`,
top: `${rightCenterY}px`,
transform: "translateY(-100%)",
width: `${Math.max(sharedLineWidth.value, sharedEvents.value.length * TIMELINE_ITEM_WIDTH_PX)}px`
};
});
const topLineEndRef = ref(null);
const bottomLineEndRef = ref(null);
......@@ -547,13 +622,18 @@ const updateRightTop = () => {
}
}
/* .top 高 260px,时间线 top:242px;卡片下缘应对齐线顶:距容器底 18px */
.content-box {
position: relative;
margin-left: 134px;
margin-bottom: 19px;
margin-bottom: 0;
align-self: stretch;
min-height: 0;
.item-box {
position: absolute;
top: auto;
bottom: 18px;
}
}
}
......@@ -808,7 +888,7 @@ const updateRightTop = () => {
.shared-content-box {
position: absolute;
top: 170px;
top: 254px;
display: flex;
.item-box {
......
......@@ -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"
......
......@@ -849,6 +849,9 @@ onBeforeUnmount(() => {
position: absolute;
right: 0;
bottom: 18px;
width: 74px;
height: 28px;
z-index: 2;
.btn-box {
width: 74px;
......@@ -953,6 +956,9 @@ onBeforeUnmount(() => {
position: absolute;
right: 0;
bottom: 18px;
width: 74px;
height: 28px;
z-index: 2;
.btn-box {
width: 74px;
......
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">
......@@ -121,7 +121,7 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体数量变化情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入实体清单的中国实体数量变化趋势,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -130,7 +130,7 @@
</AnalysisBox>
</div>
<div class="main-item">
<AnalysisBox title="制裁实体地域分布情况">
<AnalysisBox title="制裁实体各省分布情况">
<template #header-btn>
<el-select v-model="regionTime" class="time-select" placeholder="请选择" @change="getRegionCountData">
<el-option v-for="item in timeOptions" :key="item.value" :label="item.label" :value="item.value" />
......@@ -170,7 +170,7 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体地域分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入实体清单的中国实体各省分布情况,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -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">
......@@ -202,7 +202,7 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体领域分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -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">
......@@ -234,7 +234,7 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体类型分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入实体清单的中国实体类型分布情况,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -357,7 +357,7 @@ const getRegionCountData = async () => {
}));
// Sort by value descending
rankData.value.sort((a, b) => b.value - a.value);
rankChart.interpret({ type: "柱状图", name: "制裁实体地域分布情况", data: data });
rankChart.interpret({ type: "柱状图", name: "制裁实体各省分布情况", data: data });
updateMapChart();
}
} catch (error) {
......@@ -691,7 +691,7 @@ const domainChartOption = ref({
},
rich: {
name: {
fontSize: 18,
fontSize: 16,
fontWeight: 700,
color: "rgb(59, 65, 75)",
padding: [0, 0, 5, 0],
......@@ -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
......@@ -899,7 +912,7 @@ const typeChartOption = ref({
},
rich: {
name: {
fontSize: 18,
fontSize: 16,
fontWeight: 700,
color: "rgb(59, 65, 75)",
padding: [0, 0, 5, 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;
......
......@@ -32,14 +32,14 @@
<div class="list-header">
<div class="count">{{ sanctionList.length }}次制裁</div>
<!-- 暂时隐藏,说这里可能是轮播图的效果 -->
<!-- <div class="pagination">
<div class="page-btn prev">
<el-icon><ArrowLeft /></el-icon>
</div>
<div class="page-btn next">
<el-icon><ArrowRight /></el-icon>
<div class="pagination">
<div class="page-btn prev" @click="handlePrevClick">
<el-icon><ArrowLeft /></el-icon>
</div>
<div class="page-btn next" @click="handleNextClick">
<el-icon><ArrowRight /></el-icon>
</div>
</div>
</div> -->
</div>
<div class="list-content" v-loading="loading">
<div
......@@ -243,7 +243,7 @@
</template>
<script setup>
import { ref, onMounted, nextTick } from "vue";
import { ref, onMounted, nextTick, onUnmounted } from "vue";
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import defaultTitle from "../../assets/default-icon2.png";
import {
......@@ -365,15 +365,17 @@ const getDeepMiningSelectData = async () => {
try {
const res = await getDeepMiningSelect(params);
if (res.code === 200 && res.data && res.data.content) {
sanctionList.value = res.data.content.map(item => ({
id: item.id,
date: item.postDate,
title: item.name,
count: item.cnEntityCount,
unit: "家中国实体", // 接口未返回单位,暂时固定
summary: item.summary, // 保留额外信息备用
techDomainList: item.techDomainList // 保留额外信息备用
}));
sanctionList.value = res.data.content
.map(item => ({
id: item.id,
date: item.postDate,
title: item.name,
count: item.cnEntityCount,
unit: "家中国实体", // 接口未返回单位,暂时固定
summary: item.summary, // 保留额外信息备用
techDomainList: item.techDomainList // 保留额外信息备用
}))
.reverse();
// 默认选中第一条
if (sanctionList.value.length > 0) {
......@@ -396,35 +398,74 @@ const handleDateChange = () => {
currentPage.value = 1;
getDeepMiningSelectData();
};
// ✅ 自动轮播定时器
const autoPlayTimer = ref(null);
// ✅ 启动自动轮播
const startAutoPlay = () => {
stopAutoPlay();
if (sanctionList.value.length > 1) {
autoPlayTimer.value = setInterval(() => {
handleNextClickAuto();
}, 10000);
}
};
// 翻页
const handlePageChange = page => {
if (page < 1 || page > totalPage.value) return;
currentPage.value = page;
getDeepMiningSelectData();
// ✅ 停止自动轮播
const stopAutoPlay = () => {
if (autoPlayTimer.value) {
clearInterval(autoPlayTimer.value);
autoPlayTimer.value = null;
}
};
// ✅ 自动下一个(支持循环)
const handleNextClickAuto = () => {
const currentIndex = sanctionList.value.findIndex(item => item.id === currentSanctionId.value);
let nextItem;
if (currentIndex < sanctionList.value.length - 1) {
nextItem = sanctionList.value[currentIndex + 1];
} else {
nextItem = sanctionList.value[0]; // 循环到第一个
}
if (nextItem) {
handleSanctionSelect(nextItem.id);
}
};
// ✅ 修改现有函数,添加重置定时器
const handlePrevClick = () => {
const currentIndex = sanctionList.value.findIndex(item => item.id === currentSanctionId.value);
if (currentIndex > 0) {
const prevItem = sanctionList.value[currentIndex - 1];
handleSanctionSelect(prevItem.id);
startAutoPlay();
}
};
const handleNextClick = () => {
const currentIndex = sanctionList.value.findIndex(item => item.id === currentSanctionId.value);
if (currentIndex < sanctionList.value.length - 1) {
const nextItem = sanctionList.value[currentIndex + 1];
handleSanctionSelect(nextItem.id);
startAutoPlay();
}
};
// 列表项点击事件
const handleSanctionSelect = id => {
currentSanctionId.value = id;
getFishboneData();
getCnEntityOnChainData();
startAutoPlay();
};
const activeTab = ref(["制裁时序分析"]);
const activeIndex = ref(0);
const dateRange = ref(["2025-01-01", "2025-12-31"]);
const sanctionList = ref([
{ id: 1, date: "2025年2月8日", title: "实体清单更新", count: 2, unit: "家中国实体" },
{ id: 2, date: "2025年4月10日", title: "实体清单更新", count: 5, unit: "家中国实体" },
{ id: 3, date: "2025年6月29日", title: "实体清单更新", count: 6, unit: "家中国实体" },
{ id: 4, date: "2025年8月12日", title: "实体清单更新", count: 24, unit: "家中国实体" },
{ id: 5, date: "2025年8月19日", title: "实体清单更新", count: 11, unit: "家中国实体" },
{ id: 6, date: "2025年9月12日", title: "实体清单更新", count: 3, unit: "家中国实体" },
{ id: 7, date: "2025年9月26日", title: "实体清单更新", count: 6, unit: "家中国实体" },
{ id: 8, date: "2025年10月12日", title: "实体清单更新", count: 18, unit: "家中国实体" }
]);
const sanctionList = ref([]);
const currentSanctionId = ref(5);
const cnEntityOnChainData = ref({});
......@@ -479,6 +520,14 @@ onMounted(() => {
getDeepMiningSelectData();
// 获取产业链信息
getIndustryList();
nextTick(() => {
startAutoPlay();
});
});
// 组件卸载时停止自动轮播
onUnmounted(() => {
stopAutoPlay();
});
</script>
......
......@@ -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);
......
......@@ -173,6 +173,13 @@
</div>
</template>
</el-table-column>
<el-table-column
prop="listingLocation"
label="上市地点"
width="140"
show-overflow-tooltip
align="center"
/>
<el-table-column
prop="startTime"
label="制裁时间"
......@@ -557,6 +564,10 @@ watch(customDateRange, () => {
width: 160px;
height: 32px;
}
:deep(.el-checkbox__label) {
font-size: 16px;
color: rgb(95, 101, 108);
}
}
}
......@@ -575,9 +586,14 @@ watch(customDateRange, () => {
background-color: #fff;
.checkbox-group {
display: flex;
flex-wrap: wrap;
padding: 0 0 0 24px;
// display: flex;
// flex-wrap: wrap;
// padding: 0 0 0 24px;
display: grid;
grid-template-columns: repeat(2, 160px);
gap: 8px 4px;
padding-left: 20px;
.el-checkbox {
width: 50%;
......
......@@ -54,12 +54,12 @@
</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="" />
</div>
<div class="data-origin-text">制裁实体领域分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入本次实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -104,12 +104,12 @@
</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="" />
</div>
<div class="data-origin-text">制裁实体类型分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入本次实体清单的中国实体类型分布情况,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -118,7 +118,7 @@
</AnalysisBox>
</div>
<div class="main-item">
<AnalysisBox title="制裁实体国家分布情况">
<AnalysisBox title="制裁实体国家地区分布情况">
<div class="country-list">
<div class="list-item" v-for="(item, index) in countryDistribution" :key="index">
<img :src="flag" alt="" class="flag" />
......@@ -150,7 +150,7 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体国家分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入本次实体清单的实体国家地区分布情况,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -159,7 +159,7 @@
</AnalysisBox>
</div>
<div class="main-item">
<AnalysisBox title="制裁实体地域分布情况">
<AnalysisBox title="制裁实体各省分布情况">
<div class="map-wrapper">
<div class="map-chart" ref="mapChartRef"></div>
<div class="rank-list">
......@@ -194,7 +194,7 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">制裁实体地域分布情况,数据来源:美国各行政机构官网</div>
<div class="data-origin-text">进入本次实体清单的中国实体各省分布情况,数据来源:美国商务部官网</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -251,7 +251,7 @@ const getRegionData = async () => {
regionDistribution.value = res.data || [];
maxRegionCount.value = Math.max(...regionDistribution.value.map(item => item.count), 0);
initMapChart();
regionDistributionChart.interpret({ type: "柱状图", name: "制裁实体地域分布情况", data: res.data });
regionDistributionChart.interpret({ type: "柱状图", name: "进入本次实体清单的中国实体各省分布情况", data: res.data });
}
} catch (error) {
console.log(error);
......@@ -291,7 +291,11 @@ const getCountryCount = async () => {
gradient
};
});
countryDistributionChart.interpret({ type: "柱状图", name: "制裁实体国家分布情况", data: res.data });
countryDistributionChart.interpret({
type: "柱状图",
name: "进入本次实体清单的实体国家地区分布情况",
data: res.data
});
}
} catch (error) {
console.log(error);
......@@ -312,7 +316,7 @@ const getEntityTypeCount = async () => {
const res = await getSingleSanctionEntityTypeCount(params);
if (res.code === 200) {
entityTypeCount.value = res.data || [];
typeChart.interpret({ type: "饼图", name: "制裁实体类型分布情况", data: entityTypeCount.value });
typeChart.interpret({ type: "饼图", name: "进入本次实体清单的中国实体类型分布情况", data: entityTypeCount.value });
initTypeChart();
}
} catch (error) {
......@@ -335,7 +339,7 @@ const getDomainCount = async () => {
if (res.code === 200) {
domainCount.value = res.data || [];
initDomainChart();
domainChart.interpret({ type: "饼图", name: "制裁实体领域分布情况", data: domainCount.value });
domainChart.interpret({ type: "饼图", name: "进入本次实体清单的中国实体领域分布情况", data: domainCount.value });
}
} catch (error) {
console.log(error);
......@@ -348,7 +352,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 +1268,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;
......
......@@ -1317,7 +1317,7 @@ onMounted(async () => {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-start;
padding: 30px 0;
.data-origin-icon {
width: 16px;
......
......@@ -1543,7 +1543,7 @@ onBeforeUnmount(() => {
width: 100%;
display: flex;
align-items: center;
justify-content: center;
justify-content: flex-start;
padding: 35px 0;
.data-origin-icon {
width: 16px;
......
......@@ -168,14 +168,21 @@
</el-table-column>
<el-table-column label="涉及领域" width="180" align="center">
<template #default="scope">
<span
<!-- <span
v-for="(item, index) in scope.row.fields"
:key="index"
class="tag"
:style="getTagStyle(item)"
style="margin: 0 2px"
>{{ item }}</span
>
> -->
<div class="domain-box">
<AreaTag
v-for="(domain, index) in scope.row.fields"
:key="index"
:tagName="domain"
/>
</div>
</template>
</el-table-column>
<el-table-column prop="location" label="上市地点" width="90" align="center" />
......@@ -563,10 +570,10 @@ onMounted(() => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
// * {
// margin: 0;
// padding: 0;
// }
.sanctions-overview {
width: 1601px;
......@@ -669,7 +676,7 @@ onMounted(() => {
.left-top-content {
width: 100%;
height: 234px;
// height: 234px;
padding: 19px 29px 22px 27px;
overflow: auto;
......@@ -1001,6 +1008,11 @@ onMounted(() => {
border-radius: 8px;
overflow: hidden;
.domain-box {
display: flex;
gap: 8px;
}
:deep(.el-table__header-wrapper) {
th {
background-color: var(--color-primary-100) !important;
......
......@@ -52,7 +52,7 @@
:close-on-click-modal="false"
@close="resetModal"
>
<div class="sanction-list">
<div class="sanction-list" :loading="loading">
<div
v-for="item in sanctionList"
:key="item.id"
......@@ -61,6 +61,7 @@
@click="selectSanction(item)"
>
{{ item.name }}
<div class="sanction-type">{{ item.postDate }}</div>
</div>
</div>
......@@ -193,6 +194,7 @@ const handleAnalysisClick = () => {
};
// ========== 新增响应式状态 ==========
const loading = ref(false);
const sanctionModalVisible = ref(false);
const sanctionList = ref([]);
const selectedSanctionId = ref(null);
......@@ -204,6 +206,7 @@ const totalElements = ref(0);
const openSanctionModal = async () => {
sanctionModalVisible.value = true;
console.log("制裁事件列表11:", sanctionList.value);
loading.value = true;
await fetchSanctionData();
};
......@@ -211,6 +214,7 @@ const openSanctionModal = async () => {
const fetchSanctionData = async () => {
try {
const res = await getSanctionProcess([1], currentPage.value, 10);
loading.value = false;
if (res && !!res.content) {
sanctionList.value = res.content || [];
totalElements.value = res.totalElements || 0;
......@@ -225,6 +229,7 @@ const fetchSanctionData = async () => {
}
} catch (error) {
console.error("获取制裁事件失败:", error);
loading.value = false;
sanctionList.value = [];
totalElements.value = 0;
}
......@@ -237,7 +242,7 @@ const handlePageChange = async newPage => {
};
// ========== 选择某项 ==========
const selectSanction = item => {
const selectSanction = async item => {
selectedSanctionId.value = item.id;
router.replace({
path: window.location.pathname,
......@@ -247,7 +252,10 @@ const selectSanction = item => {
}
});
sanctionModalVisible.value = false;
window.location.reload();
console.log("跳转URL:", window.location.href);
// 根据最新URL参数刷新当前页面
window.open(`${window.location.pathname}?id=${item.id}&sanTypeId=${item.sanTypeId}`, "_self");
};
// ========== 关闭弹窗时重置 ==========
......@@ -467,6 +475,8 @@ onMounted(() => {
font-size: 14px;
color: #333;
transition: background-color 0.2s;
display: flex;
justify-content: space-between;
}
.sanction-item:hover {
......
<template>
<div class="entity-list">
<div class="header">
<div class="header-title">
<img :src="headerTitle.img" alt="" />
<div>
<div class="title">
{{ headerTitle.title }}
<!-- <span>{{ headerTitle.titleEn }}</span> -->
</div>
<div class="department">
{{ headerTitle.department }}
</div>
</div>
<!-- <div class="btn">
<img :src="icon01" alt="">切换
</div> -->
</div>
</div>
<div class="main">
<div class="pdf-container">
<iframe v-if="headerTitle.srcUrl" :src="headerTitle.srcUrl" width="100%" height="100%" frameborder="0"></iframe>
<div v-else class="no-pdf">暂无原文</div>
</div>
<div class="pdf-container">
<iframe
v-if="headerTitle.transUrl"
:src="headerTitle.transUrl"
width="100%"
height="100%"
frameborder="0"
></iframe>
<div v-else class="no-pdf">暂无译文</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import title from "../assets/title.png";
import icon01 from "../assets/icon01.png";
// 单次制裁-制裁概况-基本信息
const singleSanctionOverview = ref({});
const getSingleSanctionOverviewData = async () => {
if (!sanRecordId.value) return;
try {
const res = await getSingleSanctionOverview({
sanRecordId: sanRecordId.value
});
if (res.code === 200) {
singleSanctionOverview.value = res.data || {};
// 格式化日期
let dateStr = "";
if (singleSanctionOverview.value.postDate) {
const date = new Date(singleSanctionOverview.value.postDate);
if (!isNaN(date.getTime())) {
dateStr = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
} else {
dateStr = singleSanctionOverview.value.postDate;
}
}
// 更新头部信息
headerTitle.value = {
...headerTitle.value,
title: `${dateStr}${singleSanctionOverview.value.sanTitleZh || singleSanctionOverview.value.sanTitle}》`,
titleEn: singleSanctionOverview.value.sanTitle || "",
department: singleSanctionOverview.value.fileCode || "",
srcUrl: singleSanctionOverview.value.srcUrl || "",
transUrl: singleSanctionOverview.value.transUrl || ""
};
}
} catch (error) {
console.error("获取制裁概况失败:", error);
}
};
const headerTitle = ref({
img: title
});
// 获取URL参数
const sanRecordId = ref("");
const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "";
};
onMounted(() => {
getUrlParams();
getSingleSanctionOverviewData();
});
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.entity-list {
width: 100%;
height: 100%;
.header {
width: 100%;
height: 148px;
background-color: #fff;
padding-top: 16px;
.header-title {
width: 1601px;
height: 72px;
background-color: rgba(246, 250, 255, 1);
margin: 0 auto;
border-radius: 10px;
border: 2px solid rgba(174, 214, 255, 1);
display: flex;
align-items: center;
margin-bottom: 12px;
position: relative;
img {
width: 54px;
height: 54px;
margin-left: 15px;
margin-right: 11px;
}
.title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
span {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
margin-left: 11px;
}
}
.department {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
}
.btn {
cursor: pointer;
display: flex;
align-items: center;
position: absolute;
right: 16px;
top: 25px;
font-size: 18px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(5, 95, 194);
img {
width: 20px;
height: 20px;
margin-right: 7px;
}
}
}
.header-nav {
width: 1601px;
margin: 0 auto;
height: 48px;
display: flex;
align-items: center;
.nav-item {
display: flex;
align-items: center;
height: 100%;
margin-right: 32px;
cursor: pointer;
position: relative;
font-size: 18px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgb(59, 65, 75);
&:last-child {
margin-right: 0;
}
img {
width: 16px;
height: 16px;
margin-right: 4px;
}
&.active {
color: rgb(5, 95, 194);
font-weight: 700;
}
.active-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #055fc2;
border-radius: 1.5px;
}
}
.original-text-btn {
margin-left: auto;
width: 152px;
height: 36px;
background: #ffffff;
border-radius: 4px;
border: 1px solid rgba(230, 231, 232, 1);
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
img {
width: 16px;
height: 16px;
margin-right: 8px;
}
span {
font-size: 16px;
font-weight: 400;
color: rgb(95, 101, 108);
font-family: "Microsoft YaHei";
line-height: 24px;
}
}
}
}
.main {
width: 1601px;
height: calc(100% - 148px);
background-color: #f7f8f9;
margin: 0 auto;
display: flex;
justify-content: space-between;
padding-top: 20px;
box-sizing: border-box;
.pdf-container {
width: 790px;
height: calc(100% - 20px);
background-color: #fff;
// border: 1px solid rgba(174, 214, 255, 1);
border-radius: 4px;
overflow: hidden;
.no-pdf {
display: flex;
align-items: center;
justify-content: center;
height: 100%;
color: #909399;
font-size: 16px;
background-color: #fff;
}
}
}
}
</style>
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论