提交 8e14f2ea authored 作者: 闫鹏's avatar 闫鹏

合并分支 'yp-dev' 到 'pre'

Yp dev 查看合并请求 !291
流水线 #261 已通过 于阶段
in 6 分 10 秒
......@@ -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
<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>
......@@ -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
}
});
// 打开一个新页面
......@@ -1388,8 +1390,8 @@ const fetchSanctionList = async () => {
const tags = Array.isArray(item.techDomains)
? item.techDomains
: item.techDomain
? [item.techDomain]
: item.techDomainList || [];
? [item.techDomain]
: item.techDomainList || [];
const fullTime = item.startTime
? formatAnyDateToChinese(item.startTime)
......@@ -1419,8 +1421,8 @@ const fetchSanctionList = async () => {
countTag: item.cnEntityCount
? `${item.cnEntityCount}家中国实体`
: item.ruleOrgCount
? `${item.ruleOrgCount}家关联实体`
: item.countTag || ""
? `${item.ruleOrgCount}家关联实体`
: item.countTag || ""
};
});
totalAll.value = res.totalElements;
......@@ -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;
......
......@@ -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;
......
......@@ -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>
<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>
<div class="entity-list">
<div class="header">
<div class="header-title">
<img :src="headerTitle.img" alt="" />
<div>
<div class="title">
{{ headerTitle.title }}
</div>
<div class="department">
{{ headerTitle.department }}
</div>
</div>
</div>
</div>
<div class="main">
<div class="main-header">
<div>实体清单制裁文件</div>
</div>
<!-- 外层滚动容器,统一控制两侧滚动 -->
<div class="report-box" ref="reportBoxRef">
<div class="pdf-pane-wrap">
<pdf ref="leftPdfRef" :pdfUrl="headerTitle.srcUrl" class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap">
<pdf ref="rightPdfRef" :pdfUrl="headerTitle.transUrl" class="pdf-pane-inner" />
</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"
import { ref, onMounted, watch } from "vue";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import title from "../assets/title.png";
import pdf from "./pdf.vue";
const leftPdfRef = ref(null);
const rightPdfRef = ref(null);
const reportBoxRef = ref(null);
const headerTitle = ref({
img: title,
title: "",
department: "",
srcUrl: "",
transUrl: ""
});
const sanRecordId = ref("");
const isSyncing = ref(false);
const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "";
};
// 单次制裁-制裁概况-基本信息
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;
}
}
if (!sanRecordId.value) return;
try {
const res = await getSingleSanctionOverview({
sanRecordId: sanRecordId.value
});
if (res.code === 200 && res.data) {
const singleSanctionOverview = res.data || {};
// 更新头部信息
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)
}
}
// 格式化日期
let dateStr = "";
if (singleSanctionOverview.postDate) {
const date = new Date(singleSanctionOverview.postDate);
if (!isNaN(date.getTime())) {
dateStr = `${date.getFullYear()}${date.getMonth() + 1}${date.getDate()}日`;
} else {
dateStr = singleSanctionOverview.postDate;
}
}
// 更新头部信息
headerTitle.value = {
...headerTitle.value,
title: `${dateStr}${singleSanctionOverview.sanTitleZh || singleSanctionOverview.sanTitle}》`,
department: singleSanctionOverview.fileCode || "",
srcUrl: singleSanctionOverview.srcUrl || "",
transUrl: singleSanctionOverview.transUrl || ""
};
}
} catch (error) {
console.error("获取制裁概况失败:", error);
}
};
// 同步滚动处理
const handleSyncScroll = () => {
if (isSyncing.value) return;
isSyncing.value = true;
requestAnimationFrame(() => {
isSyncing.value = false;
});
};
// 监听滚动事件
const setupScrollSync = () => {
const reportBox = reportBoxRef.value;
if (!reportBox) return;
reportBox.addEventListener("scroll", handleSyncScroll, { passive: true });
};
// 监听 PDF 加载完成
watch(
() => [headerTitle.value.srcUrl, headerTitle.value.transUrl],
() => {
// PDF URL 变化时,等待渲染完成后设置滚动监听
setTimeout(() => {
setupScrollSync();
}, 1000);
},
{ deep: true }
);
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()
})
getUrlParams();
getSingleSanctionOverviewData();
// 等待 DOM 渲染完成后设置滚动监听
setTimeout(() => {
setupScrollSync();
}, 500);
});
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
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;
}
.entity-list {
width: 100%;
height: 100%;
overflow-y: auto;
.header {
width: 100%;
height: 148px;
background-color: #fff;
padding-top: 16px;
box-sizing: border-box;
border-bottom: 1px solid rgba(234, 236, 238, 1);
box-shadow: 0px 0px 20px 0px rgba(25, 69, 130, 0.1);
.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);
}
.department {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
}
}
}
.main {
margin: 0 auto;
background: rgb(255, 255, 255);
width: 1601px;
height: calc(100vh - 148px);
margin-bottom: 20px;
border: 1px solid rgb(234, 236, 238);
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
img {
width: 16px;
height: 16px;
margin-right: 4px;
}
.main-header {
height: 64px;
border-bottom: 1px solid rgb(234, 236, 238);
background: rgb(255, 255, 255);
margin: 0 70px;
color: rgba(59, 65, 75, 1);
font-family: "Source Han Sans CN";
font-size: 20px;
font-weight: 700;
line-height: 26px;
width: 1456px;
text-align: left;
display: flex;
align-items: center;
}
&.active {
color: rgb(5, 95, 194);
font-weight: 700;
}
.report-box {
margin-left: 70px;
width: 1456px;
height: calc(100% - 64px);
display: flex;
overflow-y: auto; /* 统一滚动条,控制两侧一起滚动 */
overflow-x: hidden;
}
.active-line {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 3px;
background-color: #055fc2;
border-radius: 1.5px;
}
}
.pdf-pane-wrap {
flex: 0 0 50%;
max-width: 50%;
height: 100%;
min-width: 0;
}
.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;
}
}
}
.pdf-pane-inner {
width: 100%;
height: 100%;
}
}
}
</style>
\ No newline at end of file
</style>
<template>
<div class="pdf-viewer">
<!-- PDF 页面:canvas + textLayer 必须在同一容器内渲染 -->
<div class="page-wrap" v-for="page in pageCount" :key="page">
<canvas :ref="el => setCanvasRef(page, el)"></canvas>
<div :ref="el => setOverlayRef(page, el)" class="textLayer"></div>
</div>
<div v-if="loading" class="loading">加载中...</div>
</div>
</template>
<script>
import { ref, shallowRef, nextTick, watch } from "vue";
import * as pdfjsLib from "pdfjs-dist/legacy/build/pdf";
import PdfWorker from "pdfjs-dist/legacy/build/pdf.worker.min?worker";
// 使用 Vite 的 ?worker 直接注入 Worker,避免线上 mjs MIME 类型问题
pdfjsLib.GlobalWorkerOptions.workerPort = new PdfWorker();
export default {
name: "PdfViewer",
props: {
pdfUrl: {
type: String,
required: true
}
},
setup(props) {
const canvasMap = {};
const overlayMap = {};
const pageCount = ref(0);
const loading = ref(true);
const renderedPageCount = ref(0);
let resolveRenderAll = null;
const waitAllPagesRendered = () => {
if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
return Promise.resolve();
}
return new Promise(resolve => {
resolveRenderAll = resolve;
});
};
// pdfjs 的 document 对象内部使用 #private 字段,用 shallowRef 保持为原始对象引用
const pdfDocRef = shallowRef(null);
const searchKey = ref("");
const matchList = ref([]);
const matchIdx = ref(0);
const pdfjsApiRef = shallowRef(pdfjsLib);
// 保存 canvas
const setCanvasRef = (page, el) => {
if (!el) return;
canvasMap[page] = el;
};
// 保存 textLayer 容器(用于搜索高亮)
const setOverlayRef = (page, el) => {
if (!el) return;
overlayMap[page] = el;
};
// 清理 URL
const parsePdfUrl = pdfUrl => {
if (!pdfUrl || typeof pdfUrl !== "string") return "";
const [urlPart] = pdfUrl.split("#");
return urlPart;
};
// 清空所有高亮
const clearHighlights = () => {
Object.values(overlayMap).forEach(layer => {
if (!layer) return;
const rects = layer.querySelectorAll(".highlight-rect");
rects.forEach(n => n.remove());
});
};
// 重置搜索状态
const clearSearch = () => {
searchKey.value = "";
matchList.value = [];
matchIdx.value = 0;
clearHighlights();
};
// 渲染单页 PDF
const renderPage = async (pdf, pageNum) => {
const pdfPage = await pdf.getPage(pageNum);
const canvas = canvasMap[pageNum];
const textLayer = overlayMap[pageNum];
if (!canvas || !textLayer) return;
const baseViewport = pdfPage.getViewport({ scale: 1 });
const desiredWidth = canvas.clientWidth || 726;
const scale = desiredWidth / baseViewport.width;
const viewport = pdfPage.getViewport({ scale });
const context = canvas.getContext("2d");
canvas.width = viewport.width;
canvas.height = viewport.height;
canvas.style.width = `${viewport.width}px`;
canvas.style.height = `${viewport.height}px`;
textLayer.style.width = canvas.width + "px";
textLayer.style.height = canvas.height + "px";
textLayer.innerHTML = "";
textLayer.style.setProperty("--scale-factor", String(viewport.scale || 1));
await pdfPage.render({ canvasContext: context, viewport }).promise;
try {
const textContent = await pdfPage.getTextContent();
let api = pdfjsApiRef.value || pdfjsLib;
let rt = api?.renderTextLayer;
if (typeof rt !== "function") {
try {
const legacy = await import("pdfjs-dist/legacy/build/pdf");
pdfjsApiRef.value = legacy;
api = legacy;
rt = legacy?.renderTextLayer;
} catch (_) {}
}
if (typeof rt === "function") {
await rt({
textContent,
container: textLayer,
viewport,
textDivs: [],
enhanceTextSelection: false
}).promise;
}
} catch (e) {
console.warn("textLayer 渲染失败", e);
}
renderedPageCount.value += 1;
if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
if (typeof resolveRenderAll === "function") {
const fn = resolveRenderAll;
resolveRenderAll = null;
fn();
}
}
};
// 渲染 PDF
const renderPdf = async pdfUrl => {
const url = parsePdfUrl(pdfUrl);
if (!url) return;
loading.value = true;
pdfDocRef.value = null;
clearHighlights();
matchList.value = [];
searchKey.value = "";
renderedPageCount.value = 0;
resolveRenderAll = null;
try {
const pdf = await pdfjsLib.getDocument(url).promise;
pdfDocRef.value = pdf;
pageCount.value = pdf.numPages;
await nextTick();
for (let p = 1; p <= pdf.numPages; p++) {
await renderPage(pdf, p);
}
} catch (err) {
console.error("PDF 加载失败", err);
} finally {
loading.value = false;
}
};
// 搜索关键词 + 高亮
const doSearch = async () => {
const doc = pdfDocRef.value;
const key = searchKey.value.trim();
clearHighlights();
matchList.value = [];
matchIdx.value = 0;
if (!doc || !key) return;
await waitAllPagesRendered();
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
const layer = overlayMap[pageNum];
if (!layer) continue;
const nodes = Array.from(layer.querySelectorAll("span"));
for (const el of nodes) {
const t = el.textContent || "";
if (!t) continue;
let start = 0;
while (true) {
const idx = t.indexOf(key, start);
if (idx === -1) break;
matchList.value.push({ pageNum, el, startIdx: idx, endIdx: idx + key.length });
start = idx + Math.max(1, key.length);
}
}
}
if (matchList.value.length > 0) jumpTo(0);
};
// 跳转到第 N 个匹配项
const jumpTo = idx => {
if (idx < 0 || idx >= matchList.value.length) return;
matchIdx.value = idx;
const m = matchList.value[idx];
const el = m?.el;
if (!el) return;
clearHighlights();
const textNode = el.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
try {
const range = document.createRange();
range.setStart(textNode, Math.max(0, m.startIdx ?? 0));
range.setEnd(textNode, Math.max(0, m.endIdx ?? 0));
const rectList = Array.from(range.getClientRects());
const pageWrap = el.closest(".page-wrap");
const layer = overlayMap[m.pageNum];
if (pageWrap && layer && rectList.length) {
const pageRect = pageWrap.getBoundingClientRect();
rectList.forEach(r => {
const mark = document.createElement("div");
mark.className = "highlight-rect";
mark.style.left = r.left - pageRect.left + "px";
mark.style.top = r.top - pageRect.top + "px";
mark.style.width = r.width + "px";
mark.style.height = r.height + "px";
layer.appendChild(mark);
});
}
range.detach?.();
} catch (e) {
// ignore
}
}
// 滚动到匹配位置
const container = el.closest(".report-box");
if (container) {
const TOP_OFFSET = 72;
const containerRect = container.getBoundingClientRect();
const elRect = el.getBoundingClientRect();
const targetTop = elRect.top - containerRect.top + container.scrollTop - TOP_OFFSET;
container.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
} else {
el.scrollIntoView({ behavior: "smooth", block: "center" });
}
};
const prevMatch = () => jumpTo(matchIdx.value - 1);
const nextMatch = () => jumpTo(matchIdx.value + 1);
const getMatchInfo = () => {
const total = matchList.value.length;
const current = total ? matchIdx.value + 1 : 0;
return { current, total };
};
// 外部调用方法
const searchKeyword = async keyword => {
searchKey.value = keyword;
await doSearch();
return matchList.value.length > 0 ? matchList.value[0].pageNum : 0;
};
const goToPage = pageNum => {
const canvasEl = canvasMap[pageNum];
if (!canvasEl) return;
const container = canvasEl.closest(".report-box");
if (container) {
const containerRect = container.getBoundingClientRect();
const canvasRect = canvasEl.getBoundingClientRect();
const targetTop = canvasRect.top - containerRect.top + container.scrollTop;
container.scrollTo({ top: Math.max(0, targetTop), behavior: "smooth" });
} else {
canvasEl.scrollIntoView({ behavior: "smooth", block: "start" });
}
};
// 获取容器元素(用于同步滚动)
const getContainer = () => {
const firstCanvas = canvasMap[1];
return firstCanvas ? firstCanvas.closest(".report-box") : null;
};
watch(
() => props.pdfUrl,
newVal => {
if (newVal) renderPdf(newVal);
},
{ immediate: true }
);
return {
pageCount,
setCanvasRef,
setOverlayRef,
loading,
searchKey,
doSearch,
prevMatch,
nextMatch,
getMatchInfo,
matchList,
matchIdx,
searchKeyword,
clearSearch,
goToPage,
getContainer
};
}
};
</script>
<style scoped>
.pdf-viewer {
position: relative;
width: 100%;
}
.page-wrap {
position: relative;
margin-bottom: 16px;
width: 100%;
}
canvas {
width: 100%;
height: auto;
display: block;
}
.textLayer {
position: absolute;
left: 0;
top: 0;
inset: 0;
overflow: hidden;
pointer-events: none;
z-index: 2;
line-height: 1;
}
.textLayer :deep(span) {
position: absolute;
transform-origin: 0% 0%;
white-space: pre;
line-height: 1;
font-size: calc(var(--font-height, 0px) * var(--scale-factor, 1));
transform: scaleX(var(--scale-x, 1));
color: transparent;
}
.textLayer :deep(.highlight-text) {
background: #ff0;
opacity: 0.6;
padding: 0 1px;
border-radius: 2px;
}
.textLayer :deep(.highlight-rect) {
position: absolute;
background: #ff0;
opacity: 0.6;
border-radius: 2px;
pointer-events: none;
}
.loading {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
font-size: 18px;
color: #333;
}
</style>
......@@ -12,8 +12,13 @@
<div class="home-top-bg"></div>
<div class="home-main-header">
<div class="home-main-header-center">
<SearchContainer style="margin-bottom: 0; height: fit-content" v-if="containerRef" placeholder="搜索投融资限制政策"
:containerRef="containerRef" areaName="" />
<SearchContainer
style="margin-bottom: 0; height: fit-content"
v-if="containerRef"
placeholder="搜索投融资限制政策"
:containerRef="containerRef"
areaName=""
/>
<!-- <el-input
v-model="searchKey"
style="width: 100%; height: 48px"
......@@ -76,12 +81,23 @@
<ClickableCard text="资源库" @click="scrollToTop('position4')" target="_blank" />
</div> -->
<div class="home-main-header-footer-info">
<div class="card" v-for="(item, index) in infoList" :key="index">
<!-- <div class="card" v-for="(item, index) in infoList" :key="index">
<div class="icon" :style="{ background: item.color }"></div>
<div class="title">{{ item.title }}</div>
<div class="content">{{ item.des }}</div>
<div class="num" :style="{ color: item.color }">{{ item.num + "项" }}</div>
</div>
</div> -->
<InfoCard
v-for="(item, index) in infoList"
:key="item.id"
:title="item.nameZh"
:subtitle="item.nameAbbr"
:description="item.description"
:quantity="item.postCount"
unit="个"
:color="infoListColor[index]"
@click="handleToEntityListNoId(item)"
/>
</div>
</div>
......@@ -110,9 +126,17 @@
</div>
<div class="box1-top-content-item">
<span class="box1-top-content-item-title">· 涉及领域:</span>
<div class="box1-top-content-item-tags" v-for="item in ['航空航天', '人工智能', '集成电路']" :key="item">
<el-tag :type="item === '航空航天' ? 'primary' : item === '人工智能' ? 'danger' : 'info'
">{{ item }}</el-tag>
<div
class="box1-top-content-item-tags"
v-for="item in ['航空航天', '人工智能', '集成电路']"
:key="item"
>
<el-tag
:type="
item === '航空航天' ? 'primary' : item === '人工智能' ? 'danger' : 'info'
"
>{{ item }}</el-tag
>
</div>
</div>
</div>
......@@ -170,8 +194,14 @@
</template>
</custom-container> -->
<RiskSignal :list="warningList" @item-click="handleToRiskSignalDetail" @more-click="handleToMoreRiskSignal"
riskLevel="status" postDate="time" name="title" />
<RiskSignal
:list="warningList"
@item-click="handleToRiskSignalDetail"
@more-click="handleToMoreRiskSignal"
riskLevel="status"
postDate="time"
name="title"
/>
</el-col>
</el-row>
<el-row :gutter="20" style="width: 1600px; margin: 0 auto">
......@@ -195,11 +225,22 @@
<div class="center-center">
<div class="center-center-news">
<NewsList :newsList="customNewsData" @item-click="handleNewsInfoClick" @more-click="handleToMoreNews"
from="from" content="description" title="title" img="image" />
<NewsList
:newsList="customNewsData"
@item-click="handleNewsInfoClick"
@more-click="handleToMoreNews"
from="from"
content="description"
title="title"
img="image"
/>
</div>
<MessageBubble :messageList="messageList" @person-click="handlePerClick" imageUrl="avatar"
@more-click="handleToSocialDetail" />
<MessageBubble
:messageList="messageList"
@person-click="handlePerClick"
imageUrl="avatar"
@more-click="handleToSocialDetail"
/>
<!-- <div class="boxs4">
<custom-container title="社交媒体" :titleIcon="dialogIcon" height="450px">
<template #default>
......@@ -239,18 +280,27 @@
<template #default="scope">
<div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress :percentage="scope.row.percent * 100" :show-text="false"
:status="getStatus(scope.row.percent)" />
<el-progress
:percentage="scope.row.percent * 100"
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="重点领域" width="180">
<template #default="scope">
<div style="display: flex; align-items: center; gap: 5px">
<el-tag v-for="tag in scope.row.tags" :key="tag" :type="tag === '通信网络'
? 'primary'
: TAGTYPE[Math.floor(Math.random() * 5)]
">{{ tag }}</el-tag>
<el-tag
v-for="tag in scope.row.tags"
:key="tag"
:type="
tag === '通信网络'
? 'primary'
: TAGTYPE[Math.floor(Math.random() * 5)]
"
>{{ tag }}</el-tag
>
</div>
</template>
</el-table-column>
......@@ -264,18 +314,27 @@
<template #default="scope">
<div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress :percentage="scope.row.percent * 100" :show-text="false"
:status="getStatus(scope.row.percent)" />
<el-progress
:percentage="scope.row.percent * 100"
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="重点领域" width="180">
<template #default="scope">
<div style="display: flex; align-items: center; gap: 5px">
<el-tag v-for="tag in scope.row.tags" :key="tag" :type="tag === '通信网络'
? 'primary'
: TAGTYPE[Math.floor(Math.random() * 5)]
">{{ tag }}</el-tag>
<el-tag
v-for="tag in scope.row.tags"
:key="tag"
:type="
tag === '通信网络'
? 'primary'
: TAGTYPE[Math.floor(Math.random() * 5)]
"
>{{ tag }}</el-tag
>
</div>
</template>
</el-table-column>
......@@ -289,18 +348,27 @@
<template #default="scope">
<div style="display: flex; align-items: center">
<span style="margin-right: 10px; width: 40px">{{ scope.row.num }}</span>
<el-progress :percentage="scope.row.percent * 100" :show-text="false"
:status="getStatus(scope.row.percent)" />
<el-progress
:percentage="scope.row.percent * 100"
:show-text="false"
:status="getStatus(scope.row.percent)"
/>
</div>
</template>
</el-table-column>
<el-table-column label="重点领域" width="180">
<template #default="scope">
<div style="display: flex; align-items: center; gap: 5px">
<el-tag v-for="tag in scope.row.tags" :key="tag" :type="tag === '通信网络'
? 'primary'
: TAGTYPE[Math.floor(Math.random() * 5)]
">{{ tag }}</el-tag>
<el-tag
v-for="tag in scope.row.tags"
:key="tag"
:type="
tag === '通信网络'
? 'primary'
: TAGTYPE[Math.floor(Math.random() * 5)]
"
>{{ tag }}</el-tag
>
</div>
</template>
</el-table-column>
......@@ -357,7 +425,8 @@
</div>
</div>
<div class="box4-footer">
<el-button type="primary" link :icon="DownRight">查看更多
<el-button type="primary" link :icon="DownRight"
>查看更多
<el-icon>
<DArrowRight />
</el-icon>
......@@ -374,8 +443,15 @@
</template>
<template #default>
<div class="box5">
<el-table :data="paginatedData" class="sanction-table" stripe empty-text="暂无数据" height="700px"
header-row-class-name="table-header" row-class-name="table-row">
<el-table
:data="paginatedData"
class="sanction-table"
stripe
empty-text="暂无数据"
height="700px"
header-row-class-name="table-header"
row-class-name="table-row"
>
<!-- <el-table-column prop="index" label="序号" width="80" align="center">
<template #default="scope">
{{ scope.$index + 1 + (currentPage - 1) * pageSize }}
......@@ -384,12 +460,14 @@
<el-table-column prop="name" label="实体名称" min-width="200">
<template #default="scope">
<div style="
<div
style="
font-weight: 700;
font-size: 16px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
">
"
>
{{ scope.row.name }}
</div>
</template>
......@@ -398,8 +476,12 @@
<el-table-column prop="domains" label="涉及领域" min-width="180">
<template #default="scope">
<div class="domain-tags">
<el-tag v-for="tag in scope.row.domains" :key="tag"
:type="tag === '通信网络' ? 'primary' : 'danger'">{{ tag }}</el-tag>
<el-tag
v-for="tag in scope.row.domains"
:key="tag"
:type="tag === '通信网络' ? 'primary' : 'danger'"
>{{ tag }}</el-tag
>
</div>
</template>
</el-table-column>
......@@ -435,7 +517,9 @@
<el-table-column prop="revenue" label="50%规则子企业" width="140" align="right">
<template #default="scope">
<span :class="['revenue-cell', scope.row.revenue === '无营收数据' ? 'no-revenue' : '']">
<span
:class="['revenue-cell', scope.row.revenue === '无营收数据' ? 'no-revenue' : '']"
>
{{ scope.row.revenue }}
</span>
</template>
......@@ -446,8 +530,14 @@
<!-- <div class="pagination-info">
第{{ currentPage }}页,共{{ totalPages }}页
</div> -->
<el-pagination v-model:current-page="currentPage" :page-size="pageSize" :total="total"
:pager-count="5" layout="prev, pager, next" background />
<el-pagination
v-model:current-page="currentPage"
:page-size="pageSize"
:total="total"
:pager-count="5"
layout="prev, pager, next"
background
/>
</div>
</div>
</template>
......@@ -494,12 +584,6 @@ import entityIcon from "./assets/images/icon-entity.png";
import newsImg from "@/assets/images/news-img.png";
import headerIcon1 from "./assets/icons/header-icon1.png";
import headerIcon2 from "./assets/icons/header-icon2.png";
import headerIcon3 from "./assets/icons/header-icon3.png";
import headerIcon4 from "./assets/icons/header-icon4.png";
import headerIcon5 from "./assets/icons/header-icon5.png";
import getMultiLineChart from "./utils/multiLineChart";
import bill1 from "./assets/images/bill1.png";
......@@ -1218,7 +1302,7 @@ const handleGetHylyList = async () => {
hylymc: "全部分类"
};
categoryList.value = [obj, ...categoryList.value];
} catch (error) { }
} catch (error) {}
};
const chart1Data = ref({
......@@ -1264,7 +1348,7 @@ const handleGetBillsByType = async () => {
img: bill1
};
});
} catch (error) { }
} catch (error) {}
};
// 查看社交媒体详情
const handleToSocialDetail = item => {
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论