fix: msg
This commit is contained in:
@@ -29,3 +29,11 @@ export const getCardTemplateContentList = async () => {
|
||||
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="watermark">年禧集.马年春节祝福</view>
|
||||
<!-- <view class="title">
|
||||
<text class="main">新春快乐</text>
|
||||
<text class="sub">2026 YEAR OF THE HORSE</text>
|
||||
</view> -->
|
||||
<!-- 选中的标题图片 -->
|
||||
<image
|
||||
v-if="currentTitle"
|
||||
class="selected-title-img"
|
||||
:src="currentTitle.imageUrl"
|
||||
mode="widthFix"
|
||||
:style="{
|
||||
transform: `translate(${titleOffsetX}rpx, ${titleOffsetY}rpx) scale(${titleScale})`,
|
||||
top: '40rpx'
|
||||
}"
|
||||
/>
|
||||
<view
|
||||
class="bubble"
|
||||
@tap="activeTool = 'text'"
|
||||
@@ -54,6 +61,22 @@
|
||||
<view class="editor-panel">
|
||||
<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
|
||||
@@ -74,6 +97,70 @@
|
||||
</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 class="section-title">
|
||||
@@ -293,22 +380,6 @@
|
||||
<button class="btn" @tap="toggleAvatarDecor">切换挂饰</button>
|
||||
</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>
|
||||
|
||||
<canvas
|
||||
@@ -332,6 +403,7 @@ import {
|
||||
updateCard,
|
||||
getCardTemplateList,
|
||||
getCardTemplateContentList,
|
||||
getCardTemplateTitleList,
|
||||
} from "@/api/make";
|
||||
import { createShareToken, abilityCheck, getShareReward } from "@/api/system";
|
||||
import {
|
||||
@@ -353,6 +425,16 @@ const loadingTemplates = ref(false);
|
||||
const hasMoreTemplates = ref(true);
|
||||
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 signatureName = ref(userStore?.userInfo?.nickName || "xxx");
|
||||
const userAvatar = ref(
|
||||
@@ -445,6 +527,7 @@ const userOffsetY = ref(0);
|
||||
onLoad((options) => {
|
||||
getTemplateList();
|
||||
getTemplateContentList();
|
||||
getTemplateTitleList();
|
||||
});
|
||||
|
||||
onShow(() => {
|
||||
@@ -481,6 +564,8 @@ onShow(() => {
|
||||
onReachBottom(() => {
|
||||
if (activeTool.value === "template") {
|
||||
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 res = await getCardTemplateContentList();
|
||||
if (res.length) {
|
||||
@@ -603,8 +730,9 @@ const selectGreeting = (text) => {
|
||||
|
||||
const tools = [
|
||||
{ type: "template", text: "1. 选模板", icon: "🎨", step: 1 },
|
||||
{ type: "text", text: "2. 改文字", icon: "✍️", step: 2 },
|
||||
{ type: "position", text: "3. 调位置", icon: "🎯", step: 3 },
|
||||
{ type: "title", text: "2. 选标题", icon: "🧧", step: 2 },
|
||||
{ type: "text", text: "3. 改文字", icon: "✍️", step: 3 },
|
||||
{ type: "position", text: "4. 调位置", icon: "🎯", step: 4 },
|
||||
];
|
||||
const activeTool = ref("template");
|
||||
|
||||
@@ -659,7 +787,7 @@ const preview = async () => {
|
||||
return;
|
||||
}
|
||||
const tempPath = await saveByCanvas(true);
|
||||
id = createCard();
|
||||
const id = createCard();
|
||||
shareOrSave(id);
|
||||
saveRecordRequest(tempPath, id, "card_generate");
|
||||
|
||||
@@ -728,9 +856,10 @@ const saveByCanvas = async (save = true) => {
|
||||
try {
|
||||
// 1️⃣ 画背景
|
||||
// ⭐ 先加载背景图
|
||||
const [bgImg, avatarImg] = await Promise.all([
|
||||
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);
|
||||
@@ -739,17 +868,14 @@ const saveByCanvas = async (save = true) => {
|
||||
ctx.fillStyle = "rgba(0,0,0,0.08)";
|
||||
ctx.fillRect(0, 0, W, H);
|
||||
|
||||
// 3️⃣ 标题
|
||||
// ctx.fillStyle = "#ffffff";
|
||||
// ctx.font = "42px sans-serif"; // 默认字体
|
||||
// ctx.textAlign = "center";
|
||||
// ctx.textBaseline = "alphabetic"; // Canvas 2D 默认是 alphabetic
|
||||
// ctx.fillText("新春快乐", W / 2, 120);
|
||||
|
||||
// ctx.font = "22px sans-serif";
|
||||
// ctx.globalAlpha = 0.9;
|
||||
// ctx.fillText("2026 YEAR OF THE HORSE", W / 2, 165);
|
||||
// ctx.globalAlpha = 1;
|
||||
// 3️⃣ 标题图片
|
||||
if (titleImg) {
|
||||
const titleW = titleImg.width * titleScale.value;
|
||||
const titleH = titleImg.height * titleScale.value;
|
||||
const titleX = (W - titleW) / 2 + titleOffsetX.value;
|
||||
const titleY = 40 + titleOffsetY.value;
|
||||
ctx.drawImage(titleImg, titleX, titleY, titleW, titleH);
|
||||
}
|
||||
|
||||
// 4️⃣ 祝福语气泡
|
||||
drawBubbleText(ctx, {
|
||||
@@ -1130,7 +1256,7 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
||||
.step-bar {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
padding: 30rpx 60rpx 10rpx;
|
||||
padding: 20rpx 60rpx 10rpx;
|
||||
position: relative;
|
||||
}
|
||||
.step-item {
|
||||
@@ -1236,6 +1362,23 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
||||
position: relative;
|
||||
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 {
|
||||
outline: 4rpx solid #ff3b30;
|
||||
}
|
||||
@@ -1423,33 +1566,53 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
|
||||
}
|
||||
|
||||
/* 按钮区 */
|
||||
.bottom-actions {
|
||||
.main-actions {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20rpx 24rpx 30rpx;
|
||||
padding: 24rpx 32rpx 32rpx;
|
||||
background: #fff;
|
||||
position: relative;
|
||||
}
|
||||
.btn {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 88rpx;
|
||||
border-radius: 999rpx;
|
||||
padding: 0 40rpx;
|
||||
font-size: 28rpx;
|
||||
height: 96rpx;
|
||||
border-radius: 48rpx;
|
||||
margin: 0 12rpx;
|
||||
font-size: 30rpx;
|
||||
font-weight: 600;
|
||||
transition: all 0.2s active;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
border: none;
|
||||
}
|
||||
.btn view {
|
||||
margin-left: 10rpx;
|
||||
.btn::after {
|
||||
border: none;
|
||||
}
|
||||
.btn:active {
|
||||
transform: scale(0.96);
|
||||
opacity: 0.9;
|
||||
}
|
||||
|
||||
.btn.secondary {
|
||||
background: #fff;
|
||||
color: #000;
|
||||
border: 1px solid #ccc;
|
||||
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;
|
||||
}
|
||||
|
||||
.btn.primary {
|
||||
background: #ff3b30;
|
||||
background: linear-gradient(135deg, #ff6b66 0%, #ff3b30 100%);
|
||||
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 {
|
||||
position: fixed;
|
||||
|
||||
Reference in New Issue
Block a user