fix: msg
This commit is contained in:
@@ -29,3 +29,11 @@ export const getCardTemplateContentList = async () => {
|
|||||||
method: "GET",
|
method: "GET",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const getCardTemplateTitleList = async (page = 1) => {
|
||||||
|
return request({
|
||||||
|
url: "/api/blessing/card/template-title/list?page=" + page,
|
||||||
|
method: "GET",
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -9,10 +9,17 @@
|
|||||||
/>
|
/>
|
||||||
<view class="card-overlay">
|
<view class="card-overlay">
|
||||||
<view class="watermark">年禧集.马年春节祝福</view>
|
<view class="watermark">年禧集.马年春节祝福</view>
|
||||||
<!-- <view class="title">
|
<!-- 选中的标题图片 -->
|
||||||
<text class="main">新春快乐</text>
|
<image
|
||||||
<text class="sub">2026 YEAR OF THE HORSE</text>
|
v-if="currentTitle"
|
||||||
</view> -->
|
class="selected-title-img"
|
||||||
|
:src="currentTitle.imageUrl"
|
||||||
|
mode="widthFix"
|
||||||
|
:style="{
|
||||||
|
transform: `translate(${titleOffsetX}rpx, ${titleOffsetY}rpx) scale(${titleScale})`,
|
||||||
|
top: '40rpx'
|
||||||
|
}"
|
||||||
|
/>
|
||||||
<view
|
<view
|
||||||
class="bubble"
|
class="bubble"
|
||||||
@tap="activeTool = 'text'"
|
@tap="activeTool = 'text'"
|
||||||
@@ -54,6 +61,22 @@
|
|||||||
<view class="editor-panel">
|
<view class="editor-panel">
|
||||||
<view class="drag-handle"></view>
|
<view class="drag-handle"></view>
|
||||||
|
|
||||||
|
<!-- 主操作按钮 -->
|
||||||
|
<view class="main-actions">
|
||||||
|
<button class="btn secondary" @tap="preview">
|
||||||
|
<uni-icons type="cloud-download" size="20" color="#888"></uni-icons>
|
||||||
|
<view>保存</view>
|
||||||
|
</button>
|
||||||
|
<button open-type="share" class="btn primary">
|
||||||
|
<uni-icons
|
||||||
|
type="paperplane-filled"
|
||||||
|
size="20"
|
||||||
|
color="#fff"
|
||||||
|
></uni-icons>
|
||||||
|
<view>分享给好友</view>
|
||||||
|
</button>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 功能入口 (步骤条式) -->
|
<!-- 功能入口 (步骤条式) -->
|
||||||
<view class="step-bar">
|
<view class="step-bar">
|
||||||
<view
|
<view
|
||||||
@@ -74,6 +97,70 @@
|
|||||||
</view>
|
</view>
|
||||||
</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="currentTitle = 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 class="form-item" style="margin-top: 20rpx;">
|
||||||
|
<text class="label">标题位置 (上下)</text>
|
||||||
|
<slider
|
||||||
|
:value="titleOffsetY"
|
||||||
|
min="-100"
|
||||||
|
max="300"
|
||||||
|
show-value
|
||||||
|
@change="(e) => (titleOffsetY = e.detail.value)"
|
||||||
|
activeColor="#ff3b30"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">标题位置 (左右)</text>
|
||||||
|
<slider
|
||||||
|
:value="titleOffsetX"
|
||||||
|
min="-200"
|
||||||
|
max="200"
|
||||||
|
show-value
|
||||||
|
@change="(e) => (titleOffsetX = e.detail.value)"
|
||||||
|
activeColor="#ff3b30"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
<view class="form-item">
|
||||||
|
<text class="label">标题缩放</text>
|
||||||
|
<slider
|
||||||
|
:value="titleScale * 100"
|
||||||
|
min="50"
|
||||||
|
max="150"
|
||||||
|
show-value
|
||||||
|
@change="(e) => (titleScale = e.detail.value / 100)"
|
||||||
|
activeColor="#ff3b30"
|
||||||
|
/>
|
||||||
|
</view>
|
||||||
|
|
||||||
|
<!-- 下一步引导 -->
|
||||||
|
<view class="next-step-tip" @tap="activeTool = 'template'">
|
||||||
|
<text>选好标题了,去选模板</text>
|
||||||
|
<uni-icons type="arrow-right" size="14" color="#ff3b30"></uni-icons>
|
||||||
|
</view>
|
||||||
|
</view>
|
||||||
|
|
||||||
<!-- 模板区 -->
|
<!-- 模板区 -->
|
||||||
<view v-if="activeTool === 'template'" class="section">
|
<view v-if="activeTool === 'template'" class="section">
|
||||||
<view class="section-title">
|
<view class="section-title">
|
||||||
@@ -293,22 +380,6 @@
|
|||||||
<button class="btn" @tap="toggleAvatarDecor">切换挂饰</button>
|
<button class="btn" @tap="toggleAvatarDecor">切换挂饰</button>
|
||||||
</view>
|
</view>
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<!-- 底部操作 -->
|
|
||||||
<view class="bottom-actions">
|
|
||||||
<button class="btn secondary" @tap="preview">
|
|
||||||
<uni-icons type="cloud-download" size="20" color="#888"></uni-icons>
|
|
||||||
<view>保存</view>
|
|
||||||
</button>
|
|
||||||
<button open-type="share" class="btn primary">
|
|
||||||
<uni-icons
|
|
||||||
type="paperplane-filled"
|
|
||||||
size="20"
|
|
||||||
color="#fff"
|
|
||||||
></uni-icons>
|
|
||||||
<view>分享给好友</view>
|
|
||||||
</button>
|
|
||||||
</view>
|
|
||||||
</view>
|
</view>
|
||||||
|
|
||||||
<canvas
|
<canvas
|
||||||
@@ -332,6 +403,7 @@ import {
|
|||||||
updateCard,
|
updateCard,
|
||||||
getCardTemplateList,
|
getCardTemplateList,
|
||||||
getCardTemplateContentList,
|
getCardTemplateContentList,
|
||||||
|
getCardTemplateTitleList,
|
||||||
} from "@/api/make";
|
} from "@/api/make";
|
||||||
import { createShareToken, abilityCheck, getShareReward } from "@/api/system";
|
import { createShareToken, abilityCheck, getShareReward } from "@/api/system";
|
||||||
import {
|
import {
|
||||||
@@ -353,6 +425,16 @@ const loadingTemplates = ref(false);
|
|||||||
const hasMoreTemplates = ref(true);
|
const hasMoreTemplates = ref(true);
|
||||||
const cardId = ref("");
|
const cardId = ref("");
|
||||||
|
|
||||||
|
// 标题相关
|
||||||
|
const titles = ref([]);
|
||||||
|
const currentTitle = ref(null);
|
||||||
|
const titlePage = ref(1);
|
||||||
|
const loadingTitles = ref(false);
|
||||||
|
const hasMoreTitles = ref(true);
|
||||||
|
const titleOffsetX = ref(0);
|
||||||
|
const titleOffsetY = ref(0);
|
||||||
|
const titleScale = ref(1);
|
||||||
|
|
||||||
const targetName = ref("祝您");
|
const targetName = ref("祝您");
|
||||||
const signatureName = ref(userStore?.userInfo?.nickName || "xxx");
|
const signatureName = ref(userStore?.userInfo?.nickName || "xxx");
|
||||||
const userAvatar = ref(
|
const userAvatar = ref(
|
||||||
@@ -445,6 +527,7 @@ const userOffsetY = ref(0);
|
|||||||
onLoad((options) => {
|
onLoad((options) => {
|
||||||
getTemplateList();
|
getTemplateList();
|
||||||
getTemplateContentList();
|
getTemplateContentList();
|
||||||
|
getTemplateTitleList();
|
||||||
});
|
});
|
||||||
|
|
||||||
onShow(() => {
|
onShow(() => {
|
||||||
@@ -481,6 +564,8 @@ onShow(() => {
|
|||||||
onReachBottom(() => {
|
onReachBottom(() => {
|
||||||
if (activeTool.value === "template") {
|
if (activeTool.value === "template") {
|
||||||
loadMoreTemplates();
|
loadMoreTemplates();
|
||||||
|
} else if (activeTool.value === "title") {
|
||||||
|
loadMoreTitles();
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -536,6 +621,48 @@ const getTemplateList = async (isLoadMore = false) => {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const getTemplateTitleList = async (isLoadMore = false) => {
|
||||||
|
if (loadingTitles.value || (!hasMoreTitles.value && isLoadMore)) return;
|
||||||
|
|
||||||
|
loadingTitles.value = true;
|
||||||
|
try {
|
||||||
|
const res = await getCardTemplateTitleList(titlePage.value);
|
||||||
|
const list = Array.isArray(res) ? res : res.list || [];
|
||||||
|
|
||||||
|
if (list.length > 0) {
|
||||||
|
if (isLoadMore) {
|
||||||
|
titles.value = [...titles.value, ...list];
|
||||||
|
} else {
|
||||||
|
titles.value = list;
|
||||||
|
if (list.length > 0 && !currentTitle.value) {
|
||||||
|
currentTitle.value = list[0];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (typeof res.hasNext !== "undefined") {
|
||||||
|
hasMoreTitles.value = res.hasNext;
|
||||||
|
} else {
|
||||||
|
hasMoreTitles.value = list.length >= 8;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hasMoreTitles.value) {
|
||||||
|
titlePage.value++;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (!isLoadMore) titles.value = [];
|
||||||
|
hasMoreTitles.value = false;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("加载标题失败:", error);
|
||||||
|
} finally {
|
||||||
|
loadingTitles.value = false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const loadMoreTitles = () => {
|
||||||
|
getTemplateTitleList(true);
|
||||||
|
};
|
||||||
|
|
||||||
const getTemplateContentList = async () => {
|
const getTemplateContentList = async () => {
|
||||||
const res = await getCardTemplateContentList();
|
const res = await getCardTemplateContentList();
|
||||||
if (res.length) {
|
if (res.length) {
|
||||||
@@ -603,8 +730,9 @@ const selectGreeting = (text) => {
|
|||||||
|
|
||||||
const tools = [
|
const tools = [
|
||||||
{ type: "template", text: "1. 选模板", icon: "🎨", step: 1 },
|
{ type: "template", text: "1. 选模板", icon: "🎨", step: 1 },
|
||||||
{ type: "text", text: "2. 改文字", icon: "✍️", step: 2 },
|
{ type: "title", text: "2. 选标题", icon: "🧧", step: 2 },
|
||||||
{ type: "position", text: "3. 调位置", icon: "🎯", step: 3 },
|
{ type: "text", text: "3. 改文字", icon: "✍️", step: 3 },
|
||||||
|
{ type: "position", text: "4. 调位置", icon: "🎯", step: 4 },
|
||||||
];
|
];
|
||||||
const activeTool = ref("template");
|
const activeTool = ref("template");
|
||||||
|
|
||||||
@@ -659,7 +787,7 @@ const preview = async () => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const tempPath = await saveByCanvas(true);
|
const tempPath = await saveByCanvas(true);
|
||||||
id = createCard();
|
const id = createCard();
|
||||||
shareOrSave(id);
|
shareOrSave(id);
|
||||||
saveRecordRequest(tempPath, id, "card_generate");
|
saveRecordRequest(tempPath, id, "card_generate");
|
||||||
|
|
||||||
@@ -728,9 +856,10 @@ const saveByCanvas = async (save = true) => {
|
|||||||
try {
|
try {
|
||||||
// 1️⃣ 画背景
|
// 1️⃣ 画背景
|
||||||
// ⭐ 先加载背景图
|
// ⭐ 先加载背景图
|
||||||
const [bgImg, avatarImg] = await Promise.all([
|
const [bgImg, avatarImg, titleImg] = await Promise.all([
|
||||||
loadCanvasImage(currentTemplate?.value?.imageUrl),
|
loadCanvasImage(currentTemplate?.value?.imageUrl),
|
||||||
loadCanvasImage(userAvatar.value),
|
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);
|
||||||
@@ -739,17 +868,14 @@ const saveByCanvas = async (save = true) => {
|
|||||||
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
||||||
ctx.fillRect(0, 0, W, H);
|
ctx.fillRect(0, 0, W, H);
|
||||||
|
|
||||||
// 3️⃣ 标题
|
// 3️⃣ 标题图片
|
||||||
// ctx.fillStyle = "#ffffff";
|
if (titleImg) {
|
||||||
// ctx.font = "42px sans-serif"; // 默认字体
|
const titleW = titleImg.width * titleScale.value;
|
||||||
// ctx.textAlign = "center";
|
const titleH = titleImg.height * titleScale.value;
|
||||||
// ctx.textBaseline = "alphabetic"; // Canvas 2D 默认是 alphabetic
|
const titleX = (W - titleW) / 2 + titleOffsetX.value;
|
||||||
// ctx.fillText("新春快乐", W / 2, 120);
|
const titleY = 40 + titleOffsetY.value;
|
||||||
|
ctx.drawImage(titleImg, titleX, titleY, titleW, titleH);
|
||||||
// ctx.font = "22px sans-serif";
|
}
|
||||||
// ctx.globalAlpha = 0.9;
|
|
||||||
// ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
|
|
||||||
// ctx.globalAlpha = 1;
|
|
||||||
|
|
||||||
// 4️⃣ 祝福语气泡
|
// 4️⃣ 祝福语气泡
|
||||||
drawBubbleText(ctx, {
|
drawBubbleText(ctx, {
|
||||||
@@ -1130,7 +1256,7 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
|||||||
.step-bar {
|
.step-bar {
|
||||||
display: flex;
|
display: flex;
|
||||||
justify-content: space-between;
|
justify-content: space-between;
|
||||||
padding: 30rpx 60rpx 10rpx;
|
padding: 20rpx 60rpx 10rpx;
|
||||||
position: relative;
|
position: relative;
|
||||||
}
|
}
|
||||||
.step-item {
|
.step-item {
|
||||||
@@ -1236,6 +1362,23 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
|||||||
position: relative;
|
position: relative;
|
||||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.06);
|
||||||
}
|
}
|
||||||
|
.title-card {
|
||||||
|
height: 120rpx;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
padding: 10rpx;
|
||||||
|
}
|
||||||
|
.title-cover {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.selected-title-img {
|
||||||
|
position: absolute;
|
||||||
|
width: 400rpx;
|
||||||
|
z-index: 2;
|
||||||
|
transition: transform 0.1s;
|
||||||
|
}
|
||||||
.tpl-card.selected {
|
.tpl-card.selected {
|
||||||
outline: 4rpx solid #ff3b30;
|
outline: 4rpx solid #ff3b30;
|
||||||
}
|
}
|
||||||
@@ -1423,33 +1566,53 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* 按钮区 */
|
/* 按钮区 */
|
||||||
.bottom-actions {
|
.main-actions {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
padding: 20rpx 24rpx 30rpx;
|
padding: 24rpx 32rpx 32rpx;
|
||||||
|
background: #fff;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
.btn {
|
.btn {
|
||||||
|
flex: 1;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
height: 88rpx;
|
height: 96rpx;
|
||||||
border-radius: 999rpx;
|
border-radius: 48rpx;
|
||||||
padding: 0 40rpx;
|
margin: 0 12rpx;
|
||||||
font-size: 28rpx;
|
font-size: 30rpx;
|
||||||
|
font-weight: 600;
|
||||||
|
transition: all 0.2s active;
|
||||||
|
position: relative;
|
||||||
|
overflow: hidden;
|
||||||
|
border: none;
|
||||||
}
|
}
|
||||||
.btn view {
|
.btn::after {
|
||||||
margin-left: 10rpx;
|
border: none;
|
||||||
}
|
}
|
||||||
|
.btn:active {
|
||||||
|
transform: scale(0.96);
|
||||||
|
opacity: 0.9;
|
||||||
|
}
|
||||||
|
|
||||||
.btn.secondary {
|
.btn.secondary {
|
||||||
background: #fff;
|
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
|
||||||
color: #000;
|
color: #333;
|
||||||
border: 1px solid #ccc;
|
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.05),
|
||||||
|
inset 0 0 0 2rpx #eee;
|
||||||
}
|
}
|
||||||
|
|
||||||
.btn.primary {
|
.btn.primary {
|
||||||
background: #ff3b30;
|
background: linear-gradient(135deg, #ff6b66 0%, #ff3b30 100%);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
box-shadow: 0 12rpx 24rpx rgba(255, 59, 48, 0.35);
|
box-shadow: 0 12rpx 30rpx rgba(255, 59, 48, 0.3);
|
||||||
|
text-shadow: 0 2rpx 4rpx rgba(0, 0, 0, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn view {
|
||||||
|
margin-left: 12rpx;
|
||||||
}
|
}
|
||||||
.hidden-canvas {
|
.hidden-canvas {
|
||||||
position: fixed;
|
position: fixed;
|
||||||
|
|||||||
Reference in New Issue
Block a user