fix: metadata
This commit is contained in:
@@ -13,7 +13,9 @@
|
||||
<view class="step-num-wrap">
|
||||
<view class="step-line" v-if="idx > 0"></view>
|
||||
<view class="step-num">
|
||||
<text v-if="activeTool === tool.type && showPanel">{{ tool.icon }}</text>
|
||||
<text v-if="activeTool === tool.type && showPanel">{{
|
||||
tool.icon
|
||||
}}</text>
|
||||
<text v-else>{{ tool.step }}</text>
|
||||
</view>
|
||||
</view>
|
||||
@@ -62,12 +64,17 @@
|
||||
fontSize: fontSize + 'rpx',
|
||||
lineHeight: fontSize * 1.5 + 'rpx',
|
||||
}"
|
||||
>{{ targetName + "\n " + blessingText.content }}</text
|
||||
>{{
|
||||
(targetName || "") + "\n " + (blessingText.content || "")
|
||||
}}</text
|
||||
>
|
||||
</view>
|
||||
<view
|
||||
class="user"
|
||||
:style="{ left: 160 + userOffsetX + 'rpx', bottom: 40 - userOffsetY + 'rpx' }"
|
||||
:style="{
|
||||
left: 160 + userOffsetX + 'rpx',
|
||||
bottom: 40 - userOffsetY + 'rpx',
|
||||
}"
|
||||
@touchstart.stop="handleUserTouchStart"
|
||||
@touchmove.stop="handleUserTouchMove"
|
||||
@touchend.stop="handleUserTouchEnd"
|
||||
@@ -113,31 +120,45 @@
|
||||
<!-- 弹出编辑面板 -->
|
||||
<view class="panel-container" :class="{ show: showPanel }">
|
||||
<view class="panel-mask" @tap="closePanel"></view>
|
||||
<view class="panel-content" :class="{ 'glass-effect': activeTool === 'text' || activeTool === 'position' }">
|
||||
<view
|
||||
class="panel-content"
|
||||
:class="{
|
||||
'glass-effect': activeTool === 'text' || activeTool === 'position',
|
||||
}"
|
||||
>
|
||||
<view class="panel-handle" @tap="closePanel"></view>
|
||||
|
||||
<!-- 标题选择区 -->
|
||||
<view v-if="activeTool === 'title'" class="section">
|
||||
<view class="section-title">
|
||||
<text>选择标题</text>
|
||||
</view>
|
||||
<view class="tpl-scroll">
|
||||
<view class="tpl-grid">
|
||||
<view
|
||||
v-for="(title, i) in titles"
|
||||
:key="i"
|
||||
class="tpl-card title-card"
|
||||
:class="{ selected: title?.id === currentTitle?.id }"
|
||||
@tap="selectTitle(title)"
|
||||
>
|
||||
<image :src="title.imageUrl" class="title-cover" mode="aspectFit" />
|
||||
<view v-if="title?.id === currentTitle?.id" class="tpl-check">✔</view>
|
||||
</view>
|
||||
<!-- 标题选择区 -->
|
||||
<view v-if="activeTool === 'title'" class="section">
|
||||
<view class="section-title">
|
||||
<text>选择标题</text>
|
||||
</view>
|
||||
<view class="tpl-scroll">
|
||||
<view class="tpl-grid">
|
||||
<view
|
||||
v-for="(title, i) in titles"
|
||||
:key="i"
|
||||
class="tpl-card title-card"
|
||||
:class="{ selected: title?.id === currentTitle?.id }"
|
||||
@tap="selectTitle(title)"
|
||||
>
|
||||
<image
|
||||
:src="title.imageUrl"
|
||||
class="title-cover"
|
||||
mode="aspectFit"
|
||||
/>
|
||||
<view v-if="title?.id === currentTitle?.id" class="tpl-check"
|
||||
>✔</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loadingTitles" class="loading-more">加载中...</view>
|
||||
<view
|
||||
v-else-if="!hasMoreTitles && titles.length > 0"
|
||||
class="no-more"
|
||||
>没有更多了</view
|
||||
>
|
||||
</view>
|
||||
<view v-if="loadingTitles" class="loading-more">加载中...</view>
|
||||
<view v-else-if="!hasMoreTitles && titles.length > 0" class="no-more">没有更多了</view>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 模板区 -->
|
||||
@@ -154,7 +175,11 @@
|
||||
:class="{ selected: tpl?.id === currentTemplate?.id }"
|
||||
@tap="applyTemplate(tpl)"
|
||||
>
|
||||
<image :src="tpl.imageUrl" class="tpl-cover" mode="aspectFill" />
|
||||
<image
|
||||
:src="tpl.imageUrl"
|
||||
class="tpl-cover"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<view class="tpl-name">{{ tpl.name }}</view>
|
||||
<view v-if="tpl?.id === currentTemplate?.id" class="tpl-check"
|
||||
>✔</view
|
||||
@@ -168,7 +193,6 @@
|
||||
>没有更多了</view
|
||||
>
|
||||
</view>
|
||||
|
||||
</view>
|
||||
|
||||
<!-- 文字编辑 -->
|
||||
@@ -193,7 +217,11 @@
|
||||
<text class="refresh-icon">↻</text> 换一批
|
||||
</view>
|
||||
</view>
|
||||
<scroll-view scroll-x class="greeting-scroll" show-scrollbar="false">
|
||||
<scroll-view
|
||||
scroll-x
|
||||
class="greeting-scroll"
|
||||
show-scrollbar="false"
|
||||
>
|
||||
<view class="greeting-list">
|
||||
<view
|
||||
v-for="(text, index) in displayedGreetings"
|
||||
@@ -272,7 +300,9 @@
|
||||
:style="{ background: color }"
|
||||
@tap="selectedColor = color"
|
||||
>
|
||||
<view v-if="selectedColor === color" class="color-check">✔</view>
|
||||
<view v-if="selectedColor === color" class="color-check"
|
||||
>✔</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -284,7 +314,7 @@
|
||||
<text>调整位置</text>
|
||||
</view>
|
||||
|
||||
<view class="form-item" style="margin-top: 20rpx;">
|
||||
<view class="form-item" style="margin-top: 20rpx">
|
||||
<text class="label">祝福语气泡 (上下)</text>
|
||||
<slider
|
||||
:value="bubbleOffsetY"
|
||||
@@ -342,7 +372,9 @@
|
||||
:style="{ background: color }"
|
||||
@tap="signatureColor = color"
|
||||
>
|
||||
<view v-if="signatureColor === color" class="color-check">✔</view>
|
||||
<view v-if="signatureColor === color" class="color-check"
|
||||
>✔</view
|
||||
>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
@@ -408,9 +440,9 @@ const titleState = ref({
|
||||
const titleStyle = computed(() => {
|
||||
return {
|
||||
transform: `translate(${titleState.value.offsetX}rpx, ${titleState.value.offsetY}rpx) scale(${titleState.value.scale})`,
|
||||
top: '40rpx',
|
||||
pointerEvents: 'auto',
|
||||
transition: 'none'
|
||||
top: "40rpx",
|
||||
pointerEvents: "auto",
|
||||
transition: "none",
|
||||
};
|
||||
});
|
||||
|
||||
@@ -560,7 +592,7 @@ const fontList = [
|
||||
name: "中圆",
|
||||
family: "ZhongYuan",
|
||||
url: "https://file.lihailezzc.com/ddcd9621740449a29c329f573bc1d0c5.woff2", // 示例地址
|
||||
}
|
||||
},
|
||||
];
|
||||
const selectedFont = ref(fontList[0]);
|
||||
const loadedFonts = ref(new Set()); // 记录已加载的字体
|
||||
@@ -957,69 +989,71 @@ const saveByCanvas = async (save = true) => {
|
||||
});
|
||||
};
|
||||
|
||||
// 辅助函数:rpx 转 px (基于预览容器宽度 506rpx 对应 Canvas 540px)
|
||||
const r2p = (rpx) => (rpx * 540) / 506;
|
||||
// 辅助函数:rpx 转 px (基于预览容器宽度 506rpx 对应 Canvas 540px)
|
||||
const r2p = (rpx) => (rpx * 540) / 506;
|
||||
|
||||
try {
|
||||
// 1️⃣ 画背景
|
||||
// ⭐ 先加载背景图
|
||||
const [bgImg, avatarImg, titleImg] = await Promise.all([
|
||||
loadCanvasImage(currentTemplate?.value?.imageUrl),
|
||||
loadCanvasImage(userAvatar.value),
|
||||
currentTitle.value ? loadCanvasImage(currentTitle.value.imageUrl) : Promise.resolve(null),
|
||||
]);
|
||||
try {
|
||||
// 1️⃣ 画背景
|
||||
// ⭐ 先加载背景图
|
||||
const [bgImg, avatarImg, titleImg] = await Promise.all([
|
||||
loadCanvasImage(currentTemplate?.value?.imageUrl),
|
||||
loadCanvasImage(userAvatar.value),
|
||||
currentTitle.value
|
||||
? loadCanvasImage(currentTitle.value.imageUrl)
|
||||
: Promise.resolve(null),
|
||||
]);
|
||||
|
||||
ctx.drawImage(bgImg, 0, 0, W, H);
|
||||
ctx.drawImage(bgImg, 0, 0, W, H);
|
||||
|
||||
// 2️⃣ 半透明遮罩
|
||||
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
// 2️⃣ 半透明遮罩
|
||||
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// 3️⃣ 标题图片
|
||||
if (titleImg) {
|
||||
const previewBaseWidth = 400; // rpx
|
||||
const drawWidth = r2p(previewBaseWidth) * titleState.value.scale;
|
||||
const drawHeight = (titleImg.height / titleImg.width) * drawWidth;
|
||||
|
||||
// 计算绘制起点:居中 + 偏移量
|
||||
const titleX = (W - drawWidth) / 2 + r2p(titleState.value.offsetX);
|
||||
const titleY = r2p(40) + r2p(titleState.value.offsetY);
|
||||
|
||||
ctx.drawImage(titleImg, titleX, titleY, drawWidth, drawHeight);
|
||||
}
|
||||
// 3️⃣ 标题图片
|
||||
if (titleImg) {
|
||||
const previewBaseWidth = 400; // rpx
|
||||
const drawWidth = r2p(previewBaseWidth) * titleState.value.scale;
|
||||
const drawHeight = (titleImg.height / titleImg.width) * drawWidth;
|
||||
|
||||
// 4️⃣ 祝福语气泡
|
||||
// 预览中 .bubble 有 padding: 40rpx,且 .card-overlay 有 padding: 30rpx
|
||||
// 意味着文字距离容器边缘至少有 70rpx
|
||||
drawBubbleText(ctx, {
|
||||
text: targetName.value + "\n " + blessingText.value.content,
|
||||
x: 0,
|
||||
y: r2p(230 + bubbleOffsetY.value),
|
||||
maxWidth: r2p(bubbleMaxWidth.value), // 预览中 bubble-text 的宽度
|
||||
canvasWidth: W,
|
||||
fontSize: r2p(fontSize.value),
|
||||
lineHeight: r2p(fontSize.value * 1.6), // 预览中是 1.6
|
||||
padding: r2p(40 + 30), // 内部 padding 40 + 容器 padding 30
|
||||
backgroundColor: "transparent",
|
||||
textColor: selectedColor.value,
|
||||
fontFamily: selectedFont.value.family,
|
||||
});
|
||||
// 计算绘制起点:居中 + 偏移量
|
||||
const titleX = (W - drawWidth) / 2 + r2p(titleState.value.offsetX);
|
||||
const titleY = r2p(40) + r2p(titleState.value.offsetY);
|
||||
|
||||
// 5️⃣ 用户信息
|
||||
// 预览中 user 是 absolute, left: 160 + offsetX, bottom: 40 - offsetY
|
||||
drawUserBubble(ctx, {
|
||||
x: r2p(160 + userOffsetX.value),
|
||||
bottom: r2p(40 - userOffsetY.value),
|
||||
canvasHeight: H,
|
||||
avatarImg: avatarImg,
|
||||
username: signatureName.value,
|
||||
desc: "送上祝福",
|
||||
textColor: signatureColor.value,
|
||||
avatarSize: r2p(64),
|
||||
padding: r2p(15),
|
||||
fontSizeName: r2p(24),
|
||||
fontSizeDesc: r2p(20),
|
||||
});
|
||||
ctx.drawImage(titleImg, titleX, titleY, drawWidth, drawHeight);
|
||||
}
|
||||
|
||||
// 4️⃣ 祝福语气泡
|
||||
// 预览中 .bubble 有 padding: 40rpx,且 .card-overlay 有 padding: 30rpx
|
||||
// 意味着文字距离容器边缘至少有 70rpx
|
||||
drawBubbleText(ctx, {
|
||||
text: targetName.value + "\n " + blessingText.value.content,
|
||||
x: 0,
|
||||
y: r2p(230 + bubbleOffsetY.value),
|
||||
maxWidth: r2p(bubbleMaxWidth.value), // 预览中 bubble-text 的宽度
|
||||
canvasWidth: W,
|
||||
fontSize: r2p(fontSize.value),
|
||||
lineHeight: r2p(fontSize.value * 1.6), // 预览中是 1.6
|
||||
padding: r2p(40 + 30), // 内部 padding 40 + 容器 padding 30
|
||||
backgroundColor: "transparent",
|
||||
textColor: selectedColor.value,
|
||||
fontFamily: selectedFont.value.family,
|
||||
});
|
||||
|
||||
// 5️⃣ 用户信息
|
||||
// 预览中 user 是 absolute, left: 160 + offsetX, bottom: 40 - offsetY
|
||||
drawUserBubble(ctx, {
|
||||
x: r2p(160 + userOffsetX.value),
|
||||
bottom: r2p(40 - userOffsetY.value),
|
||||
canvasHeight: H,
|
||||
avatarImg: avatarImg,
|
||||
username: signatureName.value,
|
||||
desc: "送上祝福",
|
||||
textColor: signatureColor.value,
|
||||
avatarSize: r2p(64),
|
||||
padding: r2p(15),
|
||||
fontSizeName: r2p(24),
|
||||
fontSizeDesc: r2p(20),
|
||||
});
|
||||
|
||||
// 6️⃣ 输出
|
||||
uni.canvasToTempFilePath({
|
||||
@@ -1209,11 +1243,11 @@ function drawUserBubble(ctx, options) {
|
||||
const textX = avatarX + avatarSize + padding;
|
||||
const totalTextHeight = fontSizeName + fontSizeDesc + 4;
|
||||
const textStartY = drawY + (bubbleHeight - totalTextHeight) / 2;
|
||||
|
||||
|
||||
ctx.fillStyle = textColor;
|
||||
ctx.font = `${fontSizeName}px 'PingFang SC'`;
|
||||
ctx.fillText(username, textX, textStartY);
|
||||
|
||||
|
||||
ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
|
||||
ctx.globalAlpha = 0.6;
|
||||
ctx.fillText(desc, textX, textStartY + fontSizeName + 4);
|
||||
@@ -1670,7 +1704,7 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.position-section{
|
||||
.position-section {
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
.greeting-card.active .greeting-text {
|
||||
@@ -1783,8 +1817,9 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
||||
.btn.secondary {
|
||||
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||
color: #333;
|
||||
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.05),
|
||||
inset 0 0 0 2rpx #eee;
|
||||
box-shadow:
|
||||
0 8rpx 20rpx rgba(0, 0, 0, 0.05),
|
||||
inset 0 0 0 2rpx #eee;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
|
||||
Reference in New Issue
Block a user