提交 c8dfe65c authored 作者: 闫鹏's avatar 闫鹏

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

Yp dev 查看合并请求 !341
流水线 #469 已通过 于阶段
in 6 分 4 秒
......@@ -189,7 +189,8 @@ export function getDeepMiningSelect(data) {
export function getDeepMiningIndustry(params) {
return request({
method: "GET",
url: `/api/chain/getChainInfo`,
// url: `/api/chain/getChainInfo`,
url: `/api/chain/getAllChain`,
params
});
}
......@@ -204,7 +205,7 @@ export function getDeepMiningIndustry(params) {
export function getDeepMiningIndustryFishbone(params) {
return request({
method: "GET",
url: `/api/chain/getChainFishbone`,
url: `/api/chain/getChainNodes`,
params
});
}
......
<template>
<RelationGraph
style="width: 100%; height: 100%"
ref="graphRef"
:options="graphOptionsH"
:on-node-click="onNodeClick"
:on-line-click="onLineClick"
>
<template #node="{ node }">
<div
class="custom-node"
:style="{
backgroundColor: node.color || 'var(--color-primary-50)'
}"
>
<!-- 在这里自由控制文字样式 -->
<span
:style="{
color: node.fontColor || '#ffffff',
fontSize: node.customFontSize || '24px', // 可以从数据中读取
fontWeight: 'normal',
textAlign: 'center',
wordBreak: 'break-word',
padding: '0 8px'
}"
>
{{ node.text }}
</span>
</div>
</template>
</RelationGraph>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import RelationGraph from "relation-graph-vue3";
// import RelationGraph, { RGJsonData, RGOptions, RGNode, RGLine, RGLink, RGUserEvent, RelationGraphComponent } from 'relation-graph-vue3';
const graphRef = ref(null);
const graphOptionsH = {
layout: {
layoutName: "tree",
// min_per_width: 40,
// max_per_width: 70,
min_per_height: "auto"
},
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
data: "M2,2 L10,6 L2,10 L6,6 L2,2"
},
defaultNodeShape: 1,
defaultNodeWidth: 35,
defaultLineShape: 4,
defaultJunctionPoint: "tb",
defaultNodeBorderWidth: 2,
defaultNodeBorderColor: "var(--color-primary-100)",
defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: "var(--text-primary-90-color)" // 默认文字颜色:深灰色
// defaultNodeHeight: 'auto'
};
const graphOptionsV = {
layouts: [
{
// label: "中心",
layoutName: "tree",
// layoutClassName: "seeks-layout-center",
// defaultJunctionPoint: "border",
// defaultNodeShape: 0,
// defaultLineShape: 1,
from: "left",
// max_per_width: "300",
// min_per_height: 35,
max_per_width: "auto"
}
],
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
data: "M2,2 L10,6 L2,10 L6,6 L2,2"
},
defaultNodeShape: 1,
defaultNodeWidth: 150,
// defaultNodeHeight: 35,
defaultLineShape: 4,
defaultJunctionPoint: "lr",
defaultNodeBorderWidth: 2,
defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: "var(--bg-white-100)" // 默认文字颜色:深灰色
};
const props = defineProps({
isVerticalChart: {
type: Boolean,
default: false
},
graphData: {
type: Object,
default: {
rootId: "a",
nodes: [
{ id: "a", text: "a" },
{ id: "b", text: "b" },
{ id: "b1", text: "b1" },
{ id: "b1-1", text: "b1-1" },
{ id: "b1-2", text: "b1-2" },
{ id: "b1-3", text: "b1-3" },
{ id: "b1-4", text: "b1-4" },
{ id: "b1-5", text: "b1-5" },
{ id: "b1-6", text: "b1-6" },
{ id: "b2", text: "b2" },
{ id: "b2-1", text: "b2-1" },
{ id: "b2-2", text: "b2-2" },
{ id: "b2-3", text: "b2-3" },
{ id: "b2-4", text: "b2-4" },
{ id: "b3", text: "b3" },
{ id: "b3-1", text: "b3-1" },
{ id: "b3-2", text: "b3-2" },
{ id: "b3-3", text: "b3-3" },
{ id: "b3-4", text: "b3-4" },
{ id: "b3-5", text: "b3-5" },
{ id: "b3-6", text: "b3-6" },
{ id: "b3-7", text: "b3-7" },
{ id: "b4", text: "b4" },
{ id: "b4-1", text: "b4-1" },
{ id: "b4-2", text: "b4-2" },
{ id: "b4-3", text: "b4-3" },
{ id: "b4-4", text: "b4-4" },
{ id: "b4-5", text: "b4-5" },
{ id: "b4-6", text: "b4-6" },
{ id: "b4-7", text: "b4-7" },
{ id: "b4-8", text: "b4-8" },
{ id: "b4-9", text: "b4-9" },
{ id: "b5", text: "b5" },
{ id: "b5-1", text: "b5-1" },
{ id: "b5-2", text: "b5-2" },
{ id: "b5-3", text: "b5-3" },
{ id: "b5-4", text: "b5-4" },
{ id: "b6", text: "b6" },
{ id: "b6-1", text: "b6-1" },
{ id: "b6-2", text: "b6-2" },
{ id: "b6-3", text: "b6-3" },
{ id: "b6-4", text: "b6-4" },
{ id: "b6-5", text: "b6-5" },
{ id: "c", text: "c" },
{ id: "c1", text: "c1" },
{ id: "c1-1", text: "c1-1" },
{ id: "c1-2", text: "c1-2" },
{ id: "c1-3", text: "c1-3" },
{ id: "c1-4", text: "c1-4" },
{ id: "c1-5", text: "c1-5" },
{ id: "c1-6", text: "c1-6" },
{ id: "c1-7", text: "c1-7" },
{ id: "c2", text: "c2" },
{ id: "c2-1", text: "c2-1" },
{ id: "c2-2", text: "c2-2" },
{ id: "c3", text: "c3" },
{ id: "c3-1", text: "c3-1" },
{ id: "c3-2", text: "c3-2" },
{ id: "c3-3", text: "c3-3" },
{ id: "d", text: "d" },
{ id: "d1", text: "d1" },
{ id: "d1-1", text: "d1-1" },
{ id: "d1-2", text: "d1-2" },
{ id: "d1-3", text: "d1-3" },
{ id: "d1-4", text: "d1-4" },
{ id: "d1-5", text: "d1-5" },
{ id: "d1-6", text: "d1-6" },
{ id: "d1-7", text: "d1-7" },
{ id: "d1-8", text: "d1-8" },
{ id: "d2", text: "d2" },
{ id: "d2-1", text: "d2-1" },
{ id: "d2-2", text: "d2-2" },
{ id: "d3", text: "d3" },
{ id: "d3-1", text: "d3-1" },
{ id: "d3-2", text: "d3-2" },
{ id: "d3-3", text: "d3-3" },
{ id: "d3-4", text: "d3-4" },
{ id: "d3-5", text: "d3-5" },
{ id: "d4", text: "d4" },
{ id: "d4-1", text: "d4-1" },
{ id: "d4-2", text: "d4-2" },
{ id: "d4-3", text: "d4-3" },
{ id: "d4-4", text: "d4-4" },
{ id: "d4-5", text: "d4-5" },
{ id: "d4-6", text: "d4-6" },
{ id: "e", text: "e" },
{ id: "e1", text: "e1" },
{ id: "e1-1", text: "e1-1" },
{ id: "e1-2", text: "e1-2" },
{ id: "e1-3", text: "e1-3" },
{ id: "e1-4", text: "e1-4" },
{ id: "e1-5", text: "e1-5" },
{ id: "e1-6", text: "e1-6" },
{ id: "e2", text: "e2" },
{ id: "e2-1", text: "e2-1" },
{ id: "e2-2", text: "e2-2" },
{ id: "e2-3", text: "e2-3" },
{ id: "e2-4", text: "e2-4" },
{ id: "e2-5", text: "e2-5" },
{ id: "e2-6", text: "e2-6" },
{ id: "e2-7", text: "e2-7" },
{ id: "e2-8", text: "e2-8" },
{ id: "e2-9", text: "e2-9" }
],
lines: [
{ from: "a", to: "b" },
{ from: "b", to: "b1" },
{ from: "b1", to: "b1-1" },
{ from: "b1", to: "b1-2" },
{ from: "b1", to: "b1-3" },
{ from: "b1", to: "b1-4" },
{ from: "b1", to: "b1-5" },
{ from: "b1", to: "b1-6" },
{ from: "b", to: "b2" },
{ from: "b2", to: "b2-1" },
{ from: "b2", to: "b2-2" },
{ from: "b2", to: "b2-3" },
{ from: "b2", to: "b2-4" },
{ from: "b", to: "b3" },
{ from: "b3", to: "b3-1" },
{ from: "b3", to: "b3-2" },
{ from: "b3", to: "b3-3" },
{ from: "b3", to: "b3-4" },
{ from: "b3", to: "b3-5" },
{ from: "b3", to: "b3-6" },
{ from: "b3", to: "b3-7" },
{ from: "b", to: "b4" },
{ from: "b4", to: "b4-1" },
{ from: "b4", to: "b4-2" },
{ from: "b4", to: "b4-3" },
{ from: "b4", to: "b4-4" },
{ from: "b4", to: "b4-5" },
{ from: "b4", to: "b4-6" },
{ from: "b4", to: "b4-7" },
{ from: "b4", to: "b4-8" },
{ from: "b4", to: "b4-9" },
{ from: "b", to: "b5" },
{ from: "b5", to: "b5-1" },
{ from: "b5", to: "b5-2" },
{ from: "b5", to: "b5-3" },
{ from: "b5", to: "b5-4" },
{ from: "b", to: "b6" },
{ from: "b6", to: "b6-1" },
{ from: "b6", to: "b6-2" },
{ from: "b6", to: "b6-3" },
{ from: "b6", to: "b6-4" },
{ from: "b6", to: "b6-5" },
{ from: "a", to: "c" },
{ from: "c", to: "c1" },
{ from: "c1", to: "c1-1" },
{ from: "c1", to: "c1-2" },
{ from: "c1", to: "c1-3" },
{ from: "c1", to: "c1-4" },
{ from: "c1", to: "c1-5" },
{ from: "c1", to: "c1-6" },
{ from: "c1", to: "c1-7" },
{ from: "c", to: "c2" },
{ from: "c2", to: "c2-1" },
{ from: "c2", to: "c2-2" },
{ from: "c", to: "c3" },
{ from: "c3", to: "c3-1" },
{ from: "c3", to: "c3-2" },
{ from: "c3", to: "c3-3" },
{ from: "a", to: "d" },
{ from: "d", to: "d1" },
{ from: "d1", to: "d1-1" },
{ from: "d1", to: "d1-2" },
{ from: "d1", to: "d1-3" },
{ from: "d1", to: "d1-4" },
{ from: "d1", to: "d1-5" },
{ from: "d1", to: "d1-6" },
{ from: "d1", to: "d1-7" },
{ from: "d1", to: "d1-8" },
{ from: "d", to: "d2" },
{ from: "d2", to: "d2-1" },
{ from: "d2", to: "d2-2" },
{ from: "d", to: "d3" },
{ from: "d3", to: "d3-1" },
{ from: "d3", to: "d3-2" },
{ from: "d3", to: "d3-3" },
{ from: "d3", to: "d3-4" },
{ from: "d3", to: "d3-5" },
{ from: "d", to: "d4" },
{ from: "d4", to: "d4-1" },
{ from: "d4", to: "d4-2" },
{ from: "d4", to: "d4-3" },
{ from: "d4", to: "d4-4" },
{ from: "d4", to: "d4-5" },
{ from: "d4", to: "d4-6" },
{ from: "a", to: "e" },
{ from: "e", to: "e1" },
{ from: "e1", to: "e1-1" },
{ from: "e1", to: "e1-2" },
{ from: "e1", to: "e1-3" },
{ from: "e1", to: "e1-4" },
{ from: "e1", to: "e1-5" },
{ from: "e1", to: "e1-6" },
{ from: "e", to: "e2" },
{ from: "e2", to: "e2-1" },
{ from: "e2", to: "e2-2" },
{ from: "e2", to: "e2-3" },
{ from: "e2", to: "e2-4" },
{ from: "e2", to: "e2-5" },
{ from: "e2", to: "e2-6" },
{ from: "e2", to: "e2-7" },
{ from: "e2", to: "e2-8" },
{ from: "e2", to: "e2-9" }
]
}
}
});
const emit = defineEmits(["nodeClick", "lineClick"]);
const showHTree = async () => {
const __graph_json_data = props.graphData;
const graphInstance = graphRef.value?.getInstance();
if (graphInstance) {
await graphInstance.setOptions(graphOptionsH);
await graphInstance.setJsonData(__graph_json_data);
await graphInstance.moveToCenter();
// await graphInstance.zoomToFit();
await graphInstance.setZoom(35);
}
};
const showVTree = async () => {
const __graph_json_data = props.graphData;
const graphInstance = graphRef.value?.getInstance();
if (graphInstance) {
await graphInstance.setOptions(graphOptionsV);
await graphInstance.setJsonData(__graph_json_data);
await graphInstance.moveToCenter();
await graphInstance.zoomToFit();
}
};
const onNodeClick = (nodeObject, $event) => {
console.log("onNodeClick:", nodeObject);
emit("nodeClick", nodeObject);
};
const onLineClick = (lineObject, linkObject, $event) => {
console.log("onLineClick:", lineObject);
emit("lineClick", lineObject);
};
watch(
() => props.isVerticalChart,
val => {
if (val) {
showVTree();
} else {
showHTree();
}
}
);
onMounted(() => {
if (props.isVerticalChart) {
showVTree();
} else {
showHTree();
}
});
</script>
<style lang="scss" scoped>
:deep(.relation-graph) {
.c-node-text {
padding: 0px;
place-items: center;
justify-content: center;
}
}
.c-my-panel {
width: 350px;
text-align: center;
position: absolute;
left: 10px;
top: 10px;
border-radius: 10px;
z-index: 800;
padding: 10px;
background-color: rgba(239, 239, 239, 0.86);
border: #eeeeee solid 1px;
overflow: hidden;
.c-option-name {
color: #666666;
font-size: 14px;
line-height: 40px;
}
}
.custom-node {
// width: 100%;
// height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
<template>
<RelationGraph style="width: 100%; height: 100%" ref="graphRef" :options="graphOptionsH" :on-node-click="onNodeClick"
:on-line-click="onLineClick">
<template #node="{ node }">
<div class="custom-node" :style="{
backgroundColor: node.color || 'var(--color-primary-50)',
}">
<!-- 在这里自由控制文字样式 -->
<span :style="{
color: node.fontColor || '#ffffff',
fontSize: node.customFontSize || '24px', // 可以从数据中读取
fontWeight: 'normal',
textAlign: 'center',
wordBreak: 'break-word',
padding: '0 8px'
}">
{{ node.text }}
</span>
</div>
</template>
</RelationGraph>
<RelationGraph
style="width: 100%; height: 100%"
ref="graphRef"
:options="currentGraphOptions"
:on-node-click="onNodeClick"
:on-line-click="onLineClick"
>
<template #node="{ node }">
<div
class="custom-node"
:style="{
backgroundColor: node.color || 'var(--color-primary-50)'
}"
>
<span
:style="{
color: node.fontColor || '#ffffff',
fontSize: node.customFontSize || '24px',
fontWeight: 'normal',
textAlign: 'center',
wordBreak: 'break-word',
padding: '0 8px'
}"
>
{{ node.text }}
</span>
</div>
</template>
</RelationGraph>
</template>
<script setup>
import { ref, onMounted, watch } from "vue";
import { ref, onMounted, watch, computed } from "vue";
import RelationGraph from "relation-graph-vue3";
// import RelationGraph, { RGJsonData, RGOptions, RGNode, RGLine, RGLink, RGUserEvent, RelationGraphComponent } from 'relation-graph-vue3';
const graphRef = ref(null);
const graphOptionsH = {
layout: {
layoutName: "tree",
// min_per_width: 40,
// max_per_width: 70,
min_per_height: 'auto',
},
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
data: "M2,2 L10,6 L2,10 L6,6 L2,2"
},
defaultNodeShape: 1,
defaultNodeWidth: 35,
defaultLineShape: 4,
defaultJunctionPoint: "tb",
defaultNodeBorderWidth: 2,
defaultNodeBorderColor: 'var(--color-primary-100)',
defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: 'var(--text-primary-90-color)', // 默认文字颜色:深灰色
// defaultNodeHeight: 'auto'
// 定义 Props
const props = defineProps({
isVerticalChart: {
type: Boolean,
default: false
},
// 【新增】是否反向布局(根节点在右侧)
isReversed: {
type: Boolean,
default: false
},
graphData: {
type: Object,
default: () => ({ rootId: "a", nodes: [], lines: [] })
}
});
const emit = defineEmits(["nodeClick", "lineClick"]);
// 基础水平配置
const baseGraphOptionsH = {
layout: {
layoutName: "tree",
min_per_height: "auto"
},
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
data: "M2,2 L10,6 L2,10 L6,6 L2,2"
},
defaultNodeShape: 1,
defaultNodeWidth: 35,
defaultLineShape: 4,
defaultJunctionPoint: "tb",
defaultNodeBorderWidth: 2,
defaultNodeBorderColor: "var(--color-primary-100)",
defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: "var(--text-primary-90-color)"
};
const graphOptionsV = {
layouts: [
{
// label: "中心",
layoutName: "tree",
// layoutClassName: "seeks-layout-center",
// defaultJunctionPoint: "border",
// defaultNodeShape: 0,
// defaultLineShape: 1,
from: "left",
// max_per_width: "300",
// min_per_height: 35,
max_per_width: 'auto',
}
],
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
data: "M2,2 L10,6 L2,10 L6,6 L2,2"
},
defaultNodeShape: 1,
defaultNodeWidth: 150,
// defaultNodeHeight: 35,
defaultLineShape: 4,
defaultJunctionPoint: "lr",
defaultNodeBorderWidth: 2,
defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: 'var(--bg-white-100)', // 默认文字颜色:深灰色
// 基础垂直配置
const baseGraphOptionsV = {
layouts: [
{
layoutName: "tree",
from: "left", // 默认从左向右
max_per_width: "auto"
}
],
defaultLineMarker: {
markerWidth: 12,
markerHeight: 12,
refX: 6,
refY: 6,
data: "M2,2 L10,6 L2,10 L6,6 L2,2"
},
defaultNodeShape: 1,
defaultNodeWidth: 150,
defaultLineShape: 4,
defaultJunctionPoint: "lr",
defaultNodeBorderWidth: 2,
defaultLineColor: "var(--color-primary-50)",
defaultNodeColor: "var(--color-primary-50)",
defaultNodeFontColor: "var(--bg-white-100)"
};
const props = defineProps({
isVerticalChart: {
type: Boolean,
default: false
},
graphData: {
type: Object,
default: {
rootId: "a",
nodes: [
{ id: "a", text: "a" },
{ id: "b", text: "b" },
{ id: "b1", text: "b1" },
{ id: "b1-1", text: "b1-1" },
{ id: "b1-2", text: "b1-2" },
{ id: "b1-3", text: "b1-3" },
{ id: "b1-4", text: "b1-4" },
{ id: "b1-5", text: "b1-5" },
{ id: "b1-6", text: "b1-6" },
{ id: "b2", text: "b2" },
{ id: "b2-1", text: "b2-1" },
{ id: "b2-2", text: "b2-2" },
{ id: "b2-3", text: "b2-3" },
{ id: "b2-4", text: "b2-4" },
{ id: "b3", text: "b3" },
{ id: "b3-1", text: "b3-1" },
{ id: "b3-2", text: "b3-2" },
{ id: "b3-3", text: "b3-3" },
{ id: "b3-4", text: "b3-4" },
{ id: "b3-5", text: "b3-5" },
{ id: "b3-6", text: "b3-6" },
{ id: "b3-7", text: "b3-7" },
{ id: "b4", text: "b4" },
{ id: "b4-1", text: "b4-1" },
{ id: "b4-2", text: "b4-2" },
{ id: "b4-3", text: "b4-3" },
{ id: "b4-4", text: "b4-4" },
{ id: "b4-5", text: "b4-5" },
{ id: "b4-6", text: "b4-6" },
{ id: "b4-7", text: "b4-7" },
{ id: "b4-8", text: "b4-8" },
{ id: "b4-9", text: "b4-9" },
{ id: "b5", text: "b5" },
{ id: "b5-1", text: "b5-1" },
{ id: "b5-2", text: "b5-2" },
{ id: "b5-3", text: "b5-3" },
{ id: "b5-4", text: "b5-4" },
{ id: "b6", text: "b6" },
{ id: "b6-1", text: "b6-1" },
{ id: "b6-2", text: "b6-2" },
{ id: "b6-3", text: "b6-3" },
{ id: "b6-4", text: "b6-4" },
{ id: "b6-5", text: "b6-5" },
{ id: "c", text: "c" },
{ id: "c1", text: "c1" },
{ id: "c1-1", text: "c1-1" },
{ id: "c1-2", text: "c1-2" },
{ id: "c1-3", text: "c1-3" },
{ id: "c1-4", text: "c1-4" },
{ id: "c1-5", text: "c1-5" },
{ id: "c1-6", text: "c1-6" },
{ id: "c1-7", text: "c1-7" },
{ id: "c2", text: "c2" },
{ id: "c2-1", text: "c2-1" },
{ id: "c2-2", text: "c2-2" },
{ id: "c3", text: "c3" },
{ id: "c3-1", text: "c3-1" },
{ id: "c3-2", text: "c3-2" },
{ id: "c3-3", text: "c3-3" },
{ id: "d", text: "d" },
{ id: "d1", text: "d1" },
{ id: "d1-1", text: "d1-1" },
{ id: "d1-2", text: "d1-2" },
{ id: "d1-3", text: "d1-3" },
{ id: "d1-4", text: "d1-4" },
{ id: "d1-5", text: "d1-5" },
{ id: "d1-6", text: "d1-6" },
{ id: "d1-7", text: "d1-7" },
{ id: "d1-8", text: "d1-8" },
{ id: "d2", text: "d2" },
{ id: "d2-1", text: "d2-1" },
{ id: "d2-2", text: "d2-2" },
{ id: "d3", text: "d3" },
{ id: "d3-1", text: "d3-1" },
{ id: "d3-2", text: "d3-2" },
{ id: "d3-3", text: "d3-3" },
{ id: "d3-4", text: "d3-4" },
{ id: "d3-5", text: "d3-5" },
{ id: "d4", text: "d4" },
{ id: "d4-1", text: "d4-1" },
{ id: "d4-2", text: "d4-2" },
{ id: "d4-3", text: "d4-3" },
{ id: "d4-4", text: "d4-4" },
{ id: "d4-5", text: "d4-5" },
{ id: "d4-6", text: "d4-6" },
{ id: "e", text: "e" },
{ id: "e1", text: "e1" },
{ id: "e1-1", text: "e1-1" },
{ id: "e1-2", text: "e1-2" },
{ id: "e1-3", text: "e1-3" },
{ id: "e1-4", text: "e1-4" },
{ id: "e1-5", text: "e1-5" },
{ id: "e1-6", text: "e1-6" },
{ id: "e2", text: "e2" },
{ id: "e2-1", text: "e2-1" },
{ id: "e2-2", text: "e2-2" },
{ id: "e2-3", text: "e2-3" },
{ id: "e2-4", text: "e2-4" },
{ id: "e2-5", text: "e2-5" },
{ id: "e2-6", text: "e2-6" },
{ id: "e2-7", text: "e2-7" },
{ id: "e2-8", text: "e2-8" },
{ id: "e2-9", text: "e2-9" }
],
lines: [
{ from: "a", to: "b" },
{ from: "b", to: "b1" },
{ from: "b1", to: "b1-1" },
{ from: "b1", to: "b1-2" },
{ from: "b1", to: "b1-3" },
{ from: "b1", to: "b1-4" },
{ from: "b1", to: "b1-5" },
{ from: "b1", to: "b1-6" },
{ from: "b", to: "b2" },
{ from: "b2", to: "b2-1" },
{ from: "b2", to: "b2-2" },
{ from: "b2", to: "b2-3" },
{ from: "b2", to: "b2-4" },
{ from: "b", to: "b3" },
{ from: "b3", to: "b3-1" },
{ from: "b3", to: "b3-2" },
{ from: "b3", to: "b3-3" },
{ from: "b3", to: "b3-4" },
{ from: "b3", to: "b3-5" },
{ from: "b3", to: "b3-6" },
{ from: "b3", to: "b3-7" },
{ from: "b", to: "b4" },
{ from: "b4", to: "b4-1" },
{ from: "b4", to: "b4-2" },
{ from: "b4", to: "b4-3" },
{ from: "b4", to: "b4-4" },
{ from: "b4", to: "b4-5" },
{ from: "b4", to: "b4-6" },
{ from: "b4", to: "b4-7" },
{ from: "b4", to: "b4-8" },
{ from: "b4", to: "b4-9" },
{ from: "b", to: "b5" },
{ from: "b5", to: "b5-1" },
{ from: "b5", to: "b5-2" },
{ from: "b5", to: "b5-3" },
{ from: "b5", to: "b5-4" },
{ from: "b", to: "b6" },
{ from: "b6", to: "b6-1" },
{ from: "b6", to: "b6-2" },
{ from: "b6", to: "b6-3" },
{ from: "b6", to: "b6-4" },
{ from: "b6", to: "b6-5" },
{ from: "a", to: "c" },
{ from: "c", to: "c1" },
{ from: "c1", to: "c1-1" },
{ from: "c1", to: "c1-2" },
{ from: "c1", to: "c1-3" },
{ from: "c1", to: "c1-4" },
{ from: "c1", to: "c1-5" },
{ from: "c1", to: "c1-6" },
{ from: "c1", to: "c1-7" },
{ from: "c", to: "c2" },
{ from: "c2", to: "c2-1" },
{ from: "c2", to: "c2-2" },
{ from: "c", to: "c3" },
{ from: "c3", to: "c3-1" },
{ from: "c3", to: "c3-2" },
{ from: "c3", to: "c3-3" },
{ from: "a", to: "d" },
{ from: "d", to: "d1" },
{ from: "d1", to: "d1-1" },
{ from: "d1", to: "d1-2" },
{ from: "d1", to: "d1-3" },
{ from: "d1", to: "d1-4" },
{ from: "d1", to: "d1-5" },
{ from: "d1", to: "d1-6" },
{ from: "d1", to: "d1-7" },
{ from: "d1", to: "d1-8" },
{ from: "d", to: "d2" },
{ from: "d2", to: "d2-1" },
{ from: "d2", to: "d2-2" },
{ from: "d", to: "d3" },
{ from: "d3", to: "d3-1" },
{ from: "d3", to: "d3-2" },
{ from: "d3", to: "d3-3" },
{ from: "d3", to: "d3-4" },
{ from: "d3", to: "d3-5" },
{ from: "d", to: "d4" },
{ from: "d4", to: "d4-1" },
{ from: "d4", to: "d4-2" },
{ from: "d4", to: "d4-3" },
{ from: "d4", to: "d4-4" },
{ from: "d4", to: "d4-5" },
{ from: "d4", to: "d4-6" },
{ from: "a", to: "e" },
{ from: "e", to: "e1" },
{ from: "e1", to: "e1-1" },
{ from: "e1", to: "e1-2" },
{ from: "e1", to: "e1-3" },
{ from: "e1", to: "e1-4" },
{ from: "e1", to: "e1-5" },
{ from: "e1", to: "e1-6" },
{ from: "e", to: "e2" },
{ from: "e2", to: "e2-1" },
{ from: "e2", to: "e2-2" },
{ from: "e2", to: "e2-3" },
{ from: "e2", to: "e2-4" },
{ from: "e2", to: "e2-5" },
{ from: "e2", to: "e2-6" },
{ from: "e2", to: "e2-7" },
{ from: "e2", to: "e2-8" },
{ from: "e2", to: "e2-9" }
]
}
}
// 【核心修改】根据 isReversed 动态计算配置
const currentGraphOptions = computed(() => {
if (props.isVerticalChart) {
// 深拷贝配置,避免污染原始对象
const options = JSON.parse(JSON.stringify(baseGraphOptionsV));
// 如果反向,则从右向左布局
if (props.isReversed) {
options.layouts[0].from = "right";
} else {
options.layouts[0].from = "left";
}
return options;
} else {
// 水平布局通常不受 from 影响太大,或者可以通过 layoutName: 'force' 等调整
// 这里主要处理垂直树状图的反向
return baseGraphOptionsH;
}
});
const emit = defineEmits(['nodeClick', 'lineClick'])
const showHTree = async () => {
const __graph_json_data = props.graphData;
const graphInstance = graphRef.value?.getInstance();
if (graphInstance) {
await graphInstance.setOptions(graphOptionsH);
await graphInstance.setJsonData(__graph_json_data);
await graphInstance.moveToCenter();
// await graphInstance.zoomToFit();
await graphInstance.setZoom(35);
}
const __graph_json_data = props.graphData;
const graphInstance = graphRef.value?.getInstance();
if (graphInstance) {
await graphInstance.setOptions(currentGraphOptions.value);
await graphInstance.setJsonData(__graph_json_data);
await graphInstance.moveToCenter();
await graphInstance.setZoom(35);
}
};
const showVTree = async () => {
const __graph_json_data = props.graphData;
const graphInstance = graphRef.value?.getInstance();
if (graphInstance) {
await graphInstance.setOptions(graphOptionsV);
await graphInstance.setJsonData(__graph_json_data);
await graphInstance.moveToCenter();
await graphInstance.zoomToFit();
}
const __graph_json_data = props.graphData;
const graphInstance = graphRef.value?.getInstance();
if (graphInstance) {
// 使用计算后的配置
await graphInstance.setOptions(currentGraphOptions.value);
await graphInstance.setJsonData(__graph_json_data);
await graphInstance.moveToCenter();
await graphInstance.zoomToFit();
}
};
const onNodeClick = (nodeObject, $event) => {
console.log("onNodeClick:", nodeObject);
emit('nodeClick', nodeObject)
emit("nodeClick", nodeObject);
};
const onLineClick = (lineObject, linkObject, $event) => {
console.log("onLineClick:", lineObject);
emit('lineClick', lineObject)
emit("lineClick", lineObject);
};
watch(
() => props.isVerticalChart,
val => {
if (val) {
showVTree();
} else {
showHTree();
}
}
() => [props.isVerticalChart, props.isReversed, props.graphData],
() => {
if (props.isVerticalChart) {
showVTree();
} else {
showHTree();
}
},
{ deep: true }
);
onMounted(() => {
if (props.isVerticalChart) {
showVTree();
} else {
showHTree();
}
if (props.isVerticalChart) {
showVTree();
} else {
showHTree();
}
});
</script>
<style lang="scss" scoped>
:deep(.relation-graph) {
.c-node-text {
padding: 0px;
place-items: center;
justify-content: center;
}
}
.c-my-panel {
width: 350px;
text-align: center;
position: absolute;
left: 10px;
top: 10px;
border-radius: 10px;
z-index: 800;
padding: 10px;
background-color: rgba(239, 239, 239, 0.86);
border: #eeeeee solid 1px;
overflow: hidden;
.c-option-name {
color: #666666;
font-size: 14px;
line-height: 40px;
}
.c-node-text {
padding: 0px;
place-items: center;
justify-content: center;
}
}
.custom-node {
// width: 100%;
// height: 100%;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
}
</style>
......@@ -18,7 +18,12 @@ import RelationChart from '@/components/base/RelationChart/index.vue'
</el-radio-group>
</div>
<div class="chart-box">
<RelationChart @line-click="handleClickLine" @node-click="handleClickNode" :is-vertical-chart="isVerticalChart" :graph-data="graphData" />
<RelationChart
@line-click="handleClickLine"
@node-click="handleClickNode"
:is-vertical-chart="isVerticalChart"
:graph-data="graphData"
/>
</div>
</el-col>
</el-row>
......@@ -149,7 +154,15 @@ const graphData = ref({
{ id: "e2-9", text: "e2-9" }
],
lines: [
{ from: "a", to: "b", text: '从属', fontColor: 'var(--color-orange-100)', color: 'orange', textOffset_x: -20, lineWidth: 5},
{
from: "a",
to: "b",
text: "从属",
fontColor: "var(--color-orange-100)",
color: "orange",
textOffset_x: -20,
lineWidth: 5
},
{ from: "b", to: "b1" },
{ from: "b1", to: "b1-1" },
{ from: "b1", to: "b1-2" },
......@@ -254,18 +267,15 @@ const graphData = ref({
]
});
const handleClickNode = (value) => {
console.log('value', value);
alert('我点击了node-'+ value.text)
}
const handleClickLine = (value) => {
console.log('value', value);
alert('我点击了line-'+ value.text)
}
const handleClickNode = value => {
console.log("value", value);
alert("我点击了node-" + value.text);
};
const handleClickLine = value => {
console.log("value", value);
alert("我点击了line-" + value.text);
};
</script>
<style lang="scss" scoped>
......@@ -278,4 +288,4 @@ const handleClickLine = (value) => {
width: 1600px;
height: 500px;
}
</style>
\ No newline at end of file
</style>
......@@ -670,7 +670,9 @@
</el-col>
</template>
<template v-if="activeResourceTab === 'commerce'">
<listPage />
<div class="commerce-wrapper" :style="{ minHeight: '500px' }">
<listPage />
</div>
</template>
</el-row>
</div>
......@@ -1974,7 +1976,7 @@ const handleMediaClick = item => {
position: absolute;
width: 240px;
height: 89px;
top: 30px;
top: 12px;
right: -24px;
display: flex;
flex-direction: column;
......
......@@ -390,6 +390,9 @@ onMounted(async () => {
background-color: #fff;
border-radius: 4px;
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset;
&:hover {
box-shadow: 0 0 0 1px var(--el-input-border-color, var(--el-border-color)) inset !important;
}
}
:deep(.el-input__inner) {
......@@ -462,6 +465,11 @@ onMounted(async () => {
height: 24px;
color: rgb(95, 101, 108);
}
:deep(.el-checkbox__label) {
font-size: 16px;
color: #666666;
font-weight: 400;
}
.custom-date-picker {
width: 100%;
......
<template>
<div class="deep-mining">
<div class="side-nav">
<div
v-for="(item, index) in activeTab"
:key="index"
class="tab-item"
:class="{ active: index === activeIndex }"
@click="activeIndex = index"
>
{{ item }}
<span v-if="index === activeIndex" class="arrow"></span>
</div>
</div>
<div class="main">
<div class="left">
<AnalysisBox title="选择制裁">
<div class="left-main">
<div class="date-picker-box">
<el-date-picker
v-model="dateRange"
type="daterange"
range-separator="--"
start-placeholder="开始日期"
end-placeholder="结束日期"
value-format="YYYY-MM-DD"
style="width: 100%"
:clearable="false"
@change="handleDateChange"
/>
</div>
<div class="list-header">
<div class="count">{{ sanctionList.length }}次制裁</div>
<!-- 暂时隐藏,说这里可能是轮播图的效果 -->
<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 class="list-content" v-loading="loading">
<div
class="list-item"
v-for="item in sanctionList"
:key="item.id"
:class="{ active: currentSanctionId === item.id }"
@click="handleSanctionSelect(item.id)"
>
<!-- <div class="item-left">{{ item.date }}-{{ item.title }}</div> -->
<div class="item-left">{{ item.date }}-实体清单更新</div>
<div class="item-right">{{ item.count }}{{ item.unit }}</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
<div class="right">
<AnalysisBox title="制裁产业链时序图">
<template #header-btn>
<el-select
v-model="selectedIndustryId"
placeholder="请选择"
class="industry-select"
@change="
() => {
getFishboneData();
getCnEntityOnChainData();
}
"
>
<el-option v-for="item in industryList" :key="item.id" :label="item.name" :value="item.id" />
</el-select>
</template>
<div class="right-main">
<div class="right-main-content">
<!-- <div class="hintWrap">
<div class="icon1"></div>
<div class="title">
2025年实体清单制裁范围扩大至芯片制造环节,为中国的芯片制造能力划定“技术天花板”,阻止其向更先进水平发展。制裁范围向上游设备和材料、下游先进封装以及关键工具(如EDA软件)延伸,意图瓦解中国构建自主可控产业链的努力。
</div>
<div class="icon2Wrap">
<div class="icon2"></div>
</div>
</div> -->
<div class="right-main-content-main">
<div class="fishbone-wrapper">
<div class="fishbone-scroll-container" ref="scrollContainerRef">
<div class="fishbone" ref="fishboneRef" v-if="fishboneDataList.length > 0">
<div class="main-line" :style="{ width: fishboneDataList.length * 200 + 300 + 'px' }">
<!-- 主轴上的标签 -->
<div
class="main-line-text"
v-for="(item, index) in mainLineLabels"
:key="'label-' + index"
:class="{
'blue-theme': index < 2,
'green-theme': index >= 2 && index < 4,
'purple-theme': index >= 4
}"
:style="{ left: index * 200 + 220 + 'px' }"
>
{{ item }}
</div>
</div>
<!-- 奇数索引的数据组放在上方 -->
<div
v-for="(causeGroup, groupIndex) in getOddGroups(fishboneDataList)"
:key="'top-' + groupIndex"
:class="getTopBoneClass(groupIndex)"
:style="{ left: groupIndex * 400 + 420 + 'px' }"
>
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
:key="'left-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
:key="'right-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
</div>
</div>
</div>
<!-- 偶数索引的数据组放在下方 -->
<div
v-for="(causeGroup, groupIndex) in getEvenGroups(fishboneDataList)"
:key="'bottom-' + groupIndex"
:class="getBottomBoneClass(groupIndex)"
:style="{ left: groupIndex * 400 + 220 + 'px' }"
>
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
:key="'left-bottom-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
:key="'right-bottom-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
</div>
</div>
</div>
</div>
<div
v-else
style="
display: flex;
justify-content: center;
align-items: center;
height: 200px;
width: 100%;
"
>
<el-empty description="暂无相关数据" />
</div>
</div>
</div>
</div>
<div class="right-main-content-footer">
<div class="footer-item1">
<div class="footer-item1-bottom">
<div class="icon">
<img src="../../../assets/images/warning.png" alt="" />
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.upstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamInternalRate
)}%),受制裁${cnEntityOnChainData.upstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamEntityRate
)}%)`
}}
</div>
</div>
<div class="footer-item1-top">{{ "上游" }}</div>
</div>
<div class="footer-item2">
<div class="footer-item2-bottom">
<div class="icon">
<img src="../../../assets/images/warning.png" alt="" />
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.midstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamInternalRate
)}%),受制裁${cnEntityOnChainData.midstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamEntityRate
)}%)`
}}
</div>
</div>
<div class="footer-item2-top">{{ "中游" }}</div>
</div>
<div class="footer-item3">
<div class="footer-item3-bottom">
<div class="icon">
<img src="../../../assets/images/warning.png" alt="" />
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.downstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamInternalRate
)}%),受制裁${cnEntityOnChainData.downstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamEntityRate
)}%)`
}}
</div>
</div>
<div class="footer-item3-top">{{ "下游" }}</div>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
</div>
</template>
<script setup>
import { ref, onMounted, nextTick, onUnmounted } from "vue";
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import defaultTitle from "../../assets/default-icon2.png";
import {
getDeepMiningSelect,
getDeepMiningIndustry,
getDeepMiningIndustryFishbone,
getDeepMiningIndustryEntity
} from "@/api/exportControlV2.0";
import { useRoute } from "vue-router";
const route = useRoute();
// 实体清单-深度挖掘-产业链中国企业实体信息查询
const getCnEntityOnChainData = async () => {
const currentSanction = sanctionList.value.find(item => item.id === currentSanctionId.value);
const date = currentSanction ? currentSanction.date : "";
// 确保 date 格式正确
const formattedDate = date && date.includes("年") ? date.replace("年", "-").replace("月", "-").replace("日", "") : date;
const params = {
date: formattedDate
};
if (selectedIndustryId.value) {
params.chainId = selectedIndustryId.value;
}
try {
const res = await getDeepMiningIndustryEntity(params);
if (res.code === 200 && res.data) {
cnEntityOnChainData.value = res.data;
} else {
cnEntityOnChainData.value = {};
}
} catch (error) {
console.error("获取产业链中国企业实体信息失败:", error);
cnEntityOnChainData.value = {};
}
};
// 实体清单-深度挖掘-产业链鱼骨图信息
const fishboneDataList = ref([]);
const getFishboneData = async () => {
const currentSanction = sanctionList.value.find(item => item.id === currentSanctionId.value);
const date = currentSanction ? currentSanction.date : "";
// 确保 date 格式正确
const formattedDate = date && date.includes("年") ? date.replace("年", "-").replace("月", "-").replace("日", "") : date;
const params = {
date: formattedDate
};
if (selectedIndustryId.value) {
// params.chainId = selectedIndustryId.value;
params.id = selectedIndustryId.value;
}
try {
const res = await getDeepMiningIndustryFishbone(params);
if (res.code === 200 && res.data && res.data.causes && res.data.causes.length > 0) {
const rootCauses = res.data.causes;
if (rootCauses.length > 0 && rootCauses[0].causes) {
fishboneDataList.value = rootCauses.map(group => {
return {
causes: group.causes || []
};
});
mainLineLabels.value = rootCauses.map(group => group.text || "");
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
}
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
}
} catch (error) {
console.error("获取产业链鱼骨图数据失败:", error);
fishboneDataList.value = [];
}
};
// 实体清单-深度挖掘-产业链列表信息
const industryList = ref([]);
const selectedIndustryId = ref(null);
const getIndustryList = async () => {
try {
const res = await getDeepMiningIndustry();
if (res.code === 200 && res.data && res.data.length > 0) {
industryList.value = res.data;
selectedIndustryId.value = res.data[0].id;
getFishboneData();
getCnEntityOnChainData();
} else {
industryList.value = [];
selectedIndustryId.value = null;
}
} catch (error) {
console.error("获取产业链列表数据失败:", error);
industryList.value = [];
selectedIndustryId.value = null;
}
};
// 获取选择制裁
const loading = ref(false);
const currentPage = ref(1);
const pageSize = ref(100);
const total = ref(0);
const totalPage = ref(0);
const getDeepMiningSelectData = async () => {
loading.value = true;
const params = {
startDate: dateRange.value && dateRange.value[0] ? dateRange.value[0] : "",
endDate: dateRange.value && dateRange.value[1] ? dateRange.value[1] : "",
// typeName: "实体清单",
isCn: false,
pageNum: currentPage.value,
pageSize: pageSize.value,
sanTypeIds: [Number(sanTypeId.value)] || 1 // 实体清单固定1
};
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 // 保留额外信息备用
}))
.reverse();
// 默认选中第一条
if (sanctionList.value.length > 0) {
currentSanctionId.value = sanctionList.value[0].id;
// getFishboneData(); // 这里不需要调用,因为getIndustryList会调用
}
} else {
sanctionList.value = [];
}
} catch (error) {
console.error("获取选择制裁数据失败:", error);
sanctionList.value = [];
} finally {
loading.value = false;
}
};
// 日期选择变化
const handleDateChange = () => {
currentPage.value = 1;
getDeepMiningSelectData();
};
// ✅ 自动轮播定时器
const autoPlayTimer = ref(null);
// ✅ 启动自动轮播
const startAutoPlay = () => {
stopAutoPlay();
if (sanctionList.value.length > 1) {
autoPlayTimer.value = setInterval(() => {
handleNextClickAuto();
}, 10000);
}
};
// ✅ 停止自动轮播
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();
stopAutoPlay();
}
};
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();
stopAutoPlay();
}
};
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([]);
const currentSanctionId = ref(5);
const cnEntityOnChainData = ref({});
const mainLineLabels = ref(["关键原材料", "电池材料", "电子元器件", "动力电池", "电子控制系统", "动力电池"]);
// 获取奇数索引的数据组(放在上方)
const getOddGroups = data => {
return data.filter((_, index) => index % 2 !== 0);
};
// 获取偶数索引的数据组(放在下方)
const getEvenGroups = data => {
return data.filter((_, index) => index % 2 === 0);
};
// 获取上方鱼骨图位置类名
const getTopBoneClass = index => {
const positions = ["top-bone", "top-bone1", "top-bone2"];
return positions[index % 3] || "top-bone";
};
// 获取下方鱼骨图位置类名
const getBottomBoneClass = index => {
const positions = ["bottom-bone", "bottom-bone1", "bottom-bone2"];
return positions[index % 3] || "bottom-bone";
};
// 获取左侧显示的项目(前半部分)
const getLeftItems = items => {
const midpoint = Math.ceil(items.length / 2);
return items.slice(0, midpoint);
};
// 获取右侧显示的项目(后半部分)
const getRightItems = items => {
const midpoint = Math.ceil(items.length / 2);
return items.slice(midpoint);
};
// 格式化比率
const formatRate = rate => {
if (rate === undefined || rate === null) return "0.00";
return (rate * 100).toFixed(2);
};
const sanTypeId = ref("");
onMounted(() => {
// 获取路由参数中的sanTypeId
sanTypeId.value = route.query.sanTypeId || "";
// 获取选择制裁
getDeepMiningSelectData();
// 获取产业链信息
getIndustryList();
nextTick(() => {
startAutoPlay();
});
});
// 组件卸载时停止自动轮播
onUnmounted(() => {
stopAutoPlay();
});
</script>
<style scoped lang="scss">
.deep-mining {
width: 1601px;
margin: 0 auto;
position: relative;
// min-height: 800px;
.side-nav {
position: absolute;
top: 27px;
right: 100%;
margin-right: 12px;
display: flex;
flex-direction: column;
gap: 16px;
.tab-item {
cursor: pointer;
padding: 4px 20px;
border-radius: 22px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
white-space: nowrap;
display: flex;
align-items: center;
&.active {
background-color: rgb(5, 95, 194);
color: #fff;
.arrow {
display: inline-block;
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 6px solid #fff;
margin-left: 8px;
}
}
}
}
.main {
width: 100%;
padding-top: 16px;
padding-bottom: 50px;
display: flex;
justify-content: space-between;
.left {
width: 480px;
height: 828px;
.left-main {
margin-top: 11px;
padding: 0 22px 0 23px;
display: flex;
flex-direction: column;
height: calc(100% - 56px);
.date-picker-box {
margin-bottom: 16px;
}
.list-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
font-size: 14px;
color: #666;
.count {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
color: rgb(95, 101, 108);
}
.pagination {
display: flex;
gap: 12px;
.page-btn {
width: 28px;
height: 28px;
background: rgba(231, 243, 255, 1);
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
cursor: pointer;
color: rgb(5, 95, 194);
font-size: 16px;
&.disabled {
cursor: not-allowed;
background: #f5f7fa;
color: #c0c4cc;
}
&:not(.disabled):hover {
background: #e1eeff;
}
}
}
}
.list-content {
flex: 1;
overflow-y: auto;
padding-bottom: 20px;
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
.list-item {
// height: 60px;
border: 1px solid rgb(234, 236, 238);
border-radius: 4px;
margin-bottom: 8px;
display: flex;
align-items: center;
justify-content: space-between;
padding: 15px 16px;
cursor: pointer;
transition: all 0.3s;
position: relative;
background: #fff;
.item-left {
width: 260px;
font-weight: 700;
color: rgb(59, 65, 75);
font-size: 16px;
font-family: "Microsoft YaHei";
}
.item-right {
color: rgb(132, 136, 142);
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
}
&:hover {
border-color: #055fc2;
}
&.active {
border-color: rgb(5, 95, 194);
background-color: rgba(246, 250, 255, 1);
.item-left,
.item-right {
color: rgb(5, 95, 194);
}
&::after {
content: "";
position: absolute;
right: 0;
top: 10px;
bottom: 10px;
width: 4px;
background-color: rgb(5, 95, 194);
// border-radius: 4px 0 0 4px;
}
}
}
}
}
}
.right {
width: 1105px;
height: 828px;
.right-main {
margin-top: 11px;
height: calc(100% - 56px);
padding: 0 16px 16px 16px;
.right-main-content {
height: 100%;
display: flex;
flex-direction: column;
.hintWrap {
display: flex;
align-items: center;
padding: 7px 12px;
border: 1px solid rgba(231, 243, 255, 1);
border-radius: 4px;
background: rgba(246, 250, 255, 1);
margin-bottom: 9px;
.icon1 {
width: 19px;
height: 20px;
background-image: url("./assets/ai.png");
background-size: 100% 100%;
flex-shrink: 0;
}
.title {
color: rgb(5, 95, 194);
font-size: 16px;
font-weight: 400;
line-height: 24px;
margin-left: 13px;
flex: 1;
}
.icon2Wrap {
width: 24px;
height: 24px;
background-color: rgba(231, 243, 255, 1);
display: flex;
justify-content: center;
align-items: center;
border-radius: 12px;
margin-left: 20px;
flex-shrink: 0;
.icon2 {
width: 24px;
height: 24px;
background-image: url("./assets/right.png");
background-size: 100% 100%;
}
}
}
.right-main-content-main {
flex: 1;
// border: 1px solid #eaecee;
// border-radius: 4px;
// background: #f7f8f9;
position: relative;
overflow: hidden;
.fishbone-wrapper {
position: relative;
width: 100%;
height: 100%;
}
.fishbone-scroll-container {
display: flex;
align-items: center;
width: 100%;
height: 100%;
overflow-x: auto;
overflow-y: hidden;
scrollbar-width: thin;
scrollbar-color: rgba(144, 202, 249, 0.5) transparent;
&::-webkit-scrollbar {
height: 6px;
}
&::-webkit-scrollbar-track {
background: transparent;
}
&::-webkit-scrollbar-thumb {
background-color: rgba(144, 202, 249, 0.5);
border-radius: 3px;
}
}
.fishbone {
position: relative;
width: fit-content;
height: 100%;
margin-top: 40px;
min-width: 100%;
padding-left: 275px;
margin-left: 40px;
.main-line {
margin-top: 280px;
width: 1888px;
height: 3px;
background: rgb(230, 231, 232);
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 100px;
// 虚线
&::after {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
// background: repeating-linear-gradient(to right, rgba(174, 208, 255, 1) 0, rgba(174, 208, 255, 1) 10px, transparent 10px, transparent 20px);
}
// 添加中间的文字块
.main-line-text {
position: absolute;
// top: -14px;
font-size: 16px;
color: #055fc2;
font-weight: bold;
background-color: #f7f8f9;
padding: 0 10px;
z-index: 2;
// 箭头背景
height: 32px;
line-height: 32px;
width: 160px;
text-align: center;
background: rgba(231, 243, 255, 1);
clip-path: polygon(0% 0%, 90% 0%, 100% 50%, 90% 100%, 0% 100%, 10% 50%);
&.blue-theme {
background: rgba(231, 243, 255, 1);
color: rgba(22, 119, 255, 1);
}
&.green-theme {
background: rgba(225, 255, 251, 1);
color: rgba(19, 168, 168, 1);
}
&.purple-theme {
background: rgba(246, 235, 255, 1);
color: rgba(146, 84, 222, 1);
}
}
}
}
.company-icon {
width: 16px;
height: 16px;
margin: 0 4px;
object-fit: contain;
}
.top-bone {
position: absolute;
top: 20px;
right: 200px;
width: 3px;
height: 260px;
background: rgb(230, 231, 232);
transform: skew(30deg);
z-index: 1;
.left-bone {
color: #777;
position: absolute;
top: 0;
left: -150px;
width: 150px;
height: 50px;
// overflow: hidden;
.left-bone-item {
transform: skew(-30deg);
height: 45px;
margin-bottom: 2px;
margin-top: 2px;
display: flex;
justify-content: flex-end;
align-items: center;
.text {
margin-left: 4px;
height: 25px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line {
margin-left: 7px;
width: 40px;
height: 2px;
background: rgb(230, 231, 232);
}
}
}
.right-bone {
color: #777;
position: absolute;
top: 0;
right: -150px;
width: 150px;
height: 210px;
overflow: hidden;
.right-bone-item {
transform: skew(-30deg);
height: 39px;
margin-bottom: 2px;
margin-top: 2px;
display: flex;
justify-content: flex-start;
align-items: center;
.line {
margin-right: 7px;
width: 30px;
height: 2px;
background: rgb(230, 231, 232);
}
.text {
max-width: 100px;
margin-right: 4px;
height: 25px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.top-bone1 {
@extend .top-bone;
right: 500px;
}
.top-bone2 {
@extend .top-bone;
right: 800px;
}
.bottom-bone {
position: absolute;
top: 280px;
right: 360px;
width: 3px;
height: 260px;
background: rgb(230, 231, 232);
transform: skew(-30deg);
z-index: 1;
.left-bone {
color: #777;
position: absolute;
top: 50px;
left: -150px;
width: 150px;
height: 260px;
.left-bone-item {
transform: skew(30deg);
height: 39px;
margin-bottom: 2px;
margin-top: 2px;
display: flex;
justify-content: flex-end;
align-items: center;
.text {
margin-left: 4px;
height: 25px;
max-width: 130px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.line {
margin-left: 7px;
width: 40px;
height: 2px;
background: rgb(230, 231, 232);
}
}
}
.right-bone {
color: #777;
position: absolute;
top: 50px;
right: -150px;
width: 150px;
height: 260px;
.right-bone-item {
transform: skew(30deg);
height: 35px;
margin-bottom: 2px;
margin-top: 2px;
display: flex;
justify-content: flex-start;
align-items: center;
.line {
margin-right: 7px;
width: 30px;
height: 2px;
background: rgb(230, 231, 232);
}
.text {
max-width: 100px;
margin-right: 4px;
height: 25px;
line-height: 25px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
}
}
}
.bottom-bone1 {
@extend .bottom-bone;
right: 660px;
}
.bottom-bone2 {
@extend .bottom-bone;
right: 960px;
}
}
.right-main-content-footer {
height: 84px;
margin-top: 16px;
display: flex;
justify-content: space-between;
.footer-item1,
.footer-item2,
.footer-item3 {
flex: 1;
display: flex;
flex-direction: column;
justify-content: flex-end;
}
.footer-item1 {
.footer-item1-top {
height: 28px;
text-align: center;
line-height: 28px;
background: url("../../../assets/images/bg3.png");
background-size: 100% 100%;
color: rgba(22, 119, 255, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
margin-top: 15px;
margin-right: -10px; // Negative margin to overlap/connect
position: relative; // Ensure z-index works if needed
z-index: 1;
}
.footer-item1-bottom {
display: flex;
justify-content: center;
.icon {
margin-top: 9px;
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-top: 7px;
margin-left: 8px;
height: 22px;
color: rgba(206, 79, 81, 1);
font-size: 14px;
}
}
}
.footer-item2 {
.footer-item2-top {
height: 28px;
text-align: center;
line-height: 28px;
background: url("../../../assets/images/bg2.png");
background-size: 100% 100%;
color: rgba(19, 168, 168, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
margin-top: 15px;
margin-right: -10px; // Negative margin to connect with next item
margin-left: -10px; // Negative margin to connect with prev item
position: relative;
z-index: 1;
}
.footer-item2-bottom {
display: flex;
justify-content: center;
.icon {
margin-top: 9px;
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-top: 7px;
margin-left: 8px;
height: 22px;
color: rgba(206, 79, 81, 1);
font-size: 14px;
}
}
}
.footer-item3 {
.footer-item3-top {
height: 28px;
text-align: center;
line-height: 28px;
background: url("../../../assets/images/bg1.png");
background-size: 100% 100%;
color: rgba(146, 84, 222, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 700;
margin-top: 15px;
margin-left: -10px; // Negative margin to connect
position: relative;
z-index: 1;
}
.footer-item3-bottom {
display: flex;
justify-content: center;
.icon {
margin-top: 9px;
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-top: 7px;
margin-left: 8px;
height: 22px;
color: rgba(206, 79, 81, 1);
font-size: 14px;
}
}
}
}
}
}
}
}
}
.title-com {
width: 100%;
height: 56px;
display: flex;
align-items: center;
padding: 14px 12px 16px 0;
.box {
width: 8px;
height: 20px;
background-color: rgb(5, 95, 194);
border-bottom-right-radius: 4px;
border-top-right-radius: 4px;
margin-right: 14px;
}
.text {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(5, 95, 194);
}
.right-group {
margin-left: auto;
display: flex;
align-items: center;
.toggle-btns {
display: flex;
gap: 8px;
margin-right: 20px;
.t-btn {
height: 28px;
padding: 1px 8px;
border: 1px solid transparent;
border-radius: 4px;
font-size: 16px;
font-weight: 400;
line-height: 24px;
font-family: "Microsoft YaHei";
color: rgb(59, 65, 75);
cursor: pointer;
box-sizing: border-box;
border: 1px solid rgb(230, 231, 232);
&.active {
color: rgb(5, 95, 194);
background-color: rgba(246, 250, 255, 1);
border-color: rgb(5, 95, 194);
}
}
}
.btn {
margin-left: 0;
}
.industry-select {
width: 160px;
height: 28px;
margin-right: 12px;
:deep(.el-input__wrapper) {
height: 28px;
min-height: 28px;
padding: 0 8px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__inner) {
height: 28px;
line-height: 28px;
font-size: 14px;
}
}
.time-select {
width: 120px;
height: 28px;
margin-right: 14px;
:deep(.el-input__wrapper) {
height: 28px;
min-height: 28px;
padding: 0 8px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__inner) {
height: 28px;
line-height: 28px;
font-size: 14px;
}
}
}
.btn {
width: 92px;
height: 28px;
margin-left: auto;
img {
width: 28px;
height: 28px;
cursor: pointer;
}
img:first-child {
margin-right: 4px;
}
}
}
.industry-select {
width: 160px;
height: 28px;
margin-right: 12px;
:deep(.el-input__wrapper) {
height: 28px;
min-height: 28px;
padding: 0 8px;
box-shadow: 0 0 0 1px #dcdfe6 inset;
}
:deep(.el-input__inner) {
height: 28px;
line-height: 28px;
font-size: 14px;
}
}
</style>
......@@ -49,7 +49,8 @@
:class="{ active: currentSanctionId === item.id }"
@click="handleSanctionSelect(item.id)"
>
<div class="item-left">{{ item.date }}-{{ item.title }}</div>
<!-- <div class="item-left">{{ item.date }}-{{ item.title }}</div> -->
<div class="item-left">{{ item.date }}-实体清单更新</div>
<div class="item-right">{{ item.count }}{{ item.unit }}</div>
</div>
</div>
......@@ -66,7 +67,7 @@
@change="
() => {
getFishboneData();
getCnEntityOnChainData();
// getCnEntityOnChainData();
}
"
>
......@@ -88,7 +89,10 @@
<div class="fishbone-wrapper">
<div class="fishbone-scroll-container" ref="scrollContainerRef">
<div class="fishbone" ref="fishboneRef" v-if="fishboneDataList.length > 0">
<div class="main-line" :style="{ width: fishboneDataList.length * 200 + 300 + 'px' }">
<div
class="main-line"
:style="{ width: (fishboneDataList.length / boneSize) * 200 + 300 + 'px' }"
>
<!-- 主轴上的标签 -->
<div
class="main-line-text"
......@@ -114,23 +118,23 @@
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
v-for="(item, index) in getLeftItems(causeGroup)"
:key="'left-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">{{ item.companyName }}</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
v-for="(item, index) in getRightItems(causeGroup)"
:key="'right-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">{{ item.companyName }}</div>
</div>
</div>
</div>
......@@ -145,23 +149,23 @@
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
v-for="(item, index) in getLeftItems(causeGroup)"
:key="'left-bottom-' + index"
>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">{{ item.companyName }}</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
v-for="(item, index) in getRightItems(causeGroup)"
:key="'right-bottom-' + index"
>
<div class="line"></div>
<img :src="defaultTitle || item.picture" alt="" class="company-icon" />
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">{{ item.companyName }}</div>
</div>
</div>
</div>
......@@ -189,10 +193,10 @@
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.upstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamInternalRate
)}%),受制裁${cnEntityOnChainData.upstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamEntityRate
`中国企业${cnEntityOnChainData?.upstream?.internalCount || 0}家(${formatRate(
cnEntityOnChainData?.upstream?.internalRate
)}%),受制裁${cnEntityOnChainData?.upstream?.entityCount || 0}家(${formatRate(
cnEntityOnChainData?.upstream?.entityRate
)}%)`
}}
</div>
......@@ -206,10 +210,10 @@
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.midstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamInternalRate
)}%),受制裁${cnEntityOnChainData.midstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamEntityRate
`中国企业${cnEntityOnChainData?.midstream?.internalCount || 0}家(${formatRate(
cnEntityOnChainData?.midstream?.internalRate
)}%),受制裁${cnEntityOnChainData?.midstream?.entityCount || 0}家(${formatRate(
cnEntityOnChainData?.midstream?.entityRate
)}%)`
}}
</div>
......@@ -223,10 +227,10 @@
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.downstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamInternalRate
)}%),受制裁${cnEntityOnChainData.downstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamEntityRate
`中国企业${cnEntityOnChainData?.downstream?.internalCount || 0}家(${formatRate(
cnEntityOnChainData?.downstream?.internalRate
)}%),受制裁${cnEntityOnChainData?.downstream?.entityCount || 0}家(${formatRate(
cnEntityOnChainData?.downstream?.entityRate
)}%)`
}}
</div>
......@@ -294,24 +298,57 @@ const getFishboneData = async () => {
date: formattedDate
};
if (selectedIndustryId.value) {
params.chainId = selectedIndustryId.value;
// params.chainId = selectedIndustryId.value;
params.id = selectedIndustryId.value;
}
try {
const res = await getDeepMiningIndustryFishbone(params);
if (res.code === 200 && res.data && res.data.causes && res.data.causes.length > 0) {
const rootCauses = res.data.causes;
if (rootCauses.length > 0 && rootCauses[0].causes) {
fishboneDataList.value = rootCauses.map(group => {
return {
causes: group.causes || []
};
});
mainLineLabels.value = rootCauses.map(group => group.text || "");
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
}
console.log("获取产业链数据:", res);
if (res.code === 200 && res.data) {
fishboneDataList.value = res.data.children || [];
mainLineLabels.value = res.data.chains.map(group => group.name || "");
const upstream = res.data.levelInfos.find(item => item.level === "上游") || {};
const midstream = res.data.levelInfos.find(item => item.level === "中游") || {};
const downstream = res.data.levelInfos.find(item => item.level === "下游") || {};
console.log("比例计算", (upstream.isChinaCount / upstream.total) * 100);
cnEntityOnChainData.value = {
upstream: {
internalCount: upstream.isChinaCount || 0,
total: upstream.total || 0,
internalRate: upstream.isChinaCount / upstream.total || 0,
entityCount: upstream.sanCount || 0,
entityRate: upstream.sanCount / upstream.total || 0
},
midstream: {
internalCount: midstream.isChinaCount || 0,
total: midstream.total || 0,
internalRate: midstream.isChinaCount / midstream.total || 0,
entityCount: midstream.sanCount || 0,
entityRate: midstream.sanCount / midstream.total || 0
},
downstream: {
internalCount: downstream.isChinaCount || 0,
total: downstream.total || 0,
internalRate: downstream.isChinaCount / downstream.total || 0,
entityCount: downstream.sanCount || 0,
entityRate: downstream.sanCount / downstream.total || 0
}
};
// const rootCauses = res.data.causes;
// if (rootCauses.length > 0 && rootCauses[0].causes) {
// fishboneDataList.value = rootCauses.map(group => {
// return {
// causes: group.causes || []
// };
// });
// mainLineLabels.value = rootCauses.map(group => group.text || "");
// } else {
// fishboneDataList.value = [];
// mainLineLabels.value = [];
// }
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
......@@ -332,7 +369,7 @@ const getIndustryList = async () => {
industryList.value = res.data;
selectedIndustryId.value = res.data[0].id;
getFishboneData();
getCnEntityOnChainData();
// getCnEntityOnChainData();
} else {
industryList.value = [];
selectedIndustryId.value = null;
......@@ -441,7 +478,8 @@ const handlePrevClick = () => {
if (currentIndex > 0) {
const prevItem = sanctionList.value[currentIndex - 1];
handleSanctionSelect(prevItem.id);
startAutoPlay();
// startAutoPlay();
stopAutoPlay();
}
};
......@@ -450,14 +488,15 @@ const handleNextClick = () => {
if (currentIndex < sanctionList.value.length - 1) {
const nextItem = sanctionList.value[currentIndex + 1];
handleSanctionSelect(nextItem.id);
startAutoPlay();
// startAutoPlay();
stopAutoPlay();
}
};
const handleSanctionSelect = id => {
currentSanctionId.value = id;
getFishboneData();
getCnEntityOnChainData();
// getCnEntityOnChainData();
startAutoPlay();
};
......@@ -471,15 +510,33 @@ const currentSanctionId = ref(5);
const cnEntityOnChainData = ref({});
const mainLineLabels = ref(["关键原材料", "电池材料", "电子元器件", "动力电池", "电子控制系统", "动力电池"]);
const boneSize = 10;
// 获取奇数索引的数据组(放在上方)
const getOddGroups = data => {
return data.filter((_, index) => index % 2 !== 0);
// return data.filter((_, index) => index % 2 !== 0);
// 1. 筛选出奇数索引的数据
const oddItems = data.filter((_, index) => index % 2 !== 0);
// 2. 将筛选后的数据每10条分为一组
const result = [];
for (let i = 0; i < oddItems.length; i += boneSize) {
result.push(oddItems.slice(i, i + boneSize));
}
return result;
};
// 获取偶数索引的数据组(放在下方)
const getEvenGroups = data => {
return data.filter((_, index) => index % 2 === 0);
// return data.filter((_, index) => index % 2 === 0);
// 1. 筛选出偶数索引的数据
const evenItems = data.filter((_, index) => index % 2 === 0);
// 2. 将筛选后的数据每10条分为一组
const result = [];
for (let i = 0; i < evenItems.length; i += boneSize) {
result.push(evenItems.slice(i, i + boneSize));
}
return result;
};
// 获取上方鱼骨图位置类名
......@@ -496,6 +553,7 @@ const getBottomBoneClass = index => {
// 获取左侧显示的项目(前半部分)
const getLeftItems = items => {
console.log("左侧数据", items);
const midpoint = Math.ceil(items.length / 2);
return items.slice(0, midpoint);
};
......@@ -532,11 +590,6 @@ onUnmounted(() => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.deep-mining {
width: 1601px;
margin: 0 auto;
......
......@@ -4,7 +4,7 @@
<div class="left-top">
<AnalysisBox title="基本信息" :showAllBtn="false">
<div class="left-top-main">
<div class="left-top-main-title">{{ CCLInfo.description }}</div>
<div class="left-top-main-title">{{ CCLInfo.name + CCLInfo.description }}</div>
<div class="left-top-main-content">
<div class="content-item">
<span class="label">法律依据:</span>
......
......@@ -5,81 +5,80 @@
* @LastEditTime: 2026-01-07 09:58:04
-->
<template>
<div class="sanctions-overview">
<div class="side-nav">
<div v-for="(item, index) in activeTab" :key="index" class="tab-item" :class="{'active': index === activeIndex}" @click="activeIndex = index">
{{item}}
<span v-if="index === activeIndex" class="arrow"></span>
</div>
</div>
<div class="content-box">
<introductionPage v-if="activeIndex === 0"></introductionPage>
<listPage v-if="activeIndex === 1"></listPage>
</div>
</div>
<div class="sanctions-overview">
<div class="side-nav">
<div
v-for="(item, index) in activeTab"
:key="index"
class="tab-item"
:class="{ active: index === activeIndex }"
@click="activeIndex = index"
>
{{ item }}
<span v-if="index === activeIndex" class="arrow"></span>
</div>
</div>
<div class="content-box">
<introductionPage v-if="activeIndex === 0"></introductionPage>
<listPage v-if="activeIndex === 1"></listPage>
</div>
</div>
</template>
<script setup>
import { ref } from 'vue'
import introductionPage from "./components/introductionPage/index.vue"
import listPage from "./components/listPage/index.vue"
const activeTab = ref(["CCL清单简介", "CCL清单列表"])
const activeIndex = ref(0)
import { ref } from "vue";
import introductionPage from "./components/introductionPage/index.vue";
import listPage from "./components/listPage/index.vue";
const activeTab = ref(["CMC清单简介", "CMC清单列表"]);
const activeIndex = ref(0);
</script>
<style scoped lang="scss">
*{
margin: 0;
padding: 0;
}
.sanctions-overview{
width: 1601px;
margin: 0 auto;
position: relative;
// min-height: 800px;
.side-nav {
position: absolute;
top: 27px;
right: 100%;
margin-right: 12px;
display: flex;
flex-direction: column;
gap: 16px;
.tab-item {
cursor: pointer;
padding: 4px 20px;
border-radius: 22px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
white-space: nowrap;
display: flex;
align-items: center;
&.active {
background-color: rgb(5, 95, 194);
color: #fff;
.arrow {
display: inline-block;
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 6px solid #fff;
margin-left: 8px;
}
}
}
}
.content-box {
width: 100%;
}
.sanctions-overview {
width: 1601px;
margin: 0 auto;
position: relative;
// min-height: 800px;
.side-nav {
position: absolute;
top: 27px;
right: 100%;
margin-right: 12px;
display: flex;
flex-direction: column;
gap: 16px;
.tab-item {
cursor: pointer;
padding: 4px 20px;
border-radius: 22px;
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
white-space: nowrap;
display: flex;
align-items: center;
&.active {
background-color: rgb(5, 95, 194);
color: #fff;
.arrow {
display: inline-block;
width: 0;
height: 0;
border-top: 5px solid transparent;
border-bottom: 5px solid transparent;
border-left: 6px solid #fff;
margin-left: 8px;
}
}
}
}
.content-box {
width: 100%;
}
}
</style>
......@@ -55,21 +55,42 @@ import icon2Active from "../assets/icons/icon2_active.png";
import icon3 from "../assets/icons/icon3.png";
import icon3Active from "../assets/icons/icon3_active.png";
import { getCCLInfo } from "@/api/exportControlV2.0.js";
const route = useRoute();
const sanTypeId = ref("");
onMounted(() => {
// 获取路由参数sanTypeId
sanTypeId.value = route.query.sanTypeId;
console.log("CommercialControlList 页面接收到的 sanTypeId:", sanTypeId.value);
getCCLInfoFn();
});
const headerTitle = ref({
img: title,
title: "商业管制清单(CCL)",
titleEn: "Commercial Control List",
department: "美国商务部工业与安全局"
img: "",
title: "",
titleEn: "",
department: ""
});
const getCCLInfoFn = async () => {
try {
const res = await getCCLInfo(route.query.sanTypeId || 13);
if (res && res.code === 200) {
const info = res.data;
headerTitle.value = {
img: info.orgLogoUrl,
title: `${info.name}${info.shortName})`,
titleEn: info.originalName,
department: info.orgName
};
console.log("getCCLInfoFn", info);
}
} catch (error) {
console.error("获取商业管制清单基本信息失败:", error);
}
};
const activeIndex = ref(0);
const headerNavList = ref([
......
......@@ -110,7 +110,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入实体清单的中国实体数量变化趋势,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入SDN清单的中国实体数量变化趋势,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -169,7 +171,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入SDN清单的中国实体领域分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -208,7 +212,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入SDN清单的中国实体领域分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -240,7 +246,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入实体清单的中国实体类型分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入SDN清单的中国实体类型分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......
......@@ -45,7 +45,7 @@
<div class="right">
<AnalysisBox title="投融资限制举措关系图">
<div class="right-main">
<div class="relation-empty" v-if="selectedSanctionIds.length == 0">
<div class="relation-empty" v-if="emptyOfRelation">
<el-empty :image="emptyImg" :image-size="200">
<template #description>
<div class="empty">请在左侧勾选多次投融资限制制裁后点击“开始分析”查看结果</div>
......@@ -139,7 +139,24 @@
<div class="icon2"></div>
</div>
</div>
<RelationChart :is-vertical-chart="true" :graph-data="graphData" />
<!-- <div class="relation-content">
<RelationChart :is-vertical-chart="true" :graph-data="graphData" />
</div> -->
<div class="relation-charts-container">
<div v-for="(graphData, index) in graphDataList" :key="index" class="single-relation-chart-wrapper">
<!-- 可选:显示当前小图的标题,例如制裁名称 -->
<!-- <div class="chart-title" v-if="graphData.originalItem?.vertex?.name">
{{ graphData.originalItem.vertex.name }}
</div> -->
<div class="relation-content-item">
<RelationChart :is-vertical-chart="true" :graph-data="graphData" />
</div>
</div>
<!-- 空状态提示 -->
<div v-if="graphDataList.length === 0" class="empty-chart-tip">暂无关联详情数据</div>
</div>
</div>
</el-dialog>
</div>
......@@ -383,13 +400,16 @@ const fetchRecordRelation = async () => {
recordRelation.value = { noRelationVertices: [], relationVoList: [] };
}
};
const emptyOfRelation = computed(
() => recordRelation.value.noRelationVertices.length == 0 || recordRelation.value.relationVoList.length == 0
);
const vertexInfo = ref({});
const curNode = ref({});
const curLink = ref({});
const nodeVisible = ref(false);
const relationVisible = ref(false);
const tipsInfo = ref("");
const graphData = ref({});
const graphDataList = ref([]);
const getTipsInfo = (relationType, reason) => {
switch (relationType) {
case "继承":
......@@ -400,6 +420,8 @@ const getTipsInfo = (relationType, reason) => {
return `${dayjs(curLink.value.data.originInfo.fromVertex.date).format("YYYY年MM月DD日")}-SDN清单更新与${dayjs(curLink.value.data.originInfo.toVertex.date).format("YYYY年MM月DD日")}-SDN清单更新存在相同${reason},属于相似关系。`;
}
};
// 在 constrainedAssociation.vue 的 script setup 中
const handleClickNode = node => {
console.log("节点点击", node);
if (node.dataType == "node") {
......@@ -418,25 +440,126 @@ const handleClickNode = node => {
relationVisible.value = true;
curLink.value = node;
const relationType = node.data.relationType;
// 继承 - 2025年10月1日-SDN清单更新依托于2024年2月08日-SDN清单更新,两次制裁存在继承关系。
// 冲突 - 2025年2月19日-SDN清单更新中制裁的实体在2024年2月08日-SDN清单更新中被移除,属于冲突关系。
// 相似 - 2025年2月19日-SDN清单更新与2024年2月08日-SDN清单更新存在相同制裁原因,属于相似关系。
// 相似 - 2025年2月19日-SDN清单更新与2024年2月08日-SDN清单更新存在同领域制裁实体,属于相似关系。
// 相似 - 2025年2月19日-SDN清单更新与2024年2月08日-SDN清单更新存在相同依托文件,属于相似关系。
getEdgeInfo(node.data.edgeInfo.key).then(res => {
if (!!res) {
// recordRelation.value = res;
console.log("制裁之间的关系 =>", res);
let reason = "";
if (relationType == "相似") {
reason = res[0].edgeReasonList[0].reason;
// 获取边详情数据
getEdgeInfo(node.data.edgeInfo.key)
.then(res => {
if (!!res && Array.isArray(res)) {
console.log("制裁之间的关系 =>", res);
// 【核心修改】遍历 res,为每一项生成独立的图表数据
const list = [];
res.forEach((item, index) => {
const vertex = item.vertex;
if (!vertex || !vertex.id) return;
const nodes = [];
const lines = [];
const nodeMap = new Map();
// 辅助函数:添加节点
const addNode = (id, text, type = "vertex") => {
if (nodeMap.has(id)) return;
const newNode = {
id: id,
text: text,
// 样式:顶点用主题色,细节用白色
color: type === "vertex" ? "var(--color-primary-50)" : "#ffffff",
fontColor: type === "vertex" ? "var(--text-primary-90-color)" : "#333333",
customFontSize: type === "vertex" ? "14px" : "12px"
};
nodes.push(newNode);
nodeMap.set(id, newNode);
};
// 辅助函数:添加连线
const addLine = (fromId, toId, relationText) => {
lines.push({
from: fromId,
to: toId,
text: relationText,
color: "var(--color-primary-50)",
fontColor: "#666"
});
};
// 1. 添加出发点 (Vertex)
// addNode(vertex.id, vertex.name, "vertex");
// 1. 添加出发点 (Vertex)
// 【修改点】:根据 originInfo 中的 fromVertex 或 toVertex 动态生成名称
let vertexName = vertex.name; // 默认 fallback
// 尝试从 curLink (即 node) 中获取 originInfo
const originInfo = node.data?.originInfo;
if (originInfo) {
// 判断当前 vertex.id 是 from 还是 to,从而决定取哪个日期
// 通常 item.vertex 对应的是边的起点或终点之一,这里假设 item.vertex 就是我们要展示的核心节点
// 如果业务逻辑中 item.vertex 始终对应 fromVertex 或 toVertex 中的一个,我们可以这样判断:
let sourceDate = null;
if (originInfo.fromVertex && originInfo.fromVertex.id === vertex.id) {
sourceDate = originInfo.fromVertex.date;
} else if (originInfo.toVertex && originInfo.toVertex.id === vertex.id) {
sourceDate = originInfo.toVertex.date;
}
// 如果找到了对应的日期,则格式化;否则保持原名或使用默认逻辑
if (sourceDate) {
vertexName = dayjs(sourceDate).format("YYYY年MM月DD日") + " SDN清单更新";
}
}
addNode(vertex.id, vertexName, "vertex");
// 2. 处理 edgeReasonList -> reasonDetail
if (item.edgeReasonList && item.edgeReasonList.length > 0) {
item.edgeReasonList.forEach(reasonItem => {
const relationName = reasonItem.reason; // 例如: "依托文件"
if (reasonItem.reasonDetail && reasonItem.reasonDetail.length > 0) {
reasonItem.reasonDetail.forEach(detailItem => {
// 使用 detailItem.name 作为唯一 ID
// 注意:在这个独立的小图中,ID 只要不重复即可。
// 如果不同项之间有相同的 detailItem.name,它们在不同图中是隔离的,所以没问题。
const detailId = detailItem.name;
addNode(detailId, detailItem.name, "detail");
addLine(vertex.id, detailId, relationName);
});
}
});
}
// 只有当有连线时才加入列表,或者即使只有顶点也加入(视需求而定)
if (nodes.length > 0) {
list.push({
rootId: vertex.id,
nodes: nodes,
lines: lines,
// 可以保留原始数据用于调试或额外展示
originalItem: item
});
}
});
graphDataList.value = list;
// 处理提示文案 (取第一个或根据业务逻辑组合)
let reason = "";
if (relationType == "相似" && res[0]?.edgeReasonList?.[0]?.reason) {
reason = res[0].edgeReasonList[0].reason;
}
tipsInfo.value = getTipsInfo(relationType, reason);
} else {
graphDataList.value = [];
}
tipsInfo.value = getTipsInfo(relationType, reason);
} else {
// recordRelation.value = { noRelationVertices: [], relationVoList: [] };
}
});
})
.catch(err => {
console.error("获取边信息失败", err);
graphDataList.value = [];
});
}
};
......@@ -481,10 +604,6 @@ const formatChangeSummary = (addList, delList) => {
return `${item.value}${unit}${noun}`;
});
// 拼接:移除 + item1 + , + item2 ...
// 注意:题目要求“删除”,但之前代码用的是“移除”,这里统一使用“移除”或“删除”。
// 根据题目描述“展示样本为:新增12个实体,3名个人,移除1个实体”,这里使用“移除”更贴切上下文,
// 如果必须用“删除”,请将下面的 '移除' 改为 '删除'。
parts.push(`移除${delItems.join(",")}`);
}
......@@ -708,6 +827,57 @@ onMounted(() => {
flex-direction: column;
gap: 16px;
border-top: 1px solid rgb(238, 238, 238);
// 【新增】关系图容器样式
.relation-charts-container {
display: flex;
flex-direction: column;
gap: 20px;
height: 400px;
overflow-y: auto;
padding-right: 10px; // 给滚动条留空间
// 自定义滚动条样式
&::-webkit-scrollbar {
width: 6px;
}
&::-webkit-scrollbar-thumb {
background: #ccc;
border-radius: 3px;
}
}
.single-relation-chart-wrapper {
border: 1px solid #eee;
border-radius: 8px;
padding: 10px;
background-color: #fafafa;
height: 400px;
.chart-title {
font-size: 14px;
font-weight: bold;
color: #333;
margin-bottom: 10px;
padding-left: 5px;
border-left: 3px solid var(--color-primary-50);
}
.relation-content-item {
height: 400px; // 每个小图的高度,可根据需要调整
// width: 100%;
// 确保 RelationChart 内部能正确填充
:deep(.relation-graph) {
width: 100%;
height: 100%;
}
}
}
.empty-chart-tip {
text-align: center;
color: #999;
padding: 20px;
}
.hintWrap {
display: flex;
align-items: center;
......@@ -753,6 +923,10 @@ onMounted(() => {
}
}
}
.relation-content {
height: 400px;
width: 100%;
}
.info-btn {
position: absolute;
top: 20px;
......
......@@ -45,7 +45,7 @@
<div class="right">
<AnalysisBox title="投融资限制举措关系图">
<div class="right-main">
<div class="relation-empty" v-if="selectedSanctionIds.length == 0">
<div class="relation-empty" v-if="emptyOfRelation">
<el-empty :image="emptyImg" :image-size="200">
<template #description>
<div class="empty">请在左侧勾选多次投融资限制制裁后点击“开始分析”查看结果</div>
......@@ -145,12 +145,16 @@
<div class="relation-charts-container">
<div v-for="(graphData, index) in graphDataList" :key="index" class="single-relation-chart-wrapper">
<!-- 可选:显示当前小图的标题,例如制裁名称 -->
<div class="chart-title" v-if="graphData.originalItem?.vertex?.name">
<!-- <div class="chart-title" v-if="graphData.originalItem?.vertex?.name">
{{ graphData.originalItem.vertex.name }}
</div>
</div> -->
<div class="relation-content-item">
<RelationChart :is-vertical-chart="true" :graph-data="graphData" />
<RelationChart
:is-vertical-chart="true"
:graph-data="graphData"
:is-reversed="graphData.isReversed"
/>
</div>
</div>
......@@ -400,6 +404,9 @@ const fetchRecordRelation = async () => {
recordRelation.value = { noRelationVertices: [], relationVoList: [] };
}
};
const emptyOfRelation = computed(
() => recordRelation.value.noRelationVertices.length == 0 || recordRelation.value.relationVoList.length == 0
);
const vertexInfo = ref({});
const curNode = ref({});
const curLink = ref({});
......@@ -434,104 +441,211 @@ const handleClickNode = node => {
}
});
} else {
relationVisible.value = true;
curLink.value = node;
const relationType = node.data.relationType;
// 获取边详情数据
getEdgeInfo(node.data.edgeInfo.key)
.then(res => {
if (!!res && Array.isArray(res)) {
console.log("制裁之间的关系 =>", res);
// 【核心修改】遍历 res,为每一项生成独立的图表数据
const list = [];
res.forEach((item, index) => {
const vertex = item.vertex;
if (!vertex || !vertex.id) return;
const nodes = [];
const lines = [];
const nodeMap = new Map();
// 辅助函数:添加节点
const addNode = (id, text, type = "vertex") => {
if (nodeMap.has(id)) return;
const newNode = {
id: id,
text: text,
// 样式:顶点用主题色,细节用白色
color: type === "vertex" ? "var(--color-primary-50)" : "#ffffff",
fontColor: type === "vertex" ? "var(--text-primary-90-color)" : "#333333",
customFontSize: type === "vertex" ? "14px" : "12px"
};
nodes.push(newNode);
nodeMap.set(id, newNode);
};
// 辅助函数:添加连线
const addLine = (fromId, toId, relationText) => {
lines.push({
from: fromId,
to: toId,
text: relationText,
color: "var(--color-primary-50)",
fontColor: "#666"
});
};
// 1. 添加出发点 (Vertex)
addNode(vertex.id, vertex.name, "vertex");
// 2. 处理 edgeReasonList -> reasonDetail
if (item.edgeReasonList && item.edgeReasonList.length > 0) {
item.edgeReasonList.forEach(reasonItem => {
const relationName = reasonItem.reason; // 例如: "依托文件"
if (reasonItem.reasonDetail && reasonItem.reasonDetail.length > 0) {
reasonItem.reasonDetail.forEach(detailItem => {
// 使用 detailItem.name 作为唯一 ID
// 注意:在这个独立的小图中,ID 只要不重复即可。
// 如果不同项之间有相同的 detailItem.name,它们在不同图中是隔离的,所以没问题。
const detailId = detailItem.name;
handleLink(node);
}
};
addNode(detailId, detailItem.name, "detail");
addLine(vertex.id, detailId, relationName);
});
const handleLink = node => {
relationVisible.value = true;
curLink.value = node;
const relationType = node.data.relationType;
// 获取边详情数据
getEdgeInfo(node.data.edgeInfo.key)
.then(res => {
if (!!res && Array.isArray(res)) {
console.log("制裁之间的关系 =>", res);
// 【核心修改】遍历 res,为每一项生成独立的图表数据
const list = [];
res.forEach((item, index) => {
const vertex = item.vertex;
if (!vertex || !vertex.id) return;
const nodes = [];
const lines = [];
const nodeMap = new Map();
// 辅助函数:添加节点
const addNode = (id, text, type = "vertex", highlight = false) => {
if (nodeMap.has(id)) return;
// 【修改】根据 type 定义不同的样式
let nodeStyle = {};
if (type === "vertex") {
nodeStyle = {
color: "rgb(185, 220, 255)", // 背景色
fontColor: "rgb(5, 95, 194)", // 文字颜色
customFontSize: "16px", // 文字大小
fontWeight: 700, // 加粗
// width: "120px", // 固定宽度,防止长文本换行难看
// height: "40px", // 固定高度
borderRadius: "20px", // 圆角
borderColor: "rgb(185, 220, 255)"
// border: "1px solid rgb(185, 220, 255)"
};
} else {
// 细节节点样式
// nodeStyle = {
// color: "#ffffff",
// fontColor: "#333333",
// customFontSize: "12px",
// // width: "100px",
// // height: "30px",
// borderRadius: "15px",
// borderColor: "rgb(230, 231, 232)"
// };
// 细节节点样式
if (highlight) {
// 【核心逻辑】根据 relationType 设置高亮样式
if (relationType === "冲突") {
nodeStyle = {
color: "rgba(255, 241, 240, 1)", // 背景
borderColor: "rgb(206, 79, 81)", // 边框
fontColor: "rgb(206, 79, 81)", // 字体
fontWeight: 700, // 加粗
customFontSize: "12px",
borderRadius: "15px"
};
} else if (relationType === "相似" || relationType === "继承") {
// 相似和继承使用相同的蓝色系高亮
nodeStyle = {
color: "rgb(185, 220, 255)", // 背景
borderColor: "rgb(5, 95, 194)", // 边框
fontColor: "rgb(5, 95, 194)", // 字体
fontWeight: 700, // 加粗
customFontSize: "12px",
borderRadius: "15px"
};
} else {
// 默认高亮样式(以防万一)
nodeStyle = {
color: "#e6f7ff",
borderColor: "#1890ff",
fontColor: "#1890ff",
fontWeight: 700,
customFontSize: "12px",
borderRadius: "15px"
};
}
});
} else {
// 非高亮细节节点默认样式
nodeStyle = {
color: "#ffffff",
borderColor: "rgb(230, 231, 232)",
fontColor: "#333333",
customFontSize: "12px",
borderRadius: "15px",
fontWeight: 400
};
}
}
const newNode = {
id: id,
text: text,
// color: type === "vertex" ? "var(--color-primary-50)" : "#ffffff",
// fontColor: type === "vertex" ? "var(--text-primary-90-color)" : "#333333",
// customFontSize: type === "vertex" ? "14px" : "12px",
...nodeStyle
};
nodes.push(newNode);
nodeMap.set(id, newNode);
};
// 辅助函数:添加连线
const addLine = (fromId, toId, relationText) => {
lines.push({
from: fromId,
to: toId,
text: relationText,
color: "var(--color-primary-50)",
fontColor: "#666"
});
};
// 1. 添加出发点 (Vertex)
// addNode(vertex.id, vertex.name, "vertex");
// 1. 添加出发点 (Vertex)
// 【修改点】:根据 originInfo 中的 fromVertex 或 toVertex 动态生成名称
let vertexName = vertex.name; // 默认 fallback
// 尝试从 curLink (即 node) 中获取 originInfo
const originInfo = node.data?.originInfo;
if (originInfo) {
// 判断当前 vertex.id 是 from 还是 to,从而决定取哪个日期
// 通常 item.vertex 对应的是边的起点或终点之一,这里假设 item.vertex 就是我们要展示的核心节点
// 如果业务逻辑中 item.vertex 始终对应 fromVertex 或 toVertex 中的一个,我们可以这样判断:
let sourceDate = null;
if (originInfo.fromVertex && originInfo.fromVertex.id === vertex.id) {
sourceDate = originInfo.fromVertex.date;
} else if (originInfo.toVertex && originInfo.toVertex.id === vertex.id) {
sourceDate = originInfo.toVertex.date;
}
// 只有当有连线时才加入列表,或者即使只有顶点也加入(视需求而定)
if (nodes.length > 0) {
list.push({
rootId: vertex.id,
nodes: nodes,
lines: lines,
// 可以保留原始数据用于调试或额外展示
originalItem: item
});
// 如果找到了对应的日期,则格式化;否则保持原名或使用默认逻辑
if (sourceDate) {
vertexName = dayjs(sourceDate).format("YYYY年MM月DD日") + " SDN清单更新";
}
});
}
graphDataList.value = list;
addNode(vertex.id, vertexName, "vertex");
// 2. 处理 edgeReasonList -> reasonDetail
if (item.edgeReasonList && item.edgeReasonList.length > 0) {
item.edgeReasonList.forEach(reasonItem => {
const relationName = reasonItem.reason; // 例如: "依托文件"
if (reasonItem.reasonDetail && reasonItem.reasonDetail.length > 0) {
reasonItem.reasonDetail.forEach(detailItem => {
// 使用 detailItem.name 作为唯一 ID
// 注意:在这个独立的小图中,ID 只要不重复即可。
// 如果不同项之间有相同的 detailItem.name,它们在不同图中是隔离的,所以没问题。
const detailId = detailItem.name;
addNode(
detailId,
detailItem.name,
"detail",
addNode(detailId, detailItem.name, "detail", !!detailItem.highlight)
);
addLine(vertex.id, detailId, relationName);
});
}
});
}
// 处理提示文案 (取第一个或根据业务逻辑组合)
let reason = "";
if (relationType == "相似" && res[0]?.edgeReasonList?.[0]?.reason) {
reason = res[0].edgeReasonList[0].reason;
// 只有当有连线时才加入列表,或者即使只有顶点也加入(视需求而定)
if (nodes.length > 0) {
list.push({
rootId: vertex.id,
nodes: nodes,
lines: lines,
// 可以保留原始数据用于调试或额外展示
originalItem: item,
isReversed: index % 2 !== 0
});
}
tipsInfo.value = getTipsInfo(relationType, reason);
} else {
graphDataList.value = [];
});
graphDataList.value = list;
// 处理提示文案 (取第一个或根据业务逻辑组合)
let reason = "";
if (relationType == "相似" && res[0]?.edgeReasonList?.[0]?.reason) {
reason = res[0].edgeReasonList[0].reason;
}
})
.catch(err => {
console.error("获取边信息失败", err);
tipsInfo.value = getTipsInfo(relationType, reason);
} else {
graphDataList.value = [];
});
}
}
})
.catch(err => {
console.error("获取边信息失败", err);
graphDataList.value = [];
});
};
// 【新增/修改】格式化变动 summary 的函数
......@@ -575,10 +689,6 @@ const formatChangeSummary = (addList, delList) => {
return `${item.value}${unit}${noun}`;
});
// 拼接:移除 + item1 + , + item2 ...
// 注意:题目要求“删除”,但之前代码用的是“移除”,这里统一使用“移除”或“删除”。
// 根据题目描述“展示样本为:新增12个实体,3名个人,移除1个实体”,这里使用“移除”更贴切上下文,
// 如果必须用“删除”,请将下面的 '移除' 改为 '删除'。
parts.push(`移除${delItems.join(",")}`);
}
......@@ -805,10 +915,10 @@ onMounted(() => {
// 【新增】关系图容器样式
.relation-charts-container {
display: flex;
flex-direction: column;
flex-direction: row;
gap: 20px;
max-height: 60vh; // 限制最大高度,超出滚动
overflow-y: auto;
height: 400px;
overflow-x: auto;
padding-right: 10px; // 给滚动条留空间
// 自定义滚动条样式
......@@ -822,11 +932,14 @@ onMounted(() => {
}
.single-relation-chart-wrapper {
border: 1px solid #eee;
border-radius: 8px;
flex: 1;
min-width: 400px; /* 保证每个图表有最小宽度 */
// border: 1px solid #eee;
// border-radius: 8px;
padding: 10px;
background-color: #fafafa;
// background-color: #fafafa;
display: flex;
flex-direction: column;
.chart-title {
font-size: 14px;
font-weight: bold;
......@@ -837,8 +950,10 @@ onMounted(() => {
}
.relation-content-item {
height: 200px; // 每个小图的高度,可根据需要调整
flex: 1;
position: relative;
width: 100%;
height: 100%;
// 确保 RelationChart 内部能正确填充
:deep(.relation-graph) {
......
......@@ -6,7 +6,7 @@
:key="index"
class="tab-item"
:class="{ active: index === activeIndex }"
@click="activeIndex = index"
@click="handleTabActive(index)"
>
{{ item }}
<span v-if="index === activeIndex" class="arrow"></span>
......@@ -50,7 +50,8 @@
:class="{ active: currentSanctionId === item.id }"
@click="handleSanctionSelect(item.id)"
>
<div class="item-left">{{ item.date }}-{{ item.title }}</div>
<!-- <div class="item-left">{{ item.date }}-{{ item.title }}</div> -->
<div class="item-left">{{ item.date }}-SDN清单更新</div>
<div class="item-right">{{ item.count }}{{ item.unit }}</div>
</div>
</div>
......@@ -58,7 +59,7 @@
</AnalysisBox>
</div>
<div class="right">
<AnalysisBox title="制裁产业链时序图">
<AnalysisBox title="SDN清单时序图">
<template #header-btn>
<el-select
v-model="selectedIndustryId"
......@@ -67,7 +68,7 @@
@change="
() => {
getFishboneData();
getCnEntityOnChainData();
// getCnEntityOnChainData();
}
"
>
......@@ -91,7 +92,7 @@
<div class="fishbone" ref="fishboneRef" v-if="fishboneDataList.length > 0">
<div
class="main-line"
:style="{ width: fishboneDataList.length * 200 + 300 + 'px' }"
:style="{ width: (fishboneDataList.length / boneSize) * 200 + 300 + 'px' }"
>
<!-- 主轴上的标签 -->
<div
......@@ -118,31 +119,27 @@
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
v-for="(item, index) in getLeftItems(causeGroup)"
:key="'left-' + index"
>
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">
{{ item.companyName }}
</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
v-for="(item, index) in getRightItems(causeGroup)"
:key="'right-' + index"
>
<div class="line"></div>
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">
{{ item.companyName }}
</div>
</div>
</div>
</div>
......@@ -157,31 +154,27 @@
<div class="left-bone">
<div
class="left-bone-item"
v-for="(item, index) in getLeftItems(causeGroup.causes)"
v-for="(item, index) in getLeftItems(causeGroup)"
:key="'left-bottom-' + index"
>
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">
{{ item.companyName }}
</div>
<div class="line"></div>
</div>
</div>
<div class="right-bone">
<div
class="right-bone-item"
v-for="(item, index) in getRightItems(causeGroup.causes)"
v-for="(item, index) in getRightItems(causeGroup)"
:key="'right-bottom-' + index"
>
<div class="line"></div>
<img
:src="defaultTitle || item.picture"
alt=""
class="company-icon"
/>
<div class="text" :title="item.name">{{ item.name }}</div>
<img :src="item.image || defaultTitle" alt="" class="company-icon" />
<div class="text" :title="item.companyName">
{{ item.companyName }}
</div>
</div>
</div>
</div>
......@@ -209,10 +202,10 @@
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.upstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamInternalRate
)}%),受制裁${cnEntityOnChainData.upstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.upstreamEntityRate
`中国企业${cnEntityOnChainData?.upstream?.internalCount || 0}家(${formatRate(
cnEntityOnChainData?.upstream?.internalRate
)}%),受制裁${cnEntityOnChainData?.upstream?.entityCount || 0}家(${formatRate(
cnEntityOnChainData?.upstream?.entityRate
)}%)`
}}
</div>
......@@ -226,10 +219,10 @@
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.midstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamInternalRate
)}%),受制裁${cnEntityOnChainData.midstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.midstreamEntityRate
`中国企业${cnEntityOnChainData?.midstream?.internalCount || 0}家(${formatRate(
cnEntityOnChainData?.midstream?.internalRate
)}%),受制裁${cnEntityOnChainData?.midstream?.entityCount || 0}家(${formatRate(
cnEntityOnChainData?.midstream?.entityRate
)}%)`
}}
</div>
......@@ -243,10 +236,10 @@
</div>
<div class="text">
{{
`中国企业${cnEntityOnChainData.downstreamInternalCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamInternalRate
)}%),受制裁${cnEntityOnChainData.downstreamEntityCount || 0}家(${formatRate(
cnEntityOnChainData.downstreamEntityRate
`中国企业${cnEntityOnChainData?.downstream?.internalCount || 0}家(${formatRate(
cnEntityOnChainData?.downstream?.internalRate
)}%),受制裁${cnEntityOnChainData?.downstream?.entityCount || 0}家(${formatRate(
cnEntityOnChainData?.downstream?.entityRate
)}%)`
}}
</div>
......@@ -268,7 +261,6 @@
<script setup>
import { ref, onMounted, nextTick, onUnmounted } from "vue";
import { ArrowLeft, ArrowRight } from "@element-plus/icons-vue";
import defaultTitle from "../../assets/default-icon2.png";
import {
getDeepMiningSelect,
......@@ -319,24 +311,40 @@ const getFishboneData = async () => {
date: formattedDate
};
if (selectedIndustryId.value) {
params.chainId = selectedIndustryId.value;
params.id = selectedIndustryId.value;
}
try {
const res = await getDeepMiningIndustryFishbone(params);
if (res.code === 200 && res.data && res.data.causes && res.data.causes.length > 0) {
const rootCauses = res.data.causes;
if (rootCauses.length > 0 && rootCauses[0].causes) {
fishboneDataList.value = rootCauses.map(group => {
return {
causes: group.causes || []
};
});
mainLineLabels.value = rootCauses.map(group => group.text || "");
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
}
if (res.code === 200 && res.data) {
fishboneDataList.value = res.data.children || [];
mainLineLabels.value = res.data.chains.map(group => group.name || "");
const upstream = res.data.levelInfos.find(item => item.level === "上游") || {};
const midstream = res.data.levelInfos.find(item => item.level === "中游") || {};
const downstream = res.data.levelInfos.find(item => item.level === "下游") || {};
console.log("比例计算", (upstream.isChinaCount / upstream.total) * 100);
cnEntityOnChainData.value = {
upstream: {
internalCount: upstream.isChinaCount || 0,
total: upstream.total || 0,
internalRate: upstream.isChinaCount / upstream.total || 0,
entityCount: upstream.sanCount || 0,
entityRate: upstream.sanCount / upstream.total || 0
},
midstream: {
internalCount: midstream.isChinaCount || 0,
total: midstream.total || 0,
internalRate: midstream.isChinaCount / midstream.total || 0,
entityCount: midstream.sanCount || 0,
entityRate: midstream.sanCount / midstream.total || 0
},
downstream: {
internalCount: downstream.isChinaCount || 0,
total: downstream.total || 0,
internalRate: downstream.isChinaCount / downstream.total || 0,
entityCount: downstream.sanCount || 0,
entityRate: downstream.sanCount / downstream.total || 0
}
};
} else {
fishboneDataList.value = [];
mainLineLabels.value = [];
......@@ -357,7 +365,7 @@ const getIndustryList = async () => {
industryList.value = res.data;
selectedIndustryId.value = res.data[0].id;
getFishboneData();
getCnEntityOnChainData();
// getCnEntityOnChainData();
} else {
industryList.value = [];
selectedIndustryId.value = null;
......@@ -466,7 +474,7 @@ const handlePrevClick = () => {
if (currentIndex > 0) {
const prevItem = sanctionList.value[currentIndex - 1];
handleSanctionSelect(prevItem.id);
startAutoPlay();
// startAutoPlay();
}
};
......@@ -475,19 +483,27 @@ const handleNextClick = () => {
if (currentIndex < sanctionList.value.length - 1) {
const nextItem = sanctionList.value[currentIndex + 1];
handleSanctionSelect(nextItem.id);
startAutoPlay();
// startAutoPlay();
}
};
const handleSanctionSelect = id => {
currentSanctionId.value = id;
getFishboneData();
getCnEntityOnChainData();
// getCnEntityOnChainData();
startAutoPlay();
};
const activeTab = ref(["制裁时序分析", "限制关联分析"]);
const activeIndex = ref(0);
const handleTabActive = index => {
activeIndex.value = index;
if (index !== 0) {
stopAutoPlay();
} else {
startAutoPlay();
}
};
const dateRange = ref(["2025-01-01", "2025-12-31"]);
const sanctionList = ref([]);
......@@ -496,15 +512,33 @@ const currentSanctionId = ref(5);
const cnEntityOnChainData = ref({});
const mainLineLabels = ref(["关键原材料", "电池材料", "电子元器件", "动力电池", "电子控制系统", "动力电池"]);
const boneSize = 10;
// 获取奇数索引的数据组(放在上方)
const getOddGroups = data => {
return data.filter((_, index) => index % 2 !== 0);
// return data.filter((_, index) => index % 2 !== 0);
// 1. 筛选出奇数索引的数据
const oddItems = data.filter((_, index) => index % 2 !== 0);
// 2. 将筛选后的数据每10条分为一组
const result = [];
for (let i = 0; i < oddItems.length; i += boneSize) {
result.push(oddItems.slice(i, i + boneSize));
}
return result;
};
// 获取偶数索引的数据组(放在下方)
const getEvenGroups = data => {
return data.filter((_, index) => index % 2 === 0);
// return data.filter((_, index) => index % 2 === 0);
// 1. 筛选出偶数索引的数据
const evenItems = data.filter((_, index) => index % 2 === 0);
// 2. 将筛选后的数据每10条分为一组
const result = [];
for (let i = 0; i < evenItems.length; i += boneSize) {
result.push(evenItems.slice(i, i + boneSize));
}
return result;
};
// 获取上方鱼骨图位置类名
......
......@@ -155,15 +155,8 @@ const getTagStyle = tag => {
// 跳转公司详情页
const handleCompClick = item => {
console.log("item", item);
window.sessionStorage.setItem("curTabName", item.entityNameZh || item.entityName);
window.sessionStorage.setItem("curTabName", item.name || item.orgName);
gotoCompanyPages(item.id);
// const route = router.resolve({
// name: "companyPages",
// params: {
// id: item.id
// }
// });
// window.open(route.href, "_blank");
};
</script>
......
......@@ -253,7 +253,7 @@ const handleCompClick = item => {
// }
// });
// window.open(route.href, "_blank");
gotoCompanyPages(item.entityId);
gotoCompanyPages(item.id);
};
const searchKeyword = ref("");
......
......@@ -175,7 +175,9 @@
<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 />
......@@ -210,7 +212,7 @@
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">
美国商务部发布商业管制清单的频次,数据来源:美国商务部官网
美国商务部发布商业管制清单的频次,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
......@@ -260,7 +262,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入SDN清单的中国实体领域分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -285,7 +289,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入实体清单的中国实体数量变化趋势,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入SDN清单的中国实体数量变化趋势,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -306,7 +312,7 @@
</div>
</div>
<template v-if="activeResourceTab === 'entity'">
<el-col :span="8" style="padding: 0">
<el-col :span="8" style="padding-left: 0">
<custom-container title="历次制裁过程" :titleIcon="listIcon" height="845px">
<template #default>
<div class="box4">
......@@ -341,7 +347,7 @@
</template>
</custom-container>
</el-col>
<el-col :span="16" style="padding: 0">
<el-col :span="16" style="padding-right: 0">
<custom-container title="制裁实体清单" :titleIcon="entityIcon" height="845px">
<template #header-right>
<div class="box5-header-right">{{ total }}家实体</div>
......@@ -1706,15 +1712,6 @@ const handleMediaClick = item => {
</script>
<style lang="scss" scoped>
// * {
// margin: 0;
// padding: 0;
// }
:deep(.el-input__wrapper) {
// box-shadow: none;
}
.home-header {
height: 64px;
background: url("@/assets/images/nav-bg.png");
......@@ -1783,7 +1780,7 @@ const handleMediaClick = item => {
position: absolute;
width: 240px;
height: 89px;
top: 30px;
top: 12px;
right: -24px;
display: flex;
flex-direction: column;
......@@ -3095,7 +3092,7 @@ const handleMediaClick = item => {
align-items: center;
margin-top: 6px;
margin-bottom: 36px;
padding-left: 10px;
// padding-left: 10px;
.resource-tab-item {
margin-right: 12px;
......@@ -3164,7 +3161,7 @@ const handleMediaClick = item => {
}
.text {
font-size: 20px;
font-size: 16px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
......
......@@ -59,7 +59,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入本次实体清单的中国实体领域分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入本次SDN清单的中国实体领域分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -109,7 +111,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入本次实体清单的中国实体类型分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入本次SDN清单的中国实体类型分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -150,7 +154,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入本次实体清单的实体国家地区分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入本次SDN清单的实体国家地区分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -194,7 +200,9 @@
<div class="data-origin-icon">
<img :src="tipsIcon" alt="" />
</div>
<div class="data-origin-text">进入本次实体清单的中国实体各省分布情况,数据来源:美国商务部官网</div>
<div class="data-origin-text">
进入本次SDN清单的中国实体各省分布情况,数据来源:美国财政部海外资产管理办公室官网
</div>
</div>
<div class="ai-pane">
<AiButton />
......@@ -845,11 +853,6 @@ onMounted(() => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.data-statistics {
width: 1601px;
margin: 0 auto;
......
......@@ -540,11 +540,6 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.deepMiningChartmode {
height: calc(100vh - 220px) !important;
// overflow: hidden;
......
......@@ -990,11 +990,6 @@ onMounted(async () => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.industrial-impact {
width: 100%;
padding-top: 16px;
......
......@@ -1118,11 +1118,6 @@ onBeforeUnmount(() => {
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.industrial-impact {
width: 100%;
padding-top: 16px;
......
......@@ -29,10 +29,6 @@ const activeIndex = ref(0);
</script>
<style scoped lang="scss">
* {
margin: 0;
padding: 0;
}
.impact-analysis {
width: 1601px;
margin: 0 auto;
......
......@@ -95,7 +95,7 @@
</div>
</div>
<div class="right" v-loading="isLoading">
<AnalysisBox title="制裁清单" :showAllBtn="false">
<AnalysisBox title="制裁清单" :showAllBtn="false" style="height: auto">
<div class="right-title">
<div class="filter-row">
<div class="filter-left">
......@@ -234,7 +234,31 @@
</div>
</AnalysisBox>
<div :style="{ height: '20px' }"></div>
<AnalysisBox title="制裁原因及相关历史制裁" :showAllBtn="false"></AnalysisBox>
<AnalysisBox title="制裁原因及相关历史制裁" :showAllBtn="false" style="height: auto">
<div class="reason-history">
<div class="reason-history-item" v-for="(item, index) in reasonHistoryList" :key="item.id">
<div class="item-header">
<div class="item-header-title">
<div class="item-header-title-idx">{{ index + 1 }}</div>
<div class="item-header-title-text">{{ item.sanReason }}</div>
</div>
<div class="item-header-domain">
<AreaTag v-for="(domain, index) in item.techDomains" :key="index" :tagName="domain" />
</div>
</div>
<div class="item-content" v-for="(record, index) in item.relateSanRecords" :key="index">
<div class="item-content-record">
<div class="item-content-record-header">
{{ record.postDate + " " + record.title }}
</div>
<div class="item-content-record-content">
{{ record.content }}
</div>
</div>
</div>
</div>
</div>
</AnalysisBox>
</div>
</div>
<!-- 50%规则子企业弹框 -->
......@@ -252,7 +276,6 @@ import { ref, defineProps, computed, onMounted, watch } from "vue";
import { useRouter } from "vue-router";
import { ElMessage } from "element-plus";
import AreaTag from "@/components/base/AreaTag/index.vue";
import { DArrowRight, Search } from "@element-plus/icons-vue";
import { debounce } from "lodash";
import title from "../../assets/title.png";
import defaultTitle from "../../assets/default-icon1.png";
......@@ -265,6 +288,7 @@ import {
} from "@/api/exportControlV2.0";
import { getRelateNews } from "@/api/finance";
import RuleSubsidiaryDialog from "../../../entityList/components/sanctionsOverview/components/listPage/RuleSubsidiaryDialog.vue";
import { getReasonAndSan } from "@/api/finance";
import { useRoute } from "vue-router";
import { useGotoCompanyPages } from "@/router/modules/company";
......@@ -365,7 +389,7 @@ const getSanctionOverviewList = async () => {
// 单次制裁-制裁概况-制裁背景
const timelinePage = ref(1);
const timelinePageSize = ref(3);
const timelinePageSize = ref(5);
const hasMore = ref(true);
const getSanctionBackground = async (isLoadMore = false) => {
......@@ -594,6 +618,18 @@ const entityDistribution = ref([
gradient: "linear-gradient(270deg, rgba(5,95,194,1) 0%, rgba(255,255,255,0) 100%)"
}
]);
const reasonHistoryList = ref([]);
const getReasonHistoryList = async () => {
try {
const res = await getReasonAndSan(sanRecordId.value);
console.log("制裁原因及相关历史制裁", res);
reasonHistoryList.value = res;
} catch (error) {
console.log(error);
}
};
const sanTypeId = ref("");
onMounted(() => {
// 获取路由参数中的sanTypeId
......@@ -609,6 +645,8 @@ onMounted(() => {
getSanctionBackground();
// 单次制裁-制裁概况-制裁清单
getSanctionOverviewList();
// 单次制裁-制裁概况-制裁原因及相关历史制裁
getReasonHistoryList();
});
</script>
......@@ -828,7 +866,7 @@ onMounted(() => {
.left-bottom-content {
padding: 20px 25px 0 25px;
height: calc(100% - 45px); // 减去标题高度
height: calc(100% - 20px); // 减去标题高度
display: flex;
flex-direction: column;
......@@ -836,7 +874,7 @@ onMounted(() => {
display: flex;
flex-direction: column;
gap: 24px;
margin-bottom: 24px;
margin-bottom: 5px;
overflow-y: auto; // 允许垂直滚动
flex: 1; // 占据剩余空间
padding-right: 10px; // 防止滚动条遮挡内容
......@@ -1183,6 +1221,95 @@ onMounted(() => {
}
}
}
.reason-history {
display: flex;
flex-direction: column;
gap: 16px;
padding: 5px 24px;
min-height: 500px;
max-height: 1000px;
margin-bottom: 15px;
.reason-history-item {
.item-header {
border-top: 1px solid #ddd;
border-bottom: 1px solid #ddd;
padding: 15px;
display: flex;
justify-content: space-between;
align-items: center;
.item-header-title {
display: flex;
align-items: center;
width: 85%;
.item-header-title-idx {
width: 24px;
height: 24px;
border-radius: 50%;
background-color: rgb(231, 243, 255);
font-size: 12px;
line-height: 24px;
text-align: center;
}
.item-header-title-text {
margin-left: 13px;
font-size: 16px;
font-weight: 700;
line-height: 28px;
font-family: "Source Han Sans CN";
color: rgb(59, 65, 75);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
}
.item-header-domain {
// min-width: 100px;
}
}
.item-content {
padding: 10px 20px;
border-radius: 4px;
.item-content-record {
position: relative;
margin-top: 20px;
// border-left: 3px solid rgb(5, 95, 194);
background-color: rgb(247, 248, 249);
padding: 10px 0;
border-radius: 4px;
&::before {
content: "";
position: absolute;
left: 0;
top: 10px;
bottom: 10px;
width: 4px;
background-color: rgb(5, 95, 194);
}
.item-content-record-header {
padding: 0px 15px;
font-size: 16px;
font-weight: 700;
height: 30px;
line-height: 30px;
color: rgb(5, 95, 194);
font-family: "Source Han Sans CN";
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.item-content-record-content {
padding: 5px 15px;
font-size: 16px;
font-family: "Source Han Sans CN";
color: rgb(59, 65, 75);
line-height: 30px;
}
}
}
}
}
}
}
}
......
......@@ -108,7 +108,7 @@ import router from "@/router";
// 处理点击实体清单原文按钮
const handleClickOriginalText = () => {
// 打开新标签页
window.open(`/exportControl/origin?id=${sanRecordId.value}`, "_blank");
window.open(`/finance/origin?id=${sanRecordId.value}`, "_blank");
};
// 获取URL参数
......@@ -241,7 +241,6 @@ const handlePageChange = async newPage => {
await fetchSanctionData();
};
// ========== 选择某项 ==========
const selectSanction = async item => {
selectedSanctionId.value = item.id;
router.replace({
......@@ -254,6 +253,7 @@ const selectSanction = async item => {
sanctionModalVisible.value = false;
console.log("跳转URL:", window.location.href);
// 根据最新URL参数刷新当前页面
window.sessionStorage.setItem("curTabName", item.postDate + " 《实体清单新增条目》");
window.open(`${window.location.pathname}?id=${item.id}&sanTypeId=${item.sanTypeId}`, "_self");
};
......
......@@ -15,26 +15,21 @@
<div class="header-right">
<!-- 中英文切换开关 -->
<div class="toggle-group">
<span :class="{ active: !showChinese }">英文</span>
<el-switch
v-model="showChinese"
active-text="中"
inactive-text="英"
:inline-prompt="true"
@change="handleToggleChange"
/>
<span :class="{ active: showChinese }">中文</span>
<!-- <span :class="{ active: !showChinese }">英文</span> -->
<el-switch v-model="showChinese" @change="handleToggleChange" />
<img :src="transIcon" alt="" />
<span :class="{ active: showChinese }">显示原文</span>
</div>
<!-- 下载按钮 -->
<el-button type="primary" :icon="Download" @click="handleDownload"> 下载 </el-button>
<el-button plain :icon="Download" @click="handleDownload"> 下载 </el-button>
</div>
</div>
<!-- 外层滚动容器,统一控制两侧滚动 -->
<div class="report-box" ref="reportBoxRef">
<div class="pdf-pane-wrap" :class="{ 'center-mode': !showChinese }">
<div class="pdf-pane-wrap" v-if="showChinese" :class="{ 'center-mode': !showChinese }">
<pdf ref="leftPdfRef" :pdfUrl="headerTitle.srcUrl" class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap" v-if="showChinese">
<div class="pdf-pane-wrap">
<pdf ref="rightPdfRef" :pdfUrl="headerTitle.transUrl" class="pdf-pane-inner" />
</div>
</div>
......@@ -47,6 +42,7 @@ import { ref, onMounted, watch, computed } from "vue";
import { Download } from "@element-plus/icons-vue";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import title from "../assets/title.png";
import transIcon from "../assets/icon-translation.png";
import pdf from "./pdf.vue";
const leftPdfRef = ref(null);
......@@ -299,15 +295,15 @@ onMounted(() => {
}
}
:deep(.el-button) {
--el-button-bg-color: #055fc2;
--el-button-border-color: #055fc2;
--el-button-hover-bg-color: #044c9b;
--el-button-hover-border-color: #044c9b;
// :deep(.el-button) {
// --el-button-bg-color: #055fc2;
// --el-button-border-color: #055fc2;
// --el-button-hover-bg-color: #044c9b;
// --el-button-hover-border-color: #044c9b;
font-size: 14px;
padding: 10px 20px;
}
// font-size: 14px;
// padding: 10px 20px;
// }
}
}
......@@ -318,6 +314,8 @@ onMounted(() => {
display: flex;
overflow-y: auto;
overflow-x: hidden;
// ✅ 添加居中对齐
justify-content: center;
}
.pdf-pane-wrap {
......@@ -330,6 +328,8 @@ onMounted(() => {
&.center-mode {
flex: 0 0 100%;
max-width: 100%;
// ✅ 添加居中样式
width: 728px; // 约一半宽度,保持单栏时美观
margin: 0 auto;
}
}
......
<template>
<div class="entity-list">
<div class="wrap">
<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 class="header-top">
<div class="header-top-left">
<img :src="headerTitle.img" alt="" />
<div>
<div class="title">{{ headerTitle.title }}</div>
<div class="en-title">
{{ headerTitle.sanTitle }}
</div>
</div>
</div>
<div class="header-top-right">
<div class="image-name-box">
<div class="image"><img :src="headerTitle.postOrgLogoUrl" alt="" /></div>
<div class="name">{{ headerTitle.postOrgName }}</div>
</div>
<div class="time">{{ headerTitle.postDate }}</div>
</div>
</div>
</div>
<div class="main">
<div class="main-header">
<div class="header-left">实体清单制裁文件</div>
<div class="header-right">
<!-- 中英文切换开关 -->
<div class="toggle-group">
<!-- <span :class="{ active: !showChinese }">英文</span> -->
<el-switch v-model="showChinese" @change="handleToggleChange" />
<img :src="transIcon" alt="" />
<span :class="{ active: showChinese }">显示原文</span>
<div>实体清单制裁文件</div>
<div class="btn-box">
<div class="translate">
<div class="search-input-wrap" v-if="showSearchInput">
<input
v-model="searchKeywordText"
class="search-input"
placeholder="回车查询"
@keyup.enter="handleSearchInPdf"
/>
<div class="search-match-count">{{ matchInfo.current }}/{{ matchInfo.total }}</div>
<button
class="search-nav-btn"
type="button"
@click="handlePrevMatch"
:disabled="matchInfo.total === 0 || matchInfo.current <= 1"
>
上一个
</button>
<button
class="search-nav-btn"
type="button"
@click="handleNextMatch"
:disabled="matchInfo.total === 0 || matchInfo.current >= matchInfo.total"
>
下一个
</button>
</div>
<div class="switch">
<el-switch v-model="valueSwitch" />
</div>
<div class="translate-image">
<img
class="translate-icon"
src="../assets/icon-translation.png"
alt=""
style="
width: 16px;
height: 16px;
max-width: 16px;
max-height: 16px;
display: block;
object-fit: contain;
"
/>
</div>
<div class="translate-text">{{ "显示原文" }}</div>
</div>
<div class="btn" @click="handleDownload">
<div class="icon">
<img src="../assets/image-pdf.png" alt="" />
</div>
<div class="text">{{ "下载" }}</div>
</div>
<!-- 下载按钮 -->
<el-button plain :icon="Download" @click="handleDownload"> 下载 </el-button>
</div>
</div>
<!-- 外层滚动容器,统一控制两侧滚动 -->
<div class="report-box" ref="reportBoxRef">
<div class="pdf-pane-wrap" v-if="showChinese" :class="{ 'center-mode': !showChinese }">
<pdf ref="leftPdfRef" :pdfUrl="headerTitle.srcUrl" class="pdf-pane-inner" />
<div class="report-box">
<div class="pdf-pane-wrap" v-if="valueSwitch && reportUrlEnWithPage">
<pdf ref="leftPdfRef" :pdfUrl="reportUrlEnWithPage" class="pdf-pane-inner" />
</div>
<div class="pdf-pane-wrap">
<pdf ref="rightPdfRef" :pdfUrl="headerTitle.transUrl" class="pdf-pane-inner" />
<div class="pdf-pane-wrap" :class="{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf
:key="`right-pdf-${valueSwitch ? 'split' : 'full'}`"
ref="rightPdfRef"
:pdfUrl="reportUrlWithPage"
class="pdf-pane-inner"
/>
</div>
</div>
</div>
......@@ -38,16 +96,23 @@
</template>
<script setup>
import { ref, onMounted, watch, computed } from "vue";
import { Download } from "@element-plus/icons-vue";
import { computed, ref, onMounted, watch } from "vue";
import pdf from "./pdf.vue";
import { getThinkTankReportSummary, getThinkTankReportcontentUrl } from "@/api/thinkTank/overview";
import { useRoute, useRouter } from "vue-router";
import { getSingleSanctionOverview } from "@/api/exportControlV2.0.js";
import title from "../assets/title.png";
import transIcon from "../assets/icon-translation.png";
import pdf from "./pdf.vue";
const leftPdfRef = ref(null);
const rightPdfRef = ref(null);
const reportBoxRef = ref(null);
const router = useRouter();
const route = useRoute();
const reportUrl = ref("");
const reportUrlEn = ref("");
const thinkInfo = ref({});
const defaultPdfPage = ref(1);
const sourceCurrentPage = ref(Number(route.query.currentPage) || 1);
const sourcePageSize = ref(Number(route.query.pageSize) || 12);
const opinionId = ref(route.query.opinionId || "");
const opinionContent = ref(route.query.opinionContent || "");
const headerTitle = ref({
img: title,
......@@ -56,47 +121,188 @@ const headerTitle = ref({
srcUrl: "",
transUrl: ""
});
const sanRecordId = ref("");
const isSyncing = ref(false);
const buildPdfPageUrl = url => {
if (!url) return "";
return `${url}#page=${defaultPdfPage.value}`;
};
// ✅ 控制中文 PDF 显示
const showChinese = ref(true);
const reportUrlWithPage = computed(() => buildPdfPageUrl(headerTitle.value.srcUrl));
const reportUrlEnWithPage = computed(() => buildPdfPageUrl(headerTitle.value.transUrl));
const valueSwitch = ref(true);
const showSearchInput = ref(true);
const searchKeywordText = ref("");
const leftPdfRef = ref(null);
const rightPdfRef = ref(null);
const matchInfo = ref({ current: 0, total: 0 });
const activePdfRef = ref(null);
const clearPdfSearchState = () => {
activePdfRef.value = null;
matchInfo.value = { current: 0, total: 0 };
const leftPdf = leftPdfRef.value;
const rightPdf = rightPdfRef.value;
if (leftPdf && typeof leftPdf.clearSearch === "function") {
leftPdf.clearSearch();
}
if (rightPdf && typeof rightPdf.clearSearch === "function") {
rightPdf.clearSearch();
}
};
// ✅ 计算当前显示模式
const showMode = computed(() => {
return showChinese.value ? "both" : "en";
});
const updateMatchInfo = () => {
const pdf = activePdfRef.value;
if (pdf && typeof pdf.getMatchInfo === "function") {
matchInfo.value = pdf.getMatchInfo();
return;
}
matchInfo.value = { current: 0, total: 0 };
};
watch(
() => searchKeywordText.value,
val => {
const keyword = String(val ?? "").trim();
if (!keyword) {
clearPdfSearchState();
}
}
);
watch(
() => valueSwitch.value,
() => {
// 切换「显示原文」会导致 PDF 重新挂载/布局变化:清空搜索与计数,回到初始状态
searchKeywordText.value = "";
clearPdfSearchState();
}
);
// ✅ 切换中英文显示
const handleToggleChange = value => {
console.log("切换中英文显示:", value ? "中英双栏" : "仅英文");
showChinese.value = value;
const handleSearchInPdf = async () => {
const keyword = searchKeywordText.value?.trim();
if (!keyword) return;
activePdfRef.value = null;
matchInfo.value = { current: 0, total: 0 };
const leftPdf = leftPdfRef.value;
const rightPdf = rightPdfRef.value;
let page = 0;
let targetRef = null;
if (leftPdf && typeof leftPdf.searchKeyword === "function") {
page = await leftPdf.searchKeyword(keyword);
if (page) targetRef = leftPdf;
}
if (!page && rightPdf && typeof rightPdf.searchKeyword === "function") {
page = await rightPdf.searchKeyword(keyword);
if (page) targetRef = rightPdf;
}
if (page && targetRef && typeof targetRef.goToPage === "function") {
targetRef.goToPage(page);
activePdfRef.value = targetRef;
updateMatchInfo();
} else {
try {
const { ElMessage } = await import("element-plus");
ElMessage.warning("未找到包含该关键词的页面");
} catch (_) {}
}
};
const handlePrevMatch = () => {
const pdf = activePdfRef.value;
if (!pdf || typeof pdf.prevMatch !== "function") return;
pdf.prevMatch();
updateMatchInfo();
};
const handleNextMatch = () => {
const pdf = activePdfRef.value;
if (!pdf || typeof pdf.nextMatch !== "function") return;
pdf.nextMatch();
updateMatchInfo();
};
// 下载:中英文都下载,与政令原文页相同的 fetch → blob → a 标签触发下载
const downloadOnePdf = async (url, filename) => {
const response = await fetch(url, {
method: "GET",
headers: { "Content-Type": "application/pdf" }
});
if (!response.ok) throw new Error(`HTTP error! status: ${response.status}`);
const blob = await response.blob();
const blobUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = blobUrl;
link.download = filename;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
window.URL.revokeObjectURL(blobUrl);
};
// ✅ 下载功能
const handleDownload = async () => {
const files = [
{ url: headerTitle.value.srcUrl, name: "英文原版.pdf" },
{ url: headerTitle.value.transUrl, name: "中文翻译.pdf" }
];
for (const file of files) {
if (file.url) {
try {
const response = await fetch(file.url);
const blob = await response.blob();
const link = document.createElement("a");
link.href = URL.createObjectURL(blob);
link.download = file.name;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
URL.revokeObjectURL(link.href);
} catch (error) {
console.error(`下载${file.name}失败:`, error);
}
const urlZh = headerTitle.value.srcUrl ? String(headerTitle.value.srcUrl) : "";
const urlEn = headerTitle.value.transUrl ? String(headerTitle.value.transUrl) : "";
if (!urlZh && !urlEn) {
try {
const { ElMessage } = await import("element-plus");
ElMessage.warning("暂无下载链接");
} catch (_) {}
return;
}
const baseName = (thinkInfo.value?.name || "报告原文").replace(/[/\\?%*:|"<>]/g, "-");
const { ElMessage } = await import("element-plus");
try {
if (urlZh) {
await downloadOnePdf(urlZh, `${baseName}_中文.pdf`);
}
if (urlEn) {
if (urlZh) await new Promise(r => setTimeout(r, 300));
await downloadOnePdf(urlEn, `${baseName}_英文.pdf`);
}
if (urlZh || urlEn) {
ElMessage.success(urlZh && urlEn ? "已下载中文、英文两份 PDF" : "下载成功");
}
} catch (error) {
console.error("下载失败:", error);
ElMessage.error("PDF 下载失败,请稍后重试");
}
};
/** 接口可能返回对象或数组;字段名兼容 snake/camel */
const normalizeReportSummaryRow = row => {
if (!row || typeof row !== "object") return {};
return {
...row,
name: row.name || "",
imageUrl: row.imageUrl || row.image || "",
ename: row.ename || row.nameEn || row.englishName || "",
times: row.times || row.postDate || "",
thinkTankName: row.thinkTankName || row.thinktankName || "",
thinkTankLogoUrl: row.thinkTankLogoUrl || row.thinktankLogo || ""
};
};
const applyReportOriginalDocumentTitle = title => {
const text = String(title || "").trim();
if (!text) return;
window.sessionStorage.setItem("reportOriginalTabName", text);
document.title = text;
};
// 获取报告全局信息
const handleGetThinkTankReportSummary = async () => {
try {
const res = await getThinkTankReportSummary(router.currentRoute._value.params.id);
console.log("报告全局信息", res);
if (res.code === 200 && res.data) {
const raw = res.data;
const first = Array.isArray(raw) ? raw[0] : raw;
const normalized = normalizeReportSummaryRow(first);
thinkInfo.value = normalized;
applyReportOriginalDocumentTitle(normalized.name);
}
} catch (error) {
console.error("获取报告全局信息error", error);
}
};
......@@ -104,7 +310,6 @@ const getUrlParams = () => {
const urlParams = new URLSearchParams(window.location.search);
sanRecordId.value = urlParams.get("id") || "";
};
const getSingleSanctionOverviewData = async () => {
if (!sanRecordId.value) return;
try {
......@@ -127,195 +332,430 @@ const getSingleSanctionOverviewData = async () => {
// 更新头部信息
headerTitle.value = {
...singleSanctionOverview,
...headerTitle.value,
title: `${dateStr}${singleSanctionOverview.sanTitleZh || singleSanctionOverview.sanTitle}》`,
department: singleSanctionOverview.fileCode || "",
srcUrl: singleSanctionOverview.srcUrl || "",
transUrl: singleSanctionOverview.transUrl || ""
};
applyReportOriginalDocumentTitle(headerTitle.value.title || "");
}
} catch (error) {
console.error("获取制裁概况失败:", error);
}
};
// 同步滚动处理
const handleSyncScroll = () => {
if (isSyncing.value) return;
isSyncing.value = true;
requestAnimationFrame(() => {
isSyncing.value = false;
});
//获取原文
const handleGetThinkTankReportcontentUrl = async () => {
try {
const res = await getThinkTankReportcontentUrl(router.currentRoute._value.params.id);
console.log("获取原文", res);
if (res.code === 200 && res.data) {
reportUrl.value = res.data.content;
reportUrlEn.value = res.data.contentEn;
}
} catch (error) {
console.error("获取原文error", error);
}
};
// 监听滚动事件
const setupScrollSync = () => {
const reportBox = reportBoxRef.value;
if (!reportBox) return;
const tabActiveName = ref("报告分析");
reportBox.addEventListener("scroll", handleSyncScroll, { passive: true });
const switchTab = name => {
tabActiveName.value = name;
};
// 监听 PDF 加载完成
watch(
() => [headerTitle.value.srcUrl, headerTitle.value.transUrl],
() => {
setTimeout(() => {
setupScrollSync();
}, 1000);
},
{ deep: true }
);
onMounted(() => {
onMounted(async () => {
window.sessionStorage.setItem("curTabName", "实体清单原文");
handleGetThinkTankReportSummary();
handleGetThinkTankReportcontentUrl();
console.log("原文展示");
getUrlParams();
getSingleSanctionOverviewData();
setTimeout(() => {
setupScrollSync();
}, 500);
});
</script>
<style scoped lang="scss">
// * {
// margin: 0;
// padding: 0;
// }
.entity-list {
width: 100%;
height: 100%;
overflow-y: auto;
<style lang="scss" scoped>
.wrap {
overflow-y: hidden;
height: 100vh;
display: flex;
flex-direction: column;
.header {
width: 100%;
height: 148px;
background-color: #fff;
padding-top: 16px;
height: 88px;
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);
background: rgba(255, 255, 255, 1);
position: sticky;
top: 0;
z-index: 99999;
overflow: hidden;
.header-title {
width: 1601px;
height: 72px;
background-color: rgba(246, 250, 255, 1);
.header-top {
margin: 0 auto;
border-radius: 10px;
border: 2px solid rgba(174, 214, 255, 1);
margin-top: 20px;
width: 1600px;
display: flex;
align-items: center;
margin-bottom: 12px;
position: relative;
img {
width: 54px;
height: 54px;
margin-left: 15px;
margin-right: 11px;
justify-content: space-between;
.header-top-left {
display: flex;
img {
width: 44px;
height: 54px;
}
.title {
margin-left: 20px;
height: 26px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
text-align: left;
}
.en-title {
margin-top: 4px;
margin-left: 20px;
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
/* 👇 下面是 两行文本超出省略 核心代码 */
display: -webkit-box;
-webkit-line-clamp: 1;
/* 限制显示 2 行 */
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-all;
}
.tag-box {
margin-top: 11px;
display: flex;
gap: 8px;
margin-left: 20px;
}
}
.title {
font-size: 20px;
font-weight: 700;
font-family: "Microsoft YaHei";
line-height: 26px;
color: rgb(59, 65, 75);
.header-top-right {
display: flex;
flex-direction: column;
text-align: right;
align-items: flex-end;
.image-name-box {
width: 200px;
height: 24px;
gap: 6px;
text-align: right;
display: flex;
justify-content: flex-end;
.name {
height: 24px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
.image {
width: 16px;
height: 16px;
margin-top: 5px;
img {
width: 100%;
height: 100%;
}
}
}
.time {
height: 24px;
margin-top: 5px;
color: rgba(95, 101, 108, 1);
font-family: Microsoft YaHei;
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: right;
}
}
}
.header-bottom {
margin: 0 auto;
margin-top: 30px;
width: 1600px;
height: 48px;
display: flex;
justify-content: space-between;
.tab-box {
width: 224px;
height: 48px;
display: flex;
gap: 24px;
.department {
font-size: 16px;
font-weight: 400;
font-family: "Microsoft YaHei";
line-height: 24px;
color: rgb(95, 101, 108);
.tab {
width: 94px;
height: 48px;
display: flex;
align-items: center;
justify-content: center;
gap: 4px;
cursor: pointer;
border-bottom: 2px solid transparent;
.icon {
width: 16px;
height: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
height: 24px;
color: rgba(59, 65, 75, 1);
font-family: Microsoft YaHei;
font-size: 18px;
font-weight: 400;
line-height: 24px;
}
.textActive {
color: rgba(5, 95, 194, 1);
font-weight: 700;
}
}
.tabActive {
border-bottom: 2px solid rgba(5, 95, 194, 1);
}
}
}
}
.main {
margin: 0 auto;
margin: 0px auto;
margin-top: 20px;
background: rgb(255, 255, 255);
width: 1601px;
height: calc(100vh - 148px);
width: 1600px;
// height: 900px;
margin-bottom: 20px;
border: 1px solid rgb(234, 236, 238);
padding-bottom: 15px;
border: 1px, solid, rgb(234, 236, 238);
box-shadow: 0 0 20px 0 rgba(25, 69, 130, 0.1);
.main-header {
height: 64px;
/* box-sizing: border-box; */
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-style: Bold;
font-size: 20px;
font-weight: 700;
line-height: 26px;
letter-spacing: 0px;
width: 1456px;
text-align: left;
display: flex;
align-items: center;
justify-content: space-between;
align-items: center;
overflow: visible;
.header-right {
.btn-box {
display: flex;
align-items: center;
gap: 24px;
gap: 8px;
flex-shrink: 0;
.toggle-group {
.translate {
display: flex;
flex-wrap: nowrap;
align-items: center;
gap: 10px;
height: 24px;
margin-right: 16px;
flex-shrink: 0;
span {
font-size: 14px;
color: rgb(150, 150, 150);
transition: color 0.3s;
:deep(.el-switch) {
width: 22px !important;
height: 14px !important;
margin-bottom: 5px;
margin-right: 8px;
}
&.active {
color: rgb(5, 95, 194);
font-weight: 600;
:deep(.el-switch__core) {
width: 22px !important;
height: 14px !important;
min-width: 22px !important;
}
:deep(.el-switch__button),
:deep(.el-switch__action) {
width: 10px !important;
height: 10px !important;
}
/* 打开时圆球从左边移到最右边:轨道 22px - 圆球 10px = 12px */
:deep(.el-switch.is-checked .el-switch__button),
:deep(.el-switch.is-checked .el-switch__action) {
transform: translateX(6px) !important;
}
.translate-image {
display: flex;
width: 16px;
height: 16px;
overflow: hidden;
img {
width: 100%;
height: 100%;
}
}
:deep(.el-switch) {
--el-switch-on-color: #055fc2;
--el-switch-off-color: #e6e7e8;
.translate-text {
font-size: 14px;
font-weight: 400;
line-height: 22px;
}
}
.el-switch__label {
color: #fff;
font-size: 12px;
font-weight: 600;
.btn {
width: 88px;
height: 32px;
&.is-active {
color: #fff;
}
box-sizing: border-box;
border: 1px solid rgba(230, 231, 232, 1);
border-radius: 6px;
background: rgba(255, 255, 255, 1);
display: flex;
gap: 8px;
cursor: pointer;
.icon {
width: 16px;
height: 16px;
display: inline-flex;
margin-top: 8px;
margin-left: 16px;
img {
width: 100%;
height: 100%;
}
}
.text {
margin-top: 4px;
width: 32px;
height: 24px;
color: rgb(59, 65, 75);
font-family: "Source Han Sans CN";
font-size: 16px;
font-weight: 400;
line-height: 24px;
letter-spacing: 0px;
text-align: left;
}
}
.search-btn {
cursor: pointer;
}
.search-input-wrap {
display: inline-flex;
align-items: center;
gap: 8px;
margin-left: 4px;
flex-shrink: 0;
}
// :deep(.el-button) {
// --el-button-bg-color: #055fc2;
// --el-button-border-color: #055fc2;
// --el-button-hover-bg-color: #044c9b;
// --el-button-hover-border-color: #044c9b;
.search-input {
width: 160px;
height: 24px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
background: rgba(246, 250, 255, 1);
border-radius: 4px;
padding: 0 10px;
font-family: "Source Han Sans CN";
font-size: 14px;
line-height: 22px;
outline: none;
}
.search-match-count {
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
min-width: 48px;
text-align: center;
flex-shrink: 0;
}
.search-nav-btn {
width: 68px;
height: 24px;
box-sizing: border-box;
border: 1px solid rgba(231, 243, 255, 1);
background: rgba(246, 250, 255, 1);
border-radius: 4px;
font-family: "Source Han Sans CN";
font-weight: 400;
font-size: 14px;
line-height: 22px;
cursor: pointer;
padding: 0;
flex-shrink: 0;
white-space: nowrap;
}
// font-size: 14px;
// padding: 10px 20px;
// }
.search-nav-btn:disabled {
opacity: 0.45;
cursor: not-allowed;
}
}
}
.report-box {
margin-left: 70px;
width: 1456px;
height: calc(100% - 64px);
height: 881px;
display: flex;
overflow-y: auto;
/* 右侧统一滚动条,控制两侧原文+译文一起滚动 */
overflow-x: hidden;
// ✅ 添加居中对齐
justify-content: center;
}
.pdf-pane-wrap {
......@@ -323,15 +763,11 @@ onMounted(() => {
max-width: 50%;
height: 100%;
min-width: 0;
transition: all 0.3s;
&.center-mode {
flex: 0 0 100%;
max-width: 100%;
// ✅ 添加居中样式
width: 728px; // 约一半宽度,保持单栏时美观
margin: 0 auto;
}
}
.pdf-pane-wrap.is-full {
flex: 0 0 100%;
max-width: 100%;
}
.pdf-pane-inner {
......
<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>
......@@ -17,7 +17,6 @@ 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: {
......@@ -33,7 +32,6 @@ export default {
const loading = ref(true);
const renderedPageCount = ref(0);
let resolveRenderAll = null;
const waitAllPagesRendered = () => {
if (pageCount.value > 0 && renderedPageCount.value >= pageCount.value) {
return Promise.resolve();
......@@ -42,12 +40,14 @@ export default {
resolveRenderAll = resolve;
});
};
// pdfjs 的 document 对象内部使用 #private 字段,用 shallowRef 保持为原始对象引用
// pdfjs 的 document 对象内部使用 #private 字段,
// 若被 Vue 响应式深度代理会触发 "Cannot read from private field"。
// 因此用 shallowRef 保持为原始对象引用。
const pdfDocRef = shallowRef(null);
const searchKey = ref("");
const matchList = ref([]);
const matchIdx = ref(0);
// pdfjs 3.x 的 renderTextLayer 在不同入口下导出不一致,这里做一次缓存 + 兜底加载
const pdfjsApiRef = shallowRef(pdfjsLib);
// 保存 canvas
......@@ -69,7 +69,7 @@ export default {
return urlPart;
};
// 清空所有高亮
// 清空所有高亮(不销毁 textLayer)
const clearHighlights = () => {
Object.values(overlayMap).forEach(layer => {
if (!layer) return;
......@@ -78,7 +78,7 @@ export default {
});
};
// 重置搜索状态
// 重置搜索状态:清空关键词、匹配列表与高亮
const clearSearch = () => {
searchKey.value = "";
matchList.value = [];
......@@ -86,7 +86,7 @@ export default {
clearHighlights();
};
// 渲染单页 PDF
// 渲染单页 PDF(canvas + textLayer)
const renderPage = async (pdf, pageNum) => {
const pdfPage = await pdf.getPage(pageNum);
const canvas = canvasMap[pageNum];
......@@ -94,6 +94,7 @@ export default {
if (!canvas || !textLayer) return;
// 以画布的可视宽度为基准自适应缩放,避免 CSS 强行拉伸导致 textLayer/高亮错位
const baseViewport = pdfPage.getViewport({ scale: 1 });
const desiredWidth = canvas.clientWidth || 726;
const scale = desiredWidth / baseViewport.width;
......@@ -102,19 +103,23 @@ export default {
const context = canvas.getContext("2d");
canvas.width = viewport.width;
canvas.height = viewport.height;
// 保证 canvas 不再被 CSS 拉伸,和 textLayer 共享同一坐标系
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 = "";
// pdf.js v5 text layer 依赖 scale-factor 参与定位计算
textLayer.style.setProperty("--scale-factor", String(viewport.scale || 1));
await pdfPage.render({ canvasContext: context, viewport }).promise;
// 渲染 textLayer(pdfjs-dist 3.x):使用 renderTextLayer(不要用 TextLayer 构造器)
try {
const textContent = await pdfPage.getTextContent();
let api = pdfjsApiRef.value || pdfjsLib;
let rt = api?.renderTextLayer;
// 兜底:某些入口下 renderTextLayer 不在 pdfjsLib 上,尝试 legacy 入口
if (typeof rt !== "function") {
try {
const legacy = await import("pdfjs-dist/legacy/build/pdf");
......@@ -128,6 +133,7 @@ export default {
textContent,
container: textLayer,
viewport,
// pdfjs 3.x 需要传入 textDivs 数组
textDivs: [],
enhanceTextSelection: false
}).promise;
......@@ -175,7 +181,7 @@ export default {
}
};
// 搜索关键词 + 高亮
// 搜索关键词 + 高亮(记录每个命中的子串范围)
const doSearch = async () => {
const doc = pdfDocRef.value;
const key = searchKey.value.trim();
......@@ -184,6 +190,7 @@ export default {
matchIdx.value = 0;
if (!doc || !key) return;
// 首次搜索时确保所有页的 textLayer 已渲染完成,避免“越搜越多”
await waitAllPagesRendered();
for (let pageNum = 1; pageNum <= doc.numPages; pageNum++) {
......@@ -214,7 +221,7 @@ export default {
const el = m?.el;
if (!el) return;
clearHighlights();
// 用 Range 精确计算“子串”在页面上的矩形位置,再画黄色块,避免把整段 span 都标黄
const textNode = el.firstChild;
if (textNode && textNode.nodeType === Node.TEXT_NODE) {
try {
......@@ -242,7 +249,7 @@ export default {
}
}
// 滚动到匹配位置
// 优先只滚动右侧 report-box,避免触发整页滚动导致 header 遮挡
const container = el.closest(".report-box");
if (container) {
const TOP_OFFSET = 72;
......@@ -285,12 +292,6 @@ export default {
}
};
// 获取容器元素(用于同步滚动)
const getContainer = () => {
const firstCanvas = canvasMap[1];
return firstCanvas ? firstCanvas.closest(".report-box") : null;
};
watch(
() => props.pdfUrl,
newVal => {
......@@ -313,8 +314,7 @@ export default {
matchIdx,
searchKeyword,
clearSearch,
goToPage,
getContainer
goToPage
};
}
};
......@@ -349,11 +349,13 @@ canvas {
line-height: 1;
}
/* 不展示整页“文字层”,只在命中时显示黄色背景 */
.textLayer :deep(span) {
position: absolute;
transform-origin: 0% 0%;
white-space: pre;
line-height: 1;
/* pdf.js v5 TextLayer:用变量计算真实字形盒子尺寸,否则背景宽高会不准 */
font-size: calc(var(--font-height, 0px) * var(--scale-factor, 1));
transform: scaleX(var(--scale-x, 1));
color: transparent;
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论