Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
risk-monitor
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
蔡建
risk-monitor
Commits
c8dfe65c
提交
c8dfe65c
authored
4月 14, 2026
作者:
闫鹏
浏览文件
操作
浏览文件
下载
差异文件
合并分支 'yp-dev' 到 'pre'
Yp dev 查看合并请求
!341
上级
44759732
e74a1041
流水线
#469
已通过 于阶段
in 6 分 4 秒
变更
30
流水线
1
显示空白字符变更
内嵌
并排
正在显示
30 个修改的文件
包含
3658 行增加
和
743 行删除
+3658
-743
exportControlV2.0.js
src/api/exportControlV2.0.js
+3
-2
back.vue
src/components/base/RelationChart/back.vue
+416
-0
index.vue
src/components/base/RelationChart/index.vue
+73
-287
index.vue
src/styles/components/RelationChart/index.vue
+23
-13
index.vue
src/views/exportControl/index.vue
+3
-1
index.vue
...omponents/sanctionsOverview/components/listPage/index.vue
+8
-0
index-back.vue
...ntrol/v2.0EntityList/components/deepMining/index-back.vue
+1364
-0
index.vue
...ortControl/v2.0EntityList/components/deepMining/index.vue
+105
-52
index.vue
...s/sanctionsOverview/components/introductionPage/index.vue
+1
-1
index.vue
...mercialControlList/components/sanctionsOverview/index.vue
+14
-15
index.vue
src/views/finance/commercialControlList/index.vue
+25
-4
index.vue
...ws/finance/entityList/components/dataStatistics/index.vue
+12
-4
back.vue
...ance/entityList/components/deepMining/components/back.vue
+190
-16
constrainedAssociation.vue
...mponents/deepMining/components/constrainedAssociation.vue
+140
-25
index.vue
src/views/finance/entityList/components/deepMining/index.vue
+101
-67
RuleSubsidiaryDialog.vue
...ionsOverview/components/listPage/RuleSubsidiaryDialog.vue
+1
-8
index.vue
...omponents/sanctionsOverview/components/listPage/index.vue
+1
-1
index.vue
src/views/finance/index.vue
+15
-18
image-pdf.png
src/views/finance/singleSanction/assets/image-pdf.png
+0
-0
index.vue
...inance/singleSanction/components/dataStatistics/index.vue
+12
-9
index.vue
...ws/finance/singleSanction/components/deepMining/index.vue
+0
-5
index.vue
...ents/impactAnalysis/components/industrialImpact/index.vue
+0
-5
index.vue
...onents/impactAnalysis/components/researchImpact/index.vue
+0
-5
index.vue
...inance/singleSanction/components/impactAnalysis/index.vue
+0
-4
index.vue
...nce/singleSanction/components/sanctionsOverview/index.vue
+133
-6
index.vue
src/views/finance/singleSanction/index.vue
+2
-2
index-back.vue
src/views/finance/singleSanction/originPage/index-back.vue
+20
-20
index.vue
src/views/finance/singleSanction/originPage/index.vue
+591
-155
pdf-back.vue
src/views/finance/singleSanction/originPage/pdf-back.vue
+385
-0
pdf.vue
src/views/finance/singleSanction/originPage/pdf.vue
+20
-18
没有找到文件。
src/api/exportControlV2.0.js
浏览文件 @
c8dfe65c
...
...
@@ -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/getChain
Fishbone
`
,
url
:
`/api/chain/getChain
Nodes
`
,
params
});
}
...
...
src/components/base/RelationChart/back.vue
0 → 100644
浏览文件 @
c8dfe65c
<
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
>
src/components/base/RelationChart/index.vue
浏览文件 @
c8dfe65c
<
template
>
<RelationGraph
style=
"width: 100%; height: 100%"
ref=
"graphRef"
:options=
"graphOptionsH"
:on-node-click=
"onNodeClick"
:on-line-click=
"onLineClick"
>
<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=
"
{
<div
class=
"custom-node"
:style=
"
{
backgroundColor: node.color || 'var(--color-primary-50)'
}"
>
<span
:style=
"
{
color: node.fontColor || '#ffffff',
fontSize: node.customFontSize || '24px', // 可以从数据中读取
fontSize: node.customFontSize || '24px',
fontWeight: 'normal',
textAlign: 'center',
wordBreak: 'break-word',
padding: '0 8px'
}">
}"
>
{{
node
.
text
}}
</span>
</div>
...
...
@@ -22,18 +31,35 @@
</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
=
{
// 定义 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_width: 40,
// max_per_width: 70,
min_per_height
:
'auto'
,
min_per_height
:
"auto"
},
defaultLineMarker
:
{
markerWidth
:
12
,
...
...
@@ -47,26 +73,19 @@ const graphOptionsH = {
defaultLineShape
:
4
,
defaultJunctionPoint
:
"tb"
,
defaultNodeBorderWidth
:
2
,
defaultNodeBorderColor
:
'var(--color-primary-100)'
,
defaultNodeBorderColor
:
"var(--color-primary-100)"
,
defaultLineColor
:
"var(--color-primary-50)"
,
defaultNodeColor
:
"var(--color-primary-50)"
,
defaultNodeFontColor
:
'var(--text-primary-90-color)'
,
// 默认文字颜色:深灰色
// defaultNodeHeight: 'auto'
defaultNodeFontColor
:
"var(--text-primary-90-color)"
};
const
graphOptionsV
=
{
// 基础垂直配置
const
baseGraphOptionsV
=
{
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'
,
from
:
"left"
,
// 默认从左向右
max_per_width
:
"auto"
}
],
defaultLineMarker
:
{
...
...
@@ -78,259 +97,50 @@ const graphOptionsV = {
},
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)'
,
// 默认文字颜色:深灰色
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
.
setOptions
(
currentGraphOptions
.
value
);
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
.
setOptions
(
currentGraphOptions
.
value
);
await
graphInstance
.
setJsonData
(
__graph_json_data
);
await
graphInstance
.
moveToCenter
();
await
graphInstance
.
zoomToFit
();
...
...
@@ -338,24 +148,23 @@ const showVTree = async () => {
};
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
)
{
()
=>
[
props
.
isVerticalChart
,
props
.
isReversed
,
props
.
graphData
]
,
()
=>
{
if
(
props
.
isVerticalChart
)
{
showVTree
();
}
else
{
showHTree
();
}
}
},
{
deep
:
true
}
);
onMounted
(()
=>
{
...
...
@@ -375,30 +184,7 @@ onMounted(() => {
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
;
...
...
src/styles/components/RelationChart/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
>
...
...
src/views/exportControl/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -670,7 +670,9 @@
</el-col>
</
template
>
<
template
v-if=
"activeResourceTab === 'commerce'"
>
<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
:
30
px
;
top
:
12
px
;
right
:
-24px
;
display
:
flex
;
flex-direction
:
column
;
...
...
src/views/exportControl/v2.0CommercialControlList/components/sanctionsOverview/components/listPage/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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%
;
...
...
src/views/exportControl/v2.0EntityList/components/deepMining/index-back.vue
0 → 100644
浏览文件 @
c8dfe65c
<
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
>
src/views/exportControl/v2.0EntityList/components/deepMining/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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.pictur
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
name"
>
{{ item.n
ame }}
</div>
<img
:src=
"
item.image || defaultTitl
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
companyName"
>
{{ item.companyN
ame }}
</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.pictur
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
name"
>
{{ item.n
ame }}
</div>
<img
:src=
"
item.image || defaultTitl
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
companyName"
>
{{ item.companyN
ame }}
</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.pictur
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
name"
>
{{ item.n
ame }}
</div>
<img
:src=
"
item.image || defaultTitl
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
companyName"
>
{{ item.companyN
ame }}
</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.pictur
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
name"
>
{{ item.n
ame }}
</div>
<img
:src=
"
item.image || defaultTitl
e"
alt=
""
class=
"company-icon"
/>
<div
class=
"text"
:title=
"item.
companyName"
>
{{ item.companyN
ame }}
</div>
</div>
</div>
</div>
...
...
@@ -189,10 +193,10 @@
</div>
<div
class=
"text"
>
{{
`中国企业${cnEntityOnChainData
.upstreamI
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
.upstreamI
nternalRate
)}%),受制裁${cnEntityOnChainData
.upstreamE
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
.upstreamE
ntityRate
`中国企业${cnEntityOnChainData
?.upstream?.i
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
?.upstream?.i
nternalRate
)}%),受制裁${cnEntityOnChainData
?.upstream?.e
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
?.upstream?.e
ntityRate
)}%)`
}}
</div>
...
...
@@ -206,10 +210,10 @@
</div>
<div
class=
"text"
>
{{
`中国企业${cnEntityOnChainData
.midstreamI
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
.midstreamI
nternalRate
)}%),受制裁${cnEntityOnChainData
.midstreamE
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
.midstreamE
ntityRate
`中国企业${cnEntityOnChainData
?.midstream?.i
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
?.midstream?.i
nternalRate
)}%),受制裁${cnEntityOnChainData
?.midstream?.e
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
?.midstream?.e
ntityRate
)}%)`
}}
</div>
...
...
@@ -223,10 +227,10 @@
</div>
<div
class=
"text"
>
{{
`中国企业${cnEntityOnChainData
.downstreamI
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
.downstreamI
nternalRate
)}%),受制裁${cnEntityOnChainData
.downstreamE
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
.downstreamE
ntityRate
`中国企业${cnEntityOnChainData
?.downstream?.i
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
?.downstream?.i
nternalRate
)}%),受制裁${cnEntityOnChainData
?.downstream?.e
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
?.downstream?.e
ntityRate
)}%)`
}}
</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
||
[]
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
}
};
});
mainLineLabels
.
value
=
rootCauses
.
map
(
group
=>
group
.
text
||
""
);
}
else
{
fishboneDataList
.
value
=
[];
mainLineLabels
.
value
=
[];
}
// 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
;
...
...
src/views/finance/commercialControlList/components/sanctionsOverview/components/introductionPage/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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>
...
...
src/views/finance/commercialControlList/components/sanctionsOverview/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -7,8 +7,14 @@
<
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
}}
<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>
...
...
@@ -20,23 +26,16 @@
</
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
{
.sanctions-overview
{
width
:
1601px
;
margin
:
0
auto
;
position
:
relative
;
...
...
src/views/finance/commercialControlList/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
([
...
...
src/views/finance/entityList/components/dataStatistics/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
/>
...
...
src/views/finance/entityList/components/deepMining/components/back.vue
浏览文件 @
c8dfe65c
...
...
@@ -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>
<!-- <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
graphData
List
=
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,24 +440,125 @@ 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;
// 获取边详情数据
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
==
"相似"
)
{
if
(
relationType
==
"相似"
&&
res
[
0
]?.
edgeReasonList
?.[
0
]?.
reason
)
{
reason
=
res
[
0
].
edgeReasonList
[
0
].
reason
;
}
tipsInfo
.
value
=
getTipsInfo
(
relationType
,
reason
);
}
else
{
// recordRelation.value = { noRelationVertices: [], relationVoList: [] }
;
graphDataList
.
value
=
[]
;
}
})
.
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
;
...
...
src/views/finance/entityList/components/deepMining/components/constrainedAssociation.vue
浏览文件 @
c8dfe65c
...
...
@@ -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,6 +441,11 @@ const handleClickNode = node => {
}
});
}
else
{
handleLink
(
node
);
}
};
const
handleLink
=
node
=>
{
relationVisible
.
value
=
true
;
curLink
.
value
=
node
;
const
relationType
=
node
.
data
.
relationType
;
...
...
@@ -456,15 +468,86 @@ const handleClickNode = node => {
const
nodeMap
=
new
Map
();
// 辅助函数:添加节点
const
addNode
=
(
id
,
text
,
type
=
"vertex"
)
=>
{
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"
// 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
);
...
...
@@ -482,7 +565,33 @@ const handleClickNode = node => {
};
// 1. 添加出发点 (Vertex)
addNode
(
vertex
.
id
,
vertex
.
name
,
"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
)
{
...
...
@@ -496,7 +605,12 @@ const handleClickNode = node => {
// 如果不同项之间有相同的 detailItem.name,它们在不同图中是隔离的,所以没问题。
const
detailId
=
detailItem
.
name
;
addNode
(
detailId
,
detailItem
.
name
,
"detail"
);
addNode
(
detailId
,
detailItem
.
name
,
"detail"
,
addNode
(
detailId
,
detailItem
.
name
,
"detail"
,
!!
detailItem
.
highlight
)
);
addLine
(
vertex
.
id
,
detailId
,
relationName
);
});
}
...
...
@@ -510,7 +624,8 @@ const handleClickNode = node => {
nodes
:
nodes
,
lines
:
lines
,
// 可以保留原始数据用于调试或额外展示
originalItem
:
item
originalItem
:
item
,
isReversed
:
index
%
2
!==
0
});
}
});
...
...
@@ -531,7 +646,6 @@ const handleClickNode = node => {
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
)
{
...
...
src/views/finance/entityList/components/deepMining/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
.upstreamI
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
.upstreamI
nternalRate
)}%),受制裁${cnEntityOnChainData
.upstreamE
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
.upstreamE
ntityRate
`中国企业${cnEntityOnChainData
?.upstream?.i
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
?.upstream?.i
nternalRate
)}%),受制裁${cnEntityOnChainData
?.upstream?.e
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
?.upstream?.e
ntityRate
)}%)`
}}
</div>
...
...
@@ -226,10 +219,10 @@
</div>
<div
class=
"text"
>
{{
`中国企业${cnEntityOnChainData
.midstreamI
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
.midstreamI
nternalRate
)}%),受制裁${cnEntityOnChainData
.midstreamE
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
.midstreamE
ntityRate
`中国企业${cnEntityOnChainData
?.midstream?.i
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
?.midstream?.i
nternalRate
)}%),受制裁${cnEntityOnChainData
?.midstream?.e
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
?.midstream?.e
ntityRate
)}%)`
}}
</div>
...
...
@@ -243,10 +236,10 @@
</div>
<div
class=
"text"
>
{{
`中国企业${cnEntityOnChainData
.downstreamI
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
.downstreamI
nternalRate
)}%),受制裁${cnEntityOnChainData
.downstreamE
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
.downstreamE
ntityRate
`中国企业${cnEntityOnChainData
?.downstream?.i
nternalCount || 0}家(${formatRate(
cnEntityOnChainData
?.downstream?.i
nternalRate
)}%),受制裁${cnEntityOnChainData
?.downstream?.e
ntityCount || 0}家(${formatRate(
cnEntityOnChainData
?.downstream?.e
ntityRate
)}%)`
}}
</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
.
chainI
d
=
selectedIndustryId
.
value
;
params
.
i
d
=
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
;
};
// 获取上方鱼骨图位置类名
...
...
src/views/finance/entityList/components/sanctionsOverview/components/listPage/RuleSubsidiaryDialog.vue
浏览文件 @
c8dfe65c
...
...
@@ -155,15 +155,8 @@ const getTagStyle = tag => {
// 跳转公司详情页
const
handleCompClick
=
item
=>
{
console
.
log
(
"item"
,
item
);
window
.
sessionStorage
.
setItem
(
"curTabName"
,
item
.
entityNameZh
||
item
.
entity
Name
);
window
.
sessionStorage
.
setItem
(
"curTabName"
,
item
.
name
||
item
.
org
Name
);
gotoCompanyPages
(
item
.
id
);
// const route = router.resolve({
// name: "companyPages",
// params: {
// id: item.id
// }
// });
// window.open(route.href, "_blank");
};
</
script
>
...
...
src/views/finance/entityList/components/sanctionsOverview/components/listPage/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -253,7 +253,7 @@ const handleCompClick = item => {
// }
// });
// window.open(route.href, "_blank");
gotoCompanyPages
(
item
.
entityI
d
);
gotoCompanyPages
(
item
.
i
d
);
};
const
searchKeyword
=
ref
(
""
);
...
...
src/views/finance/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
:
30
px
;
top
:
12
px
;
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
:
20
px
;
font-size
:
16
px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
26px
;
...
...
src/views/finance/singleSanction/assets/image-pdf.png
0 → 100644
浏览文件 @
c8dfe65c
26.7 KB
src/views/finance/singleSanction/components/dataStatistics/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
;
...
...
src/views/finance/singleSanction/components/deepMining/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -540,11 +540,6 @@ onMounted(async () => {
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.deepMiningChartmode
{
height
:
calc
(
100vh
-
220px
)
!
important
;
// overflow: hidden;
...
...
src/views/finance/singleSanction/components/impactAnalysis/components/industrialImpact/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -990,11 +990,6 @@ onMounted(async () => {
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.industrial-impact
{
width
:
100%
;
padding-top
:
16px
;
...
...
src/views/finance/singleSanction/components/impactAnalysis/components/researchImpact/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -1118,11 +1118,6 @@ onBeforeUnmount(() => {
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.industrial-impact
{
width
:
100%
;
padding-top
:
16px
;
...
...
src/views/finance/singleSanction/components/impactAnalysis/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -29,10 +29,6 @@ const activeIndex = ref(0);
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.impact-analysis
{
width
:
1601px
;
margin
:
0
auto
;
...
...
src/views/finance/singleSanction/components/sanctionsOverview/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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%
-
45
px
);
// 减去标题高度
height
:
calc
(
100%
-
20
px
);
// 减去标题高度
display
:
flex
;
flex-direction
:
column
;
...
...
@@ -836,7 +874,7 @@ onMounted(() => {
display
:
flex
;
flex-direction
:
column
;
gap
:
24px
;
margin-bottom
:
24
px
;
margin-bottom
:
5
px
;
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
;
}
}
}
}
}
}
}
}
...
...
src/views/finance/singleSanction/index.vue
浏览文件 @
c8dfe65c
...
...
@@ -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"
);
};
...
...
src/views/finance/singleSanction/originPage/index-back.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
;
}
}
...
...
src/views/finance/singleSanction/originPage/index.vue
浏览文件 @
c8dfe65c
<
template
>
<div
class=
"
entity-list
"
>
<div
class=
"
wrap
"
>
<div
class=
"header"
>
<div
class=
"header-title"
>
<div
class=
"header-top"
>
<div
class=
"header-top-left"
>
<img
:src=
"headerTitle.img"
alt=
""
/>
<div>
<div
class=
"title"
>
{{
headerTitle
.
title
}}
</div>
<div
class=
"department"
>
{{
headerTitle
.
department
}}
</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>
<!-- 下载按钮 -->
<el-button
plain
:icon=
"Download"
@
click=
"handleDownload"
>
下载
</el-button>
<div
class=
"btn"
@
click=
"handleDownload"
>
<div
class=
"icon"
>
<img
src=
"../assets/image-pdf.png"
alt=
""
/>
</div>
<div
class=
"text"
>
{{
"下载"
}}
</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>
<div
class=
"pdf-pane-wrap"
>
<pdf
ref=
"rightPdfRef"
:pdfUrl=
"headerTitle.transUrl"
class=
"pdf-pane-inner"
/>
</div>
<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"
: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
);
// ✅ 控制中文 PDF 显示
const
showChinese
=
ref
(
true
)
;
const
buildPdfPageUrl
=
url
=>
{
if
(
!
url
)
return
""
;
return
`
${
url
}
#page=
${
defaultPdfPage
.
value
}
`
;
}
;
// ✅ 计算当前显示模式
const
showMode
=
computed
(()
=>
{
return
showChinese
.
value
?
"both"
:
"en"
;
});
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
handleToggleChange
=
value
=>
{
console
.
log
(
"切换中英文显示:"
,
value
?
"中英双栏"
:
"仅英文"
);
showChinese
.
value
=
value
;
const
updateMatchInfo
=
()
=>
{
const
pdf
=
activePdfRef
.
value
;
if
(
pdf
&&
typeof
pdf
.
getMatchInfo
===
"function"
)
{
matchInfo
.
value
=
pdf
.
getMatchInfo
();
return
;
}
matchInfo
.
value
=
{
current
:
0
,
total
:
0
};
};
// ✅ 下载功能
const
handleDownload
=
async
()
=>
{
const
files
=
[
{
url
:
headerTitle
.
value
.
srcUrl
,
name
:
"英文原版.pdf"
},
{
url
:
headerTitle
.
value
.
transUrl
,
name
:
"中文翻译.pdf"
}
];
watch
(
()
=>
searchKeywordText
.
value
,
val
=>
{
const
keyword
=
String
(
val
??
""
).
trim
();
if
(
!
keyword
)
{
clearPdfSearchState
();
}
}
);
for
(
const
file
of
files
)
{
if
(
file
.
url
)
{
watch
(
()
=>
valueSwitch
.
value
,
()
=>
{
// 切换「显示原文」会导致 PDF 重新挂载/布局变化:清空搜索与计数,回到初始状态
searchKeywordText
.
value
=
""
;
clearPdfSearchState
();
}
);
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
response
=
await
fetch
(
file
.
url
);
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
=
URL
.
createObjectURL
(
blob
)
;
link
.
download
=
file
.
name
;
link
.
href
=
blobUrl
;
link
.
download
=
file
name
;
document
.
body
.
appendChild
(
link
);
link
.
click
();
document
.
body
.
removeChild
(
link
);
URL
.
revokeObjectURL
(
link
.
href
);
window
.
URL
.
revokeObjectURL
(
blobUrl
);
};
const
handleDownload
=
async
()
=>
{
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
(
`下载
${
file
.
name
}
失败:`
,
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
;
justify-content
:
space-between
;
.header-top-left
{
display
:
flex
;
align-items
:
center
;
margin-bottom
:
12px
;
position
:
relative
;
img
{
width
:
5
4px
;
width
:
4
4px
;
height
:
54px
;
margin-left
:
15px
;
margin-right
:
11px
;
}
.title
{
margin-left
:
20px
;
height
:
26px
;
color
:
rgba
(
59
,
65
,
75
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
20px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
26px
;
color
:
rgb
(
59
,
65
,
75
);
letter-spacing
:
0px
;
text-align
:
left
;
}
.department
{
.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
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
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
;
}
}
.main
{
.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
;
.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
:
0px
auto
;
margin-top
:
20px
;
background
:
rgb
(
255
,
255
,
255
);
width
:
160
1
px
;
height
:
calc
(
100vh
-
148px
)
;
width
:
160
0
px
;
// 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
;
.t
oggle-group
{
.t
ranslate
{
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
;
}
:deep
(
.el-switch
)
{
--el-switch-on-color
:
#055fc2
;
--el-switch-off-color
:
#e6e7e8
;
/* 打开时圆球从左边移到最右边:轨道 22px - 圆球 10px = 12px */
:deep
(
.el-switch.is-checked
.el-switch__button
),
:deep
(
.el-switch.is-checked
.el-switch__action
)
{
transform
:
translateX
(
6px
)
!
important
;
}
.el-switch__label
{
color
:
#fff
;
font-size
:
12px
;
font-weight
:
600
;
.translate-image
{
display
:
flex
;
width
:
16px
;
height
:
16px
;
overflow
:
hidden
;
img
{
width
:
100%
;
height
:
100%
;
}
}
.translate-text
{
font-size
:
14px
;
font-weight
:
400
;
line-height
:
22px
;
}
}
.btn
{
width
:
88px
;
height
:
32px
;
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
;
}
}
&
.is-active
{
color
:
#fff
;
.search-btn
{
cursor
:
pointer
;
}
.search-input-wrap
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
margin-left
:
4px
;
flex-shrink
:
0
;
}
.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
;
}
// :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-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
;
// font-size: 14px;
// padding: 10px 20px;
// }
cursor
:
pointer
;
padding
:
0
;
flex-shrink
:
0
;
white-space
:
nowrap
;
}
.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
{
.pdf-pane-wrap.is-full
{
flex
:
0
0
100%
;
max-width
:
100%
;
// ✅ 添加居中样式
width
:
728px
;
// 约一半宽度,保持单栏时美观
margin
:
0
auto
;
}
}
.pdf-pane-inner
{
...
...
src/views/finance/singleSanction/originPage/pdf-back.vue
0 → 100644
浏览文件 @
c8dfe65c
<
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
>
src/views/finance/singleSanction/originPage/pdf.vue
浏览文件 @
c8dfe65c
...
...
@@ -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
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论