Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
risk-monitor
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
蔡建
risk-monitor
Commits
7da93833
提交
7da93833
authored
4月 08, 2026
作者:
yanpeng
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
问题修复
上级
3f53461a
隐藏空白字符变更
内嵌
并排
正在显示
22 个修改的文件
包含
1154 行增加
和
333 行删除
+1154
-333
finance.js
src/router/modules/finance.js
+1
-1
index.vue
src/views/exportControl/index.vue
+5
-10
index.vue
...ortControl/v2.0EntityList/components/deepMining/index.vue
+2
-2
index.vue
...s/sanctionsOverview/components/introductionPage/index.vue
+0
-5
RuleSubsidiaryDialog.vue
...ionsOverview/components/listPage/RuleSubsidiaryDialog.vue
+1
-8
index.vue
...omponents/sanctionsOverview/components/listPage/index.vue
+12
-7
image-pdf.png
...ews/exportControl/v2.0SingleSanction/assets/image-pdf.png
+0
-0
index.vue
...ol/v2.0SingleSanction/components/dataStatistics/index.vue
+36
-16
index.vue
...ontrol/v2.0SingleSanction/components/deepMining/index.vue
+3
-5
index.vue
...ents/impactAnalysis/components/industrialImpact/index.vue
+5
-9
index.vue
...onents/impactAnalysis/components/researchImpact/index.vue
+0
-5
index.vue
...ol/v2.0SingleSanction/components/impactAnalysis/index.vue
+0
-4
index.vue
...v2.0SingleSanction/components/sanctionsOverview/index.vue
+26
-30
index.vue
src/views/exportControl/v2.0SingleSanction/index.vue
+15
-7
index-back.vue
...xportControl/v2.0SingleSanction/originPage/index-back.vue
+20
-20
index.vue
...ews/exportControl/v2.0SingleSanction/originPage/index.vue
+615
-179
pdf-back.vue
.../exportControl/v2.0SingleSanction/originPage/pdf-back.vue
+385
-0
pdf.vue
...views/exportControl/v2.0SingleSanction/originPage/pdf.vue
+20
-18
index.vue
src/views/finance/index.vue
+1
-1
index.vue
...ws/finance/singleSanction/components/deepMining/index.vue
+3
-0
index.vue
...ents/impactAnalysis/components/industrialImpact/index.vue
+3
-0
index.vue
...nce/singleSanction/components/sanctionsOverview/index.vue
+1
-6
没有找到文件。
src/router/modules/finance.js
浏览文件 @
7da93833
...
...
@@ -33,7 +33,7 @@ const financeRoutes = [
},
// V2.0单条制裁详情-实体清单原文
{
path
:
"/
exportControl
/origin"
,
path
:
"/
finance
/origin"
,
name
:
"financeEntityListOrigin"
,
component
:
()
=>
import
(
"@/views/finance/singleSanction/originPage/index.vue"
)
// meta: {
...
...
src/views/exportControl/index.vue
浏览文件 @
7da93833
...
...
@@ -476,12 +476,6 @@
header-row-class-name=
"table-header"
row-class-name=
"table-row"
>
<!--
<el-table-column
prop=
"index"
label=
"序号"
width=
"80"
align=
"center"
>
<template
#
default=
"scope"
>
{{
scope
.
$index
+
1
+
(
currentPage
-
1
)
*
pageSize
}}
</
template
>
</el-table-column>
-->
<el-table-column
prop=
"name"
label=
"实体名称"
min-width=
"200"
>
<template
#
default=
"scope"
>
<div
class=
"tableName"
@
click=
"handleCompClick(scope.row)"
>
...
...
@@ -970,7 +964,7 @@ onMounted(async () => {
label
:
item
.
nameZh
,
value
:
tabMap
[
item
.
id
],
id
:
[
item
.
id
],
disabled
:
item
.
id
==
"13"
// 商业管制清单不展示
disabled
:
false
}));
resourceTabs
.
value
.
unshift
({
label
:
"全部制裁"
,
value
:
"all"
,
id
:
""
,
disabled
:
false
});
console
.
log
(
"返回的数据结构 infoList =》"
,
infoList
.
value
);
...
...
@@ -1108,7 +1102,7 @@ const processYearDomainCountData = yearDomainCountData => {
const
handleEntityClick
=
item
=>
{
console
.
log
(
"item"
,
item
);
window
.
sessionStorage
.
setItem
(
"curTabName"
,
item
.
name
||
item
.
entityNameZh
);
gotoCompanyPages
(
item
.
entityI
d
);
gotoCompanyPages
(
item
.
i
d
);
// const route = router.resolve({
// name: "companyPages",
// params: {
...
...
@@ -1494,7 +1488,7 @@ watch(
const
fetchEntitiesList
=
async
(
page
=
1
,
size
=
10
)
=>
{
try
{
console
.
log
(
"activeResourceTabItem.value.id"
,
activeResourceTabItem
.
value
.
id
);
const
res
=
await
getEntitiesList
(
activeResourceTabItem
.
value
.
id
,
page
,
size
);
const
res
=
await
getEntitiesList
(
activeResourceTabItem
.
value
.
id
.
join
(
","
)
,
page
,
size
);
if
(
res
)
{
entitiesList
.
value
=
res
.
content
.
map
(
item
=>
({
...
item
,
...
...
@@ -1630,6 +1624,7 @@ const fetchSocialMediaInfo = async () => {
if
(
data
&&
Array
.
isArray
(
data
))
{
// console.log(data);
socialMediaList
.
value
=
data
.
map
(
item
=>
({
...
item
,
avatar
:
item
.
personImage
,
name
:
item
.
personName
,
time
:
formatTime
(
item
.
time
),
...
...
@@ -1662,7 +1657,7 @@ const fetchNewsInfo = async () => {
};
const
handlePerClick
=
item
=>
{
//
console.log("点击了社交媒体消息:", item);
console
.
log
(
"点击了社交媒体消息:"
,
item
);
window
.
sessionStorage
.
setItem
(
"curTabName"
,
item
.
name
);
const
route
=
router
.
resolve
({
path
:
"/characterPage"
,
...
...
src/views/exportControl/v2.0EntityList/components/deepMining/index.vue
浏览文件 @
7da93833
...
...
@@ -75,7 +75,7 @@
</
template
>
<div
class=
"right-main"
>
<div
class=
"right-main-content"
>
<div
class=
"hintWrap"
>
<
!-- <
div class="hintWrap">
<div class="icon1"></div>
<div class="title">
2025年实体清单制裁范围扩大至芯片制造环节,为中国的芯片制造能力划定“技术天花板”,阻止其向更先进水平发展。制裁范围向上游设备和材料、下游先进封装以及关键工具(如EDA软件)延伸,意图瓦解中国构建自主可控产业链的努力。
...
...
@@ -83,7 +83,7 @@
<div class="icon2Wrap">
<div class="icon2"></div>
</div>
</div>
</div>
-->
<div
class=
"right-main-content-main"
>
<div
class=
"fishbone-wrapper"
>
<div
class=
"fishbone-scroll-container"
ref=
"scrollContainerRef"
>
...
...
src/views/exportControl/v2.0EntityList/components/sanctionsOverview/components/introductionPage/index.vue
浏览文件 @
7da93833
...
...
@@ -433,11 +433,6 @@ onMounted(() => {
<
/script
>
<
style
scoped
lang
=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.
introduction
-
page
{
width
:
1601
px
;
margin
:
0
auto
;
...
...
src/views/exportControl/v2.0EntityList/components/sanctionsOverview/components/listPage/RuleSubsidiaryDialog.vue
浏览文件 @
7da93833
...
...
@@ -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/exportControl/v2.0EntityList/components/sanctionsOverview/components/listPage/index.vue
浏览文件 @
7da93833
...
...
@@ -540,10 +540,13 @@ watch(customDateRange, () => {
border
:
1px
solid
rgba
(
170
,
173
,
177
,
1
);
background-color
:
#fff
;
border-radius
:
3px
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
height
:
32px
;
}
:deep
(
.el-input__inner
)
{
font-size
:
1
6
px
;
font-size
:
1
4
px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
...
...
@@ -552,8 +555,8 @@ watch(customDateRange, () => {
}
.filters
{
display
:
flex
;
align-items
:
center
;
//
display: flex;
//
align-items: center;
.el-checkbox
{
margin-right
:
20px
;
...
...
@@ -565,8 +568,9 @@ watch(customDateRange, () => {
height
:
32px
;
}
:deep
(
.el-checkbox__label
)
{
font-size
:
1
6
px
;
font-size
:
1
4
px
;
color
:
rgb
(
95
,
101
,
108
);
margin-top
:
3px
;
}
}
}
...
...
@@ -596,9 +600,10 @@ watch(customDateRange, () => {
padding-left
:
20px
;
.el-checkbox
{
width
:
50%
;
margin-right
:
0
;
margin-bottom
:
4px
;
// width: 50%;
// margin-right: 0;
// margin-bottom: 4px;
height
:
24px
;
font-size
:
16px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
...
...
src/views/exportControl/v2.0SingleSanction/assets/image-pdf.png
0 → 100644
浏览文件 @
7da93833
26.7 KB
src/views/exportControl/v2.0SingleSanction/components/dataStatistics/index.vue
浏览文件 @
7da93833
...
...
@@ -537,26 +537,30 @@ const domainChartOption = ref({
width
:
1.1
}
},
// labelLayout: function (params) {
// const isLeft = params.labelRect.x
<
params
.
viewWidth
/
2
;
// const points = params.labelLinePoints;
// // Update the end point.
// points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
// return {
// labelLinePoints: points
// };
// },
labelLayout
:
function
(
params
)
{
const
isLeft
=
params
.
labelRect
.
x
<
params
.
viewWidth
/
2
;
// hideOverlap: true
const
points
=
params
.
labelLinePoints
;
// Update the end point.
const
isLeft
=
params
.
labelRect
.
x
<
params
.
rect
.
x
+
params
.
rect
.
width
/
2
;
// 调整指示线终点到 label 垂直中心
const
labelCenterY
=
params
.
labelRect
.
y
+
params
.
labelRect
.
height
/
2
;
points
[
2
][
1
]
=
labelCenterY
;
// 调整指示线终点到 label 水平边缘
points
[
2
][
0
]
=
isLeft
?
params
.
labelRect
.
x
:
params
.
labelRect
.
x
+
params
.
labelRect
.
width
;
return
{
labelLinePoints
:
points
};
// if (isLeft) {
// // 左侧:终点对齐到标签左边界
// points[2][0] = 0;
// } else {
// // 右侧:终点对齐到标签右边界
// points[2][0] = params.labelRect.x + params.labelRect.width;
// }
// return {
// labelLinePoints: points
// };
},
itemStyle
:
{
borderWidth
:
0
...
...
@@ -714,11 +718,27 @@ const typeChartOption = ref({
width
:
1
}
},
// labelLayout: function (params) {
// const isLeft = params.labelRect.x
<
params
.
viewWidth
/
2
;
// const points = params.labelLinePoints;
// // Update the end point.
// points[2][0] = isLeft ? params.labelRect.x : params.labelRect.x + params.labelRect.width;
// return {
// labelLinePoints: points
// };
// },
labelLayout
:
function
(
params
)
{
const
isLeft
=
params
.
labelRect
.
x
<
params
.
viewWidth
/
2
;
// hideOverlap: true
const
points
=
params
.
labelLinePoints
;
// Update the end point.
const
isLeft
=
params
.
labelRect
.
x
<
params
.
rect
.
x
+
params
.
rect
.
width
/
2
;
// 调整指示线终点到 label 垂直中心
const
labelCenterY
=
params
.
labelRect
.
y
+
params
.
labelRect
.
height
/
2
;
points
[
2
][
1
]
=
labelCenterY
;
// 调整指示线终点到 label 水平边缘
points
[
2
][
0
]
=
isLeft
?
params
.
labelRect
.
x
:
params
.
labelRect
.
x
+
params
.
labelRect
.
width
;
return
{
labelLinePoints
:
points
};
...
...
src/views/exportControl/v2.0SingleSanction/components/deepMining/index.vue
浏览文件 @
7da93833
...
...
@@ -540,11 +540,6 @@ onMounted(async () => {
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.deepMiningChartmode
{
height
:
calc
(
100vh
-
220px
)
!
important
;
// overflow: hidden;
...
...
@@ -628,6 +623,9 @@ onMounted(async () => {
.search-input
{
flex
:
1
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
height
:
32px
;
}
}
...
...
src/views/exportControl/v2.0SingleSanction/components/impactAnalysis/components/industrialImpact/index.vue
浏览文件 @
7da93833
...
...
@@ -51,7 +51,7 @@
<div
class=
"left-main"
>
<div
class=
"top-bar"
>
<el-select
v-model=
"searchDomain"
placeholder=
"全部领域"
class=
"domain-select"
>
<el-option
label=
"全部领域"
value=
""
/>
<el-option
label=
"全部领域"
value=
"
0
"
/>
<el-option
label=
"人工智能"
value=
"1"
/>
<el-option
label=
"生物科技"
value=
"2"
/>
<el-option
label=
"新一代信息技术"
value=
"3"
/>
...
...
@@ -515,7 +515,7 @@ const getNetProfitData = async () => {
// 单次制裁-影响分析-制裁企业列表
const
entityList
=
ref
([]);
const
searchDomain
=
ref
(
""
);
const
searchDomain
=
ref
(
"
0
"
);
const
searchKeyword
=
ref
(
""
);
const
selectedCompanyId
=
ref
(
null
);
const
sanRecordId
=
ref
(
""
);
...
...
@@ -584,7 +584,7 @@ const getEntityList = async () => {
const
res
=
await
getSingleSanctionEntityList
({
sanRecordId
:
sanRecordId
.
value
,
isOnlyCn
:
false
,
domainId
:
searchDomain
.
value
||
undefined
,
domainId
:
searchDomain
.
value
==
0
?
undefined
:
searchDomain
.
value
||
undefined
,
searchText
:
searchKeyword
.
value
||
undefined
});
if
(
res
.
code
===
200
)
{
...
...
@@ -990,11 +990,6 @@ onMounted(async () => {
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.industrial-impact
{
width
:
100%
;
padding-top
:
16px
;
...
...
@@ -1056,7 +1051,8 @@ onMounted(async () => {
:deep
(
.el-input__wrapper
)
{
height
:
32px
;
border-radius
:
4px
;
border
:
1px
solid
rgba
(
170
,
173
,
177
,
0
.4
);
border-radius
:
5px
;
}
}
}
...
...
src/views/exportControl/v2.0SingleSanction/components/impactAnalysis/components/researchImpact/index.vue
浏览文件 @
7da93833
...
...
@@ -1118,11 +1118,6 @@ onBeforeUnmount(() => {
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.industrial-impact
{
width
:
100%
;
padding-top
:
16px
;
...
...
src/views/exportControl/v2.0SingleSanction/components/impactAnalysis/index.vue
浏览文件 @
7da93833
...
...
@@ -28,10 +28,6 @@ const activeIndex = ref(0);
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.impact-analysis
{
width
:
1601px
;
margin
:
0
auto
;
...
...
src/views/exportControl/v2.0SingleSanction/components/sanctionsOverview/index.vue
浏览文件 @
7da93833
...
...
@@ -100,10 +100,10 @@
<el-checkbox
v-model=
"onlyChina"
label=
"只看中国实体"
/>
<el-select
v-model=
"filterField"
placeholder=
"
全部
领域"
placeholder=
"
选择
领域"
style=
"width: 150px; margin: 0 12px 0 16px"
>
<
el-option
label=
"全部领域"
value=
""
/
>
<
!--
<el-option
label=
"全部领域"
value=
""
/>
--
>
<el-option
v-for=
"item in domainOptions"
:key=
"item.value"
...
...
@@ -243,6 +243,24 @@ import { useGotoCompanyPages } from "@/router/modules/company";
const
gotoCompanyPages
=
useGotoCompanyPages
();
const
route
=
useRoute
();
const
domainOptions
=
ref
([
{
label
:
"全部领域"
,
value
:
"0"
},
{
label
:
"人工智能"
,
value
:
"1"
},
{
label
:
"生物科技"
,
value
:
"2"
},
{
label
:
"新一代信息技术"
,
value
:
"3"
},
{
label
:
"量子科技"
,
value
:
"4"
},
{
label
:
"新能源"
,
value
:
"5"
},
{
label
:
"集成电路"
,
value
:
"6"
},
{
label
:
"海洋"
,
value
:
"7"
},
{
label
:
"先进制造"
,
value
:
"8"
},
{
label
:
"新材料"
,
value
:
"9"
},
{
label
:
"航空航天"
,
value
:
"10"
},
{
label
:
"深海"
,
value
:
"11"
},
{
label
:
"极地"
,
value
:
"12"
},
{
label
:
"太空"
,
value
:
"13"
},
{
label
:
"核"
,
value
:
"14"
}
]);
// 跳转公司详情页
const
handleCompClick
=
item
=>
{
if
(
!
item
.
entityId
)
{
...
...
@@ -297,7 +315,7 @@ const getSanctionOverviewList = async () => {
const
res
=
await
getSingleSanctionOverviewList
({
sanRecordId
:
sanRecordId
.
value
,
isOnlyCn
:
onlyChina
.
value
,
domainId
:
filterField
.
value
||
undefined
,
domainId
:
filterField
.
value
==
0
?
undefined
:
filterField
.
value
||
undefined
,
searchText
:
searchKeyword
.
value
||
undefined
,
searchType
:
searchType
.
value
,
entityTypeId
:
filterEntity
.
value
||
undefined
...
...
@@ -465,7 +483,7 @@ const formattedData = computed(() => {
const
filterEntity
=
ref
(
"2"
);
const
onlyChina
=
ref
(
false
);
const
filterField
=
ref
(
""
);
const
filterField
=
ref
(
domainOptions
.
value
[
0
].
value
);
const
searchKeyword
=
ref
(
""
);
const
activeTab
=
ref
(
"add"
);
const
searchType
=
computed
(()
=>
activeTab
.
value
);
...
...
@@ -484,23 +502,6 @@ watch(searchKeyword, () => {
debouncedSearch
();
});
const
domainOptions
=
ref
([
{
label
:
"人工智能"
,
value
:
"1"
},
{
label
:
"生物科技"
,
value
:
"2"
},
{
label
:
"新一代信息技术"
,
value
:
"3"
},
{
label
:
"量子科技"
,
value
:
"4"
},
{
label
:
"新能源"
,
value
:
"5"
},
{
label
:
"集成电路"
,
value
:
"6"
},
{
label
:
"海洋"
,
value
:
"7"
},
{
label
:
"先进制造"
,
value
:
"8"
},
{
label
:
"新材料"
,
value
:
"9"
},
{
label
:
"航空航天"
,
value
:
"10"
},
{
label
:
"深海"
,
value
:
"11"
},
{
label
:
"极地"
,
value
:
"12"
},
{
label
:
"太空"
,
value
:
"13"
},
{
label
:
"核"
,
value
:
"14"
}
]);
const
tagColors
=
[
{
bg
:
"rgb(242, 235, 255)"
,
border
:
"rgb(211, 190, 255)"
,
text
:
"rgb(114, 46, 209)"
},
// Purple
{
bg
:
"rgb(225, 250, 248)"
,
border
:
"rgb(178, 242, 238)"
,
text
:
"rgb(16, 178, 166)"
},
// Cyan
...
...
@@ -570,11 +571,6 @@ onMounted(() => {
</
script
>
<
style
scoped
lang=
"scss"
>
// * {
// margin: 0;
// padding: 0;
// }
.sanctions-overview
{
width
:
1601px
;
margin
:
0
auto
;
...
...
@@ -888,19 +884,19 @@ onMounted(() => {
margin-bottom
:
20px
;
:deep
(
.el-input__inner
)
{
font-size
:
1
6
px
;
font-size
:
1
4
px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
)
;
color
:
#606266
;
&
:
:
placeholder
{
color
:
rgb
(
95
,
101
,
108
);
color
:
rgb
(
95
,
101
,
108
,
0
.8
);
}
}
:deep
(
.el-checkbox__label
)
{
font-size
:
1
6
px
;
font-size
:
1
4
px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
...
...
src/views/exportControl/v2.0SingleSanction/index.vue
浏览文件 @
7da93833
...
...
@@ -60,7 +60,9 @@
:class=
"
{ active: selectedSanctionId === item.id }"
@click="selectSanction(item)"
>
{{
item
.
name
}}
<div
class=
"sanction-name"
>
{{
item
.
name
}}
</div>
<div
class=
"sanction-type"
>
{{
item
.
postDate
}}
</div>
</div>
</div>
...
...
@@ -108,6 +110,7 @@ import router from "@/router";
// 处理点击实体清单原文按钮
const
handleClickOriginalText
=
()
=>
{
// 打开新标签页
window
.
open
(
`/exportControl/origin?id=
${
sanRecordId
.
value
}
`
,
"_blank"
);
};
...
...
@@ -206,13 +209,14 @@ const totalElements = ref(0);
const
openSanctionModal
=
async
()
=>
{
sanctionModalVisible
.
value
=
true
;
console
.
log
(
"制裁事件列表11:"
,
sanctionList
.
value
);
loading
.
value
=
true
;
await
fetchSanctionData
();
};
// ========== 获取制裁数据 ==========
const
fetchSanctionData
=
async
()
=>
{
try
{
loading
.
value
=
true
;
const
res
=
await
getSanctionProcess
([
1
],
currentPage
.
value
,
10
);
loading
.
value
=
false
;
if
(
res
&&
!!
res
.
content
)
{
...
...
@@ -254,6 +258,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"
);
};
...
...
@@ -274,15 +279,11 @@ onMounted(() => {
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.entity-list
{
width
:
100%
;
height
:
100%
;
overflow-y
:
auto
;
padding-bottom
:
5
0px
;
padding-bottom
:
0px
;
.header
{
width
:
100%
;
height
:
148px
;
...
...
@@ -464,8 +465,15 @@ onMounted(() => {
.sanction-list
{
max-height
:
400px
;
min-height
:
400px
;
overflow-y
:
auto
;
padding
:
0
10px
;
.sanction-name
{
max-width
:
85%
;
white-space
:
nowrap
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
}
}
.sanction-item
{
...
...
src/views/exportControl/v2.0SingleSanction/originPage/index-back.vue
浏览文件 @
7da93833
...
...
@@ -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/exportControl/v2.0SingleSanction/originPage/index.vue
浏览文件 @
7da93833
<
template
>
<div
class=
"
entity-list
"
>
<div
class=
"
wrap
"
>
<div
class=
"header"
>
<div
class=
"header-title"
>
<img
:src=
"headerTitle.img"
alt=
""
/>
<div>
<div
class=
"title"
>
{{
headerTitle
.
title
}}
</div>
<div
class=
"department"
>
{{
headerTitle
.
department
}}
</div>
<div
class=
"header-top"
>
<div
class=
"header-top-left"
>
<img
:src=
"headerTitle.img"
alt=
""
/>
<div>
<div
class=
"title"
>
{{
headerTitle
.
title
}}
</div>
<div
class=
"en-title"
>
{{
headerTitle
.
sanTitle
}}
</div>
</div>
</div>
<div
class=
"header-top-right"
>
<div
class=
"image-name-box"
>
<div
class=
"image"
><img
:src=
"headerTitle.postOrgLogoUrl"
alt=
""
/></div>
<div
class=
"name"
>
{{
headerTitle
.
postOrgName
}}
</div>
</div>
<div
class=
"time"
>
{{
headerTitle
.
postDate
}}
</div>
</div>
</div>
</div>
<div
class=
"main"
>
<div
class=
"main-header"
>
<div
class=
"header-left"
>
实体清单制裁文件
</div>
<div
class=
"header-right"
>
<!-- 中英文切换开关 -->
<div
class=
"toggle-group"
>
<!--
<span
:class=
"
{ active: !showChinese }">英文
</span>
-->
<el-switch
v-model=
"showChinese"
@
change=
"handleToggleChange"
/>
<img
:src=
"transIcon"
alt=
""
/>
<span
:class=
"
{ active: showChinese }">显示原文
</span>
<div>
实体清单制裁文件
</div>
<div
class=
"btn-box"
>
<div
class=
"translate"
>
<div
class=
"search-input-wrap"
v-if=
"showSearchInput"
>
<input
v-model=
"searchKeywordText"
class=
"search-input"
placeholder=
"回车查询"
@
keyup
.
enter=
"handleSearchInPdf"
/>
<div
class=
"search-match-count"
>
{{
matchInfo
.
current
}}
/
{{
matchInfo
.
total
}}
</div>
<button
class=
"search-nav-btn"
type=
"button"
@
click=
"handlePrevMatch"
:disabled=
"matchInfo.total === 0 || matchInfo.current
<
=
1
"
>
上一个
</button>
<button
class=
"search-nav-btn"
type=
"button"
@
click=
"handleNextMatch"
:disabled=
"matchInfo.total === 0 || matchInfo.current >= matchInfo.total"
>
下一个
</button>
</div>
<div
class=
"switch"
>
<el-switch
v-model=
"valueSwitch"
/>
</div>
<div
class=
"translate-image"
>
<img
class=
"translate-icon"
src=
"../assets/icon-translation.png"
alt=
""
style=
"
width: 16px;
height: 16px;
max-width: 16px;
max-height: 16px;
display: block;
object-fit: contain;
"
/>
</div>
<div
class=
"translate-text"
>
{{
"显示原文"
}}
</div>
</div>
<div
class=
"btn"
@
click=
"handleDownload"
>
<div
class=
"icon"
>
<img
src=
"../assets/image-pdf.png"
alt=
""
/>
</div>
<div
class=
"text"
>
{{
"下载"
}}
</div>
</div>
<!-- 下载按钮 -->
<el-button
plain
:icon=
"Download"
@
click=
"handleDownload"
>
下载
</el-button>
</div>
</div>
<!-- 外层滚动容器,统一控制两侧滚动 -->
<div
class=
"report-box"
ref=
"reportBoxRef"
>
<div
class=
"pdf-pane-wrap"
v-if=
"showChinese"
:class=
"
{ 'center-mode': !showChinese }">
<pdf
ref=
"leftPdfRef"
:pdfUrl=
"headerTitle.srcUrl"
class=
"pdf-pane-inner"
/>
<div
class=
"report-box"
>
<div
class=
"pdf-pane-wrap"
v-if=
"valueSwitch && reportUrlEnWithPage"
>
<pdf
ref=
"leftPdfRef"
:pdfUrl=
"reportUrlEnWithPage"
class=
"pdf-pane-inner"
/>
</div>
<div
class=
"pdf-pane-wrap"
>
<pdf
ref=
"rightPdfRef"
:pdfUrl=
"headerTitle.transUrl"
class=
"pdf-pane-inner"
/>
<div
class=
"pdf-pane-wrap"
:class=
"
{ 'is-full': !valueSwitch }" v-if="reportUrlWithPage">
<pdf
:key=
"`right-pdf-$
{valueSwitch ? 'split' : 'full'}`"
ref="rightPdfRef"
:pdfUrl="reportUrlWithPage"
class="pdf-pane-inner"
/>
</div>
</div>
</div>
...
...
@@ -38,16 +96,23 @@
</
template
>
<
script
setup
>
import
{
ref
,
onMounted
,
watch
,
computed
}
from
"vue"
;
import
{
Download
}
from
"@element-plus/icons-vue"
;
import
{
computed
,
ref
,
onMounted
,
watch
}
from
"vue"
;
import
pdf
from
"./pdf.vue"
;
import
{
getThinkTankReportSummary
,
getThinkTankReportcontentUrl
}
from
"@/api/thinkTank/overview"
;
import
{
useRoute
,
useRouter
}
from
"vue-router"
;
import
{
getSingleSanctionOverview
}
from
"@/api/exportControlV2.0.js"
;
import
title
from
"../assets/title.png"
;
import
transIcon
from
"../assets/icon-translation.png"
;
import
pdf
from
"./pdf.vue"
;
const
leftPdfRef
=
ref
(
null
);
const
rightPdfRef
=
ref
(
null
);
const
reportBoxRef
=
ref
(
null
);
const
router
=
useRouter
();
const
route
=
useRoute
();
const
reportUrl
=
ref
(
""
);
const
reportUrlEn
=
ref
(
""
);
const
thinkInfo
=
ref
({});
const
defaultPdfPage
=
ref
(
1
);
const
sourceCurrentPage
=
ref
(
Number
(
route
.
query
.
currentPage
)
||
1
);
const
sourcePageSize
=
ref
(
Number
(
route
.
query
.
pageSize
)
||
12
);
const
opinionId
=
ref
(
route
.
query
.
opinionId
||
""
);
const
opinionContent
=
ref
(
route
.
query
.
opinionContent
||
""
);
const
headerTitle
=
ref
({
img
:
title
,
...
...
@@ -56,47 +121,188 @@ const headerTitle = ref({
srcUrl
:
""
,
transUrl
:
""
});
const
sanRecordId
=
ref
(
""
);
const
isSyncing
=
ref
(
false
);
const
buildPdfPageUrl
=
url
=>
{
if
(
!
url
)
return
""
;
return
`
${
url
}
#page=
${
defaultPdfPage
.
value
}
`
;
};
// ✅ 控制中文 PDF 显示
const
showChinese
=
ref
(
true
);
const
reportUrlWithPage
=
computed
(()
=>
buildPdfPageUrl
(
headerTitle
.
value
.
srcUrl
));
const
reportUrlEnWithPage
=
computed
(()
=>
buildPdfPageUrl
(
headerTitle
.
value
.
transUrl
));
const
valueSwitch
=
ref
(
true
);
const
showSearchInput
=
ref
(
true
);
const
searchKeywordText
=
ref
(
""
);
const
leftPdfRef
=
ref
(
null
);
const
rightPdfRef
=
ref
(
null
);
const
matchInfo
=
ref
({
current
:
0
,
total
:
0
});
const
activePdfRef
=
ref
(
null
);
const
clearPdfSearchState
=
()
=>
{
activePdfRef
.
value
=
null
;
matchInfo
.
value
=
{
current
:
0
,
total
:
0
};
const
leftPdf
=
leftPdfRef
.
value
;
const
rightPdf
=
rightPdfRef
.
value
;
if
(
leftPdf
&&
typeof
leftPdf
.
clearSearch
===
"function"
)
{
leftPdf
.
clearSearch
();
}
if
(
rightPdf
&&
typeof
rightPdf
.
clearSearch
===
"function"
)
{
rightPdf
.
clearSearch
();
}
};
// ✅ 计算当前显示模式
const
showMode
=
computed
(()
=>
{
return
showChinese
.
value
?
"both"
:
"en"
;
});
const
updateMatchInfo
=
()
=>
{
const
pdf
=
activePdfRef
.
value
;
if
(
pdf
&&
typeof
pdf
.
getMatchInfo
===
"function"
)
{
matchInfo
.
value
=
pdf
.
getMatchInfo
();
return
;
}
matchInfo
.
value
=
{
current
:
0
,
total
:
0
};
};
watch
(
()
=>
searchKeywordText
.
value
,
val
=>
{
const
keyword
=
String
(
val
??
""
).
trim
();
if
(
!
keyword
)
{
clearPdfSearchState
();
}
}
);
watch
(
()
=>
valueSwitch
.
value
,
()
=>
{
// 切换「显示原文」会导致 PDF 重新挂载/布局变化:清空搜索与计数,回到初始状态
searchKeywordText
.
value
=
""
;
clearPdfSearchState
();
}
);
// ✅ 切换中英文显示
const
handleToggleChange
=
value
=>
{
console
.
log
(
"切换中英文显示:"
,
value
?
"中英双栏"
:
"仅英文"
);
showChinese
.
value
=
value
;
const
handleSearchInPdf
=
async
()
=>
{
const
keyword
=
searchKeywordText
.
value
?.
trim
();
if
(
!
keyword
)
return
;
activePdfRef
.
value
=
null
;
matchInfo
.
value
=
{
current
:
0
,
total
:
0
};
const
leftPdf
=
leftPdfRef
.
value
;
const
rightPdf
=
rightPdfRef
.
value
;
let
page
=
0
;
let
targetRef
=
null
;
if
(
leftPdf
&&
typeof
leftPdf
.
searchKeyword
===
"function"
)
{
page
=
await
leftPdf
.
searchKeyword
(
keyword
);
if
(
page
)
targetRef
=
leftPdf
;
}
if
(
!
page
&&
rightPdf
&&
typeof
rightPdf
.
searchKeyword
===
"function"
)
{
page
=
await
rightPdf
.
searchKeyword
(
keyword
);
if
(
page
)
targetRef
=
rightPdf
;
}
if
(
page
&&
targetRef
&&
typeof
targetRef
.
goToPage
===
"function"
)
{
targetRef
.
goToPage
(
page
);
activePdfRef
.
value
=
targetRef
;
updateMatchInfo
();
}
else
{
try
{
const
{
ElMessage
}
=
await
import
(
"element-plus"
);
ElMessage
.
warning
(
"未找到包含该关键词的页面"
);
}
catch
(
_
)
{}
}
};
const
handlePrevMatch
=
()
=>
{
const
pdf
=
activePdfRef
.
value
;
if
(
!
pdf
||
typeof
pdf
.
prevMatch
!==
"function"
)
return
;
pdf
.
prevMatch
();
updateMatchInfo
();
};
const
handleNextMatch
=
()
=>
{
const
pdf
=
activePdfRef
.
value
;
if
(
!
pdf
||
typeof
pdf
.
nextMatch
!==
"function"
)
return
;
pdf
.
nextMatch
();
updateMatchInfo
();
};
// 下载:中英文都下载,与政令原文页相同的 fetch → blob → a 标签触发下载
const
downloadOnePdf
=
async
(
url
,
filename
)
=>
{
const
response
=
await
fetch
(
url
,
{
method
:
"GET"
,
headers
:
{
"Content-Type"
:
"application/pdf"
}
});
if
(
!
response
.
ok
)
throw
new
Error
(
`HTTP error! status:
${
response
.
status
}
`
);
const
blob
=
await
response
.
blob
();
const
blobUrl
=
window
.
URL
.
createObjectURL
(
blob
);
const
link
=
document
.
createElement
(
"a"
);
link
.
href
=
blobUrl
;
link
.
download
=
filename
;
document
.
body
.
appendChild
(
link
);
link
.
click
();
document
.
body
.
removeChild
(
link
);
window
.
URL
.
revokeObjectURL
(
blobUrl
);
};
// ✅ 下载功能
const
handleDownload
=
async
()
=>
{
const
files
=
[
{
url
:
headerTitle
.
value
.
srcUrl
,
name
:
"英文原版.pdf"
},
{
url
:
headerTitle
.
value
.
transUrl
,
name
:
"中文翻译.pdf"
}
];
for
(
const
file
of
files
)
{
if
(
file
.
url
)
{
try
{
const
response
=
await
fetch
(
file
.
url
);
const
blob
=
await
response
.
blob
();
const
link
=
document
.
createElement
(
"a"
);
link
.
href
=
URL
.
createObjectURL
(
blob
);
link
.
download
=
file
.
name
;
document
.
body
.
appendChild
(
link
);
link
.
click
();
document
.
body
.
removeChild
(
link
);
URL
.
revokeObjectURL
(
link
.
href
);
}
catch
(
error
)
{
console
.
error
(
`下载
${
file
.
name
}
失败:`
,
error
);
}
const
urlZh
=
headerTitle
.
value
.
srcUrl
?
String
(
headerTitle
.
value
.
srcUrl
)
:
""
;
const
urlEn
=
headerTitle
.
value
.
transUrl
?
String
(
headerTitle
.
value
.
transUrl
)
:
""
;
if
(
!
urlZh
&&
!
urlEn
)
{
try
{
const
{
ElMessage
}
=
await
import
(
"element-plus"
);
ElMessage
.
warning
(
"暂无下载链接"
);
}
catch
(
_
)
{}
return
;
}
const
baseName
=
(
thinkInfo
.
value
?.
name
||
"报告原文"
).
replace
(
/
[/\\
?%*:|"<>
]
/g
,
"-"
);
const
{
ElMessage
}
=
await
import
(
"element-plus"
);
try
{
if
(
urlZh
)
{
await
downloadOnePdf
(
urlZh
,
`
${
baseName
}
_中文.pdf`
);
}
if
(
urlEn
)
{
if
(
urlZh
)
await
new
Promise
(
r
=>
setTimeout
(
r
,
300
));
await
downloadOnePdf
(
urlEn
,
`
${
baseName
}
_英文.pdf`
);
}
if
(
urlZh
||
urlEn
)
{
ElMessage
.
success
(
urlZh
&&
urlEn
?
"已下载中文、英文两份 PDF"
:
"下载成功"
);
}
}
catch
(
error
)
{
console
.
error
(
"下载失败:"
,
error
);
ElMessage
.
error
(
"PDF 下载失败,请稍后重试"
);
}
};
/** 接口可能返回对象或数组;字段名兼容 snake/camel */
const
normalizeReportSummaryRow
=
row
=>
{
if
(
!
row
||
typeof
row
!==
"object"
)
return
{};
return
{
...
row
,
name
:
row
.
name
||
""
,
imageUrl
:
row
.
imageUrl
||
row
.
image
||
""
,
ename
:
row
.
ename
||
row
.
nameEn
||
row
.
englishName
||
""
,
times
:
row
.
times
||
row
.
postDate
||
""
,
thinkTankName
:
row
.
thinkTankName
||
row
.
thinktankName
||
""
,
thinkTankLogoUrl
:
row
.
thinkTankLogoUrl
||
row
.
thinktankLogo
||
""
};
};
const
applyReportOriginalDocumentTitle
=
title
=>
{
const
text
=
String
(
title
||
""
).
trim
();
if
(
!
text
)
return
;
window
.
sessionStorage
.
setItem
(
"reportOriginalTabName"
,
text
);
document
.
title
=
text
;
};
// 获取报告全局信息
const
handleGetThinkTankReportSummary
=
async
()
=>
{
try
{
const
res
=
await
getThinkTankReportSummary
(
router
.
currentRoute
.
_value
.
params
.
id
);
console
.
log
(
"报告全局信息"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
const
raw
=
res
.
data
;
const
first
=
Array
.
isArray
(
raw
)
?
raw
[
0
]
:
raw
;
const
normalized
=
normalizeReportSummaryRow
(
first
);
thinkInfo
.
value
=
normalized
;
applyReportOriginalDocumentTitle
(
normalized
.
name
);
}
}
catch
(
error
)
{
console
.
error
(
"获取报告全局信息error"
,
error
);
}
};
...
...
@@ -104,7 +310,6 @@ const getUrlParams = () => {
const
urlParams
=
new
URLSearchParams
(
window
.
location
.
search
);
sanRecordId
.
value
=
urlParams
.
get
(
"id"
)
||
""
;
};
const
getSingleSanctionOverviewData
=
async
()
=>
{
if
(
!
sanRecordId
.
value
)
return
;
try
{
...
...
@@ -127,195 +332,430 @@ const getSingleSanctionOverviewData = async () => {
// 更新头部信息
headerTitle
.
value
=
{
...
singleSanctionOverview
,
...
headerTitle
.
value
,
title
:
`
${
dateStr
}
《
${
singleSanctionOverview
.
sanTitleZh
||
singleSanctionOverview
.
sanTitle
}
》`
,
department
:
singleSanctionOverview
.
fileCode
||
""
,
srcUrl
:
singleSanctionOverview
.
srcUrl
||
""
,
transUrl
:
singleSanctionOverview
.
transUrl
||
""
};
applyReportOriginalDocumentTitle
(
headerTitle
.
value
.
title
||
""
);
}
}
catch
(
error
)
{
console
.
error
(
"获取制裁概况失败:"
,
error
);
}
};
// 同步滚动处理
const
handleSyncScroll
=
()
=>
{
if
(
isSyncing
.
value
)
return
;
isSyncing
.
value
=
true
;
requestAnimationFrame
(()
=>
{
isSyncing
.
value
=
false
;
});
//获取原文
const
handleGetThinkTankReportcontentUrl
=
async
()
=>
{
try
{
const
res
=
await
getThinkTankReportcontentUrl
(
router
.
currentRoute
.
_value
.
params
.
id
);
console
.
log
(
"获取原文"
,
res
);
if
(
res
.
code
===
200
&&
res
.
data
)
{
reportUrl
.
value
=
res
.
data
.
content
;
reportUrlEn
.
value
=
res
.
data
.
contentEn
;
}
}
catch
(
error
)
{
console
.
error
(
"获取原文error"
,
error
);
}
};
// 监听滚动事件
const
setupScrollSync
=
()
=>
{
const
reportBox
=
reportBoxRef
.
value
;
if
(
!
reportBox
)
return
;
const
tabActiveName
=
ref
(
"报告分析"
);
reportBox
.
addEventListener
(
"scroll"
,
handleSyncScroll
,
{
passive
:
true
});
const
switchTab
=
name
=>
{
tabActiveName
.
value
=
name
;
};
// 监听 PDF 加载完成
watch
(
()
=>
[
headerTitle
.
value
.
srcUrl
,
headerTitle
.
value
.
transUrl
],
()
=>
{
setTimeout
(()
=>
{
setupScrollSync
();
},
1000
);
},
{
deep
:
true
}
);
onMounted
(()
=>
{
onMounted
(
async
()
=>
{
window
.
sessionStorage
.
setItem
(
"curTabName"
,
"实体清单原文"
);
handleGetThinkTankReportSummary
();
handleGetThinkTankReportcontentUrl
();
console
.
log
(
"原文展示"
);
getUrlParams
();
getSingleSanctionOverviewData
();
setTimeout
(()
=>
{
setupScrollSync
();
},
500
);
});
</
script
>
<
style
scoped
lang=
"scss"
>
// * {
// margin: 0;
// padding: 0;
// }
.entity-list
{
width
:
100%
;
height
:
100%
;
overflow-y
:
auto
;
<
style
lang=
"scss"
scoped
>
.wrap
{
overflow-y
:
hidden
;
height
:
100vh
;
display
:
flex
;
flex-direction
:
column
;
.header
{
width
:
100%
;
height
:
148px
;
background-color
:
#fff
;
padding-top
:
16px
;
height
:
88px
;
box-sizing
:
border-box
;
border-bottom
:
1px
solid
rgba
(
234
,
236
,
238
,
1
);
box-shadow
:
0px
0px
20px
0px
rgba
(
25
,
69
,
130
,
0
.1
);
background
:
rgba
(
255
,
255
,
255
,
1
);
position
:
sticky
;
top
:
0
;
z-index
:
99999
;
overflow
:
hidden
;
.header-title
{
width
:
1601px
;
height
:
72px
;
background-color
:
rgba
(
246
,
250
,
255
,
1
);
.header-top
{
margin
:
0
auto
;
border-radius
:
1
0px
;
border
:
2px
solid
rgba
(
174
,
214
,
255
,
1
)
;
margin-top
:
2
0px
;
width
:
1600px
;
display
:
flex
;
align-items
:
center
;
margin-bottom
:
12px
;
position
:
relative
;
img
{
width
:
54px
;
height
:
54px
;
margin-left
:
15px
;
margin-right
:
11px
;
justify-content
:
space-between
;
.header-top-left
{
display
:
flex
;
img
{
width
:
44px
;
height
:
54px
;
}
.title
{
margin-left
:
20px
;
height
:
26px
;
color
:
rgba
(
59
,
65
,
75
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
20px
;
font-weight
:
700
;
line-height
:
26px
;
letter-spacing
:
0px
;
text-align
:
left
;
}
.en-title
{
margin-top
:
4px
;
margin-left
:
20px
;
height
:
24px
;
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
left
;
/* 👇 下面是 两行文本超出省略 核心代码 */
display
:
-
webkit-box
;
-webkit-line-clamp
:
1
;
/* 限制显示 2 行 */
-webkit-box-orient
:
vertical
;
overflow
:
hidden
;
text-overflow
:
ellipsis
;
word-break
:
break-all
;
}
.tag-box
{
margin-top
:
11px
;
display
:
flex
;
gap
:
8px
;
margin-left
:
20px
;
}
}
.title
{
font-size
:
20px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
26px
;
color
:
rgb
(
59
,
65
,
75
);
.header-top-right
{
display
:
flex
;
flex-direction
:
column
;
text-align
:
right
;
align-items
:
flex-end
;
.image-name-box
{
width
:
200px
;
height
:
24px
;
gap
:
6px
;
text-align
:
right
;
display
:
flex
;
justify-content
:
flex-end
;
.name
{
height
:
24px
;
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
right
;
}
.image
{
width
:
16px
;
height
:
16px
;
margin-top
:
5px
;
img
{
width
:
100%
;
height
:
100%
;
}
}
}
.time
{
height
:
24px
;
margin-top
:
5px
;
color
:
rgba
(
95
,
101
,
108
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
right
;
}
}
}
.header-bottom
{
margin
:
0
auto
;
margin-top
:
30px
;
width
:
1600px
;
height
:
48px
;
display
:
flex
;
justify-content
:
space-between
;
.tab-box
{
width
:
224px
;
height
:
48px
;
display
:
flex
;
gap
:
24px
;
.department
{
font-size
:
16px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
.tab
{
width
:
94px
;
height
:
48px
;
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
gap
:
4px
;
cursor
:
pointer
;
border-bottom
:
2px
solid
transparent
;
.icon
{
width
:
16px
;
height
:
16px
;
img
{
width
:
100%
;
height
:
100%
;
}
}
.text
{
height
:
24px
;
color
:
rgba
(
59
,
65
,
75
,
1
);
font-family
:
Microsoft
YaHei
;
font-size
:
18px
;
font-weight
:
400
;
line-height
:
24px
;
}
.textActive
{
color
:
rgba
(
5
,
95
,
194
,
1
);
font-weight
:
700
;
}
}
.tabActive
{
border-bottom
:
2px
solid
rgba
(
5
,
95
,
194
,
1
);
}
}
}
}
.main
{
margin
:
0
auto
;
margin
:
0px
auto
;
margin-top
:
20px
;
background
:
rgb
(
255
,
255
,
255
);
width
:
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
;
}
/* 打开时圆球从左边移到最右边:轨道 22px - 圆球 10px = 12px */
:deep
(
.el-switch.is-checked
.el-switch__button
),
:deep
(
.el-switch.is-checked
.el-switch__action
)
{
transform
:
translateX
(
6px
)
!
important
;
}
.translate-image
{
display
:
flex
;
width
:
16px
;
height
:
16px
;
overflow
:
hidden
;
img
{
width
:
100%
;
height
:
100%
;
}
}
:deep
(
.el-switch
)
{
--el-switch-on-color
:
#055fc2
;
--el-switch-off-color
:
#e6e7e8
;
.translate-text
{
font-size
:
14px
;
font-weight
:
400
;
line-height
:
22px
;
}
}
.el-switch__label
{
color
:
#fff
;
font-size
:
12px
;
font-weight
:
600
;
.btn
{
width
:
88px
;
height
:
32px
;
&
.is-active
{
color
:
#fff
;
}
box-sizing
:
border-box
;
border
:
1px
solid
rgba
(
230
,
231
,
232
,
1
);
border-radius
:
6px
;
background
:
rgba
(
255
,
255
,
255
,
1
);
display
:
flex
;
gap
:
8px
;
cursor
:
pointer
;
.icon
{
width
:
16px
;
height
:
16px
;
display
:
inline-flex
;
margin-top
:
8px
;
margin-left
:
16px
;
img
{
width
:
100%
;
height
:
100%
;
}
}
.text
{
margin-top
:
4px
;
width
:
32px
;
height
:
24px
;
color
:
rgb
(
59
,
65
,
75
);
font-family
:
"Source Han Sans CN"
;
font-size
:
16px
;
font-weight
:
400
;
line-height
:
24px
;
letter-spacing
:
0px
;
text-align
:
left
;
}
}
.search-btn
{
cursor
:
pointer
;
}
.search-input-wrap
{
display
:
inline-flex
;
align-items
:
center
;
gap
:
8px
;
margin-left
:
4px
;
flex-shrink
:
0
;
}
// :deep(.el-button) {
// --el-button-bg-color: #055fc2;
// --el-button-border-color: #055fc2;
// --el-button-hover-bg-color: #044c9b;
// --el-button-hover-border-color: #044c9b;
.search-input
{
width
:
160px
;
height
:
24px
;
box-sizing
:
border-box
;
border
:
1px
solid
rgba
(
231
,
243
,
255
,
1
);
background
:
rgba
(
246
,
250
,
255
,
1
);
border-radius
:
4px
;
padding
:
0
10px
;
font-family
:
"Source Han Sans CN"
;
font-size
:
14px
;
line-height
:
22px
;
outline
:
none
;
}
.search-match-count
{
font-family
:
"Source Han Sans CN"
;
font-weight
:
400
;
font-size
:
14px
;
line-height
:
22px
;
min-width
:
48px
;
text-align
:
center
;
flex-shrink
:
0
;
}
.search-nav-btn
{
width
:
68px
;
height
:
24px
;
box-sizing
:
border-box
;
border
:
1px
solid
rgba
(
231
,
243
,
255
,
1
);
background
:
rgba
(
246
,
250
,
255
,
1
);
border-radius
:
4px
;
font-family
:
"Source Han Sans CN"
;
font-weight
:
400
;
font-size
:
14px
;
line-height
:
22px
;
cursor
:
pointer
;
padding
:
0
;
flex-shrink
:
0
;
white-space
:
nowrap
;
}
// font-size: 14px;
// padding: 10px 20px;
// }
.search-nav-btn
:disabled
{
opacity
:
0
.45
;
cursor
:
not
-
allowed
;
}
}
}
.report-box
{
margin-left
:
70px
;
width
:
1456px
;
height
:
calc
(
100%
-
64px
)
;
height
:
881px
;
display
:
flex
;
overflow-y
:
auto
;
/* 右侧统一滚动条,控制两侧原文+译文一起滚动 */
overflow-x
:
hidden
;
// ✅ 添加居中对齐
justify-content
:
center
;
}
.pdf-pane-wrap
{
...
...
@@ -323,15 +763,11 @@ onMounted(() => {
max-width
:
50%
;
height
:
100%
;
min-width
:
0
;
transition
:
all
0
.3s
;
&
.center-mode
{
flex
:
0
0
100%
;
max-width
:
100%
;
// ✅ 添加居中样式
width
:
728px
;
// 约一半宽度,保持单栏时美观
margin
:
0
auto
;
}
}
.pdf-pane-wrap.is-full
{
flex
:
0
0
100%
;
max-width
:
100%
;
}
.pdf-pane-inner
{
...
...
src/views/exportControl/v2.0SingleSanction/originPage/pdf-back.vue
0 → 100644
浏览文件 @
7da93833
<
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/exportControl/v2.0SingleSanction/originPage/pdf.vue
浏览文件 @
7da93833
...
...
@@ -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
;
...
...
src/views/finance/index.vue
浏览文件 @
7da93833
...
...
@@ -6,7 +6,7 @@
<SearchContainer
style=
"margin-bottom: 0; margin-top: 48px; height: fit-content"
v-if=
"homeMainRef"
placeholder=
"搜索
出口管
制"
placeholder=
"搜索
投融资限
制"
:containerRef=
"homeMainRef"
areaName=
"实体清单"
/>
...
...
src/views/finance/singleSanction/components/deepMining/index.vue
浏览文件 @
7da93833
...
...
@@ -628,6 +628,9 @@ onMounted(async () => {
.search-input
{
flex
:
1
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
height
:
32px
;
}
}
...
...
src/views/finance/singleSanction/components/impactAnalysis/components/industrialImpact/index.vue
浏览文件 @
7da93833
...
...
@@ -1053,6 +1053,9 @@ onMounted(async () => {
.search-input
{
width
:
288px
;
border
:
1px
solid
#ddd
;
border-radius
:
4px
;
height
:
32px
;
:deep
(
.el-input__wrapper
)
{
height
:
32px
;
...
...
src/views/finance/singleSanction/components/sanctionsOverview/index.vue
浏览文件 @
7da93833
...
...
@@ -613,11 +613,6 @@ onMounted(() => {
</
script
>
<
style
scoped
lang=
"scss"
>
// * {
// margin: 0;
// padding: 0;
// }
.sanctions-overview
{
width
:
1601px
;
margin
:
0
auto
;
...
...
@@ -960,7 +955,7 @@ onMounted(() => {
margin-bottom
:
20px
;
:deep
(
.el-input__inner
)
{
font-size
:
1
6
px
;
font-size
:
1
4
px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论