Compare commits

...

8 Commits

Author SHA1 Message Date
zzc
aee386da51 optimize: fortune page share reward 2026-01-27 23:01:47 +08:00
zzc
eb72b5556b optimize: fortune page share reward 2026-01-27 21:21:46 +08:00
zzc
115562de53 optimize: make page share reward 2026-01-27 19:35:47 +08:00
zzc
eb37c2d295 optimize: make page share reward 2026-01-27 18:18:04 +08:00
zzc
ce3a53067b optimize: make page 2026-01-27 16:46:39 +08:00
zzc
ee4f1b85c9 feat: Wallpaper page 2026-01-26 18:37:32 +08:00
zzc
3bb5127d8f feat: feedbank api 2026-01-26 17:16:52 +08:00
zzc
3226b35c4f feat: feadback 2026-01-26 11:18:24 +08:00
19 changed files with 1585 additions and 141 deletions

View File

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

9
api/mine.js Normal file
View File

@@ -0,0 +1,9 @@
import { request } from "@/utils/request.js";
export const sendFeedback = async (data) => {
return request({
url: "/api/common/feedback",
method: "POST",
data,
});
};

View File

@@ -29,3 +29,19 @@ export const getShareReward = async (data) => {
data,
});
};
export const saveRecord = async (data) => {
return request({
url: "/api/blessing/save/record",
method: "POST",
data,
});
};
export const viewRecord = async (data) => {
return request({
url: "/api/blessing/view/record",
method: "POST",
data,
});
};

15
api/wallpaper.js Normal file
View File

@@ -0,0 +1,15 @@
import { request } from "@/utils/request.js";
export const getWallpaperList = async (categoryId, page = 1) => {
return request({
url: `/api/blessing/wallpaper/list?categoryId=${categoryId}&page=${page}`,
method: "GET",
});
};
export const getWallpaperCategoryList = async () => {
return request({
url: `/api/blessing/wallpaper/category/list`,
method: "GET",
});
};

View File

@@ -33,6 +33,14 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/avatar/detail",
"style": {
"navigationBarTitleText": "头像详情",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/detail/index",
"style": {
@@ -64,6 +72,22 @@
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/feedback/index",
"style": {
"navigationBarTitleText": "意见反馈",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
},
{
"path": "pages/wallpaper/index",
"style": {
"navigationBarTitleText": "精美壁纸",
"enablePullDownRefresh": false,
"navigationStyle": "custom"
}
}
],
"globalStyle": {

BIN
pages/.DS_Store vendored Normal file

Binary file not shown.

586
pages/avatar/detail.vue Normal file
View File

@@ -0,0 +1,586 @@
<template>
<view class="avatar-detail-page" :style="{ paddingTop: navBarHeight + 'px' }">
<!-- Custom Navbar -->
<view
class="nav-bar"
:style="{
height: navBarHeight + 'px',
paddingTop: statusBarHeight + 'px',
}"
>
<view class="nav-content">
<view class="back" @tap="goBack"></view>
<text class="nav-title">新春头像详情</text>
</view>
</view>
<view class="content-wrap">
<!-- User Info -->
<view class="user-info-section" v-if="detailData">
<image
:src="detailData.from?.avatar || defaultAvatar"
class="user-avatar"
mode="aspectFill"
/>
<view class="user-text">
<view class="name-row">
<text class="nickname">{{
detailData.from?.nickname || "神秘用户"
}}</text>
<view class="tag">马年专属</view>
</view>
<text class="action-text">换上了新春头像</text>
</view>
</view>
<!-- Main Image Card -->
<view class="main-card">
<view class="card-inner">
<image
v-if="detailData?.imageUrl"
:src="detailData.imageUrl"
class="generated-avatar"
mode="aspectFill"
@tap="previewImage"
/>
<view class="loading-box" v-else>
<text>加载中...</text>
</view>
<!-- Decorative Elements -->
<view class="decor-tag">🐰</view>
<view class="card-footer-text">
<text class="icon">🌸</text> 2026 丙午马年限定
</view>
</view>
</view>
<!-- Action Buttons -->
<view class="action-group">
<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 -->
<view class="section recommended-section">
<view class="section-header">
<view class="left">
<view class="bar"></view>
<text class="title">热门新春头像框</text>
</view>
<text class="more" @tap="goToMake">查看全部</text>
</view>
<view class="frame-grid">
<view
class="frame-item"
v-for="(item, index) in frameList"
:key="index"
@tap="goToMake"
>
<view class="frame-img-box">
<image :src="item.url" class="frame-img" mode="aspectFit" />
</view>
<text class="frame-name">{{ item.name || "新春相框" }}</text>
</view>
</view>
</view>
<!-- Wallpaper Banner -->
<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>
</view>
<text class="banner-arrow"></text>
</view>
<!-- Footer -->
<view class="page-footer">
<view class="footer-line">
<text class="line"></text>
<text class="text">2026 HAPPY NEW YEAR</text>
<text class="line"></text>
</view>
<text class="footer-sub">新春祝福 · 传递温情</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getBavBarHeight } from "@/utils/system";
import { getAvatarFrameList, getPageDetail } from "@/api/avatar.js";
const defaultAvatar =
"https://file.lihailezzc.com/resource/d9b329082b32f8305101f708593a4882.png";
const detailData = ref(null);
const frameList = ref([]);
const shareToken = ref("");
const navBarHeight = ref(64);
const statusBarHeight = ref(20);
onLoad((options) => {
if (options.shareToken) {
shareToken.value = options.shareToken;
fetchDetail();
}
fetchFrames();
});
onMounted(() => {
const sysInfo = uni.getSystemInfoSync();
statusBarHeight.value = sysInfo.statusBarHeight;
navBarHeight.value = getBavBarHeight();
});
const goBack = () => {
// Check if can go back, otherwise go home
const pages = getCurrentPages();
if (pages.length > 1) {
uni.navigateBack();
} else {
uni.switchTab({ url: "/pages/index/index" });
}
};
const fetchDetail = async () => {
try {
// uni.showLoading({ title: "加载中..." });
const res = await getPageDetail(shareToken.value);
if (res) {
detailData.value = res;
}
} catch (e) {
console.error(e);
// uni.showToast({ title: "获取详情失败", icon: "none" });
} finally {
// uni.hideLoading();
}
};
const fetchFrames = async () => {
try {
const res = await getAvatarFrameList(1);
if (res) {
const list = Array.isArray(res) ? res : res.list || [];
frameList.value = list.slice(0, 3); // Take first 3
}
} catch (e) {
console.error(e);
}
};
const previewImage = () => {
if (detailData.value?.imageUrl) {
uni.previewImage({
urls: [detailData.value.imageUrl],
});
}
};
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 goToWallpaper = () => {
uni.navigateTo({
url: "/pages/wallpaper/index",
});
};
</script>
<style lang="scss" scoped>
.avatar-detail-page {
min-height: 100vh;
background: #fff0f5; /* Light Pink Background */
box-sizing: border-box;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
right: 0;
z-index: 100;
box-sizing: border-box;
background: #fff0f5;
}
.nav-content {
display: flex;
align-items: center;
height: 100%;
padding: 0 24rpx;
}
.back {
font-size: 50rpx;
margin-right: 24rpx;
line-height: 1;
color: #333;
}
.nav-title {
font-size: 34rpx;
font-weight: bold;
color: #333;
flex: 1;
text-align: center;
margin-right: 50rpx; /* Balance back button */
}
.content-scroll {
}
.content-wrap {
padding: 30rpx 40rpx 60rpx;
}
/* User Info */
.user-info-section {
display: flex;
align-items: center;
margin-bottom: 40rpx;
}
.user-avatar {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
border: 4rpx solid #fff;
margin-right: 24rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.05);
}
.user-text {
flex: 1;
}
.name-row {
display: flex;
align-items: center;
margin-bottom: 8rpx;
}
.nickname {
font-size: 32rpx;
font-weight: bold;
color: #333;
margin-right: 16rpx;
}
.tag {
background: #ff3b30;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 99rpx;
}
.action-text {
font-size: 24rpx;
color: #999;
}
/* Main Card */
.main-card {
background: linear-gradient(180deg, #ffffff 0%, #fff5f5 100%);
border-radius: 40rpx;
padding: 24rpx;
box-shadow: 0 20rpx 60rpx rgba(255, 59, 48, 0.15);
margin-bottom: 60rpx;
}
.card-inner {
position: relative;
background: #fffaf0;
border-radius: 30rpx;
padding: 60rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.generated-avatar {
width: 400rpx;
height: 400rpx;
border-radius: 20rpx;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.1);
border: 8rpx solid #d63333;
}
.loading-box {
width: 400rpx;
height: 400rpx;
display: flex;
align-items: center;
justify-content: center;
color: #999;
background: #eee;
border-radius: 20rpx;
}
.decor-tag {
position: absolute;
top: 30rpx;
right: 30rpx;
background: #fff;
width: 80rpx;
height: 80rpx;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
font-size: 40rpx;
box-shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.08);
color: #ff3b30;
}
.card-footer-text {
margin-top: 40rpx;
font-size: 28rpx;
color: #d63333;
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);
}
.card-footer-text .icon {
margin-right: 10rpx;
}
/* Buttons */
.action-group {
margin-bottom: 60rpx;
}
.btn {
height: 100rpx;
border-radius: 99rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 32rpx;
font-weight: 600;
margin-bottom: 30rpx;
border: none;
}
.btn::after {
border: none;
}
.primary-btn {
background: #ff3b30;
color: #fff;
box-shadow: 0 10rpx 20rpx rgba(255, 59, 48, 0.3);
}
.secondary-btn {
background: #eaeaea;
color: #666;
}
.btn .icon {
margin-right: 16rpx;
font-size: 36rpx;
}
/* Recommended Section */
.section {
margin-bottom: 40rpx;
}
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
}
.section-header .left {
display: flex;
align-items: center;
}
.section-header .bar {
width: 8rpx;
height: 32rpx;
background: #ff3b30;
border-radius: 4rpx;
margin-right: 16rpx;
}
.section-header .title {
font-size: 32rpx;
font-weight: bold;
color: #333;
}
.section-header .more {
font-size: 24rpx;
color: #999;
}
.frame-grid {
display: flex;
justify-content: space-between;
}
.frame-item {
width: 200rpx;
display: flex;
flex-direction: column;
align-items: center;
background: #fff;
border-radius: 24rpx;
padding: 20rpx;
box-shadow: 0 4rpx 12rpx rgba(0, 0, 0, 0.03);
}
.frame-img-box {
width: 140rpx;
height: 140rpx;
margin-bottom: 16rpx;
border-radius: 50%;
overflow: hidden;
background: #f9f9f9;
}
.frame-img {
width: 100%;
height: 100%;
}
.frame-name {
font-size: 24rpx;
color: #333;
font-weight: 500;
}
/* Wallpaper Banner */
.wallpaper-banner {
background: #fff;
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;
border-radius: 20rpx;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
}
.banner-icon text {
font-size: 40rpx;
color: #ff3b30;
}
.banner-content {
flex: 1;
}
.banner-title {
font-size: 30rpx;
font-weight: bold;
color: #333;
display: block;
margin-bottom: 8rpx;
}
.banner-desc {
font-size: 22rpx;
color: #999;
}
.banner-arrow {
font-size: 36rpx;
color: #ccc;
}
/* Footer */
.page-footer {
text-align: center;
padding-bottom: 40rpx;
}
.footer-line {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 16rpx;
}
.footer-line .line {
width: 60rpx;
height: 2rpx;
background: #ccc;
}
.footer-line .text {
font-size: 20rpx;
color: #999;
margin: 0 20rpx;
letter-spacing: 2rpx;
}
.footer-sub {
font-size: 20rpx;
color: #ccc;
}
</style>

View File

@@ -497,7 +497,7 @@ onShareAppMessage(async () => {
return {
title: "制作我的新春头像",
path: `/pages/avatar/index?shareToken=${shareTokenRes.shareToken}`,
path: `/pages/avatar/detail?shareToken=${shareTokenRes.shareToken}`,
imageUrl:
"https://file.lihailezzc.com/resource/b48c41054c2633c478463ac1b1f1ca23.png", // 使用默认封面或 popularCards 的封面
};
@@ -507,7 +507,6 @@ const getRewardByShare = async () => {
const res = await getShareReward({ scene: "avatar_download" });
if (res.success) {
uni.showToast({ title: "分享成功,可下载头像" });
checkDrawStatus();
}
};

View File

@@ -113,6 +113,7 @@ import { ref, onMounted } from "vue";
import { getBavBarHeight } from "@/utils/system";
import { onLoad } from "@dcloudio/uni-app";
import { getPageDetail } from "@/api/system.js";
import { saveViewRequest } from "@/utils/common.js";
const navBarHeight = ref(44);
const statusBarHeight = ref(20);
@@ -125,6 +126,7 @@ onLoad(async (options) => {
const card = await getPageDetail(options.shareToken);
cardId.value = card.id;
cardDetail.value = card;
saveViewRequest(options.shareToken, "card_generate", card.id);
}
});

352
pages/feedback/index.vue Normal file
View File

@@ -0,0 +1,352 @@
<template>
<view class="feedback-page" :style="{ paddingTop: getBavBarHeight() + 'px' }">
<!-- Custom Navbar -->
<view class="nav-bar">
<view class="back" @tap="goBack"></view>
<text class="nav-title">意见反馈</text>
</view>
<view class="content-wrap">
<!-- Type Selector -->
<view class="form-item">
<view class="label">反馈类型</view>
<view class="type-list">
<view
v-for="(item, index) in feedbackTypes"
:key="index"
class="type-chip"
:class="{ active: formData.type === item.value }"
@tap="formData.type = item.value"
>
{{ item.label }}
</view>
</view>
</view>
<!-- Content Input -->
<view class="form-item">
<view class="label">反馈内容 <text class="required">*</text></view>
<view class="textarea-box">
<textarea
v-model="formData.content"
class="textarea"
placeholder="请输入您的反馈意见,我们将为您不断改进..."
placeholder-class="placeholder"
maxlength="200"
/>
<text class="counter">{{ formData.content.length }}/200</text>
</view>
</view>
<!-- Image Upload -->
<view class="form-item">
<view class="label"
>图片上传 <text class="optional">(选填最多3张)</text></view
>
<view class="image-grid">
<view
v-for="(img, index) in formData.images"
:key="index"
class="image-item"
>
<image
:src="img"
mode="aspectFill"
class="thumb"
@tap="previewImage(index)"
/>
<view class="delete-btn" @tap.stop="deleteImage(index)">×</view>
</view>
<view
v-if="formData.images.length < 3"
class="upload-btn"
@tap="chooseImage"
>
<text class="plus">+</text>
</view>
</view>
</view>
<!-- Submit Button -->
<view class="submit-wrap">
<button
class="submit-btn"
:class="{ disabled: !isValid }"
:disabled="!isValid"
@tap="submitFeedback"
>
提交反馈
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed } from "vue";
import { getBavBarHeight, getDeviceInfo } from "@/utils/system";
import { sendFeedback } from "@/api/mine.js";
import { uploadImage } from "@/utils/common.js";
const feedbackTypes = [
{ label: "功能建议", value: 1 },
{ label: "问题反馈", value: 2 },
{ label: "投诉", value: 3 },
{ label: "其他", value: 4 },
];
const formData = ref({
type: 1,
content: "",
images: [],
});
const isValid = computed(() => {
return formData.value.content.trim().length > 0;
});
const goBack = () => {
uni.navigateBack();
};
const chooseImage = () => {
uni.chooseImage({
count: 3 - formData.value.images.length,
sizeType: ["compressed"],
sourceType: ["album", "camera"],
success: (res) => {
formData.value.images = [...formData.value.images, ...res.tempFilePaths];
},
});
};
const deleteImage = (index) => {
formData.value.images.splice(index, 1);
};
const previewImage = (index) => {
uni.previewImage({
urls: formData.value.images,
current: index,
});
};
const submitFeedback = async () => {
if (!isValid.value) return;
uni.showLoading({ title: "提交中..." });
try {
const uploadedImages = [];
// Upload all images first
for (const img of formData.value.images) {
const url = await uploadImage(img);
uploadedImages.push(url);
}
// Submit with real URLs
const deviceInfo = getDeviceInfo();
sendFeedback({
type: formData.value.type,
content: formData.value.content,
images: uploadedImages,
deviceInfo: deviceInfo,
});
uni.hideLoading();
uni.showToast({
title: "感谢您的反馈",
icon: "success",
duration: 2000,
});
setTimeout(() => {
uni.navigateBack();
}, 1000);
} catch (err) {
uni.hideLoading();
uni.showToast({ title: "提交失败", icon: "none" });
console.error(err);
}
};
</script>
<style lang="scss" scoped>
.feedback-page {
min-height: 100vh;
background: #f9f9f9;
box-sizing: border-box;
}
.nav-bar {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
background: #fff;
position: sticky;
top: 0;
z-index: 100;
}
.back {
font-size: 40rpx;
margin-right: 24rpx;
line-height: 1;
}
.nav-title {
font-size: 32rpx;
font-weight: 600;
color: #333;
}
.content-wrap {
padding: 32rpx;
}
.form-item {
margin-bottom: 48rpx;
}
.label {
font-size: 28rpx;
font-weight: 600;
color: #333;
margin-bottom: 20rpx;
display: flex;
align-items: center;
}
.required {
color: #ff3b30;
margin-left: 8rpx;
}
.optional {
font-size: 24rpx;
color: #999;
font-weight: normal;
margin-left: 8rpx;
}
/* Type Selector */
.type-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
}
.type-chip {
padding: 12rpx 32rpx;
background: #fff;
border-radius: 999rpx;
font-size: 26rpx;
color: #666;
border: 2rpx solid transparent;
transition: all 0.2s;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.02);
}
.type-chip.active {
background: #fff0f0;
color: #ff3b30;
border-color: #ff3b30;
font-weight: 500;
}
/* Textarea */
.textarea-box {
background: #fff;
border-radius: 24rpx;
padding: 24rpx;
position: relative;
box-shadow: 0 2rpx 12rpx rgba(0, 0, 0, 0.02);
}
.textarea {
width: 100%;
height: 240rpx;
font-size: 28rpx;
line-height: 1.5;
color: #333;
}
.placeholder {
color: #ccc;
}
.counter {
position: absolute;
bottom: 16rpx;
right: 24rpx;
font-size: 22rpx;
color: #999;
}
/* Image Grid */
.image-grid {
display: flex;
flex-wrap: wrap;
gap: 24rpx;
}
.image-item {
width: 160rpx;
height: 160rpx;
position: relative;
border-radius: 16rpx;
overflow: hidden;
}
.thumb {
width: 100%;
height: 100%;
border-radius: 16rpx;
}
.delete-btn {
position: absolute;
top: 0;
right: 0;
width: 40rpx;
height: 40rpx;
background: rgba(0, 0, 0, 0.5);
color: #fff;
display: flex;
align-items: center;
justify-content: center;
border-bottom-left-radius: 12rpx;
font-size: 32rpx;
line-height: 1;
}
.upload-btn {
width: 160rpx;
height: 160rpx;
background: #fff;
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
border: 2rpx dashed #ddd;
}
.plus {
font-size: 60rpx;
color: #ddd;
font-weight: 300;
margin-top: -8rpx;
}
/* Submit Button */
.submit-wrap {
margin-top: 60rpx;
}
.submit-btn {
background: #ff3b30;
color: #fff;
font-size: 32rpx;
font-weight: 600;
height: 88rpx;
border-radius: 999rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 10rpx 20rpx rgba(255, 59, 48, 0.2);
transition: all 0.3s;
}
.submit-btn.disabled {
background: #ffccc7;
box-shadow: none;
opacity: 0.8;
}
.submit-btn::after {
border: none;
}
</style>

View File

@@ -93,6 +93,7 @@
import { ref } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getPageDetail } from "@/api/system.js";
import { saveViewRequest } from "@/utils/common.js";
const inviterName = ref("");
const inviterAvatar = ref("");
@@ -122,6 +123,7 @@ const loadPageDetail = async (shareToken) => {
fortuneData.value = data;
inviterName.value = data?.from?.nickname || "";
inviterAvatar.value = data?.from?.avatar || "";
saveViewRequest(shareToken, "fortune_draw", data.fortuneId);
};
const goHome = () => {

View File

@@ -38,7 +38,7 @@
{{ status === "shaking" ? "抽取中..." : "立即抽取" }}
</button>
<view class="footer-info">
<view v-if="isLoggedIn" class="footer-info">
<text class="info-icon"></text>
<text v-if="allowShareCount - useShareCount > 0">
今日还有 {{ remainingCount }} 次抽取机会分享可增加次数
@@ -107,11 +107,11 @@
</view>
<!-- Canvas 用于生成图片 (隐藏) -->
<canvas
<!-- <canvas
canvas-id="shareCanvas"
class="share-canvas"
style="width: 300px; height: 500px; position: fixed; left: 9999px"
></canvas>
></canvas> -->
<LoginPopup ref="loginPopupRef" @logind="handleLogind" />
</view>
@@ -126,6 +126,7 @@ import { drawFortune } from "@/api/fortune.js";
import { createShareToken, getShareReward } from "@/api/system.js";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { useUserStore } from "@/stores/user";
import { saveRemoteImageToLocal, saveRecordRequest } from "@/utils/common.js";
const userStore = useUserStore();
const loginPopupRef = ref(null);
@@ -190,11 +191,9 @@ const checkDrawStatus = async () => {
if (!isLoggedIn.value) return;
const res = await abilityCheck("fortune_draw");
if (res.canUse) {
remainingCount.value = res.remain;
allowShareCount.value = res.allowShareCount || 0;
useShareCount.value = res.useShareCount || 0;
}
remainingCount.value = res.remain || 0;
allowShareCount.value = res.allowShareCount || 0;
useShareCount.value = res.useShareCount || 0;
};
const currentFortune = ref({});
@@ -262,107 +261,89 @@ const reset = () => {
const saveCard = () => {
if (currentFortune.value.imageUrl) {
uni.showLoading({ title: "保存中..." });
uni.downloadFile({
url: currentFortune.value.imageUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: "已保存到相册" });
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: "保存失败", icon: "none" });
},
});
} else {
uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
},
});
saveRemoteImageToLocal(currentFortune.value.imageUrl);
saveRecordRequest(
"",
cardId.value,
"fortune_draw",
currentFortune.value.imageUrl,
);
return;
}
uni.showLoading({ title: "生成中..." });
// uni.showLoading({ title: "生成中..." });
const ctx = uni.createCanvasContext("shareCanvas");
// const ctx = uni.createCanvasContext("shareCanvas");
// 绘制背景
ctx.setFillStyle("#FFFBF0");
ctx.fillRect(0, 0, 300, 500);
// // 绘制背景
// ctx.setFillStyle("#FFFBF0");
// ctx.fillRect(0, 0, 300, 500);
// 绘制外边框
ctx.setStrokeStyle("#E6CAA0");
ctx.setLineWidth(1);
ctx.strokeRect(0, 0, 300, 500);
// // 绘制外边框
// ctx.setStrokeStyle("#E6CAA0");
// ctx.setLineWidth(1);
// ctx.strokeRect(0, 0, 300, 500);
// 绘制内装饰边框
ctx.setStrokeStyle("#D4AF37");
ctx.setLineWidth(2);
ctx.strokeRect(10, 10, 280, 480);
// // 绘制内装饰边框
// ctx.setStrokeStyle("#D4AF37");
// ctx.setLineWidth(2);
// ctx.strokeRect(10, 10, 280, 480);
// 绘制年份标签
ctx.setFillStyle("#E63946");
// 圆角矩形模拟(简化)
ctx.fillRect(100, 40, 100, 24);
ctx.setFillStyle("#FFFFFF");
ctx.setFontSize(14);
ctx.setTextAlign("center");
ctx.fillText("2026 乙巳年", 150, 57);
// // 绘制年份标签
// ctx.setFillStyle("#E63946");
// // 圆角矩形模拟(简化)
// ctx.fillRect(100, 40, 100, 24);
// ctx.setFillStyle("#FFFFFF");
// ctx.setFontSize(14);
// ctx.setTextAlign("center");
// ctx.fillText("2026 乙巳年", 150, 57);
// 绘制标题
ctx.setFillStyle("#C0392B");
ctx.setFontSize(32);
ctx.font = "bold 32px serif";
ctx.fillText(currentFortune.value.title, 150, 120);
// // 绘制标题
// ctx.setFillStyle("#C0392B");
// ctx.setFontSize(32);
// ctx.font = "bold 32px serif";
// ctx.fillText(currentFortune.value.title, 150, 120);
// 绘制分隔线
ctx.setStrokeStyle("#D4AF37");
ctx.beginPath();
ctx.moveTo(130, 140);
ctx.lineTo(170, 140);
ctx.stroke();
// // 绘制分隔线
// ctx.setStrokeStyle("#D4AF37");
// ctx.beginPath();
// ctx.moveTo(130, 140);
// ctx.lineTo(170, 140);
// ctx.stroke();
// 绘制描述
ctx.setFillStyle("#333333");
ctx.setFontSize(16);
// 简单换行处理(假设文字不长)
ctx.fillText(currentFortune.value.desc, 150, 180);
// // 绘制描述
// ctx.setFillStyle("#333333");
// ctx.setFontSize(16);
// // 简单换行处理(假设文字不长)
// ctx.fillText(currentFortune.value.desc, 150, 180);
// 绘制底部文字
ctx.setFillStyle("#888888");
ctx.setFontSize(12);
ctx.fillText("旧岁千般皆如意,新年万事定称心。", 150, 220);
// // 绘制底部文字
// ctx.setFillStyle("#888888");
// ctx.setFontSize(12);
// ctx.fillText("旧岁千般皆如意,新年万事定称心。", 150, 220);
ctx.draw(false, () => {
uni.canvasToTempFilePath({
canvasId: "shareCanvas",
success: (res) => {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: "已保存到相册" });
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: "保存失败", icon: "none" });
},
});
},
fail: (err) => {
uni.hideLoading();
console.error(err);
},
});
});
// ctx.draw(false, () => {
// uni.canvasToTempFilePath({
// canvasId: "shareCanvas",
// success: (res) => {
// uni.saveImageToPhotosAlbum({
// filePath: res.tempFilePath,
// success: () => {
// uni.hideLoading();
// uni.showToast({ title: "已保存到相册" });
// },
// fail: () => {
// uni.hideLoading();
// uni.showToast({ title: "保存失败", icon: "none" });
// },
// });
// },
// fail: (err) => {
// uni.hideLoading();
// console.error(err);
// },
// });
// });
};
</script>

View File

@@ -193,7 +193,7 @@ onMounted(() => {
const features = ref([
{
title: "新春祝福卡片",
subtitle: "定制专属贺卡",
subtitle: "定制专属贺卡",
icon: "/static/icon/celebrate.png",
type: "card",
},
@@ -213,7 +213,7 @@ const features = ref([
title: "精美壁纸",
subtitle: "获取精美壁纸",
icon: "/static/icon/bizhi.png",
type: "video",
type: "wallpaper",
},
]);
@@ -270,6 +270,10 @@ const onFeatureTap = (item) => {
uni.navigateTo({ url: "/pages/avatar/index" });
return;
}
if (item.type === "wallpaper") {
uni.navigateTo({ url: "/pages/wallpaper/index" });
return;
}
uni.showToast({ title: `进入:${item.title}`, icon: "none" });
};

View File

@@ -56,7 +56,7 @@
<uni-icons type="cloud-download" size="20" color="#888"></uni-icons>
<view>保存</view>
</button>
<button open-type="share" class="btn primary" @tap="shareOrSave">
<button open-type="share" class="btn primary">
<uni-icons
type="paperplane-filled"
size="20"
@@ -290,7 +290,7 @@ import {
getCardTemplateList,
getCardTemplateContentList,
} from "@/api/make";
import { createShareToken } from "@/api/system";
import { createShareToken, abilityCheck, getShareReward } from "@/api/system";
import {
onShareAppMessage,
onLoad,
@@ -299,6 +299,7 @@ import {
} from "@dcloudio/uni-app";
import { useUserStore } from "@/stores/user";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
import { saveRecordRequest, uploadImage } from "@/utils/common.js";
const userStore = useUserStore();
const loginPopupRef = ref(null);
@@ -481,6 +482,7 @@ const loadMoreTemplates = () => {
};
onShareAppMessage(async () => {
getShareReward({ scene: "card_generate" });
if (!isLoggedIn.value) {
return {
title: "新春祝福",
@@ -502,6 +504,7 @@ onShareAppMessage(async () => {
targetId: id,
...deviceInfo,
});
shareOrSave(id);
return {
title: "新春祝福",
path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken,
@@ -563,46 +566,52 @@ const toggleAvatarDecor = () => {
uni.showToast({ title: "挂饰功能即将上线~", icon: "none" });
};
const preview = () => {
saveByCanvas();
// uni.showToast({ title: '已保存到相册', icon: 'checkmarkempty' })
};
const shareOrSave = async () => {
const preview = async () => {
if (!isLoggedIn.value) {
loginPopupRef.value.open();
return;
}
const abilityRes = await abilityCheck("card_generate");
if (!abilityRes.canUse) {
if (
abilityRes?.blockType === "need_share" &&
abilityRes?.message === "分享可继续"
) {
uni.showToast({
title: "分享给好友可继续使用",
icon: "none",
});
return;
}
uni.showToast({
title: "您今日祝福卡下载次数已用完,直接分享给好友或者明日再试",
icon: "none",
});
return;
}
const tempPath = await saveByCanvas(true);
id = createCard();
shareOrSave(id);
saveRecordRequest(tempPath, id, "card_generate");
// uni.showToast({ title: '已保存到相册', icon: 'checkmarkempty' })
};
const shareOrSave = async (id) => {
if (!id) id = createCard();
const tempPath = await saveByCanvas(false);
const fileKeyRes = await uni.uploadFile({
url: "https://api.ai-meng.com/api/common/upload",
filePath: tempPath,
name: "file", // 和后端接收文件字段名一致
header: {
"x-app-id": "69665538a49b8ae3be50fe5d",
},
const imageUrl = await uploadImage(tempPath);
updateCard({
id,
imageUrl,
status: 1,
blessingId: blessingText.value?.id || "",
blessingTo: targetName.value,
blessingFrom: signatureName.value,
templateId: currentTemplate.value?.id || "",
});
if (fileKeyRes.statusCode < 400) {
const keyJson = JSON.parse(fileKeyRes.data);
const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
// const url =
// "https://file.lihailezzc.com/resource/99c9f7e0086ed66d20bd1675b4ab22e9.png";
// 1. 确保有 cardId
if (!cardId.value) {
createCard();
}
updateCard({
id: cardId.value,
imageUrl: url,
status: 1,
blessingId: blessingText.value?.id || "",
blessingTo: targetName.value,
blessingFrom: signatureName.value,
templateId: currentTemplate.value?.id || "",
});
}
// uni.showToast({ title: '已保存到相册并可分享', icon: 'none' })
};
const showMore = () => {

View File

@@ -67,7 +67,7 @@
</view>
<!-- History -->
<view class="section-title">历史记录</view>
<!-- <view class="section-title">历史记录</view>
<view class="menu-group">
<view class="menu-item" @tap="navTo('history')">
<view class="icon-box gray-bg"><text>🕒</text></view>
@@ -79,7 +79,7 @@
<text class="menu-text">历年祝福存档</text>
<text class="arrow"></text>
</view>
</view>
</view> -->
<!-- Other Actions -->
<view class="menu-group mt-30">
@@ -88,6 +88,11 @@
<text class="menu-text">分享给好友</text>
<text class="arrow"></text>
</button>
<view class="menu-item" @tap="navTo('feedback')">
<view class="icon-left"><text class="feedback-icon">📝</text></view>
<text class="menu-text">意见反馈</text>
<text class="arrow"></text>
</view>
<view class="menu-item" @tap="navTo('help')">
<view class="icon-left"><text class="help-icon"></text></view>
<text class="menu-text">使用说明 / 帮助</text>
@@ -161,6 +166,12 @@ const navTo = (page) => {
});
return;
}
if (page === "feedback") {
uni.navigateTo({
url: "/pages/feedback/index",
});
return;
}
uni.showToast({ title: "功能开发中", icon: "none" });
};
</script>
@@ -364,7 +375,8 @@ const navTo = (page) => {
margin-left: 16rpx;
}
.share-icon,
.help-icon {
.help-icon,
.feedback-icon {
font-size: 36rpx;
color: #666;
}

350
pages/wallpaper/index.vue Normal file
View File

@@ -0,0 +1,350 @@
<template>
<view
class="wallpaper-page"
:style="{ paddingTop: getBavBarHeight() + 'px' }"
>
<!-- Custom Navbar -->
<view class="nav-bar">
<view class="back" @tap="goBack"></view>
<text class="nav-title">新春精美壁纸</text>
</view>
<!-- Category Tabs -->
<view class="category-tabs">
<scroll-view scroll-x class="tabs-scroll" :show-scrollbar="false">
<view class="tabs-content">
<view
v-for="(item, index) in categories"
:key="index"
class="tab-item"
:class="{ active: currentCategoryId === item.id }"
@tap="switchCategory(item.id)"
>
{{ item.name }}
</view>
</view>
</scroll-view>
</view>
<!-- Wallpaper Grid -->
<scroll-view
scroll-y
class="wallpaper-scroll"
@scrolltolower="loadMore"
refresher-enabled
:refresher-triggered="isRefreshing"
@refresherrefresh="onRefresh"
>
<view class="grid-container">
<view
class="grid-item"
v-for="(item, index) in wallpapers"
:key="index"
>
<image
:src="item.imageUrl"
mode="aspectFill"
class="wallpaper-img"
@tap="previewImage(index)"
/>
<view class="action-overlay">
<view
class="action-btn download"
@tap.stop="downloadWallpaper(item)"
>
<text class="icon"></text>
</view>
<view class="action-btn share" @tap.stop="shareWallpaper(item)">
<text class="icon"></text>
</view>
</view>
</view>
</view>
<!-- Loading State -->
<view class="loading-state" v-if="loading">
<text>加载中...</text>
</view>
<view class="empty-state" v-if="!loading && wallpapers.length === 0">
<text>暂无壁纸</text>
</view>
<view
class="no-more"
v-if="!loading && !hasMore && wallpapers.length > 0"
>
<text>没有更多了</text>
</view>
</scroll-view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { getBavBarHeight } from "@/utils/system";
import { getWallpaperList, getWallpaperCategoryList } from "@/api/wallpaper.js";
const categories = ref([]);
const currentCategoryId = ref(null);
const wallpapers = ref([]);
const page = ref(1);
const loading = ref(false);
const hasMore = ref(true);
const isRefreshing = ref(false);
onMounted(async () => {
await fetchCategories();
});
const goBack = () => {
uni.navigateBack();
};
const fetchCategories = async () => {
try {
const res = await getWallpaperCategoryList();
const list = Array.isArray(res) ? res : res?.list || [];
if (list.length > 0) {
categories.value = list;
currentCategoryId.value = list[0].id;
loadWallpapers(true);
}
} catch (e) {
console.error("Failed to fetch categories", e);
uni.showToast({ title: "获取分类失败", icon: "none" });
}
};
const switchCategory = (id) => {
if (currentCategoryId.value === id) return;
currentCategoryId.value = id;
loadWallpapers(true);
};
const loadWallpapers = async (reset = false) => {
if (loading.value) return;
if (reset) {
page.value = 1;
hasMore.value = true;
wallpapers.value = [];
}
if (!hasMore.value) return;
loading.value = true;
try {
const res = await getWallpaperList(currentCategoryId.value, page.value);
const list = res?.list || [];
hasMore.value = !!res?.hasNext;
if (reset) {
wallpapers.value = list;
} else {
wallpapers.value = [...wallpapers.value, ...list];
}
if (hasMore.value) {
page.value++;
}
} catch (e) {
console.error("Failed to fetch wallpapers", e);
uni.showToast({ title: "获取壁纸失败", icon: "none" });
} finally {
loading.value = false;
isRefreshing.value = false;
}
};
const loadMore = () => {
loadWallpapers();
};
const onRefresh = () => {
isRefreshing.value = true;
loadWallpapers(true);
};
const previewImage = (index) => {
const urls = wallpapers.value.map((item) => item.url);
uni.previewImage({
urls,
current: index,
});
};
const downloadWallpaper = (item) => {
uni.showLoading({ title: "下载中..." });
uni.downloadFile({
url: item.url,
success: (res) => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: "保存成功", icon: "success" });
},
fail: (err) => {
uni.hideLoading();
console.error(err);
uni.showToast({ title: "保存失败", icon: "none" });
},
});
} else {
uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
},
});
};
const shareWallpaper = (item) => {
// uni.share is for App, specific provider.
// For general sharing, we might just preview it or use button open-type="share" if it was a button component.
// Since this is a custom UI, we can guide user to preview and long press, or just preview.
// Or if on MP-Weixin, show share menu.
uni.previewImage({
urls: [item.url],
});
// uni.showToast({ title: '长按图片发送给朋友', icon: 'none' });
};
</script>
<style lang="scss" scoped>
.wallpaper-page {
height: 100vh;
background-color: #7a0909; /* Dark Red Background */
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.nav-bar {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
/* background: #7A0909; */
position: sticky;
top: 0;
z-index: 100;
}
.back {
font-size: 50rpx;
margin-right: 24rpx;
line-height: 1;
color: #ffd700; /* Gold */
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #ffd700; /* Gold */
}
.category-tabs {
padding: 20rpx 0;
/* background-color: #7A0909; */
}
.tabs-scroll {
white-space: nowrap;
width: 100%;
}
.tabs-content {
display: inline-flex;
padding: 0 24rpx;
gap: 20rpx;
}
.tab-item {
padding: 12rpx 32rpx;
border-radius: 999rpx;
font-size: 28rpx;
color: #ffd700;
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid transparent;
transition: all 0.3s;
}
.tab-item.active {
background: linear-gradient(90deg, #ff3b30 0%, #ff9500 100%);
color: #fff;
border-color: #ffd700;
font-weight: 600;
box-shadow: 0 4rpx 12rpx rgba(255, 215, 0, 0.3);
}
.wallpaper-scroll {
flex: 1;
overflow: hidden;
/* padding: 24rpx; */
box-sizing: border-box;
}
.grid-container {
display: flex;
flex-wrap: wrap;
padding: 24rpx;
justify-content: space-between;
}
.grid-item {
width: 340rpx;
height: 600rpx;
border-radius: 24rpx;
overflow: hidden;
margin-bottom: 24rpx;
position: relative;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.3);
background: #333;
}
.wallpaper-img {
width: 100%;
height: 100%;
}
.action-overlay {
position: absolute;
bottom: 20rpx;
right: 20rpx;
display: flex;
flex-direction: column;
gap: 16rpx;
}
.action-btn {
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid rgba(255, 255, 255, 0.3);
}
.action-btn .icon {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
.action-btn.share .icon {
font-size: 28rpx;
}
.loading-state,
.empty-state,
.no-more {
text-align: center;
padding: 40rpx;
color: rgba(255, 255, 255, 0.6);
font-size: 24rpx;
}
</style>

View File

@@ -1,6 +1,89 @@
import { getDeviceInfo } from "@/utils/system";
import { saveRecord, viewRecord } from "@/api/system";
export const generateObjectId = (
m = Math,
d = Date,
h = 16,
s = (s) => m.floor(s).toString(h),
) => s(d.now() / 1000) + " ".repeat(h).replace(/./g, () => s(m.random() * h));
export const uploadImage = (filePath) => {
return new Promise((resolve, reject) => {
uni.uploadFile({
url: "https://api.ai-meng.com/api/common/upload",
filePath: filePath,
name: "file",
header: {
"x-app-id": "69665538a49b8ae3be50fe5d",
},
success: (res) => {
if (res.statusCode < 400) {
try {
const keyJson = JSON.parse(res.data);
const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
resolve(url);
} catch (e) {
reject(e);
}
} else {
reject(new Error("Upload failed"));
}
},
fail: (err) => {
reject(err);
},
});
});
};
export const saveRecordRequest = async (path, targetId, scene, imageUrl) => {
if (!imageUrl) {
imageUrl = await uploadImage(path);
}
const deviceInfo = getDeviceInfo();
saveRecord({
scene,
targetId,
imageUrl,
deviceInfo,
});
};
export const saveViewRequest = async (shareToken, scene, targetId) => {
const deviceInfo = getDeviceInfo();
viewRecord({
shareToken,
scene,
targetId,
deviceInfo,
});
};
export const saveRemoteImageToLocal = (imageUrl) => {
uni.downloadFile({
url: imageUrl,
success: (res) => {
if (res.statusCode === 200) {
uni.saveImageToPhotosAlbum({
filePath: res.tempFilePath,
success: () => {
uni.hideLoading();
uni.showToast({ title: "已保存到相册" });
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: "保存失败", icon: "none" });
},
});
} else {
uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
}
},
fail: () => {
uni.hideLoading();
uni.showToast({ title: "下载失败", icon: "none" });
},
});
};

View File

@@ -1,6 +1,6 @@
// const BASE_URL = 'https://apis.lihailezzc.com'
const BASE_URL = 'http://127.0.0.1:3999'
// const BASE_URL = "http://192.168.1.3:3999";
// const BASE_URL = 'http://127.0.0.1:3999'
const BASE_URL = "http://192.168.1.3:3999";
// const BASE_URL = "http://192.168.31.253:3999";
import { useUserStore } from "@/stores/user";

View File

@@ -59,5 +59,6 @@ export function getDeviceInfo() {
language: info.language,
version: info.version,
SDKVersion: info.SDKVersion,
appId: "69665538a49b8ae3be50fe5d",
};
}