Skip to content
Projects
Groups
Snippets
Help
Loading...
Help
Submit feedback
Contribute to GitLab
Sign in
Toggle navigation
飞
飞鹤小程序
Project
Project
Details
Activity
Releases
Cycle Analytics
Repository
Repository
Files
Commits
Branches
Tags
Contributors
Graph
Compare
Charts
Issues
0
Issues
0
List
Board
Labels
Milestones
Merge Requests
0
Merge Requests
0
Wiki
Wiki
Snippets
Snippets
Members
Members
Collapse sidebar
Close sidebar
Activity
Graph
Charts
Create a new issue
Commits
Issue Boards
Open sidebar
FH
飞鹤小程序
Commits
ff0e8c7f
Commit
ff0e8c7f
authored
Nov 12, 2025
by
spc
Browse files
Options
Browse Files
Download
Email Patches
Plain Diff
fixed
parent
a889ca63
Changes
1
Hide whitespace changes
Inline
Side-by-side
Showing
1 changed file
with
191 additions
and
23 deletions
+191
-23
XingmaLabPosterPage.vue
components/xingmaLab/XingmaLabPosterPage.vue
+191
-23
No files found.
components/xingmaLab/XingmaLabPosterPage.vue
View file @
ff0e8c7f
...
...
@@ -110,10 +110,14 @@ const qrcodeImageUrl = ref('') // 二维码图片 URL
const
isDownloading
=
ref
(
false
)
// 是否正在下载
// Canvas 尺寸(海报尺寸:542rpx x 956rpx)
// 使用 3 倍像素密度提高分辨率
const
pixelRatio
=
3
const
systemInfo
=
uni
.
getSystemInfoSync
()
const
screenWidth
=
systemInfo
.
windowWidth
||
375
const
canvasWidth
=
(
542
/
750
)
*
screenWidth
const
canvasHeight
=
(
956
/
750
)
*
screenWidth
const
baseCanvasWidth
=
(
542
/
750
)
*
screenWidth
const
baseCanvasHeight
=
(
956
/
750
)
*
screenWidth
const
canvasWidth
=
baseCanvasWidth
*
pixelRatio
const
canvasHeight
=
baseCanvasHeight
*
pixelRatio
// 监听 visible 变化,生成二维码
watch
(()
=>
props
.
visible
,
(
newVal
)
=>
{
...
...
@@ -221,9 +225,12 @@ const handleDownload = async () => {
// 保存初始状态
ctx
.
save
()
// 海报尺寸(rpx 转 px)
const
posterWidth
=
canvasWidth
const
posterHeight
=
canvasHeight
// 缩放 canvas 上下文以适应高分辨率
ctx
.
scale
(
pixelRatio
,
pixelRatio
)
// 海报尺寸(使用基础尺寸,因为已经缩放)
const
posterWidth
=
baseCanvasWidth
const
posterHeight
=
baseCanvasHeight
// 绘制背景
await
drawImage
(
ctx
,
`
${
$baseUrl
}
homepage/Q3Res/xingmaLabPosterBg.png`
,
0
,
0
,
posterWidth
,
posterHeight
)
...
...
@@ -265,7 +272,10 @@ const handleDownload = async () => {
ctx
.
setTextAlign
(
'left'
)
const
nicknameTextX
=
nicknameBgX
+
(
props
.
avatar
?
(
48
+
20
)
/
750
*
screenWidth
:
30
/
750
*
screenWidth
)
const
nicknameTextY
=
nicknameBgY
+
nicknameBgH
/
2
+
(
28
/
750
)
*
screenWidth
/
3
ctx
.
fillText
(
nickname
.
value
||
''
,
nicknameTextX
,
nicknameTextY
)
// 计算昵称文字最大宽度(昵称背景宽度 - 左边距 - 右边距)
const
nicknameMaxWidth
=
nicknameBgW
-
(
props
.
avatar
?
(
48
+
20
)
/
750
*
screenWidth
:
30
/
750
*
screenWidth
)
-
(
30
/
750
*
screenWidth
)
const
truncatedNickname
=
truncateText
(
ctx
,
nickname
.
value
||
''
,
nicknameMaxWidth
)
ctx
.
fillText
(
truncatedNickname
,
nicknameTextX
,
nicknameTextY
)
// 绘制图片区域背景(居中)
const
imageBgW
=
(
494
/
750
)
*
screenWidth
...
...
@@ -275,13 +285,13 @@ const handleDownload = async () => {
await
drawImage
(
ctx
,
`
${
$baseUrl
}
homepage/Q3Res/xingmaLabPosterImgBg.png`
,
imageBgX
,
imageBgY
,
imageBgW
,
imageBgH
)
// 绘制主图片
// 绘制主图片
(自适应模式,保持宽高比)
if
(
props
.
imageUrl
)
{
const
imageX
=
imageBgX
+
(
26
/
750
)
*
screenWidth
const
imageY
=
imageBgY
+
(
26
/
750
)
*
screenWidth
const
imageW
=
imageBgW
-
(
52
/
750
)
*
screenWidth
const
imageH
=
(
586
/
750
)
*
screenWidth
await
drawImage
(
ctx
,
props
.
imageUrl
,
imageX
,
imageY
,
imageW
,
imageH
)
await
drawImage
(
ctx
,
props
.
imageUrl
,
imageX
,
imageY
,
imageW
,
imageH
,
true
)
}
// 绘制文字内容
...
...
@@ -290,7 +300,10 @@ const handleDownload = async () => {
ctx
.
setTextAlign
(
'left'
)
const
contentX
=
imageBgX
+
(
22
/
750
)
*
screenWidth
const
contentY
=
imageBgY
+
imageBgH
-
(
146
/
750
)
*
screenWidth
ctx
.
fillText
(
props
.
content
||
''
,
contentX
,
contentY
)
// 计算内容文字最大宽度(图片区域宽度 - 左右边距)
const
contentMaxWidth
=
imageBgW
-
(
44
/
750
)
*
screenWidth
const
truncatedContent
=
truncateText
(
ctx
,
props
.
content
||
''
,
contentMaxWidth
)
ctx
.
fillText
(
truncatedContent
,
contentX
,
contentY
)
// 绘制唯一藏品编号
ctx
.
setFillStyle
(
'#D3A458'
)
...
...
@@ -329,13 +342,13 @@ const handleDownload = async () => {
ctx
.
draw
(
false
,
async
()
=>
{
await
nextTick
()
// 转换为临时文件
// 转换为临时文件
(使用高分辨率)
uni
.
canvasToTempFilePath
({
canvasId
:
'posterCanvas'
,
width
:
poster
Width
,
height
:
poster
Height
,
destWidth
:
poster
Width
,
destHeight
:
poster
Height
,
width
:
canvas
Width
,
height
:
canvas
Height
,
destWidth
:
canvas
Width
,
destHeight
:
canvas
Height
,
success
:
(
res
)
=>
{
// 保存到相册
uni
.
saveImageToPhotosAlbum
({
...
...
@@ -344,7 +357,7 @@ const handleDownload = async () => {
uni
.
hideLoading
()
uni
.
showToast
({
title
:
'保存成功'
,
icon
:
'
success
'
icon
:
'
none
'
})
isDownloading
.
value
=
false
},
...
...
@@ -455,9 +468,163 @@ const base64ToTempFilePath = (base64Data) => {
})
}
// 截断文字(canvas 中文字超出宽度时添加省略号)
const
truncateText
=
(
ctx
,
text
,
maxWidth
)
=>
{
if
(
!
text
)
return
''
// 测量文字宽度
const
textWidth
=
ctx
.
measureText
(
text
).
width
// 如果文字宽度小于等于最大宽度,直接返回
if
(
textWidth
<=
maxWidth
)
{
return
text
}
// 如果超出,需要截断并添加省略号
const
ellipsis
=
'...'
const
ellipsisWidth
=
ctx
.
measureText
(
ellipsis
).
width
const
availableWidth
=
maxWidth
-
ellipsisWidth
// 二分查找合适的截断位置
let
left
=
0
let
right
=
text
.
length
let
mid
=
0
while
(
left
<
right
)
{
mid
=
Math
.
floor
((
left
+
right
)
/
2
)
const
testText
=
text
.
substring
(
0
,
mid
)
const
testWidth
=
ctx
.
measureText
(
testText
).
width
if
(
testWidth
<=
availableWidth
)
{
left
=
mid
+
1
}
else
{
right
=
mid
}
}
// 确保至少保留一个字符
const
truncatePos
=
Math
.
max
(
1
,
left
-
1
)
return
text
.
substring
(
0
,
truncatePos
)
+
ellipsis
}
// 获取图片信息(宽高)
const
getImageInfo
=
(
src
)
=>
{
return
new
Promise
((
resolve
,
reject
)
=>
{
uni
.
getImageInfo
({
src
:
src
,
success
:
(
res
)
=>
{
resolve
({
width
:
res
.
width
,
height
:
res
.
height
})
},
fail
:
(
err
)
=>
{
reject
(
err
)
}
})
})
}
// 计算自适应尺寸(保持宽高比,类似 aspectFit)
const
calculateFitSize
=
(
imageWidth
,
imageHeight
,
containerWidth
,
containerHeight
)
=>
{
const
imageRatio
=
imageWidth
/
imageHeight
const
containerRatio
=
containerWidth
/
containerHeight
let
drawWidth
,
drawHeight
,
drawX
,
drawY
if
(
imageRatio
>
containerRatio
)
{
// 图片更宽,以宽度为准
drawWidth
=
containerWidth
drawHeight
=
containerWidth
/
imageRatio
drawX
=
0
drawY
=
(
containerHeight
-
drawHeight
)
/
2
}
else
{
// 图片更高,以高度为准
drawWidth
=
containerHeight
*
imageRatio
drawHeight
=
containerHeight
drawX
=
(
containerWidth
-
drawWidth
)
/
2
drawY
=
0
}
return
{
drawWidth
,
drawHeight
,
drawX
,
drawY
}
}
// 绘制图片到 canvas(异步加载图片)
const
drawImage
=
(
ctx
,
src
,
x
,
y
,
w
,
h
)
=>
{
const
drawImage
=
(
ctx
,
src
,
x
,
y
,
w
,
h
,
fitMode
=
false
)
=>
{
return
new
Promise
((
resolve
)
=>
{
// 如果是自适应模式,需要先获取图片尺寸
if
(
fitMode
)
{
// 先获取图片路径(Base64 需要转换)
if
(
src
.
startsWith
(
'data:image'
))
{
// Base64 图片:先转换为临时文件,再获取信息
base64ToTempFilePath
(
src
).
then
((
filePath
)
=>
{
// 获取图片信息
return
getImageInfo
(
filePath
).
then
((
info
)
=>
{
const
{
drawWidth
,
drawHeight
,
drawX
,
drawY
}
=
calculateFitSize
(
info
.
width
,
info
.
height
,
w
,
h
)
ctx
.
drawImage
(
filePath
,
x
+
drawX
,
y
+
drawY
,
drawWidth
,
drawHeight
)
resolve
()
}).
catch
(()
=>
{
// 如果获取图片信息失败,使用原始尺寸
ctx
.
drawImage
(
filePath
,
x
,
y
,
w
,
h
)
resolve
()
})
}).
catch
((
err
)
=>
{
console
.
error
(
'Base64 图片处理失败:'
,
err
)
// 如果转换失败,尝试直接使用(某些平台可能支持)
try
{
ctx
.
drawImage
(
src
,
x
,
y
,
w
,
h
)
resolve
()
}
catch
(
e
)
{
console
.
warn
(
'无法绘制 Base64 图片,跳过'
)
resolve
()
// 即使失败也继续
}
})
return
}
else
{
// 普通图片 URL
getImageInfo
(
src
).
then
((
info
)
=>
{
const
{
drawWidth
,
drawHeight
,
drawX
,
drawY
}
=
calculateFitSize
(
info
.
width
,
info
.
height
,
w
,
h
)
// 先下载图片
uni
.
downloadFile
({
url
:
src
,
success
:
(
res
)
=>
{
if
(
res
.
statusCode
===
200
)
{
ctx
.
drawImage
(
res
.
tempFilePath
,
x
+
drawX
,
y
+
drawY
,
drawWidth
,
drawHeight
)
resolve
()
}
else
{
console
.
warn
(
'图片下载失败:'
,
src
,
'statusCode:'
,
res
.
statusCode
)
resolve
()
// 即使失败也继续
}
},
fail
:
(
err
)
=>
{
console
.
warn
(
'图片下载失败:'
,
src
,
err
)
resolve
()
// 即使失败也继续
}
})
}).
catch
(()
=>
{
// 如果获取图片信息失败,使用原始逻辑
uni
.
downloadFile
({
url
:
src
,
success
:
(
res
)
=>
{
if
(
res
.
statusCode
===
200
)
{
ctx
.
drawImage
(
res
.
tempFilePath
,
x
,
y
,
w
,
h
)
resolve
()
}
else
{
console
.
warn
(
'图片下载失败:'
,
src
,
'statusCode:'
,
res
.
statusCode
)
resolve
()
// 即使失败也继续
}
},
fail
:
(
err
)
=>
{
console
.
warn
(
'图片下载失败:'
,
src
,
err
)
resolve
()
// 即使失败也继续
}
})
})
return
}
}
// 非自适应模式,使用原始逻辑
// 如果是 Base64 Data URI,先转换为临时文件
if
(
src
.
startsWith
(
'data:image'
))
{
base64ToTempFilePath
(
src
).
then
((
filePath
)
=>
{
...
...
@@ -569,16 +736,13 @@ const drawImage = (ctx, src, x, y, w, h) => {
position: absolute;
left: 0;
top: 0;
width:
25
6rpx;
width:
18
6rpx;
height: 58rpx;
padding:
0 3
0rpx;
padding:
5rpx 0 0 7
0rpx;
font-size: 28rpx;
color: #
333333
;
color: #
000000
;
text-align: center;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
...
...
@@ -618,7 +782,8 @@ const drawImage = (ctx, src, x, y, w, h) => {
position: absolute;
bottom: 146rpx;
left: 22rpx;
width: calc(100% - 44rpx);
width: 450rpx;
height: 40rpx;
z-index: 2;
}
...
...
@@ -626,9 +791,12 @@ const drawImage = (ctx, src, x, y, w, h) => {
font-size: 28rpx;
font-weight: 500;
color: #000000;
width: 450rpx;
height: 40rpx;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
position: absolute;
}
.poster-collection-number {
...
...
Write
Preview
Markdown
is supported
0%
Try again
or
attach a new file
Attach a file
Cancel
You are about to add
0
people
to the discussion. Proceed with caution.
Finish editing this message first!
Cancel
Please
register
or
sign in
to comment