Compare commits

...

6 Commits

Author SHA1 Message Date
zzc
227a5b035e fix: recommend list 2026-01-28 15:55:26 +08:00
zzc
e3edccbd65 fix: recommend list 2026-01-28 15:44:30 +08:00
zzc
b22bbb8f7c optimize: avatar page share reward 2026-01-28 10:55:39 +08:00
zzc
29498e4994 optimize: avatar page share reward 2026-01-28 10:35:31 +08:00
zzc
4a1cbf1f5a optimize: avatar page share reward 2026-01-28 09:31:58 +08:00
zzc
2fa6584e0c optimize: avatar page share reward 2026-01-28 08:55:59 +08:00
7 changed files with 485 additions and 202 deletions

View File

@@ -21,6 +21,14 @@ export const getAvatarFrameList = async (page = 1) => {
}); });
}; };
export const avatarCreateComplete = async (data) => {
return request({
url: "/api/blessing/avatar/create/complete",
method: "POST",
data,
});
};
export const avatarDownloadRecord = async (data) => { export const avatarDownloadRecord = async (data) => {
return request({ return request({
url: "/api/blessing/avatar/download", url: "/api/blessing/avatar/download",

View File

@@ -45,3 +45,10 @@ export const viewRecord = async (data) => {
data, data,
}); });
}; };
export const getRecommendList = async (page = 1) => {
return request({
url: `/api/blessing/recommend/list?page=${page}`,
method: "get",
});
};

View File

@@ -47,7 +47,6 @@
<text>加载中...</text> <text>加载中...</text>
</view> </view>
<!-- Decorative Elements --> <!-- Decorative Elements -->
<view class="decor-tag">🐰</view>
<view class="card-footer-text"> <view class="card-footer-text">
<text class="icon">🌸</text> 2026 丙午马年限定 <text class="icon">🌸</text> 2026 丙午马年限定
</view> </view>
@@ -59,9 +58,6 @@
<button class="btn primary-btn" @tap="goToMake"> <button class="btn primary-btn" @tap="goToMake">
<text class="icon">🎨</text> 我也要领同款制作 <text class="icon">🎨</text> 我也要领同款制作
</button> </button>
<button class="btn secondary-btn" @tap="saveImage">
<text class="icon">📥</text> 保存到相册
</button>
</view> </view>
<!-- Recommended Frames --> <!-- Recommended Frames -->
@@ -89,13 +85,33 @@
</view> </view>
<!-- Wallpaper Banner --> <!-- Wallpaper Banner -->
<view class="wallpaper-banner" @tap="goToFortune">
<view class="banner-icon">
<text>🏮</text>
</view>
<view class="banner-content">
<text class="banner-title">去抽取新年运势</text>
<text class="banner-desc">每日一签开启你的新年好运</text>
</view>
<text class="banner-arrow"></text>
</view>
<view class="wallpaper-banner" @tap="goToGreeting">
<view class="banner-icon">
<text>🧧</text>
</view>
<view class="banner-content">
<text class="banner-title">去制作新年贺卡</text>
<text class="banner-desc">定制专属祝福传递浓浓年味</text>
</view>
<text class="banner-arrow"></text>
</view>
<view class="wallpaper-banner" @tap="goToWallpaper"> <view class="wallpaper-banner" @tap="goToWallpaper">
<view class="banner-icon"> <view class="banner-icon">
<text>🖼</text> <text>🖼</text>
</view> </view>
<view class="banner-content"> <view class="banner-content">
<text class="banner-title">去挑选更多壁纸</text> <text class="banner-title">去挑选新年壁纸</text>
<text class="banner-desc">新年新气象全套皮肤限时领</text> <text class="banner-desc">精选新年壁纸让手机也过年</text>
</view> </view>
<text class="banner-arrow"></text> <text class="banner-arrow"></text>
</view> </view>
@@ -117,7 +133,8 @@
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app"; import { onLoad } from "@dcloudio/uni-app";
import { getBavBarHeight } from "@/utils/system"; import { getBavBarHeight } from "@/utils/system";
import { getAvatarFrameList, getPageDetail } from "@/api/avatar.js"; import { getAvatarFrameList } from "@/api/avatar.js";
import { getPageDetail } from "@/api/system.js";
const defaultAvatar = const defaultAvatar =
"https://file.lihailezzc.com/resource/d9b329082b32f8305101f708593a4882.png"; "https://file.lihailezzc.com/resource/d9b329082b32f8305101f708593a4882.png";
@@ -187,43 +204,22 @@ const previewImage = () => {
} }
}; };
const saveImage = () => {
if (!detailData.value?.imageUrl) return;
uni.showLoading({ title: "保存中..." });
uni.downloadFile({
url: detailData.value.imageUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
// uni.hideLoading();
uni.showToast({ title: "保存成功", icon: "success" });
},
fail: () => {
// uni.hideLoading();
uni.showToast({ title: "保存失败", icon: "none" });
},
});
} else {
// uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
}
},
fail: () => {
// uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
},
});
};
const goToMake = () => { const goToMake = () => {
uni.navigateTo({ uni.navigateTo({
url: "/pages/avatar/index", url: "/pages/avatar/index",
}); });
}; };
const goToFortune = () => {
uni.navigateTo({
url: "/pages/fortune/index",
});
};
const goToGreeting = () => {
uni.switchTab({ url: "/pages/make/index" });
};
const goToWallpaper = () => { const goToWallpaper = () => {
uni.navigateTo({ uni.navigateTo({
url: "/pages/wallpaper/index", url: "/pages/wallpaper/index",
@@ -234,7 +230,7 @@ const goToWallpaper = () => {
<style lang="scss" scoped> <style lang="scss" scoped>
.avatar-detail-page { .avatar-detail-page {
min-height: 100vh; min-height: 100vh;
background: #fff0f5; /* Light Pink Background */ background: #ffffff;
box-sizing: border-box; box-sizing: border-box;
} }
@@ -245,7 +241,7 @@ const goToWallpaper = () => {
right: 0; right: 0;
z-index: 100; z-index: 100;
box-sizing: border-box; box-sizing: border-box;
background: #fff0f5; background: #ffffff;
} }
.nav-content { .nav-content {
@@ -271,9 +267,6 @@ const goToWallpaper = () => {
margin-right: 50rpx; /* Balance back button */ margin-right: 50rpx; /* Balance back button */
} }
.content-scroll {
}
.content-wrap { .content-wrap {
padding: 30rpx 40rpx 60rpx; padding: 30rpx 40rpx 60rpx;
} }
@@ -326,16 +319,17 @@ const goToWallpaper = () => {
/* Main Card */ /* Main Card */
.main-card { .main-card {
background: linear-gradient(180deg, #ffffff 0%, #fff5f5 100%); background: #ffffff;
border-radius: 40rpx; border-radius: 40rpx;
padding: 24rpx; padding: 24rpx;
box-shadow: 0 20rpx 60rpx rgba(255, 59, 48, 0.15); box-shadow: 0 10rpx 40rpx rgba(0, 0, 0, 0.06);
margin-bottom: 60rpx; margin-bottom: 60rpx;
border: 2rpx solid #f5f5f5;
} }
.card-inner { .card-inner {
position: relative; position: relative;
background: #fffaf0; background: #fff9f9;
border-radius: 30rpx; border-radius: 30rpx;
padding: 60rpx; padding: 60rpx;
display: flex; display: flex;
@@ -348,7 +342,7 @@ const goToWallpaper = () => {
height: 400rpx; height: 400rpx;
border-radius: 20rpx; border-radius: 20rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1); box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
border: 8rpx solid #d63333; border: 8rpx solid #ff3b30;
} }
.loading-box { .loading-box {
@@ -358,7 +352,7 @@ const goToWallpaper = () => {
align-items: center; align-items: center;
justify-content: center; justify-content: center;
color: #999; color: #999;
background: #eee; background: #f0f0f0;
border-radius: 20rpx; border-radius: 20rpx;
} }
@@ -381,14 +375,14 @@ const goToWallpaper = () => {
.card-footer-text { .card-footer-text {
margin-top: 40rpx; margin-top: 40rpx;
font-size: 28rpx; font-size: 28rpx;
color: #d63333; color: #ff3b30;
font-weight: bold; font-weight: bold;
display: flex; display: flex;
align-items: center; align-items: center;
background: #fff; background: #fff;
padding: 10rpx 30rpx; padding: 10rpx 30rpx;
border-radius: 99rpx; border-radius: 99rpx;
box-shadow: 0 4rpx 10rpx rgba(214, 51, 51, 0.1); box-shadow: 0 4rpx 10rpx rgba(255, 59, 48, 0.1);
} }
.card-footer-text .icon { .card-footer-text .icon {
@@ -423,8 +417,8 @@ const goToWallpaper = () => {
} }
.secondary-btn { .secondary-btn {
background: #eaeaea; background: #f5f5f5;
color: #666; color: #333;
} }
.btn .icon { .btn .icon {
@@ -478,10 +472,9 @@ const goToWallpaper = () => {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
align-items: center; align-items: center;
background: #fff; background: #f8f8f8;
border-radius: 24rpx; border-radius: 24rpx;
padding: 20rpx; padding: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
} }
.frame-img-box { .frame-img-box {
@@ -490,7 +483,7 @@ const goToWallpaper = () => {
margin-bottom: 16rpx; margin-bottom: 16rpx;
border-radius: 50%; border-radius: 50%;
overflow: hidden; overflow: hidden;
background: #f9f9f9; background: #fff;
} }
.frame-img { .frame-img {
@@ -506,19 +499,18 @@ const goToWallpaper = () => {
/* Wallpaper Banner */ /* Wallpaper Banner */
.wallpaper-banner { .wallpaper-banner {
background: #fff; background: #f8f8f8;
border-radius: 24rpx; border-radius: 24rpx;
padding: 30rpx; padding: 30rpx;
display: flex; display: flex;
align-items: center; align-items: center;
margin-bottom: 60rpx; margin-bottom: 60rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
} }
.banner-icon { .banner-icon {
width: 80rpx; width: 80rpx;
height: 80rpx; height: 80rpx;
background: #fff0f5; background: #fff;
border-radius: 20rpx; border-radius: 20rpx;
display: flex; display: flex;
align-items: center; align-items: center;
@@ -569,18 +561,18 @@ const goToWallpaper = () => {
.footer-line .line { .footer-line .line {
width: 60rpx; width: 60rpx;
height: 2rpx; height: 2rpx;
background: #ccc; background: #eee;
} }
.footer-line .text { .footer-line .text {
font-size: 20rpx; font-size: 20rpx;
color: #999; color: #ccc;
margin: 0 20rpx; margin: 0 20rpx;
letter-spacing: 2rpx; letter-spacing: 2rpx;
} }
.footer-sub { .footer-sub {
font-size: 20rpx; font-size: 20rpx;
color: #ccc; color: #ddd;
} }
</style> </style>

View File

@@ -6,17 +6,21 @@
</view> </view>
<view class="preview-card"> <view class="preview-card">
<view class="preview-square"> <view class="preview-square">
<image class="avatar-img" :src="currentAvatar" mode="aspectFill" /> <image
class="avatar-img"
:src="currentAvatar?.imageUrl"
mode="aspectFill"
/>
<image <image
v-if="selectedFrame" v-if="selectedFrame"
class="frame-img" class="frame-img"
:src="selectedFrame" :src="selectedFrame?.imageUrl"
mode="aspectFill" mode="aspectFill"
/> />
<image <image
v-if="selectedDecor" v-if="selectedDecor"
class="decor-img" class="decor-img"
:src="selectedDecor" :src="selectedDecor?.imageUrl"
mode="aspectFit" mode="aspectFit"
:style="decorStyle" :style="decorStyle"
@touchstart.stop="onTouchStart" @touchstart.stop="onTouchStart"
@@ -58,7 +62,11 @@
:class="{ active: currentAvatar === item }" :class="{ active: currentAvatar === item }"
@tap="currentAvatar = item" @tap="currentAvatar = item"
> >
<image :src="item" class="avatar-thumb" mode="aspectFill" /> <image
:src="item.imageUrl"
class="avatar-thumb"
mode="aspectFill"
/>
<view v-if="currentAvatar === item" class="check"></view> <view v-if="currentAvatar === item" class="check"></view>
</view> </view>
</view> </view>
@@ -88,7 +96,7 @@
:class="{ active: selectedFrame === frame }" :class="{ active: selectedFrame === frame }"
@tap="toggleFrame(frame)" @tap="toggleFrame(frame)"
> >
<image :src="frame" class="grid-img" mode="aspectFill" /> <image :src="frame.imageUrl" class="grid-img" mode="aspectFill" />
<view v-if="selectedFrame === frame" class="check"></view> <view v-if="selectedFrame === frame" class="check"></view>
</view> </view>
</view> </view>
@@ -101,12 +109,14 @@
:class="{ active: selectedDecor === decor }" :class="{ active: selectedDecor === decor }"
@tap="selectedDecor = decor" @tap="selectedDecor = decor"
> >
<image :src="decor" class="grid-img" mode="aspectFit" /> <image :src="decor.imageUrl" class="grid-img" mode="aspectFit" />
<view v-if="selectedDecor === decor" class="check"></view> <view v-if="selectedDecor === decor" class="check"></view>
</view> </view>
</view> </view>
<canvas <canvas
type="2d"
id="avatarCanvas"
canvas-id="avatarCanvas" canvas-id="avatarCanvas"
class="hidden-canvas" class="hidden-canvas"
style="width: 600px; height: 600px" style="width: 600px; height: 600px"
@@ -134,7 +144,7 @@
class="popup-item" class="popup-item"
@tap="selectMoreAvatar(item)" @tap="selectMoreAvatar(item)"
> >
<image :src="item" class="popup-img" mode="aspectFill" /> <image :src="item.imageUrl" class="popup-img" mode="aspectFill" />
</view> </view>
</view> </view>
<view v-if="loading" class="loading-text">加载中...</view> <view v-if="loading" class="loading-text">加载中...</view>
@@ -162,7 +172,14 @@ import {
getAvatarSystemList, getAvatarSystemList,
getAvatarFrameList, getAvatarFrameList,
getAvatarDecorList, getAvatarDecorList,
avatarCreateComplete,
} from "@/api/avatar.js"; } from "@/api/avatar.js";
import {
saveRecordRequest,
getShareToken,
generateObjectId,
uploadImage,
} from "@/utils/common.js";
const userStore = useUserStore(); const userStore = useUserStore();
const loginPopupRef = ref(null); const loginPopupRef = ref(null);
@@ -182,9 +199,9 @@ const decorPage = ref(1);
const decorHasNext = ref(true); const decorHasNext = ref(true);
const decorLoading = ref(false); const decorLoading = ref(false);
const currentAvatar = ref(""); const currentAvatar = ref(null);
const selectedFrame = ref(""); const selectedFrame = ref(null);
const selectedDecor = ref(""); const selectedDecor = ref(null);
const activeTab = ref("frame"); const activeTab = ref("frame");
// More Popup logic // More Popup logic
@@ -201,7 +218,12 @@ const loadFrames = async () => {
const res = await getAvatarFrameList(framePage.value); const res = await getAvatarFrameList(framePage.value);
const list = res?.list || []; const list = res?.list || [];
if (list.length > 0) { if (list.length > 0) {
frames.value.push(...list.map((item) => item.imageUrl)); frames.value.push(
...list.map((item) => ({
id: item.id,
imageUrl: item.imageUrl,
})),
);
framePage.value++; framePage.value++;
} }
if (typeof res.hasNext !== "undefined") { if (typeof res.hasNext !== "undefined") {
@@ -223,7 +245,12 @@ const loadDecors = async () => {
const res = await getAvatarDecorList(decorPage.value); const res = await getAvatarDecorList(decorPage.value);
const list = res?.list || []; const list = res?.list || [];
if (list.length > 0) { if (list.length > 0) {
decors.value.push(...list.map((item) => item.imageUrl)); decors.value.push(
...list.map((item) => ({
id: item.id,
imageUrl: item.imageUrl,
})),
);
decorPage.value++; decorPage.value++;
} }
if (typeof res.hasNext !== "undefined") { if (typeof res.hasNext !== "undefined") {
@@ -244,7 +271,9 @@ const initSystemAvatars = async () => {
const list = res?.list || []; const list = res?.list || [];
if (list.length > 0) { if (list.length > 0) {
// 取前3个展示在首页 // 取前3个展示在首页
systemAvatars.value = list.slice(0, 3).map((item) => item.imageUrl); systemAvatars.value = list
.slice(0, 3)
.map((item) => ({ id: item.id, imageUrl: item.imageUrl }));
// 默认选中第一个 // 默认选中第一个
if (systemAvatars.value.length > 0) { if (systemAvatars.value.length > 0) {
currentAvatar.value = systemAvatars.value[0]; currentAvatar.value = systemAvatars.value[0];
@@ -255,10 +284,36 @@ const initSystemAvatars = async () => {
} }
}; };
onLoad(() => { onLoad((options) => {
initSystemAvatars(); initSystemAvatars();
loadFrames(); loadFrames();
loadDecors(); loadDecors();
// 处理推荐跳转参数
if (options && options.recommendId && options.type && options.imageUrl) {
const recommendItem = {
id: options.recommendId,
imageUrl: decodeURIComponent(options.imageUrl),
};
if (options.type === "frame") {
activeTab.value = "frame";
selectedFrame.value = recommendItem;
// 如果 frames 列表中有这个 id更新引用以保持选中状态可选但推荐
const found = frames.value.find((f) => f.id === recommendItem.id);
if (found) selectedFrame.value = found;
} else if (options.type === "decor") {
activeTab.value = "decor";
selectedDecor.value = recommendItem;
const found = decors.value.find((d) => d.id === recommendItem.id);
if (found) selectedDecor.value = found;
} else if (options.type === "avatar") {
currentAvatar.value = recommendItem;
// 检查系统头像列表
const found = systemAvatars.value.find((a) => a.id === recommendItem.id);
if (found) currentAvatar.value = found;
}
}
}); });
onReachBottom(() => { onReachBottom(() => {
@@ -292,7 +347,10 @@ const loadMoreAvatars = async () => {
const list = res?.list || []; const list = res?.list || [];
if (list.length > 0) { if (list.length > 0) {
const newAvatars = list.map((item) => item.imageUrl); const newAvatars = list.map((item) => ({
id: item.id,
imageUrl: item.imageUrl,
}));
moreAvatars.value.push(...newAvatars); moreAvatars.value.push(...newAvatars);
page.value++; page.value++;
} }
@@ -313,14 +371,20 @@ const loadMoreAvatars = async () => {
} }
}; };
const selectMoreAvatar = (url) => { const createAvatarId = () => {
currentAvatar.value = url; const id = generateObjectId();
// avatarCreateComplete({ id });
return id;
};
const selectMoreAvatar = (item) => {
currentAvatar.value = item;
closeMorePopup(); closeMorePopup();
}; };
const toggleFrame = (frame) => { const toggleFrame = (frame) => {
if (selectedFrame.value === frame) { if (selectedFrame.value === frame) {
selectedFrame.value = ""; selectedFrame.value = null;
} else { } else {
selectedFrame.value = frame; selectedFrame.value = frame;
} }
@@ -409,7 +473,10 @@ const useWeChatAvatar = () => {
if (!isLoggedIn.value) { if (!isLoggedIn.value) {
loginPopupRef.value.open(); loginPopupRef.value.open();
} else { } else {
currentAvatar.value = userStore.userInfo.avatarUrl; currentAvatar.value = {
id: "wechat_" + Date.now(),
imageUrl: userStore.userInfo.avatarUrl,
};
} }
}; };
@@ -417,7 +484,108 @@ const goBack = () => {
uni.navigateBack(); uni.navigateBack();
}; };
const saveByCanvas = async (save = true) => {
return new Promise((resolve, reject) => {
const query = uni.createSelectorQuery();
query
.select("#avatarCanvas")
.fields({ node: true, size: true })
.exec(async (res) => {
if (!res[0] || !res[0].node) {
reject("Canvas not found");
return;
}
const canvas = res[0].node;
const ctx = canvas.getContext("2d");
const loadCanvasImage = (url) => {
return new Promise((resolve, reject) => {
const img = canvas.createImage();
img.onload = () => resolve(img);
img.onerror = (e) => reject(e);
img.src = url;
});
};
// 初始化画布尺寸
const size = 600;
canvas.width = size;
canvas.height = size;
const avatarPath = await loadCanvasImage(currentAvatar.value.imageUrl);
ctx.clearRect(0, 0, size, size);
ctx.drawImage(avatarPath, 0, 0, size, size);
if (selectedFrame.value) {
const framePath = await loadCanvasImage(selectedFrame.value.imageUrl);
ctx.drawImage(framePath, 0, 0, size, size);
}
if (selectedDecor.value) {
const decorPath = await loadCanvasImage(selectedDecor.value.imageUrl);
ctx.save();
// 映射 rpx 坐标到 Canvas 坐标 (假设 1rpx = 1 unit for 600x600 canvas logic)
// Canvas size is 600, Preview is 600rpx. Ratio is 1:1 in logical space.
ctx.translate(decorState.value.x, decorState.value.y);
ctx.rotate((decorState.value.rotate * Math.PI) / 180);
const scale = decorState.value.scale;
// 绘制图片,宽高 240
ctx.drawImage(
decorPath,
-120 * scale,
-120 * scale,
240 * scale,
240 * scale,
);
ctx.restore();
}
uni.canvasToTempFilePath({
canvas: canvas, // Canvas 2D 必须传 canvas 实例
width: 600,
height: 600,
destWidth: 600,
destHeight: 600,
success: (res) => {
if (save) saveImage(res.tempFilePath);
resolve(res.tempFilePath);
},
fail: (err) => reject(err),
});
// ctx.draw(false, () => {
// uni.canvasToTempFilePath({
// canvasId: "avatarCanvas",
// success: (res) => {
// uni.saveImageToPhotosAlbum({
// filePath: res.tempFilePath,
// success: () => {
// uni.showToast({ title: "已保存到相册", icon: "success" });
// },
// });
// },
// });
// });
});
});
};
const saveImage = (path) => {
uni.saveImageToPhotosAlbum({
filePath: path,
success() {
uni.showToast({ title: "已保存到相册" });
},
fail() {
uni.showModal({
title: "提示",
content: "请授权保存到相册",
});
},
});
};
const saveAndUse = async () => { const saveAndUse = async () => {
if (!isLoggedIn.value) {
loginPopupRef.value.open();
return;
}
const abilityRes = await abilityCheck("avatar_download"); const abilityRes = await abilityCheck("avatar_download");
if (!abilityRes.canUse) { if (!abilityRes.canUse) {
if ( if (
@@ -436,65 +604,86 @@ const saveAndUse = async () => {
}); });
return; return;
} }
const tempPath = saveByCanvas(true);
const id = createAvatarId();
saveRecordRequest(tempPath, id, "avatar_download");
completeCardInfo(id);
return;
// 调用avatarDownloadRecord API记录下载次数 // 调用avatarDownloadRecord API记录下载次数
await avatarDownloadRecord({ // await avatarDownloadRecord({
avatarUrl: currentAvatar.value, // avatarUrl: currentAvatar.value,
}); // });
const ctx = uni.createCanvasContext("avatarCanvas"); // saveRecordRequest('', )
const size = 600; // const ctx = uni.createCanvasContext("avatarCanvas");
const avatarPath = await loadImage(currentAvatar.value); // const size = 600;
ctx.clearRect(0, 0, size, size); // const avatarPath = await loadImage(currentAvatar.value);
ctx.drawImage(avatarPath, 0, 0, size, size); // ctx.clearRect(0, 0, size, size);
if (selectedFrame.value) { // ctx.drawImage(avatarPath, 0, 0, size, size);
const framePath = await loadImage(selectedFrame.value); // if (selectedFrame.value) {
ctx.drawImage(framePath, 0, 0, size, size); // const framePath = await loadImage(selectedFrame.value);
} // ctx.drawImage(framePath, 0, 0, size, size);
if (selectedDecor.value) { // }
const decorPath = await loadImage(selectedDecor.value); // if (selectedDecor.value) {
ctx.save(); // const decorPath = await loadImage(selectedDecor.value);
// 映射 rpx 坐标到 Canvas 坐标 (假设 1rpx = 1 unit for 600x600 canvas logic) // ctx.save();
// Canvas size is 600, Preview is 600rpx. Ratio is 1:1 in logical space. // // 映射 rpx 坐标到 Canvas 坐标 (假设 1rpx = 1 unit for 600x600 canvas logic)
ctx.translate(decorState.value.x, decorState.value.y); // // Canvas size is 600, Preview is 600rpx. Ratio is 1:1 in logical space.
ctx.rotate((decorState.value.rotate * Math.PI) / 180); // ctx.translate(decorState.value.x, decorState.value.y);
const scale = decorState.value.scale; // ctx.rotate((decorState.value.rotate * Math.PI) / 180);
// 绘制图片,宽高 240 // const scale = decorState.value.scale;
ctx.drawImage( // // 绘制图片,宽高 240
decorPath, // ctx.drawImage(
-120 * scale, // decorPath,
-120 * scale, // -120 * scale,
240 * scale, // -120 * scale,
240 * scale, // 240 * scale,
); // 240 * scale,
ctx.restore(); // );
} // ctx.restore();
ctx.draw(false, () => { // }
uni.canvasToTempFilePath({ // ctx.draw(false, () => {
canvasId: "avatarCanvas", // uni.canvasToTempFilePath({
success: (res) => { // canvasId: "avatarCanvas",
uni.saveImageToPhotosAlbum({ // success: (res) => {
filePath: res.tempFilePath, // uni.saveImageToPhotosAlbum({
success: () => { // filePath: res.tempFilePath,
uni.showToast({ title: "已保存到相册", icon: "success" }); // success: () => {
}, // uni.showToast({ title: "已保存到相册", icon: "success" });
}); // },
}, // });
}); // },
}); // });
// });
}; };
const share = () => { const completeCardInfo = async (id) => {
uni.showToast({ title: "已生成,可在相册分享", icon: "none" }); const tempPath = await saveByCanvas(false);
const imageUrl = await uploadImage(tempPath);
avatarCreateComplete({
id,
imageUrl,
avatarId: currentAvatar?.value?.id,
decorId: selectedDecor?.value?.id,
frameId: selectedFrame?.value?.id,
});
}; };
onShareAppMessage(async () => { onShareAppMessage(async () => {
const deviceInfo = getDeviceInfo(); getShareReward({ scene: "avatar_download" });
const shareTokenRes = await createShareToken({ if (!isLoggedIn.value) {
targetId: "", const shareTokenRes = await getShareToken("avatar_download_not_login", "");
scene: "avatar_download", return {
...deviceInfo, title: "新春祝福",
}); path: `/pages/index/index?shareToken=${shareTokenRes.shareToken}`,
getRewardByShare(); };
}
const id = createAvatarId();
// const shareTokenRes = {
// shareToken: "iFmK8WjRm6TK",
// };
const shareTokenRes = await getShareToken("avatar_download", id);
completeCardInfo(id);
return { return {
title: "制作我的新春头像", title: "制作我的新春头像",
path: `/pages/avatar/detail?shareToken=${shareTokenRes.shareToken}`, path: `/pages/avatar/detail?shareToken=${shareTokenRes.shareToken}`,
@@ -503,13 +692,6 @@ onShareAppMessage(async () => {
}; };
}); });
const getRewardByShare = async () => {
const res = await getShareReward({ scene: "avatar_download" });
if (res.success) {
uni.showToast({ title: "分享成功,可下载头像" });
}
};
// onShareTimeline(() => { // onShareTimeline(() => {
// return { // return {
// title: "制作我的新春头像", // title: "制作我的新春头像",

View File

@@ -69,40 +69,45 @@
</view> </view>
<view class="use-grid"> <view class="use-grid">
<view <view
v-for="(card, i) in popularCards" v-for="(card, i) in recommendList"
:key="i" :key="i"
class="use-card" class="use-card"
@tap="previewCard(card)" @tap="onCardClick(card)"
> >
<view class="card-cover-wrap"> <view class="card-cover-wrap">
<image :src="card.cover" class="card-cover" mode="aspectFill" /> <image :src="card.imageUrl" class="card-cover" mode="aspectFill" />
<view <view v-if="card.tag" class="card-tag" :class="`tag--${card.tag}`">
v-if="card.tag" {{ getTagText(card.tag) }}
class="card-tag"
:class="`tag--${card.tagType || 'default'}`"
>
{{ card.tag }}
</view> </view>
</view> </view>
<view class="card-info"> <view class="card-info">
<view class="card-title">{{ card.title }}</view> <view class="card-title">{{ card.title }}</view>
<view class="card-desc">{{ card.desc }}</view> <view class="card-desc">{{ card.content }}</view>
<view class="card-footer"> <view class="card-footer">
<view class="cta-btn" @tap.stop="onCta(card)"> <view class="cta-btn" @tap.stop="onCardClick(card)">
{{ card.cta }} {{ getCtaText(card.type) }}
</view> </view>
</view> </view>
</view> </view>
</view> </view>
</view> </view>
<view v-if="loading" class="loading-text">加载中...</view>
<view v-if="!hasMore && recommendList.length > 0" class="no-more-text"
>没有更多了</view
>
</view> </view>
</view> </view>
</template> </template>
<script setup> <script setup>
import { ref, onMounted } from "vue"; import { ref, onMounted } from "vue";
import { onPullDownRefresh, onShareAppMessage } from "@dcloudio/uni-app"; import {
onPullDownRefresh,
onShareAppMessage,
onReachBottom,
} from "@dcloudio/uni-app";
import { getBavBarHeight } from "@/utils/system"; import { getBavBarHeight } from "@/utils/system";
import { getRecommendList } from "@/api/system";
const countdownText = ref(""); const countdownText = ref("");
@@ -188,6 +193,8 @@ onMounted(() => {
const index = dayOfYear % inspirationList.length; const index = dayOfYear % inspirationList.length;
dailyGreeting.value = inspirationList[index]; dailyGreeting.value = inspirationList[index];
fetchRecommendList();
}); });
const features = ref([ const features = ref([
@@ -217,45 +224,99 @@ const features = ref([
}, },
]); ]);
const popularCards = ref([ const recommendList = ref([]);
{ const page = ref(1);
title: "招财进宝金框", const hasMore = ref(true);
tag: "热门", const loading = ref(false);
tagType: "hot",
desc: "2026马年限定汉字金框金光闪烁财运亨通。适合送亲友的新春祝福。", const fetchRecommendList = async (isRefresh = false) => {
cta: "立即制作", if (loading.value || (!hasMore.value && !isRefresh)) return;
cover: loading.value = true;
"https://file.lihailezzc.com/9a929a32-439f-453b-b603-fda7b04cbe08.png", if (isRefresh) {
}, page.value = 1;
{ hasMore.value = true;
title: "大吉大利卡片", }
tag: "精选", try {
tagType: "featured", const res = await getRecommendList(page.value);
desc: "经典红色大拜年卡片,适合送长辈、老师、同学,传递满满的新春喜气。", const list = res?.list || [];
cta: "去写祝福", if (isRefresh) {
cover: recommendList.value = list;
"https://file.lihailezzc.com/b5fe8ffb-5901-48d2-94fb-48191e36cbf5.png", } else {
}, recommendList.value = [...recommendList.value, ...list];
{ }
title: "合家团圆模板",
tag: "爆款", if (res.hasNext !== undefined) {
tagType: "hot2", hasMore.value = res.hasNext;
desc: "一键生成合家福贺图,支持换装、特效装饰、朋友圈海报等。", } else {
cta: "开始创作", // Fallback if API doesn't return hasNext
cover: if (list.length < 10) hasMore.value = false;
"https://file.lihailezzc.com/91cd1611-bb87-442b-a338-24e9d79e4ee9.png", }
type: "video",
}, if (list.length > 0) {
{ page.value++;
title: "福气满满", } else {
tag: "新款", hasMore.value = false;
tagType: "new", }
desc: "福字当头,好运连连。送给最爱的人。", } catch (e) {
cta: "立即制作", console.error(e);
cover: } finally {
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png", loading.value = false;
}, }
]); };
const getTagText = (tag) => {
const map = {
hot: "热门",
new: "新款",
featured: "精选",
hot2: "爆款",
};
return map[tag] || tag;
};
const getCtaText = (type) => {
const map = {
frame: "去制作",
decor: "去装饰",
avatar: "去查看",
card: "去写祝福",
fortune: "去抽取",
};
return map[type] || "立即查看";
};
const onCardClick = (card) => {
// 构造传递的数据
const query = `recommendId=${card.recommendId || ""}&type=${card.type || ""}&imageUrl=${encodeURIComponent(card.imageUrl || "")}`;
if (
card.scene === "avatar_download" ||
["frame", "decor", "avatar"].includes(card.type)
) {
uni.navigateTo({
url: `/pages/avatar/index?${query}`,
});
return;
}
// Default fallback based on type
if (card.type === "card") {
// 贺卡制作通常是 Tab 页,通过 Storage 传递参数
uni.setStorageSync("RECOMMEND_CARD_DATA", {
recommendId: card.recommendId,
imageUrl: card.imageUrl,
type: card.type,
});
uni.switchTab({ url: "/pages/make/index" });
} else if (card.type === "fortune") {
uni.navigateTo({ url: "/pages/fortune/index" });
} else {
// 默认跳转到头像页
uni.navigateTo({
url: `/pages/avatar/index?${query}`,
});
}
};
const onFeatureTap = (item) => { const onFeatureTap = (item) => {
if (item.type === "fortune") { if (item.type === "fortune") {
@@ -277,20 +338,16 @@ const onFeatureTap = (item) => {
uni.showToast({ title: `进入:${item.title}`, icon: "none" }); uni.showToast({ title: `进入:${item.title}`, icon: "none" });
}; };
const previewCard = (card) => { onReachBottom(() => {
uni.previewImage({ urls: [card.cover] }); fetchRecommendList();
}; });
const onMore = () => { const onMore = () => {
uni.showToast({ title: "更多模板即将上线~", icon: "none" }); uni.showToast({ title: "更多模板即将上线~", icon: "none" });
}; };
const onCta = (card) => { onPullDownRefresh(async () => {
// uni.showToast({ title: `${card.cta} · ${card.title}`, icon: "none" }); await fetchRecommendList(true);
uni.switchTab({ url: "/pages/make/index" });
};
onPullDownRefresh(() => {
setTimeout(() => { setTimeout(() => {
uni.stopPullDownRefresh(); uni.stopPullDownRefresh();
uni.showToast({ title: "已为你更新内容", icon: "success" }); uni.showToast({ title: "已为你更新内容", icon: "success" });
@@ -647,4 +704,13 @@ onShareAppMessage(() => {
border-radius: 999rpx; border-radius: 999rpx;
font-weight: 500; font-weight: 500;
} }
.loading-text,
.no-more-text {
text-align: center;
font-size: 24rpx;
color: #999;
padding: 20rpx 0;
width: 100%;
}
</style> </style>

View File

@@ -403,10 +403,33 @@ onLoad((options) => {
}); });
onShow(() => { onShow(() => {
const recommendData = uni.getStorageSync("RECOMMEND_CARD_DATA");
if (recommendData) {
uni.removeStorageSync("RECOMMEND_CARD_DATA");
if (recommendData.imageUrl) {
const tpl = {
id: recommendData.recommendId,
imageUrl: recommendData.imageUrl,
name: "推荐模板", // 暂时使用通用名称,如果需要可以从接口获取更多信息
};
// 切换到模板 Tab
activeTool.value = "template";
// 应用模板
currentTemplate.value = tpl;
// 如果模板列表中存在,更新引用
const found = templates.value.find((t) => t.id === tpl.id);
if (found) currentTemplate.value = found;
}
}
const tempBlessing = uni.getStorageSync("TEMP_BLESSING_TEXT"); const tempBlessing = uni.getStorageSync("TEMP_BLESSING_TEXT");
if (tempBlessing) { if (tempBlessing) {
blessingText.value = { content: tempBlessing, id: "" }; blessingText.value = { content: tempBlessing, id: "" };
uni.removeStorageSync("TEMP_BLESSING_TEXT"); uni.removeStorageSync("TEMP_BLESSING_TEXT");
activeTool.value = "text";
} }
}); });

View File

@@ -1,5 +1,5 @@
import { getDeviceInfo } from "@/utils/system"; import { getDeviceInfo } from "@/utils/system";
import { saveRecord, viewRecord } from "@/api/system"; import { saveRecord, viewRecord, createShareToken } from "@/api/system";
export const generateObjectId = ( export const generateObjectId = (
m = Math, m = Math,
@@ -87,3 +87,8 @@ export const saveRemoteImageToLocal = (imageUrl) => {
}, },
}); });
}; };
export const getShareToken = async (scene, targetId) => {
const deviceInfo = getDeviceInfo();
return await createShareToken({ scene, targetId, ...deviceInfo });
};