Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
R
risk-monitor
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
蔡建
risk-monitor
Commits
506f41e6
提交
506f41e6
authored
3月 31, 2026
作者:
yanpeng
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
实体制裁文件同步滚动
上级
4d30163e
流水线
#260
已通过 于阶段
in 1 分 26 秒
变更
4
流水线
1
隐藏空白字符变更
内嵌
并排
正在显示
4 个修改的文件
包含
874 行增加
和
249 行删除
+874
-249
RelationGraph.vue
...nction/components/deepMining/components/RelationGraph.vue
+2
-2
index-back.vue
...xportControl/v2.0SingleSanction/originPage/index-back.vue
+271
-0
index.vue
...ews/exportControl/v2.0SingleSanction/originPage/index.vue
+216
-247
pdf.vue
...views/exportControl/v2.0SingleSanction/originPage/pdf.vue
+385
-0
没有找到文件。
src/views/exportControl/v2.0SingleSanction/components/deepMining/components/RelationGraph.vue
浏览文件 @
506f41e6
...
...
@@ -239,8 +239,8 @@ const initNormalGraph = (layoutType, width, height) => {
// 中心节点坐标
const
centerX
=
width
/
2
;
const
centerY
=
height
/
2
;
const
upperY
=
centerY
-
2
0
0
;
// 上方节点 Y 坐标
const
lowerY
=
centerY
+
2
0
0
;
// 下方节点 Y 坐标
const
upperY
=
centerY
-
2
4
0
;
// 上方节点 Y 坐标
const
lowerY
=
centerY
+
2
4
0
;
// 下方节点 Y 坐标
const
nodeSpacing
=
100
;
// 节点水平间距
// 分离中心节点和其他节点
...
...
src/views/exportControl/v2.0SingleSanction/originPage/index-back.vue
0 → 100644
浏览文件 @
506f41e6
<
template
>
<div
class=
"entity-list"
>
<div
class=
"header"
>
<div
class=
"header-title"
>
<img
:src=
"headerTitle.img"
alt=
""
/>
<div>
<div
class=
"title"
>
{{
headerTitle
.
title
}}
<!--
<span>
{{
headerTitle
.
titleEn
}}
</span>
-->
</div>
<div
class=
"department"
>
{{
headerTitle
.
department
}}
</div>
</div>
<!--
<div
class=
"btn"
>
<img
:src=
"icon01"
alt=
""
>
切换
</div>
-->
</div>
</div>
<div
class=
"main"
>
<div
class=
"pdf-container"
>
<iframe
v-if=
"headerTitle.srcUrl"
:src=
"headerTitle.srcUrl"
width=
"100%"
height=
"100%"
frameborder=
"0"
></iframe>
<div
v-else
class=
"no-pdf"
>
暂无原文
</div>
</div>
<div
class=
"pdf-container"
>
<iframe
v-if=
"headerTitle.transUrl"
:src=
"headerTitle.transUrl"
width=
"100%"
height=
"100%"
frameborder=
"0"
></iframe>
<div
v-else
class=
"no-pdf"
>
暂无译文
</div>
</div>
</div>
</div>
</
template
>
<
script
setup
>
import
{
ref
,
onMounted
}
from
"vue"
;
import
{
getSingleSanctionOverview
}
from
"@/api/exportControlV2.0.js"
;
import
title
from
"../assets/title.png"
;
import
icon01
from
"../assets/icon01.png"
;
// 单次制裁-制裁概况-基本信息
const
singleSanctionOverview
=
ref
({});
const
getSingleSanctionOverviewData
=
async
()
=>
{
if
(
!
sanRecordId
.
value
)
return
;
try
{
const
res
=
await
getSingleSanctionOverview
({
sanRecordId
:
sanRecordId
.
value
});
if
(
res
.
code
===
200
)
{
singleSanctionOverview
.
value
=
res
.
data
||
{};
// 格式化日期
let
dateStr
=
""
;
if
(
singleSanctionOverview
.
value
.
postDate
)
{
const
date
=
new
Date
(
singleSanctionOverview
.
value
.
postDate
);
if
(
!
isNaN
(
date
.
getTime
()))
{
dateStr
=
`
${
date
.
getFullYear
()}
年
${
date
.
getMonth
()
+
1
}
月
${
date
.
getDate
()}
日`
;
}
else
{
dateStr
=
singleSanctionOverview
.
value
.
postDate
;
}
}
// 更新头部信息
headerTitle
.
value
=
{
...
headerTitle
.
value
,
title
:
`
${
dateStr
}
《
${
singleSanctionOverview
.
value
.
sanTitleZh
||
singleSanctionOverview
.
value
.
sanTitle
}
》`
,
titleEn
:
singleSanctionOverview
.
value
.
sanTitle
||
""
,
department
:
singleSanctionOverview
.
value
.
fileCode
||
""
,
srcUrl
:
singleSanctionOverview
.
value
.
srcUrl
||
""
,
transUrl
:
singleSanctionOverview
.
value
.
transUrl
||
""
};
}
}
catch
(
error
)
{
console
.
error
(
"获取制裁概况失败:"
,
error
);
}
};
const
headerTitle
=
ref
({
img
:
title
});
// 获取URL参数
const
sanRecordId
=
ref
(
""
);
const
getUrlParams
=
()
=>
{
const
urlParams
=
new
URLSearchParams
(
window
.
location
.
search
);
sanRecordId
.
value
=
urlParams
.
get
(
"id"
)
||
""
;
};
onMounted
(()
=>
{
getUrlParams
();
getSingleSanctionOverviewData
();
});
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
}
.entity-list
{
width
:
100%
;
height
:
100%
;
.header
{
width
:
100%
;
height
:
148px
;
background-color
:
#fff
;
padding-top
:
16px
;
.header-title
{
width
:
1601px
;
height
:
72px
;
background-color
:
rgba
(
246
,
250
,
255
,
1
);
margin
:
0
auto
;
border-radius
:
10px
;
border
:
2px
solid
rgba
(
174
,
214
,
255
,
1
);
display
:
flex
;
align-items
:
center
;
margin-bottom
:
12px
;
position
:
relative
;
img
{
width
:
54px
;
height
:
54px
;
margin-left
:
15px
;
margin-right
:
11px
;
}
.title
{
font-size
:
20px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
26px
;
color
:
rgb
(
59
,
65
,
75
);
span
{
font-size
:
16px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
margin-left
:
11px
;
}
}
.department
{
font-size
:
16px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
}
.btn
{
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
position
:
absolute
;
right
:
16px
;
top
:
25px
;
font-size
:
18px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
5
,
95
,
194
);
img
{
width
:
20px
;
height
:
20px
;
margin-right
:
7px
;
}
}
}
.header-nav
{
width
:
1601px
;
margin
:
0
auto
;
height
:
48px
;
display
:
flex
;
align-items
:
center
;
.nav-item
{
display
:
flex
;
align-items
:
center
;
height
:
100%
;
margin-right
:
32px
;
cursor
:
pointer
;
position
:
relative
;
font-size
:
18px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
color
:
rgb
(
59
,
65
,
75
);
&
:last-child
{
margin-right
:
0
;
}
img
{
width
:
16px
;
height
:
16px
;
margin-right
:
4px
;
}
&
.active
{
color
:
rgb
(
5
,
95
,
194
);
font-weight
:
700
;
}
.active-line
{
position
:
absolute
;
bottom
:
0
;
left
:
0
;
width
:
100%
;
height
:
3px
;
background-color
:
#055fc2
;
border-radius
:
1
.5px
;
}
}
.original-text-btn
{
margin-left
:
auto
;
width
:
152px
;
height
:
36px
;
background
:
#ffffff
;
border-radius
:
4px
;
border
:
1px
solid
rgba
(
230
,
231
,
232
,
1
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
cursor
:
pointer
;
img
{
width
:
16px
;
height
:
16px
;
margin-right
:
8px
;
}
span
{
font-size
:
16px
;
font-weight
:
400
;
color
:
rgb
(
95
,
101
,
108
);
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
}
}
}
}
.main
{
width
:
1601px
;
height
:
calc
(
100%
-
148px
);
background-color
:
#f7f8f9
;
margin
:
0
auto
;
display
:
flex
;
justify-content
:
space-between
;
padding-top
:
20px
;
box-sizing
:
border-box
;
.pdf-container
{
width
:
790px
;
height
:
calc
(
100%
-
20px
);
background-color
:
#fff
;
// border: 1px solid rgba(174, 214, 255, 1);
border-radius
:
4px
;
overflow
:
hidden
;
.no-pdf
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
height
:
100%
;
color
:
#909399
;
font-size
:
16px
;
background-color
:
#fff
;
}
}
}
}
</
style
>
src/views/exportControl/v2.0SingleSanction/originPage/index.vue
浏览文件 @
506f41e6
<
template
>
<div
class=
"entity-list"
>
<div
class=
"header"
>
<div
class=
"header-title"
>
<img
:src=
"headerTitle.img"
alt=
""
>
<div>
<div
class=
"title"
>
{{
headerTitle
.
title
}}
<!--
<span>
{{
headerTitle
.
titleEn
}}
</span>
--
>
</div
>
<div
class=
"department"
>
{{
headerTitle
.
department
}}
</div>
</div>
<!--
<div
class=
"btn"
>
<img
:src=
"icon01"
alt=
""
>
切换
</div>
--
>
</div>
</div>
<div
class=
"main"
>
<div
class=
"pdf-container
"
>
<iframe
v-if=
"headerTitle.srcUrl"
:src=
"headerTitle.srcUrl"
width=
"100%"
height=
"100%"
frameborder=
"0"
></iframe
>
<div
v-else
class=
"no-pdf"
>
暂无原文
</div
>
</div>
<div
class=
"pdf-container
"
>
<iframe
v-if=
"headerTitle.transUrl"
:src=
"headerTitle.transUrl"
width=
"100%"
height=
"100%"
frameborder=
"0"
></iframe
>
<div
v-else
class=
"no-pdf"
>
暂无译文
</div>
</div>
</div>
</div>
<div
class=
"entity-list"
>
<div
class=
"header"
>
<div
class=
"header-title"
>
<img
:src=
"headerTitle.img"
alt=
""
/
>
<div>
<div
class=
"title"
>
{{
headerTitle
.
title
}}
</div
>
<div
class=
"department"
>
{{
headerTitle
.
department
}}
</div>
</div>
</div>
</div
>
<div
class=
"main"
>
<div
class=
"main-header"
>
<div>
实体清单制裁文件
</div>
</div>
<!-- 外层滚动容器,统一控制两侧滚动 --
>
<div
class=
"report-box"
ref=
"reportBoxRef
"
>
<div
class=
"pdf-pane-wrap"
>
<pdf
ref=
"leftPdfRef"
:pdfUrl=
"headerTitle.srcUrl"
class=
"pdf-pane-inner"
/
>
</div>
<div
class=
"pdf-pane-wrap
"
>
<pdf
ref=
"rightPdfRef"
:pdfUrl=
"headerTitle.transUrl"
class=
"pdf-pane-inner"
/
>
</div>
</div>
</div>
</div>
</
template
>
<
script
setup
>
import
{
ref
,
onMounted
}
from
'vue'
import
{
getSingleSanctionOverview
}
from
"@/api/exportControlV2.0.js"
import
title
from
"../assets/title.png"
import
icon01
from
"../assets/icon01.png"
import
{
ref
,
onMounted
,
watch
}
from
"vue"
;
import
{
getSingleSanctionOverview
}
from
"@/api/exportControlV2.0.js"
;
import
title
from
"../assets/title.png"
;
import
pdf
from
"./pdf.vue"
;
const
leftPdfRef
=
ref
(
null
);
const
rightPdfRef
=
ref
(
null
);
const
reportBoxRef
=
ref
(
null
);
const
headerTitle
=
ref
({
img
:
title
,
title
:
""
,
department
:
""
,
srcUrl
:
""
,
transUrl
:
""
});
const
sanRecordId
=
ref
(
""
);
const
isSyncing
=
ref
(
false
);
const
getUrlParams
=
()
=>
{
const
urlParams
=
new
URLSearchParams
(
window
.
location
.
search
);
sanRecordId
.
value
=
urlParams
.
get
(
"id"
)
||
""
;
};
// 单次制裁-制裁概况-基本信息
const
singleSanctionOverview
=
ref
({})
const
getSingleSanctionOverviewData
=
async
()
=>
{
if
(
!
sanRecordId
.
value
)
return
try
{
const
res
=
await
getSingleSanctionOverview
({
sanRecordId
:
sanRecordId
.
value
})
if
(
res
.
code
===
200
)
{
singleSanctionOverview
.
value
=
res
.
data
||
{}
// 格式化日期
let
dateStr
=
""
;
if
(
singleSanctionOverview
.
value
.
postDate
)
{
const
date
=
new
Date
(
singleSanctionOverview
.
value
.
postDate
);
if
(
!
isNaN
(
date
.
getTime
()))
{
dateStr
=
`
${
date
.
getFullYear
()}
年
${
date
.
getMonth
()
+
1
}
月
${
date
.
getDate
()}
日`
;
}
else
{
dateStr
=
singleSanctionOverview
.
value
.
postDate
;
}
}
if
(
!
sanRecordId
.
value
)
return
;
try
{
const
res
=
await
getSingleSanctionOverview
({
sanRecordId
:
sanRecordId
.
value
});
if
(
res
.
code
===
200
&&
res
.
data
)
{
const
singleSanctionOverview
=
res
.
data
||
{};
// 更新头部信息
headerTitle
.
value
=
{
...
headerTitle
.
value
,
title
:
`
${
dateStr
}
《
${
singleSanctionOverview
.
value
.
sanTitleZh
||
singleSanctionOverview
.
value
.
sanTitle
}
》`
,
titleEn
:
singleSanctionOverview
.
value
.
sanTitle
||
""
,
department
:
singleSanctionOverview
.
value
.
fileCode
||
""
,
srcUrl
:
singleSanctionOverview
.
value
.
srcUrl
||
""
,
transUrl
:
singleSanctionOverview
.
value
.
transUrl
||
""
}
}
}
catch
(
error
)
{
console
.
error
(
"获取制裁概况失败:"
,
error
)
}
}
// 格式化日期
let
dateStr
=
""
;
if
(
singleSanctionOverview
.
postDate
)
{
const
date
=
new
Date
(
singleSanctionOverview
.
postDate
);
if
(
!
isNaN
(
date
.
getTime
()))
{
dateStr
=
`
${
date
.
getFullYear
()}
年
${
date
.
getMonth
()
+
1
}
月
${
date
.
getDate
()}
日`
;
}
else
{
dateStr
=
singleSanctionOverview
.
postDate
;
}
}
// 更新头部信息
headerTitle
.
value
=
{
...
headerTitle
.
value
,
title
:
`
${
dateStr
}
《
${
singleSanctionOverview
.
sanTitleZh
||
singleSanctionOverview
.
sanTitle
}
》`
,
department
:
singleSanctionOverview
.
fileCode
||
""
,
srcUrl
:
singleSanctionOverview
.
srcUrl
||
""
,
transUrl
:
singleSanctionOverview
.
transUrl
||
""
};
}
}
catch
(
error
)
{
console
.
error
(
"获取制裁概况失败:"
,
error
);
}
};
// 同步滚动处理
const
handleSyncScroll
=
()
=>
{
if
(
isSyncing
.
value
)
return
;
isSyncing
.
value
=
true
;
requestAnimationFrame
(()
=>
{
isSyncing
.
value
=
false
;
});
};
// 监听滚动事件
const
setupScrollSync
=
()
=>
{
const
reportBox
=
reportBoxRef
.
value
;
if
(
!
reportBox
)
return
;
reportBox
.
addEventListener
(
"scroll"
,
handleSyncScroll
,
{
passive
:
true
});
};
// 监听 PDF 加载完成
watch
(
()
=>
[
headerTitle
.
value
.
srcUrl
,
headerTitle
.
value
.
transUrl
],
()
=>
{
// PDF URL 变化时,等待渲染完成后设置滚动监听
setTimeout
(()
=>
{
setupScrollSync
();
},
1000
);
},
{
deep
:
true
}
);
const
headerTitle
=
ref
({
img
:
title
,
})
// 获取URL参数
const
sanRecordId
=
ref
(
""
)
const
getUrlParams
=
()
=>
{
const
urlParams
=
new
URLSearchParams
(
window
.
location
.
search
);
sanRecordId
.
value
=
urlParams
.
get
(
"id"
)
||
""
}
onMounted
(()
=>
{
getUrlParams
()
getSingleSanctionOverviewData
()
})
getUrlParams
();
getSingleSanctionOverviewData
();
// 等待 DOM 渲染完成后设置滚动监听
setTimeout
(()
=>
{
setupScrollSync
();
},
500
);
});
</
script
>
<
style
scoped
lang=
"scss"
>
*
{
margin
:
0
;
padding
:
0
;
margin
:
0
;
padding
:
0
;
}
.entity-list
{
width
:
100%
;
height
:
100%
;
.header
{
width
:
100%
;
height
:
148px
;
background-color
:
#fff
;
padding-top
:
16px
;
.header-title
{
width
:
1601px
;
height
:
72px
;
background-color
:
rgba
(
246
,
250
,
255
,
1
);
margin
:
0
auto
;
border-radius
:
10px
;
border
:
2px
solid
rgba
(
174
,
214
,
255
,
1
);
display
:
flex
;
align-items
:
center
;
margin-bottom
:
12px
;
position
:
relative
;
img
{
width
:
54px
;
height
:
54px
;
margin-left
:
15px
;
margin-right
:
11px
;
}
.title
{
font-size
:
20px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
26px
;
color
:
rgb
(
59
,
65
,
75
);
span
{
font-size
:
16px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
margin-left
:
11px
;
}
}
.department
{
font-size
:
16px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
}
.btn
{
cursor
:
pointer
;
display
:
flex
;
align-items
:
center
;
position
:
absolute
;
right
:
16px
;
top
:
25px
;
font-size
:
18px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
5
,
95
,
194
);
img
{
width
:
20px
;
height
:
20px
;
margin-right
:
7px
;
}
}
}
.header-nav
{
width
:
1601px
;
margin
:
0
auto
;
height
:
48px
;
display
:
flex
;
align-items
:
center
;
.nav-item
{
display
:
flex
;
align-items
:
center
;
height
:
100%
;
margin-right
:
32px
;
cursor
:
pointer
;
position
:
relative
;
font-size
:
18px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
color
:
rgb
(
59
,
65
,
75
);
&
:last-child
{
margin-right
:
0
;
}
.entity-list
{
width
:
100%
;
height
:
100%
;
overflow-y
:
auto
;
.header
{
width
:
100%
;
height
:
148px
;
background-color
:
#fff
;
padding-top
:
16px
;
box-sizing
:
border-box
;
border-bottom
:
1px
solid
rgba
(
234
,
236
,
238
,
1
);
box-shadow
:
0px
0px
20px
0px
rgba
(
25
,
69
,
130
,
0
.1
);
.header-title
{
width
:
1601px
;
height
:
72px
;
background-color
:
rgba
(
246
,
250
,
255
,
1
);
margin
:
0
auto
;
border-radius
:
10px
;
border
:
2px
solid
rgba
(
174
,
214
,
255
,
1
);
display
:
flex
;
align-items
:
center
;
margin-bottom
:
12px
;
position
:
relative
;
img
{
width
:
54px
;
height
:
54px
;
margin-left
:
15px
;
margin-right
:
11px
;
}
.title
{
font-size
:
20px
;
font-weight
:
700
;
font-family
:
"Microsoft YaHei"
;
line-height
:
26px
;
color
:
rgb
(
59
,
65
,
75
);
}
.department
{
font-size
:
16px
;
font-weight
:
400
;
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
color
:
rgb
(
95
,
101
,
108
);
}
}
}
.main
{
margin
:
0
auto
;
background
:
rgb
(
255
,
255
,
255
);
width
:
1601px
;
height
:
calc
(
100vh
-
148px
);
margin-bottom
:
20px
;
border
:
1px
solid
rgb
(
234
,
236
,
238
);
box-shadow
:
0
0
20px
0
rgba
(
25
,
69
,
130
,
0
.1
);
img
{
width
:
16px
;
height
:
16px
;
margin-right
:
4px
;
}
.main-header
{
height
:
64px
;
border-bottom
:
1px
solid
rgb
(
234
,
236
,
238
);
background
:
rgb
(
255
,
255
,
255
);
margin
:
0
70px
;
color
:
rgba
(
59
,
65
,
75
,
1
);
font-family
:
"Source Han Sans CN"
;
font-size
:
20px
;
font-weight
:
700
;
line-height
:
26px
;
width
:
1456px
;
text-align
:
left
;
display
:
flex
;
align-items
:
center
;
}
&
.active
{
color
:
rgb
(
5
,
95
,
194
);
font-weight
:
700
;
}
.report-box
{
margin-left
:
70px
;
width
:
1456px
;
height
:
calc
(
100%
-
64px
);
display
:
flex
;
overflow-y
:
auto
;
/* 统一滚动条,控制两侧一起滚动 */
overflow-x
:
hidden
;
}
.active-line
{
position
:
absolute
;
bottom
:
0
;
left
:
0
;
width
:
100%
;
height
:
3px
;
background-color
:
#055fc2
;
border-radius
:
1
.5px
;
}
}
.pdf-pane-wrap
{
flex
:
0
0
50%
;
max-width
:
50%
;
height
:
100%
;
min-width
:
0
;
}
.original-text-btn
{
margin-left
:
auto
;
width
:
152px
;
height
:
36px
;
background
:
#FFFFFF
;
border-radius
:
4px
;
border
:
1px
solid
rgba
(
230
,
231
,
232
,
1
);
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
cursor
:
pointer
;
img
{
width
:
16px
;
height
:
16px
;
margin-right
:
8px
;
}
span
{
font-size
:
16px
;
font-weight
:
400
;
color
:
rgb
(
95
,
101
,
108
);
font-family
:
"Microsoft YaHei"
;
line-height
:
24px
;
}
}
}
}
.main
{
width
:
1601px
;
height
:
calc
(
100%
-
148px
);
background-color
:
#F7F8F9
;
margin
:
0
auto
;
display
:
flex
;
justify-content
:
space-between
;
padding-top
:
20px
;
box-sizing
:
border-box
;
.pdf-container
{
width
:
790px
;
height
:
calc
(
100%
-
20px
);
background-color
:
#fff
;
// border: 1px solid rgba(174, 214, 255, 1);
border-radius
:
4px
;
overflow
:
hidden
;
.no-pdf
{
display
:
flex
;
align-items
:
center
;
justify-content
:
center
;
height
:
100%
;
color
:
#909399
;
font-size
:
16px
;
background-color
:
#fff
;
}
}
}
.pdf-pane-inner
{
width
:
100%
;
height
:
100%
;
}
}
}
</
style
>
\ No newline at end of file
</
style
>
src/views/exportControl/v2.0SingleSanction/originPage/pdf.vue
0 → 100644
浏览文件 @
506f41e6
<
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
>
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论