Compare commits
6 Commits
aee386da51
...
227a5b035e
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
227a5b035e | ||
|
|
e3edccbd65 | ||
|
|
b22bbb8f7c | ||
|
|
29498e4994 | ||
|
|
4a1cbf1f5a | ||
|
|
2fa6584e0c |
@@ -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) => {
|
||||
return request({
|
||||
url: "/api/blessing/avatar/download",
|
||||
|
||||
@@ -45,3 +45,10 @@ export const viewRecord = async (data) => {
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const getRecommendList = async (page = 1) => {
|
||||
return request({
|
||||
url: `/api/blessing/recommend/list?page=${page}`,
|
||||
method: "get",
|
||||
});
|
||||
};
|
||||
|
||||
@@ -47,7 +47,6 @@
|
||||
<text>加载中...</text>
|
||||
</view>
|
||||
<!-- Decorative Elements -->
|
||||
<view class="decor-tag">🐰</view>
|
||||
<view class="card-footer-text">
|
||||
<text class="icon">🌸</text> 2026 丙午马年限定
|
||||
</view>
|
||||
@@ -59,9 +58,6 @@
|
||||
<button class="btn primary-btn" @tap="goToMake">
|
||||
<text class="icon">🎨</text> 我也要领同款制作
|
||||
</button>
|
||||
<button class="btn secondary-btn" @tap="saveImage">
|
||||
<text class="icon">📥</text> 保存到相册
|
||||
</button>
|
||||
</view>
|
||||
|
||||
<!-- Recommended Frames -->
|
||||
@@ -89,13 +85,33 @@
|
||||
</view>
|
||||
|
||||
<!-- 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="banner-icon">
|
||||
<text>🖼</text>
|
||||
</view>
|
||||
<view class="banner-content">
|
||||
<text class="banner-title">去挑选更多壁纸</text>
|
||||
<text class="banner-desc">新年新气象,全套皮肤限时领</text>
|
||||
<text class="banner-title">去挑选新年壁纸</text>
|
||||
<text class="banner-desc">精选新年壁纸,让手机也过年</text>
|
||||
</view>
|
||||
<text class="banner-arrow">›</text>
|
||||
</view>
|
||||
@@ -117,7 +133,8 @@
|
||||
import { ref, onMounted } from "vue";
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
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 =
|
||||
"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 = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/avatar/index",
|
||||
});
|
||||
};
|
||||
|
||||
const goToFortune = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/fortune/index",
|
||||
});
|
||||
};
|
||||
|
||||
const goToGreeting = () => {
|
||||
uni.switchTab({ url: "/pages/make/index" });
|
||||
};
|
||||
|
||||
const goToWallpaper = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/wallpaper/index",
|
||||
@@ -234,7 +230,7 @@ const goToWallpaper = () => {
|
||||
<style lang="scss" scoped>
|
||||
.avatar-detail-page {
|
||||
min-height: 100vh;
|
||||
background: #fff0f5; /* Light Pink Background */
|
||||
background: #ffffff;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
@@ -245,7 +241,7 @@ const goToWallpaper = () => {
|
||||
right: 0;
|
||||
z-index: 100;
|
||||
box-sizing: border-box;
|
||||
background: #fff0f5;
|
||||
background: #ffffff;
|
||||
}
|
||||
|
||||
.nav-content {
|
||||
@@ -271,9 +267,6 @@ const goToWallpaper = () => {
|
||||
margin-right: 50rpx; /* Balance back button */
|
||||
}
|
||||
|
||||
.content-scroll {
|
||||
}
|
||||
|
||||
.content-wrap {
|
||||
padding: 30rpx 40rpx 60rpx;
|
||||
}
|
||||
@@ -326,16 +319,17 @@ const goToWallpaper = () => {
|
||||
|
||||
/* Main Card */
|
||||
.main-card {
|
||||
background: linear-gradient(180deg, #ffffff 0%, #fff5f5 100%);
|
||||
background: #ffffff;
|
||||
border-radius: 40rpx;
|
||||
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;
|
||||
border: 2rpx solid #f5f5f5;
|
||||
}
|
||||
|
||||
.card-inner {
|
||||
position: relative;
|
||||
background: #fffaf0;
|
||||
background: #fff9f9;
|
||||
border-radius: 30rpx;
|
||||
padding: 60rpx;
|
||||
display: flex;
|
||||
@@ -348,7 +342,7 @@ const goToWallpaper = () => {
|
||||
height: 400rpx;
|
||||
border-radius: 20rpx;
|
||||
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
|
||||
border: 8rpx solid #d63333;
|
||||
border: 8rpx solid #ff3b30;
|
||||
}
|
||||
|
||||
.loading-box {
|
||||
@@ -358,7 +352,7 @@ const goToWallpaper = () => {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
background: #eee;
|
||||
background: #f0f0f0;
|
||||
border-radius: 20rpx;
|
||||
}
|
||||
|
||||
@@ -381,14 +375,14 @@ const goToWallpaper = () => {
|
||||
.card-footer-text {
|
||||
margin-top: 40rpx;
|
||||
font-size: 28rpx;
|
||||
color: #d63333;
|
||||
color: #ff3b30;
|
||||
font-weight: bold;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
padding: 10rpx 30rpx;
|
||||
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 {
|
||||
@@ -423,8 +417,8 @@ const goToWallpaper = () => {
|
||||
}
|
||||
|
||||
.secondary-btn {
|
||||
background: #eaeaea;
|
||||
color: #666;
|
||||
background: #f5f5f5;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
.btn .icon {
|
||||
@@ -478,10 +472,9 @@ const goToWallpaper = () => {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
background: #fff;
|
||||
background: #f8f8f8;
|
||||
border-radius: 24rpx;
|
||||
padding: 20rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.frame-img-box {
|
||||
@@ -490,7 +483,7 @@ const goToWallpaper = () => {
|
||||
margin-bottom: 16rpx;
|
||||
border-radius: 50%;
|
||||
overflow: hidden;
|
||||
background: #f9f9f9;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.frame-img {
|
||||
@@ -506,19 +499,18 @@ const goToWallpaper = () => {
|
||||
|
||||
/* Wallpaper Banner */
|
||||
.wallpaper-banner {
|
||||
background: #fff;
|
||||
background: #f8f8f8;
|
||||
border-radius: 24rpx;
|
||||
padding: 30rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin-bottom: 60rpx;
|
||||
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.banner-icon {
|
||||
width: 80rpx;
|
||||
height: 80rpx;
|
||||
background: #fff0f5;
|
||||
background: #fff;
|
||||
border-radius: 20rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
@@ -569,18 +561,18 @@ const goToWallpaper = () => {
|
||||
.footer-line .line {
|
||||
width: 60rpx;
|
||||
height: 2rpx;
|
||||
background: #ccc;
|
||||
background: #eee;
|
||||
}
|
||||
|
||||
.footer-line .text {
|
||||
font-size: 20rpx;
|
||||
color: #999;
|
||||
color: #ccc;
|
||||
margin: 0 20rpx;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
|
||||
.footer-sub {
|
||||
font-size: 20rpx;
|
||||
color: #ccc;
|
||||
color: #ddd;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -6,17 +6,21 @@
|
||||
</view>
|
||||
<view class="preview-card">
|
||||
<view class="preview-square">
|
||||
<image class="avatar-img" :src="currentAvatar" mode="aspectFill" />
|
||||
<image
|
||||
class="avatar-img"
|
||||
:src="currentAvatar?.imageUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image
|
||||
v-if="selectedFrame"
|
||||
class="frame-img"
|
||||
:src="selectedFrame"
|
||||
:src="selectedFrame?.imageUrl"
|
||||
mode="aspectFill"
|
||||
/>
|
||||
<image
|
||||
v-if="selectedDecor"
|
||||
class="decor-img"
|
||||
:src="selectedDecor"
|
||||
:src="selectedDecor?.imageUrl"
|
||||
mode="aspectFit"
|
||||
:style="decorStyle"
|
||||
@touchstart.stop="onTouchStart"
|
||||
@@ -58,7 +62,11 @@
|
||||
:class="{ active: 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>
|
||||
</view>
|
||||
@@ -88,7 +96,7 @@
|
||||
:class="{ active: selectedFrame === 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>
|
||||
</view>
|
||||
@@ -101,12 +109,14 @@
|
||||
:class="{ active: 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>
|
||||
</view>
|
||||
|
||||
<canvas
|
||||
type="2d"
|
||||
id="avatarCanvas"
|
||||
canvas-id="avatarCanvas"
|
||||
class="hidden-canvas"
|
||||
style="width: 600px; height: 600px"
|
||||
@@ -134,7 +144,7 @@
|
||||
class="popup-item"
|
||||
@tap="selectMoreAvatar(item)"
|
||||
>
|
||||
<image :src="item" class="popup-img" mode="aspectFill" />
|
||||
<image :src="item.imageUrl" class="popup-img" mode="aspectFill" />
|
||||
</view>
|
||||
</view>
|
||||
<view v-if="loading" class="loading-text">加载中...</view>
|
||||
@@ -162,7 +172,14 @@ import {
|
||||
getAvatarSystemList,
|
||||
getAvatarFrameList,
|
||||
getAvatarDecorList,
|
||||
avatarCreateComplete,
|
||||
} from "@/api/avatar.js";
|
||||
import {
|
||||
saveRecordRequest,
|
||||
getShareToken,
|
||||
generateObjectId,
|
||||
uploadImage,
|
||||
} from "@/utils/common.js";
|
||||
|
||||
const userStore = useUserStore();
|
||||
const loginPopupRef = ref(null);
|
||||
@@ -182,9 +199,9 @@ const decorPage = ref(1);
|
||||
const decorHasNext = ref(true);
|
||||
const decorLoading = ref(false);
|
||||
|
||||
const currentAvatar = ref("");
|
||||
const selectedFrame = ref("");
|
||||
const selectedDecor = ref("");
|
||||
const currentAvatar = ref(null);
|
||||
const selectedFrame = ref(null);
|
||||
const selectedDecor = ref(null);
|
||||
const activeTab = ref("frame");
|
||||
|
||||
// More Popup logic
|
||||
@@ -201,7 +218,12 @@ const loadFrames = async () => {
|
||||
const res = await getAvatarFrameList(framePage.value);
|
||||
const list = res?.list || [];
|
||||
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++;
|
||||
}
|
||||
if (typeof res.hasNext !== "undefined") {
|
||||
@@ -223,7 +245,12 @@ const loadDecors = async () => {
|
||||
const res = await getAvatarDecorList(decorPage.value);
|
||||
const list = res?.list || [];
|
||||
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++;
|
||||
}
|
||||
if (typeof res.hasNext !== "undefined") {
|
||||
@@ -244,7 +271,9 @@ const initSystemAvatars = async () => {
|
||||
const list = res?.list || [];
|
||||
if (list.length > 0) {
|
||||
// 取前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) {
|
||||
currentAvatar.value = systemAvatars.value[0];
|
||||
@@ -255,10 +284,36 @@ const initSystemAvatars = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
onLoad(() => {
|
||||
onLoad((options) => {
|
||||
initSystemAvatars();
|
||||
loadFrames();
|
||||
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(() => {
|
||||
@@ -292,7 +347,10 @@ const loadMoreAvatars = async () => {
|
||||
const list = res?.list || [];
|
||||
|
||||
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);
|
||||
page.value++;
|
||||
}
|
||||
@@ -313,14 +371,20 @@ const loadMoreAvatars = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const selectMoreAvatar = (url) => {
|
||||
currentAvatar.value = url;
|
||||
const createAvatarId = () => {
|
||||
const id = generateObjectId();
|
||||
// avatarCreateComplete({ id });
|
||||
return id;
|
||||
};
|
||||
|
||||
const selectMoreAvatar = (item) => {
|
||||
currentAvatar.value = item;
|
||||
closeMorePopup();
|
||||
};
|
||||
|
||||
const toggleFrame = (frame) => {
|
||||
if (selectedFrame.value === frame) {
|
||||
selectedFrame.value = "";
|
||||
selectedFrame.value = null;
|
||||
} else {
|
||||
selectedFrame.value = frame;
|
||||
}
|
||||
@@ -409,7 +473,10 @@ const useWeChatAvatar = () => {
|
||||
if (!isLoggedIn.value) {
|
||||
loginPopupRef.value.open();
|
||||
} else {
|
||||
currentAvatar.value = userStore.userInfo.avatarUrl;
|
||||
currentAvatar.value = {
|
||||
id: "wechat_" + Date.now(),
|
||||
imageUrl: userStore.userInfo.avatarUrl,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
@@ -417,7 +484,108 @@ const goBack = () => {
|
||||
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 () => {
|
||||
if (!isLoggedIn.value) {
|
||||
loginPopupRef.value.open();
|
||||
return;
|
||||
}
|
||||
const abilityRes = await abilityCheck("avatar_download");
|
||||
if (!abilityRes.canUse) {
|
||||
if (
|
||||
@@ -436,65 +604,86 @@ const saveAndUse = async () => {
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
const tempPath = saveByCanvas(true);
|
||||
const id = createAvatarId();
|
||||
saveRecordRequest(tempPath, id, "avatar_download");
|
||||
completeCardInfo(id);
|
||||
return;
|
||||
// 调用avatarDownloadRecord API记录下载次数
|
||||
await avatarDownloadRecord({
|
||||
avatarUrl: currentAvatar.value,
|
||||
});
|
||||
const ctx = uni.createCanvasContext("avatarCanvas");
|
||||
const size = 600;
|
||||
const avatarPath = await loadImage(currentAvatar.value);
|
||||
ctx.clearRect(0, 0, size, size);
|
||||
ctx.drawImage(avatarPath, 0, 0, size, size);
|
||||
if (selectedFrame.value) {
|
||||
const framePath = await loadImage(selectedFrame.value);
|
||||
ctx.drawImage(framePath, 0, 0, size, size);
|
||||
}
|
||||
if (selectedDecor.value) {
|
||||
const decorPath = await loadImage(selectedDecor.value);
|
||||
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();
|
||||
}
|
||||
ctx.draw(false, () => {
|
||||
uni.canvasToTempFilePath({
|
||||
canvasId: "avatarCanvas",
|
||||
success: (res) => {
|
||||
uni.saveImageToPhotosAlbum({
|
||||
filePath: res.tempFilePath,
|
||||
success: () => {
|
||||
uni.showToast({ title: "已保存到相册", icon: "success" });
|
||||
},
|
||||
});
|
||||
},
|
||||
});
|
||||
});
|
||||
// await avatarDownloadRecord({
|
||||
// avatarUrl: currentAvatar.value,
|
||||
// });
|
||||
// saveRecordRequest('', )
|
||||
// const ctx = uni.createCanvasContext("avatarCanvas");
|
||||
// const size = 600;
|
||||
// const avatarPath = await loadImage(currentAvatar.value);
|
||||
// ctx.clearRect(0, 0, size, size);
|
||||
// ctx.drawImage(avatarPath, 0, 0, size, size);
|
||||
// if (selectedFrame.value) {
|
||||
// const framePath = await loadImage(selectedFrame.value);
|
||||
// ctx.drawImage(framePath, 0, 0, size, size);
|
||||
// }
|
||||
// if (selectedDecor.value) {
|
||||
// const decorPath = await loadImage(selectedDecor.value);
|
||||
// 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();
|
||||
// }
|
||||
// ctx.draw(false, () => {
|
||||
// uni.canvasToTempFilePath({
|
||||
// canvasId: "avatarCanvas",
|
||||
// success: (res) => {
|
||||
// uni.saveImageToPhotosAlbum({
|
||||
// filePath: res.tempFilePath,
|
||||
// success: () => {
|
||||
// uni.showToast({ title: "已保存到相册", icon: "success" });
|
||||
// },
|
||||
// });
|
||||
// },
|
||||
// });
|
||||
// });
|
||||
};
|
||||
|
||||
const share = () => {
|
||||
uni.showToast({ title: "已生成,可在相册分享", icon: "none" });
|
||||
const completeCardInfo = async (id) => {
|
||||
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 () => {
|
||||
const deviceInfo = getDeviceInfo();
|
||||
const shareTokenRes = await createShareToken({
|
||||
targetId: "",
|
||||
scene: "avatar_download",
|
||||
...deviceInfo,
|
||||
});
|
||||
getRewardByShare();
|
||||
|
||||
getShareReward({ scene: "avatar_download" });
|
||||
if (!isLoggedIn.value) {
|
||||
const shareTokenRes = await getShareToken("avatar_download_not_login", "");
|
||||
return {
|
||||
title: "新春祝福",
|
||||
path: `/pages/index/index?shareToken=${shareTokenRes.shareToken}`,
|
||||
};
|
||||
}
|
||||
const id = createAvatarId();
|
||||
// const shareTokenRes = {
|
||||
// shareToken: "iFmK8WjRm6TK",
|
||||
// };
|
||||
const shareTokenRes = await getShareToken("avatar_download", id);
|
||||
completeCardInfo(id);
|
||||
return {
|
||||
title: "制作我的新春头像",
|
||||
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(() => {
|
||||
// return {
|
||||
// title: "制作我的新春头像",
|
||||
|
||||
@@ -69,40 +69,45 @@
|
||||
</view>
|
||||
<view class="use-grid">
|
||||
<view
|
||||
v-for="(card, i) in popularCards"
|
||||
v-for="(card, i) in recommendList"
|
||||
:key="i"
|
||||
class="use-card"
|
||||
@tap="previewCard(card)"
|
||||
@tap="onCardClick(card)"
|
||||
>
|
||||
<view class="card-cover-wrap">
|
||||
<image :src="card.cover" class="card-cover" mode="aspectFill" />
|
||||
<view
|
||||
v-if="card.tag"
|
||||
class="card-tag"
|
||||
:class="`tag--${card.tagType || 'default'}`"
|
||||
>
|
||||
{{ card.tag }}
|
||||
<image :src="card.imageUrl" class="card-cover" mode="aspectFill" />
|
||||
<view v-if="card.tag" class="card-tag" :class="`tag--${card.tag}`">
|
||||
{{ getTagText(card.tag) }}
|
||||
</view>
|
||||
</view>
|
||||
<view class="card-info">
|
||||
<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="cta-btn" @tap.stop="onCta(card)">
|
||||
{{ card.cta }}
|
||||
<view class="cta-btn" @tap.stop="onCardClick(card)">
|
||||
{{ getCtaText(card.type) }}
|
||||
</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>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
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 { getRecommendList } from "@/api/system";
|
||||
|
||||
const countdownText = ref("");
|
||||
|
||||
@@ -188,6 +193,8 @@ onMounted(() => {
|
||||
|
||||
const index = dayOfYear % inspirationList.length;
|
||||
dailyGreeting.value = inspirationList[index];
|
||||
|
||||
fetchRecommendList();
|
||||
});
|
||||
|
||||
const features = ref([
|
||||
@@ -217,45 +224,99 @@ const features = ref([
|
||||
},
|
||||
]);
|
||||
|
||||
const popularCards = ref([
|
||||
{
|
||||
title: "招财进宝金框",
|
||||
tag: "热门",
|
||||
tagType: "hot",
|
||||
desc: "2026马年限定汉字金框,金光闪烁,财运亨通。适合送亲友的新春祝福。",
|
||||
cta: "立即制作",
|
||||
cover:
|
||||
"https://file.lihailezzc.com/9a929a32-439f-453b-b603-fda7b04cbe08.png",
|
||||
},
|
||||
{
|
||||
title: "大吉大利卡片",
|
||||
tag: "精选",
|
||||
tagType: "featured",
|
||||
desc: "经典红色大拜年卡片,适合送长辈、老师、同学,传递满满的新春喜气。",
|
||||
cta: "去写祝福",
|
||||
cover:
|
||||
"https://file.lihailezzc.com/b5fe8ffb-5901-48d2-94fb-48191e36cbf5.png",
|
||||
},
|
||||
{
|
||||
title: "合家团圆模板",
|
||||
tag: "爆款",
|
||||
tagType: "hot2",
|
||||
desc: "一键生成合家福贺图,支持换装、特效装饰、朋友圈海报等。",
|
||||
cta: "开始创作",
|
||||
cover:
|
||||
"https://file.lihailezzc.com/91cd1611-bb87-442b-a338-24e9d79e4ee9.png",
|
||||
type: "video",
|
||||
},
|
||||
{
|
||||
title: "福气满满",
|
||||
tag: "新款",
|
||||
tagType: "new",
|
||||
desc: "福字当头,好运连连。送给最爱的人。",
|
||||
cta: "立即制作",
|
||||
cover:
|
||||
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png",
|
||||
},
|
||||
]);
|
||||
const recommendList = ref([]);
|
||||
const page = ref(1);
|
||||
const hasMore = ref(true);
|
||||
const loading = ref(false);
|
||||
|
||||
const fetchRecommendList = async (isRefresh = false) => {
|
||||
if (loading.value || (!hasMore.value && !isRefresh)) return;
|
||||
loading.value = true;
|
||||
if (isRefresh) {
|
||||
page.value = 1;
|
||||
hasMore.value = true;
|
||||
}
|
||||
try {
|
||||
const res = await getRecommendList(page.value);
|
||||
const list = res?.list || [];
|
||||
if (isRefresh) {
|
||||
recommendList.value = list;
|
||||
} else {
|
||||
recommendList.value = [...recommendList.value, ...list];
|
||||
}
|
||||
|
||||
if (res.hasNext !== undefined) {
|
||||
hasMore.value = res.hasNext;
|
||||
} else {
|
||||
// Fallback if API doesn't return hasNext
|
||||
if (list.length < 10) hasMore.value = false;
|
||||
}
|
||||
|
||||
if (list.length > 0) {
|
||||
page.value++;
|
||||
} else {
|
||||
hasMore.value = false;
|
||||
}
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
} finally {
|
||||
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) => {
|
||||
if (item.type === "fortune") {
|
||||
@@ -277,20 +338,16 @@ const onFeatureTap = (item) => {
|
||||
uni.showToast({ title: `进入:${item.title}`, icon: "none" });
|
||||
};
|
||||
|
||||
const previewCard = (card) => {
|
||||
uni.previewImage({ urls: [card.cover] });
|
||||
};
|
||||
onReachBottom(() => {
|
||||
fetchRecommendList();
|
||||
});
|
||||
|
||||
const onMore = () => {
|
||||
uni.showToast({ title: "更多模板即将上线~", icon: "none" });
|
||||
};
|
||||
|
||||
const onCta = (card) => {
|
||||
// uni.showToast({ title: `${card.cta} · ${card.title}`, icon: "none" });
|
||||
uni.switchTab({ url: "/pages/make/index" });
|
||||
};
|
||||
|
||||
onPullDownRefresh(() => {
|
||||
onPullDownRefresh(async () => {
|
||||
await fetchRecommendList(true);
|
||||
setTimeout(() => {
|
||||
uni.stopPullDownRefresh();
|
||||
uni.showToast({ title: "已为你更新内容", icon: "success" });
|
||||
@@ -647,4 +704,13 @@ onShareAppMessage(() => {
|
||||
border-radius: 999rpx;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.loading-text,
|
||||
.no-more-text {
|
||||
text-align: center;
|
||||
font-size: 24rpx;
|
||||
color: #999;
|
||||
padding: 20rpx 0;
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -403,10 +403,33 @@ onLoad((options) => {
|
||||
});
|
||||
|
||||
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");
|
||||
if (tempBlessing) {
|
||||
blessingText.value = { content: tempBlessing, id: "" };
|
||||
uni.removeStorageSync("TEMP_BLESSING_TEXT");
|
||||
activeTool.value = "text";
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { getDeviceInfo } from "@/utils/system";
|
||||
import { saveRecord, viewRecord } from "@/api/system";
|
||||
import { saveRecord, viewRecord, createShareToken } from "@/api/system";
|
||||
|
||||
export const generateObjectId = (
|
||||
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 });
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user