Compare commits

..

15 Commits

Author SHA1 Message Date
zzc
239db8e609 fix: gretting page 2026-02-03 12:35:26 +08:00
zzc
72ebb8253d fix: gretting page 2026-02-03 12:22:15 +08:00
zzc
fcc728465b fix: gretting page 2026-02-03 11:31:18 +08:00
zzc
9c1382c503 fix: gretting page 2026-02-03 11:26:47 +08:00
zzc
e664356a5f fix: gretting page 2026-02-03 11:17:58 +08:00
zzc
ad40704642 fix: gretting page 2026-02-03 11:01:58 +08:00
zzc
82fc0f80a5 fix: gretting page 2026-02-03 10:25:47 +08:00
zzc
b2e41c9a7b fix: make audit 2026-02-03 09:47:25 +08:00
zzc
b6091de037 fix: 抽签 2026-02-03 05:55:04 +08:00
zzc
7c80f9a6a6 fix: 抽签 2026-02-03 05:21:03 +08:00
zzc
b2b59cb61a fix: metadata 2026-02-03 05:14:31 +08:00
zzc
7d0b79bd16 fix: metadata 2026-02-03 05:04:10 +08:00
zzc
13d1ea604e fix: update page 2026-02-03 02:56:17 +08:00
zzc
bd45082544 fix: update page 2026-02-03 02:41:19 +08:00
zzc
52af2aad8f feat: PrivacyPopup 2026-02-02 16:49:53 +08:00
22 changed files with 1161 additions and 631 deletions

View File

@@ -22,3 +22,10 @@ export const updateUserInfo = async (body) => {
data: body,
});
};
export const reportPrivacy = async () => {
// return request({
// url: "/api/common/privacy/report",
// method: "POST",
// });
};

View File

@@ -52,3 +52,10 @@ export const getRecommendList = async (page = 1) => {
method: "get",
});
};
export const msgCheckApi = async (content) => {
return request({
url: "/api/common/msg-check?content=" + content,
method: "get",
});
};

View File

@@ -1,32 +1,37 @@
<template>
<uni-popup ref="popupRef" type="bottom" :safe-area="false">
<view class="popup-container">
<view class="popup-header">
<text class="popup-title">登录授权</text>
</view>
<view>
<uni-popup ref="popupRef" type="bottom" :safe-area="false">
<view class="popup-container">
<view class="popup-header">
<text class="popup-title">登录授权</text>
</view>
<view class="avatar-nickname">
<button
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
class="avatar-selector custom-button"
>
<image v-if="avatarUrl" :src="avatarUrl" class="avatar-preview" />
<text v-else>点击获取头像</text>
<view class="avatar-nickname">
<button
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
class="avatar-selector custom-button"
>
<image v-if="avatarUrl" :src="avatarUrl" class="avatar-preview" />
<text v-else>点击获取头像</text>
</button>
<input
class="nickname-input"
type="nickname"
v-model="nickname"
placeholder="请输入昵称"
/>
</view>
<button class="confirm-btn custom-button" @tap="confirmLogin">
确认登录
</button>
<input
class="nickname-input"
type="nickname"
v-model="nickname"
placeholder="请输入昵称"
/>
</view>
</uni-popup>
<button class="confirm-btn custom-button" @tap="confirmLogin">
确认登录
</button>
</view>
</uni-popup>
<!-- 隐私协议弹窗 -->
<PrivacyPopup ref="privacyRef" @agree="onPrivacyAgree" />
</view>
</template>
<script setup>
@@ -36,8 +41,10 @@ import { getPlatformProvider } from "@/utils/system";
import { uploadImage } from "@/utils/common";
import { apiLogin } from "@/api/auth.js";
import { wxLogin } from "@/utils/login.js";
import PrivacyPopup from "@/components/PrivacyPopup/PrivacyPopup.vue";
const popupRef = ref(null);
const privacyRef = ref(null);
const avatarUrl = ref("");
const nickname = ref("");
@@ -46,19 +53,65 @@ const userStore = useUserStore();
const emit = defineEmits(["logind"]);
const festivalNames = [
'春意','福星','小福','新禧','瑞雪','花灯','喜乐','元宝','春芽','年年',
'花灯','月圆','灯影','小灯','星灯','彩灯',
'清风','微风','小晴','碧波','流泉',
'月光','玉轮','桂香','秋叶','星河','小月','露华','秋水',
'雪落','冰晶','暖阳','小雪','冬影','雪花','松影'
"春意",
"福星",
"小福",
"新禧",
"瑞雪",
"花灯",
"喜乐",
"元宝",
"春芽",
"年年",
"花灯",
"月圆",
"灯影",
"小灯",
"星灯",
"彩灯",
"清风",
"微风",
"小晴",
"碧波",
"流泉",
"月光",
"玉轮",
"桂香",
"秋叶",
"星河",
"小月",
"露华",
"秋水",
"雪落",
"冰晶",
"暖阳",
"小雪",
"冬影",
"雪花",
"松影",
];
const getFestivalName = () => {
const idx = Math.floor(Math.random() * festivalNames.length);
return festivalNames[idx];
}
};
const open = () => {
const open = async () => {
console.log(22223333);
// #ifdef MP-WEIXIN
const isAgreed = await privacyRef.value.check();
console.log(1111, isAgreed);
if (isAgreed) {
popupRef.value.open();
}
// #endif
// #ifndef MP-WEIXIN
popupRef.value.open();
// #endif
};
const onPrivacyAgree = () => {
popupRef.value.open();
};
@@ -75,7 +128,9 @@ const confirmLogin = async () => {
const platform = getPlatformProvider();
if (platform === "mp-weixin") {
const code = await wxLogin();
const imageUrl = avatarUrl.value ? await uploadImage(avatarUrl.value) : "";
const imageUrl = avatarUrl.value
? await uploadImage(avatarUrl.value)
: "";
const loginRes = await apiLogin({
code,

View File

@@ -0,0 +1,205 @@
<template>
<view v-if="show" class="privacy-popup">
<view class="mask"></view>
<view class="content">
<view class="title">用户隐私保护指引</view>
<view class="desc">
感谢您使用本小程序在使用前请您仔细阅读
<text class="link" @click="openPrivacyContract">{{
privacyContractName
}}</text>
当您点击同意并开始使用产品服务时即表示您已理解并同意该条款内容该条款将对您产生法律约束力如您拒绝将无法进入小程序
</view>
<view class="btns">
<button class="btn refuse" @click="handleDisagree">拒绝</button>
<button
id="agree-btn"
class="btn agree"
open-type="agreePrivacyAuthorization"
@agreeprivacyauthorization="handleAgree"
>
同意
</button>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { reportPrivacy } from "@/api/auth.js";
const show = ref(false);
const privacyContractName = ref("《用户隐私保护指引》");
const emit = defineEmits(["agree"]);
let resolveCheck = null;
// #ifdef MP-WEIXIN
const check = () => {
return new Promise((resolve) => {
if (uni.getPrivacySetting) {
uni.getPrivacySetting({
success: (res) => {
if (res.needAuthorization) {
privacyContractName.value =
res.privacyContractName || "《用户隐私保护指引》";
show.value = true;
resolveCheck = resolve;
} else {
resolve(true);
}
},
fail: (err) => {
console.error("getPrivacySetting fail:", err);
resolve(true);
},
});
} else {
resolve(true);
}
});
};
// #endif
// Only for WeChat Mini Program
// #ifdef MP-WEIXIN
// onMounted(() => {
// check();
// });
// #endif
const openPrivacyContract = () => {
uni.openPrivacyContract({
success: () => {},
fail: (err) => {
console.error("openPrivacyContract fail", err);
},
});
};
const handleAgree = async (e) => {
if (!show.value) return; // Prevent double execution
console.log("handleAgree triggered", e);
// 1. Save to local storage
uni.setStorageSync("hasAgreedPrivacy", true);
// 2. Hide popup
show.value = false;
emit("agree");
// 3. Resolve the check promise
if (resolveCheck) {
resolveCheck(true);
resolveCheck = null;
}
// 4. Call server API
try {
await reportPrivacy();
} catch (e) {
console.error("Report privacy failed", e);
}
};
const handleDisagree = () => {
// Exit mini program
// #ifdef MP-WEIXIN
uni.exitMiniProgram({
success: () => {
console.log("Exit success");
},
});
// #endif
};
defineExpose({
check,
});
</script>
<style lang="scss" scoped>
.privacy-popup {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 9999;
display: flex;
align-items: center;
justify-content: center;
.mask {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.6);
}
.content {
position: relative;
width: 600rpx;
background: #fff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
display: flex;
flex-direction: column;
align-items: center;
}
.title {
font-size: 36rpx;
font-weight: bold;
color: #333;
margin-bottom: 32rpx;
}
.desc {
font-size: 28rpx;
color: #666;
line-height: 1.6;
margin-bottom: 48rpx;
text-align: justify;
.link {
color: #576b95;
display: inline;
}
}
.btns {
display: flex;
justify-content: space-between;
width: 100%;
.btn {
width: 240rpx;
height: 88rpx;
line-height: 88rpx;
text-align: center;
border-radius: 12rpx;
font-size: 32rpx;
font-weight: 500;
margin: 0;
&::after {
border: none;
}
&.refuse {
background: #f2f2f2;
color: #666;
}
&.agree {
background: #07c160;
color: #fff;
}
}
}
}
</style>

View File

@@ -51,6 +51,7 @@
/* */
"mp-weixin": {
"appid": "wx3fcc07a061af049a",
"__usePrivacyCheck__": true,
"setting": {
"urlCheck": false
},

View File

@@ -48,9 +48,15 @@
</view>
<scroll-view scroll-x class="avatar-scroll" show-scrollbar="false">
<view class="avatar-list">
<view class="avatar-card upload-card" @tap="useWeChatAvatar">
<view class="upload-icon">📷</view>
<text class="upload-text">微信头像</text>
<view class="avatar-card upload-card">
<button
class="wechat-avatar-btn"
open-type="chooseAvatar"
@chooseavatar="onChooseAvatar"
>
<view class="upload-icon">📷</view>
<text class="upload-text">微信头像</text>
</button>
</view>
<view
v-for="(item, i) in systemAvatars"
@@ -465,6 +471,26 @@ const handleLogind = async () => {
// Logic after successful login if needed
};
const onChooseAvatar = async (e) => {
const avatarUrl = e.detail.avatarUrl;
if (!avatarUrl) return;
uni.showLoading({ title: "上传中...", mask: true });
try {
const imageUrl = await uploadImage(avatarUrl);
currentAvatar.value = {
id: "wechat_" + Date.now(),
imageUrl: imageUrl,
};
uni.hideLoading();
} catch (e) {
uni.hideLoading();
uni.showToast({ title: e || "上传失败", icon: "none" });
console.error("Upload avatar error", e);
}
};
const useWeChatAvatar = () => {
if (!isLoggedIn.value) {
loginPopupRef.value.open();
@@ -832,28 +858,49 @@ const loadImage = (url) => {
position: relative;
background: #fff;
box-shadow: 0 6rpx 16rpx rgba(0, 0, 0, 0.06);
}
.avatar-card.active {
outline: 4rpx solid #ff3b30;
}
.upload-card {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
background: #fff5f5;
border: 2rpx dashed #ffccc7;
box-sizing: border-box;
}
.upload-icon {
font-size: 48rpx;
margin-bottom: 8rpx;
}
.upload-text {
font-size: 22rpx;
color: #ff3b30;
font-weight: 500;
&.active {
outline: 4rpx solid #ff3b30;
}
&.upload-card {
background: #fff5f5;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
border: 2rpx dashed #ffccc7;
box-sizing: border-box;
.wechat-avatar-btn {
width: 100%;
height: 100%;
padding: 0;
margin: 0;
background: transparent;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
line-height: normal;
border: none;
&::after {
border: none;
}
}
.upload-icon {
font-size: 48rpx;
margin-bottom: 8rpx;
}
.upload-text {
font-size: 22rpx;
color: #ff3b30;
font-weight: 500;
}
}
}
.avatar-thumb {

View File

@@ -1,6 +1,6 @@
<template>
<view class="detail-page" >
<NavBar title="祝福贺卡" />
<view class="detail-page">
<NavBar title="祝福贺卡" />
<scroll-view scroll-y class="content-scroll">
<view class="content-wrap">
@@ -46,7 +46,7 @@
</view>
<!-- Recommendations -->
<view class="recommend-section">
<!-- <view class="recommend-section">
<view class="section-header">
<text class="section-title">大家都在玩的头像挂饰</text>
<text class="more-link">查看更多</text>
@@ -65,24 +65,38 @@
</view>
</view>
</scroll-view>
</view>
</view> -->
<!-- Banner -->
<view class="banner-card">
<view class="wallpaper-banner" @tap="goToFortune">
<view class="banner-icon">
<image
src="/static/logo.png"
mode="aspectFit"
style="width: 100%; height: 100%"
v-if="false"
/>
<view class="placeholder-icon"></view>
<text>🏮</text>
</view>
<view class="banner-content">
<view class="banner-title">领取我的马年头像框</view>
<view class="banner-desc">定制专属新春社交形象</view>
<text class="banner-title">去抽取新年运势</text>
<text class="banner-desc">每日一签开启你的新年好运</text>
</view>
<button class="banner-btn">去领取</button>
<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>
</view>
<text class="banner-arrow"></text>
</view>
<!-- Footer -->
@@ -170,6 +184,29 @@ const decorList = ref([
img: "https://file.lihailezzc.com/resource/1463f294244c11cf274a5eaae115872a.jpeg",
},
]);
const goToMake = () => {
uni.navigateTo({
url: "/pages/avatar/index",
});
};
const goToFortune = () => {
uni.navigateTo({
url: "/pages/fortune/index",
});
};
const goToGreeting = () => {
uni.navigateTo({
url: "/pages/avatar/index",
});
};
const goToWallpaper = () => {
uni.navigateTo({
url: "/pages/wallpaper/index",
});
};
</script>
<style lang="scss" scoped>
@@ -229,6 +266,53 @@ const decorList = ref([
margin-right: 6rpx;
}
.wallpaper-banner {
background: #f8f8f8;
border-radius: 24rpx;
padding: 30rpx;
display: flex;
align-items: center;
margin-bottom: 60rpx;
}
.banner-icon {
width: 80rpx;
height: 80rpx;
background: #fff;
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;
}
/* Card Container */
.card-container {
background: #fff;

View File

@@ -1,6 +1,6 @@
<template>
<view class="fortune-detail-page">
<NavBar title="2026 灵马贺岁" :transparent="true"/>
<NavBar title="" :transparent="true" color="#ffd700" />
<!-- 顶部提示条 -->
<!-- <view class="top-banner" v-if="inviterName">
@@ -44,7 +44,7 @@
}}</view>
<view class="content-desc">
{{
fortuneData.desc ||
fortuneData.content ||
"灵马奔腾瑞气盈门。此签预示您在2026年如同千里骏马不仅拥有敏锐的洞察力更有贵人暗中相助。事业将如破竹之势学业更有意外惊喜心之所向皆能圆满。"
}}
</view>

View File

@@ -1,6 +1,6 @@
<template>
<view class="fortune-page" >
<NavBar title="2026 新年运势" :transparent="true" color="#ffd700"/>
<view class="fortune-page">
<NavBar title="2026 新年运势" :transparent="true" color="#ffd700" />
<!-- 初始状态签筒 -->
<view class="state-initial" v-if="status !== 'result'">
@@ -166,9 +166,10 @@ onShareAppMessage(async () => {
});
getRewardByShare();
return {
title: "新春祝福",
title: "马年运势我已经抽过了,你的会是什么?",
path: `${cardId.value ? `/pages/fortune/detail?shareToken=${shareTokenRes.shareToken}` : `/pages/fortune/index?shareToken=${shareTokenRes.shareToken}`}`,
imageUrl: "/static/images/bg.jpg",
imageUrl:
"https://file.lihailezzc.com/resource/cfed2edbfa19250b836a87a4bbf0d5ad.png",
};
});
@@ -592,7 +593,7 @@ const saveCard = () => {
/* 结果状态 */
.state-result {
width: 100%;
padding: 20px 30px;
padding: 130px 30px 20px;
animation: fadeIn 0.8s ease-out;
position: relative;
z-index: 1;

View File

@@ -1,6 +1,6 @@
<template>
<view class="record-page">
<NavBar title="我的运势记录" />
<NavBar title="我的运势记录" />
<scroll-view
scroll-y
@@ -18,7 +18,7 @@
<view class="stats-body">
<view class="stats-row">
<text class="stats-label">已收集</text>
<text class="stats-num">{{ records.length }}</text>
<text class="stats-num">{{ totalCount }}</text>
<text class="stats-label">张好运卡</text>
</view>
<view class="progress-bar">
@@ -46,22 +46,20 @@
>
<view class="item-image-box">
<image
:src="item.imageUrl"
:src="getThumbUrl(item.imageUrl)"
mode="aspectFill"
class="item-image"
/>
<view class="item-tag" :class="getTagClass(item.tag)">
{{ item.tag || "大吉" }}
<view class="item-tag" :class="getTagClass(item.fortuneLevel)">
{{ getFortuneName(item.fortuneLevel) }}
</view>
</view>
<view class="item-info">
<text class="item-title">{{ item.title }}</text>
<view class="item-footer">
<text class="item-date">{{ item.date }}</text>
<view class="item-link">
<text>详情</text>
<text class="arrow"></text>
</view>
<text class="item-date">{{
formatDate(item.day, "YYYY-MM-DD")
}}</text>
</view>
</view>
</view>
@@ -90,11 +88,11 @@ import { ref, computed } from "vue";
import { onLoad } from "@dcloudio/uni-app";
import { getList } from "@/api/fortune.js";
import NavBar from "@/components/NavBar/NavBar.vue";
import { formatDate } from "@/utils/date.js";
// 状态管理
const records = ref([]);
const page = ref(1);
const curCount = ref(0);
const totalCount = ref(0);
const loading = ref(false);
const hasMore = ref(true);
@@ -105,14 +103,30 @@ const progressWidth = computed(() => {
return `${percentage}%`;
});
const getTagClass = (tag) => {
const getFortuneName = (level) => {
const map = {
大吉: "tag-gold",
安康: "tag-green",
顺遂: "tag-red",
上吉: "tag-orange",
1: "吉签",
2: "中吉签",
3: "上吉签",
4: "上上签",
5: "大吉签",
};
return map[tag] || "tag-gold";
return map[level] || "吉签";
};
const getTagClass = (level) => {
const map = {
1: "tag-blue",
2: "tag-green",
3: "tag-orange",
4: "tag-red",
5: "tag-gold",
};
return map[level] || "tag-blue";
};
const getThumbUrl = (url) => {
return `${url}?imageView2/1/w/360/h/480/q/80`;
};
const loadData = async () => {
@@ -129,7 +143,7 @@ const loadData = async () => {
page.value++;
// 简单判断是否还有更多数据
hasMore.value = res.hasNext;
curCount.value = res.totalCount || 0;
totalCount.value = res.totalCount || 0;
} else {
hasMore.value = false;
}
@@ -145,19 +159,13 @@ const loadData = async () => {
};
const loadMore = () => {
console.log(666666666);
loadData();
};
const goBack = () => {
uni.navigateBack();
};
const goDetail = (item) => {
// 传递数据到详情页
const data = encodeURIComponent(JSON.stringify(item));
uni.navigateTo({
url: `/pages/fortune/detail?data=${data}`,
uni.previewImage({
current: item.imageUrl,
urls: records.value.map((r) => r.imageUrl),
});
};
@@ -307,17 +315,22 @@ onLoad(() => {
color: #fff;
backdrop-filter: blur(4px);
}
.tag-gold {
background: rgba(212, 175, 55, 0.9);
.tag-blue {
background: rgba(0, 122, 255, 0.9);
}
.tag-green {
background: rgba(46, 139, 87, 0.9);
}
.tag-red {
background: rgba(178, 34, 34, 0.9);
background: rgba(52, 199, 89, 0.9);
}
.tag-orange {
background: rgba(255, 140, 0, 0.9);
background: rgba(255, 149, 0, 0.9);
}
.tag-red {
background: rgba(255, 59, 48, 0.9);
}
.tag-gold {
background: rgba(212, 175, 55, 0.9);
color: #fff;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.item-info {

View File

@@ -358,7 +358,8 @@ onShareAppMessage(() => {
return {
title: "新春祝福",
path: "/pages/detail/index",
imageUrl: "/static/images/bg.jpg",
imageUrl:
"https://file.lihailezzc.com/resource/cfed2edbfa19250b836a87a4bbf0d5ad.png",
success: function (res) {
uni.showToast({ title: "分享成功", icon: "success" });
},

View File

@@ -13,7 +13,9 @@
<view class="step-num-wrap">
<view class="step-line" v-if="idx > 0"></view>
<view class="step-num">
<text v-if="activeTool === tool.type && showPanel">{{ tool.icon }}</text>
<text v-if="activeTool === tool.type && showPanel">{{
tool.icon
}}</text>
<text v-else>{{ tool.step }}</text>
</view>
</view>
@@ -62,12 +64,17 @@
fontSize: fontSize + 'rpx',
lineHeight: fontSize * 1.5 + 'rpx',
}"
>{{ targetName + "\n " + blessingText.content }}</text
>{{
(targetName || "") + "\n " + (blessingText.content || "")
}}</text
>
</view>
<view
class="user"
:style="{ left: 160 + userOffsetX + 'rpx', bottom: 40 - userOffsetY + 'rpx' }"
:style="{
left: 160 + userOffsetX + 'rpx',
bottom: 40 - userOffsetY + 'rpx',
}"
@touchstart.stop="handleUserTouchStart"
@touchmove.stop="handleUserTouchMove"
@touchend.stop="handleUserTouchEnd"
@@ -113,31 +120,45 @@
<!-- 弹出编辑面板 -->
<view class="panel-container" :class="{ show: showPanel }">
<view class="panel-mask" @tap="closePanel"></view>
<view class="panel-content" :class="{ 'glass-effect': activeTool === 'text' || activeTool === 'position' }">
<view
class="panel-content"
:class="{
'glass-effect': activeTool === 'text' || activeTool === 'position',
}"
>
<view class="panel-handle" @tap="closePanel"></view>
<!-- 标题选择区 -->
<view v-if="activeTool === 'title'" class="section">
<view class="section-title">
<text>选择标题</text>
</view>
<view class="tpl-scroll">
<view class="tpl-grid">
<view
v-for="(title, i) in titles"
:key="i"
class="tpl-card title-card"
:class="{ selected: title?.id === currentTitle?.id }"
@tap="selectTitle(title)"
>
<image :src="title.imageUrl" class="title-cover" mode="aspectFit" />
<view v-if="title?.id === currentTitle?.id" class="tpl-check"></view>
</view>
<!-- 标题选择区 -->
<view v-if="activeTool === 'title'" class="section">
<view class="section-title">
<text>选择标题</text>
</view>
<view class="tpl-scroll">
<view class="tpl-grid">
<view
v-for="(title, i) in titles"
:key="i"
class="tpl-card title-card"
:class="{ selected: title?.id === currentTitle?.id }"
@tap="selectTitle(title)"
>
<image
:src="title.imageUrl"
class="title-cover"
mode="aspectFit"
/>
<view v-if="title?.id === currentTitle?.id" class="tpl-check"
></view
>
</view>
</view>
<view v-if="loadingTitles" class="loading-more">加载中...</view>
<view
v-else-if="!hasMoreTitles && titles.length > 0"
class="no-more"
>没有更多了</view
>
</view>
<view v-if="loadingTitles" class="loading-more">加载中...</view>
<view v-else-if="!hasMoreTitles && titles.length > 0" class="no-more">没有更多了</view>
</view>
</view>
<!-- 模板区 -->
@@ -154,7 +175,11 @@
:class="{ selected: tpl?.id === currentTemplate?.id }"
@tap="applyTemplate(tpl)"
>
<image :src="tpl.imageUrl" class="tpl-cover" mode="aspectFill" />
<image
:src="tpl.imageUrl"
class="tpl-cover"
mode="aspectFill"
/>
<view class="tpl-name">{{ tpl.name }}</view>
<view v-if="tpl?.id === currentTemplate?.id" class="tpl-check"
></view
@@ -168,12 +193,10 @@
>没有更多了</view
>
</view>
</view>
<!-- 文字编辑 -->
<view v-if="activeTool === 'text'" class="section text-edit-section">
<!-- 祝贺对象 -->
<view class="form-item">
<text class="label">祝福对象</text>
<input
@@ -182,6 +205,7 @@
placeholder="请输入称呼"
placeholder-style="color:#ccc"
maxlength="10"
@blur="handleTargetNameBlur"
/>
</view>
@@ -193,7 +217,11 @@
<text class="refresh-icon"></text> 换一批
</view>
</view>
<scroll-view scroll-x class="greeting-scroll" show-scrollbar="false">
<scroll-view
scroll-x
class="greeting-scroll"
show-scrollbar="false"
>
<view class="greeting-list">
<view
v-for="(text, index) in displayedGreetings"
@@ -222,7 +250,8 @@
v-model="signatureName"
placeholder="请输入署名"
placeholder-style="color:#ccc"
maxlength="5"
maxlength="10"
@blur="handleSignatureBlur"
/>
<text class="edit-icon"></text>
</view>
@@ -272,7 +301,9 @@
:style="{ background: color }"
@tap="selectedColor = color"
>
<view v-if="selectedColor === color" class="color-check"></view>
<view v-if="selectedColor === color" class="color-check"
></view
>
</view>
</view>
</view>
@@ -284,7 +315,7 @@
<text>调整位置</text>
</view>
<view class="form-item" style="margin-top: 20rpx;">
<view class="form-item" style="margin-top: 20rpx">
<text class="label">祝福语气泡 (上下)</text>
<slider
:value="bubbleOffsetY"
@@ -342,7 +373,9 @@
:style="{ background: color }"
@tap="signatureColor = color"
>
<view v-if="signatureColor === color" class="color-check"></view>
<view v-if="signatureColor === color" class="color-check"
></view
>
</view>
</view>
</view>
@@ -364,7 +397,7 @@
<script setup>
import { ref, computed } from "vue";
import { getBavBarHeight, getDeviceInfo } from "@/utils/system";
import { generateObjectId } from "@/utils/common";
import { generateObjectId, getShareToken } from "@/utils/common";
import {
createCardTmp,
@@ -373,7 +406,12 @@ import {
getCardTemplateContentList,
getCardTemplateTitleList,
} from "@/api/make";
import { createShareToken, abilityCheck, getShareReward } from "@/api/system";
import {
createShareToken,
abilityCheck,
getShareReward,
msgCheckApi,
} from "@/api/system";
import {
onShareAppMessage,
onLoad,
@@ -395,7 +433,7 @@ const cardId = ref("");
// 标题相关
const titles = ref([]);
const currentTitle = ref(null);
const currentTitle = ref(titles.value[0]);
const titlePage = ref(1);
const loadingTitles = ref(false);
const hasMoreTitles = ref(true);
@@ -408,9 +446,9 @@ const titleState = ref({
const titleStyle = computed(() => {
return {
transform: `translate(${titleState.value.offsetX}rpx, ${titleState.value.offsetY}rpx) scale(${titleState.value.scale})`,
top: '40rpx',
pointerEvents: 'auto',
transition: 'none'
top: "40rpx",
pointerEvents: "auto",
transition: "none",
};
});
@@ -514,7 +552,38 @@ const handleTitleTouchMove = (e) => {
};
const targetName = ref("祝您");
const oldTargetName = ref("祝您");
const signatureName = ref(userStore?.userInfo?.nickName || "xxx");
const oldSignatureName = ref(userStore?.userInfo?.nickName || "xxx");
const handleTargetNameBlur = async () => {
if (!targetName.value || targetName.value === oldTargetName.value) return;
const res = await msgCheckApi(targetName.value);
if (!res.success) {
uni.showToast({
title: res.message || "消息不符合发布规范,请稍作修改后再试",
icon: "none",
});
targetName.value = oldTargetName.value;
} else {
oldTargetName.value = targetName.value;
}
};
const handleSignatureBlur = async () => {
if (!signatureName.value || signatureName.value === oldSignatureName.value)
return;
const res = await msgCheckApi(signatureName.value);
if (!res.success) {
uni.showToast({
title: res.message || "消息不符合发布规范,请稍作修改后再试",
icon: "none",
});
signatureName.value = oldSignatureName.value;
} else {
oldSignatureName.value = signatureName.value;
}
};
const userAvatar = ref(
userStore?.userInfo?.avatarUrl ||
"https://file.lihailezzc.com/resource/96023631c6ab9c3496b7620097af3d6f.png",
@@ -560,7 +629,7 @@ const fontList = [
name: "中圆",
family: "ZhongYuan",
url: "https://file.lihailezzc.com/ddcd9621740449a29c329f573bc1d0c5.woff2", // 示例地址
}
},
];
const selectedFont = ref(fontList[0]);
const loadedFonts = ref(new Set()); // 记录已加载的字体
@@ -756,35 +825,46 @@ const loadMoreTemplates = () => {
getTemplateList(true);
};
onShareAppMessage(async () => {
onShareAppMessage(async (options) => {
getShareReward({ scene: "card_generate" });
if (!isLoggedIn.value) {
return {
title: "新春祝福",
path: "/pages/index/index",
};
}
// 1. 确保有 cardId (如果内容有变动,最好是新建)
const id = createCard();
if (!id) {
return {
title: "新春祝福",
path: "/pages/index/index",
};
}
if (options.from === "button") {
if (!isLoggedIn.value) {
return {
title: "新春祝福",
path: "/pages/index/index",
};
}
// 1. 确保有 cardId (如果内容有变动,最好是新建)
const id = createCard();
if (!id) {
return {
title: "新春祝福",
path: "/pages/index/index",
};
}
const deviceInfo = getDeviceInfo();
const shareTokenRes = await createShareToken({
scene: "card_generate",
targetId: id,
...deviceInfo,
});
shareOrSave(id);
return {
title: "我刚做了一张祝福卡片,送给你",
path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken,
imageUrl: "/static/images/share.jpg",
};
const deviceInfo = getDeviceInfo();
const shareTokenRes = await createShareToken({
scene: "card_generate",
targetId: id,
...deviceInfo,
});
shareOrSave(id);
return {
title: "我刚做了一张祝福卡片,送给你",
path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken,
imageUrl:
"https://file.lihailezzc.com/resource/13ec1134e6614feadeeaaa9ef21ea96e.png",
};
} else {
const shareTokenRes = await getShareToken("card_generate_index", "");
return {
title: "新春祝福",
path: `/pages/index/index?shareToken=${shareTokenRes.shareToken}`,
imageUrl:
"https://file.lihailezzc.com/resource/cfed2edbfa19250b836a87a4bbf0d5ad.png",
};
}
});
const displayedGreetings = ref([]);
@@ -838,6 +918,7 @@ const selectTitle = (title) => {
currentTitle.value = null;
} else {
currentTitle.value = title;
// 切换标题时重置位置和缩放
titleState.value = {
offsetX: 0,
@@ -912,6 +993,7 @@ const shareOrSave = async (id) => {
blessingTo: targetName.value,
blessingFrom: signatureName.value,
templateId: currentTemplate.value?.id || "",
titleId: currentTitle?.value?.id || "",
});
};
@@ -957,69 +1039,71 @@ const saveByCanvas = async (save = true) => {
});
};
// 辅助函数rpx 转 px (基于预览容器宽度 506rpx 对应 Canvas 540px)
const r2p = (rpx) => (rpx * 540) / 506;
// 辅助函数rpx 转 px (基于预览容器宽度 506rpx 对应 Canvas 540px)
const r2p = (rpx) => (rpx * 540) / 506;
try {
// 1⃣ 画背景
// ⭐ 先加载背景图
const [bgImg, avatarImg, titleImg] = await Promise.all([
loadCanvasImage(currentTemplate?.value?.imageUrl),
loadCanvasImage(userAvatar.value),
currentTitle.value ? loadCanvasImage(currentTitle.value.imageUrl) : Promise.resolve(null),
]);
try {
// 1⃣ 画背景
// ⭐ 先加载背景图
const [bgImg, avatarImg, titleImg] = await Promise.all([
loadCanvasImage(currentTemplate?.value?.imageUrl),
loadCanvasImage(userAvatar.value),
currentTitle.value
? loadCanvasImage(currentTitle.value.imageUrl)
: Promise.resolve(null),
]);
ctx.drawImage(bgImg, 0, 0, W, H);
ctx.drawImage(bgImg, 0, 0, W, H);
// 2⃣ 半透明遮罩
ctx.fillStyle = "rgba(0,0,0,0.08)";
ctx.fillRect(0, 0, W, H);
// 2⃣ 半透明遮罩
ctx.fillStyle = "rgba(0,0,0,0.08)";
ctx.fillRect(0, 0, W, H);
// 3⃣ 标题图片
if (titleImg) {
const previewBaseWidth = 400; // rpx
const drawWidth = r2p(previewBaseWidth) * titleState.value.scale;
const drawHeight = (titleImg.height / titleImg.width) * drawWidth;
// 计算绘制起点:居中 + 偏移量
const titleX = (W - drawWidth) / 2 + r2p(titleState.value.offsetX);
const titleY = r2p(40) + r2p(titleState.value.offsetY);
ctx.drawImage(titleImg, titleX, titleY, drawWidth, drawHeight);
}
// 3⃣ 标题图片
if (titleImg) {
const previewBaseWidth = 400; // rpx
const drawWidth = r2p(previewBaseWidth) * titleState.value.scale;
const drawHeight = (titleImg.height / titleImg.width) * drawWidth;
// 4⃣ 祝福语气泡
// 预览中 .bubble 有 padding: 40rpx且 .card-overlay 有 padding: 30rpx
// 意味着文字距离容器边缘至少有 70rpx
drawBubbleText(ctx, {
text: targetName.value + "\n " + blessingText.value.content,
x: 0,
y: r2p(230 + bubbleOffsetY.value),
maxWidth: r2p(bubbleMaxWidth.value), // 预览中 bubble-text 的宽度
canvasWidth: W,
fontSize: r2p(fontSize.value),
lineHeight: r2p(fontSize.value * 1.6), // 预览中是 1.6
padding: r2p(40 + 30), // 内部 padding 40 + 容器 padding 30
backgroundColor: "transparent",
textColor: selectedColor.value,
fontFamily: selectedFont.value.family,
});
// 计算绘制起点:居中 + 偏移量
const titleX = (W - drawWidth) / 2 + r2p(titleState.value.offsetX);
const titleY = r2p(40) + r2p(titleState.value.offsetY);
// 5⃣ 用户信息
// 预览中 user 是 absolute, left: 160 + offsetX, bottom: 40 - offsetY
drawUserBubble(ctx, {
x: r2p(160 + userOffsetX.value),
bottom: r2p(40 - userOffsetY.value),
canvasHeight: H,
avatarImg: avatarImg,
username: signatureName.value,
desc: "送上祝福",
textColor: signatureColor.value,
avatarSize: r2p(64),
padding: r2p(15),
fontSizeName: r2p(24),
fontSizeDesc: r2p(20),
});
ctx.drawImage(titleImg, titleX, titleY, drawWidth, drawHeight);
}
// 4⃣ 祝福语气泡
// 预览中 .bubble 有 padding: 40rpx且 .card-overlay 有 padding: 30rpx
// 意味着文字距离容器边缘至少有 70rpx
drawBubbleText(ctx, {
text: targetName.value + "\n " + blessingText.value.content,
x: 0,
y: r2p(230 + bubbleOffsetY.value),
maxWidth: r2p(bubbleMaxWidth.value), // 预览中 bubble-text 的宽度
canvasWidth: W,
fontSize: r2p(fontSize.value),
lineHeight: r2p(fontSize.value * 1.6), // 预览中是 1.6
padding: r2p(40 + 30), // 内部 padding 40 + 容器 padding 30
backgroundColor: "transparent",
textColor: selectedColor.value,
fontFamily: selectedFont.value.family,
});
// 5⃣ 用户信息
// 预览中 user 是 absolute, left: 160 + offsetX, bottom: 40 - offsetY
drawUserBubble(ctx, {
x: r2p(160 + userOffsetX.value),
bottom: r2p(40 - userOffsetY.value),
canvasHeight: H,
avatarImg: avatarImg,
username: signatureName.value,
desc: "送上祝福",
textColor: signatureColor.value,
avatarSize: r2p(64),
padding: r2p(15),
fontSizeName: r2p(24),
fontSizeDesc: r2p(20),
});
// 6⃣ 输出
uni.canvasToTempFilePath({
@@ -1209,11 +1293,11 @@ function drawUserBubble(ctx, options) {
const textX = avatarX + avatarSize + padding;
const totalTextHeight = fontSizeName + fontSizeDesc + 4;
const textStartY = drawY + (bubbleHeight - totalTextHeight) / 2;
ctx.fillStyle = textColor;
ctx.font = `${fontSizeName}px 'PingFang SC'`;
ctx.fillText(username, textX, textStartY);
ctx.font = `${fontSizeDesc}px 'PingFang SC'`;
ctx.globalAlpha = 0.6;
ctx.fillText(desc, textX, textStartY + fontSizeName + 4);
@@ -1670,7 +1754,7 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
overflow: hidden;
}
.position-section{
.position-section {
margin-bottom: 40rpx;
}
.greeting-card.active .greeting-text {
@@ -1783,8 +1867,9 @@ function drawRoundRect(ctx, x, y, w, h, r, color) {
.btn.secondary {
background: linear-gradient(135deg, #ffffff 0%, #f8f9fa 100%);
color: #333;
box-shadow: 0 8rpx 20rpx rgba(0, 0, 0, 0.05),
inset 0 0 0 2rpx #eee;
box-shadow:
0 8rpx 20rpx rgba(0, 0, 0, 0.05),
inset 0 0 0 2rpx #eee;
}
.btn.primary {

View File

@@ -1,5 +1,5 @@
<template>
<view class="avatar-page" >
<view class="avatar-page">
<!-- Navbar -->
<NavBar title="我的头像制作" />
@@ -57,9 +57,9 @@
</view>
</view>
<view class="item-info">
<text class="item-name">{{
<!-- <text class="item-name">{{
item.decorName || getDefaultName(item)
}}</text>
}}</text> -->
<text class="item-date">{{ formatDate(item.createdAt) }} 制作</text>
</view>
</view>

View File

@@ -1,13 +1,13 @@
<template>
<view class="greeting-page" >
<NavBar title="我的祝福" />
<view class="greeting-page">
<NavBar title="我的新春祝福" background="transparent" />
<!-- Header Stats -->
<view class="header-stats">
<view class="stats-card">
<view class="stats-left">
<view class="icon-circle">
<text></text>
<text class="sparkle-emoji"></text>
</view>
<view class="stats-info">
<text class="label">已累计创作</text>
@@ -28,69 +28,66 @@
<!-- List Section -->
<view class="list-section">
<view class="section-header">
<text class="section-title">祝福列表</text>
<text class="section-tip">左滑删除记录</text>
<text class="section-title">祝福</text>
</view>
<view class="list-container">
<view v-for="item in list" :key="item.id" class="list-item-wrap">
<view class="swipe-container">
<!-- Delete Action (Behind) -->
<view class="delete-action" @tap.stop="onDelete(item)">
<text>删除</text>
<view class="list-grid">
<view
v-for="item in list"
:key="item.id"
class="card-item"
@tap="onPreview(item)"
>
<view class="card-image-wrap">
<image :src="item.imageUrl" mode="aspectFill" class="card-img" />
<view class="year-badge" v-if="item.year">{{ item.year }}</view>
<view class="draft-overlay" v-if="item.status === 'draft'">
<text class="lock-icon">🔒</text>
</view>
<!-- Card Content (Front) -->
<view
class="card-item"
:style="{
transform: `translateX(${item.translateX || 0}px)`,
transition: item.useTransition ? 'transform 0.3s' : 'none',
}"
@touchstart="onTouchStart($event, item)"
@touchmove="onTouchMove($event, item)"
@touchend="onTouchEnd($event, item)"
>
<image :src="item.imageUrl" mode="aspectFill" class="card-img" />
<view class="card-content">
<view class="card-title"
>{{ item.blessingTo
}}{{
item.blessingFrom ? item.blessingFrom : "好友"
}}身体健康</view
</view>
<view class="card-info">
<view class="card-title">{{ getTitle(item) }}</view>
<view class="card-date">{{ formatDate(item.updatedAt) }}</view>
<view class="card-footer">
<view class="tag" :class="getTagClass(item)">{{
getTagText(item)
}}</view>
<view class="actions">
<button
class="action-btn"
open-type="share"
:data-item="item"
@tap.stop
>
<view class="card-date"
>更新时间{{ formatDate(item.updatedAt) }}</view
>
<view class="tags">
<view class="tag yellow" v-if="item.festival">{{
item.festival
}}</view>
<view class="tag red" v-if="item.year">{{ item.year }}</view>
</view>
</view>
<view class="card-actions">
<view class="action-btn" @tap.stop="onShare(item)">
<text class="icon">🔗</text>
</view>
<view class="action-btn" @tap.stop="onEdit(item)">
<text class="icon"></text>
</view>
<text class="action-emoji">🔗</text>
</button>
<!-- <view class="action-btn" @tap.stop="onMore(item)">
<text class="action-emoji"></text>
</view> -->
</view>
</view>
</view>
</view>
</view>
<!-- Loading State -->
<view class="loading-state" v-if="loading">
<text>加载中...</text>
</view>
<view class="empty-state" v-if="!loading && list.length === 0">
<text>暂无祝福记录</text>
</view>
<view class="no-more" v-if="!loading && !hasMore && list.length > 0">
<text>没有更多了</text>
</view>
<!-- Loading State -->
<view class="loading-state" v-if="loading">
<text>加载中...</text>
</view>
<view class="empty-state" v-if="!loading && list.length === 0">
<text>暂无祝福记录</text>
</view>
<view class="footer-note" v-if="!loading && list.length > 0">
<text>2026 丙午马年 · 祝福管理助手</text>
</view>
</view>
<!-- FAB -->
<view class="fab-btn" @tap="onMake">
<view class="fab-content">
<text class="fab-emoji"></text>
<text>新春制作</text>
</view>
</view>
</view>
@@ -98,96 +95,56 @@
<script setup>
import { ref, onMounted } from "vue";
import { onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
import {
onPullDownRefresh,
onReachBottom,
onShareAppMessage,
} from "@dcloudio/uni-app";
import { getMyCard } from "@/api/mine.js";
import NavBar from "@/components/NavBar/NavBar.vue";
import { getShareToken } from "@/utils/common.js";
const navBarTop = ref(0);
const navBarHeight = ref(44);
const list = ref([]);
const page = ref(1);
const loading = ref(false);
const hasMore = ref(true);
const isRefreshing = ref(false);
const totalCount = ref(0);
const deleteOptions = ref([
{
text: "删除",
style: {
backgroundColor: "#ff3b30",
},
},
]);
// Swipe Logic
const startX = ref(0);
const activeItem = ref(null);
const MAX_SWIPE_WIDTH = 80;
const onTouchStart = (e, item) => {
if (e.touches.length > 1) return;
// Close other items
if (activeItem.value && activeItem.value.id !== item.id) {
activeItem.value.translateX = 0;
activeItem.value.useTransition = true;
}
startX.value = e.touches[0].clientX;
item.useTransition = false;
activeItem.value = item;
};
const onTouchMove = (e, item) => {
if (e.touches.length > 1) return;
const currentX = e.touches[0].clientX;
const deltaX = currentX - startX.value;
// Allow swiping left (negative) up to -MAX_SWIPE_WIDTH
// If already open (translateX = -80), deltaX needs to be adjusted
// But simpler: just use delta from 0 position.
// Actually, standard swipe logic needs to account for current position.
// For simplicity: assume always starting from 0 (closed) or -80 (open).
// But if we start drag from open state, we need to handle it.
// Let's stick to "start from 0" logic for now, assuming auto-close.
// If item is already open, and we swipe right, we close it.
// Re-calculate based on initial offset if we want to support dragging from open.
// For now: simple close-on-touch-other logic covers most cases.
// We assume startX is from a state where it is either 0 or -80.
// But `item.translateX` might be -80.
let targetX = deltaX;
if (item.translateX === -MAX_SWIPE_WIDTH) {
targetX = -MAX_SWIPE_WIDTH + deltaX;
}
if (targetX < -MAX_SWIPE_WIDTH) targetX = -MAX_SWIPE_WIDTH;
if (targetX > 0) targetX = 0;
item.translateX = targetX;
};
const onTouchEnd = (e, item) => {
item.useTransition = true;
if (item.translateX < -30) {
item.translateX = -MAX_SWIPE_WIDTH;
} else {
item.translateX = 0;
}
};
onMounted(() => {
fetchList(true);
});
onPullDownRefresh(() => {
onRefresh();
fetchList(true);
});
onReachBottom(() => {
loadMore();
if (hasMore.value && !loading.value) {
fetchList();
}
});
onShareAppMessage(async (options) => {
if (options.from === "button") {
const shareTokenRes = await getShareToken(
"card_generate",
options?.target?.dataset?.item?.id,
);
return {
title: "我刚做了一张祝福卡片,送给你",
path: "/pages/detail/index?shareToken=" + shareTokenRes.shareToken,
imageUrl:
"https://file.lihailezzc.com/resource/13ec1134e6614feadeeaaa9ef21ea96e.png",
};
} else {
const shareTokenRes = await getShareToken("greeting_page", "");
return {
title: "新春祝福",
path: `/pages/index/index?shareToken=${shareTokenRes.shareToken}`,
imageUrl:
"https://file.lihailezzc.com/resource/cfed2edbfa19250b836a87a4bbf0d5ad.png",
};
}
});
const fetchList = async (reset = false) => {
@@ -216,27 +173,12 @@ const fetchList = async (reset = false) => {
}
} catch (e) {
console.error("Failed to fetch greeting list", e);
uni.showToast({ title: "加载失败", icon: "none" });
} finally {
loading.value = false;
isRefreshing.value = false;
uni.stopPullDownRefresh();
}
};
const loadMore = () => {
fetchList();
};
const onRefresh = () => {
isRefreshing.value = true;
fetchList(true);
};
const goBack = () => {
uni.navigateBack();
};
const formatDate = (dateStr) => {
if (!dateStr) return "";
const date = new Date(dateStr);
@@ -246,75 +188,115 @@ const formatDate = (dateStr) => {
return `${y}-${m}-${d}`;
};
const getTagText = (item) => {
// if (item.status === "draft") return "草稿";
return item?.title?.name || item.festival || "新春快乐";
};
const getTitle = (item) => {
const title =
(item?.blessingTo || "祝您") + (item?.content?.content || "新春快乐");
return title.length > 10 ? title.substring(0, 10) + "..." : title;
};
const getTagClass = (item) => {
if (item.status === "draft") return "tag-draft";
const tagMap = {
万事如意: "tag-spring",
新春快乐: "tag-gold",
新春大吉: "tag-horse",
钱包鼓鼓: "tag-ink",
福气旺旺: "tag-spring",
龙马精神: "tag-horse",
马年纳祥: "tag-horse",
福马迎春: "tag-horse",
};
return tagMap[item?.title?.name || item.festival] || "tag-gold";
};
const onPreview = (item) => {
if (!item.imageUrl) return;
uni.previewImage({
urls: [item.imageUrl],
current: item.imageUrl,
});
};
const onMake = () => {
uni.switchTab({
url: "/pages/make/index",
});
};
// const onShare = (item) => {
// uni.showToast({ title: "分享功能开发中", icon: "none" });
// };
const onMore = (item) => {
uni.showActionSheet({
itemList: ["编辑", "删除"],
success: (res) => {
if (res.tapIndex === 0) {
// Edit
} else if (res.tapIndex === 1) {
onDelete(item);
}
},
});
};
const onDelete = (item) => {
uni.showModal({
title: "提示",
content: "确定要删除这条祝福吗?",
success: (res) => {
if (res.confirm) {
// Implement delete API call here
// For now just remove from list locally
list.value = list.value.filter((i) => i.id !== item.id);
totalCount.value = Math.max(0, totalCount.value - 1);
uni.showToast({ title: "删除成功", icon: "none" });
} else {
// Reset swipe state if cancelled
item.translateX = 0;
}
},
});
};
const onShare = (item) => {
// Implement share logic
uni.showToast({ title: "分享功能开发中", icon: "none" });
};
const onEdit = (item) => {
// Implement edit logic
uni.showToast({ title: "编辑功能开发中", icon: "none" });
};
</script>
<style lang="scss" scoped>
.greeting-page {
min-height: 100vh;
background: #f9f9f9;
display: flex;
flex-direction: column;
background: #fbf9f2;
padding-bottom: 120rpx;
box-sizing: border-box;
}
.header-stats {
padding: 20px;
background: #f9f9f9;
margin-top: 44px; // Initial offset for fixed nav
padding: 30rpx 40rpx;
margin-top: 20rpx;
.stats-card {
background: #fff;
border-radius: 20px;
padding: 24px;
border-radius: 40rpx;
padding: 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.02);
.stats-left {
display: flex;
align-items: center;
.icon-circle {
width: 48px;
height: 48px;
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: #fff0f0;
background: #fff5f5;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
margin-right: 24rpx;
text {
font-size: 24px;
.sparkle-emoji {
font-size: 50rpx;
}
}
@@ -323,9 +305,9 @@ const onEdit = (item) => {
flex-direction: column;
.label {
font-size: 12px;
font-size: 24rpx;
color: #999;
margin-bottom: 4px;
margin-bottom: 8rpx;
}
.value-wrap {
@@ -333,14 +315,14 @@ const onEdit = (item) => {
align-items: baseline;
.value {
font-size: 24px;
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-right: 4px;
margin-right: 8rpx;
}
.unit {
font-size: 12px;
font-size: 24rpx;
color: #666;
}
}
@@ -348,30 +330,29 @@ const onEdit = (item) => {
}
.divider {
width: 1px;
height: 40px;
background: #eee;
margin: 0 20px;
width: 1rpx;
height: 80rpx;
background: #f0f0f0;
}
.stats-right {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
padding-left: 20rpx;
.label {
font-size: 12px;
font-size: 24rpx;
color: #999;
margin-bottom: 8px;
margin-bottom: 12rpx;
}
.value {
font-size: 16px;
font-weight: 600;
font-size: 32rpx;
font-weight: bold;
&.red-text {
color: #ff3b30;
color: #ff4d4f;
}
}
}
@@ -379,89 +360,86 @@ const onEdit = (item) => {
}
.list-section {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 20px;
padding: 0 40rpx;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
margin: 40rpx 0 24rpx;
.section-title {
font-size: 16px;
font-weight: 600;
color: #666;
}
.section-tip {
font-size: 12px;
color: #ccc;
font-size: 32rpx;
font-weight: bold;
color: #7c6d5d;
}
}
.list-container {
padding-bottom: 40px;
.list-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30rpx;
}
}
.list-item-wrap {
margin-bottom: 16px;
border-radius: 16px;
overflow: hidden;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.03);
}
.swipe-container {
position: relative;
width: 100%;
background: #ff3b30; // Delete background color
}
.delete-action {
position: absolute;
right: 0;
top: 0;
bottom: 0;
width: 80px;
display: flex;
align-items: center;
justify-content: center;
background-color: #ff3b30;
color: #fff;
font-size: 14px;
z-index: 1;
}
.card-item {
background: #fff;
padding: 16px;
display: flex;
align-items: center;
position: relative;
z-index: 2;
width: 100%;
box-sizing: border-box;
border-radius: 48rpx;
overflow: hidden;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.03);
.card-img {
width: 80px;
height: 80px;
border-radius: 12px;
margin-right: 16px;
background: #f5f5f5;
.card-image-wrap {
position: relative;
width: 100%;
padding-bottom: 133%; // 3:4 aspect ratio
background: #fdf3e7;
.card-img {
position: absolute;
top: 20rpx;
left: 20rpx;
right: 20rpx;
bottom: 20rpx;
width: auto;
height: auto;
border-radius: 12rpx;
}
.year-badge {
position: absolute;
top: 20rpx;
left: 20rpx;
background: #ff4d4f;
color: #fff;
font-size: 20rpx;
padding: 4rpx 12rpx;
border-radius: 8rpx;
font-weight: bold;
}
.draft-overlay {
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.05);
display: flex;
align-items: center;
justify-content: center;
.icon {
font-size: 40rpx;
opacity: 0.3;
}
}
}
.card-content {
flex: 1;
margin-right: 12px;
.card-info {
padding: 24rpx;
.card-title {
font-size: 16px;
font-weight: 600;
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8px;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
@@ -469,68 +447,117 @@ const onEdit = (item) => {
}
.card-date {
font-size: 12px;
font-size: 22rpx;
color: #999;
margin-bottom: 8px;
margin-bottom: 20rpx;
}
.tags {
.card-footer {
display: flex;
gap: 8px;
justify-content: space-between;
align-items: center;
.tag {
font-size: 10px;
padding: 2px 8px;
border-radius: 8px;
font-size: 20rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
&.yellow {
background: #fff8e1;
color: #ffb300;
&.tag-gold {
background: #fff8e6;
color: #ffb800;
}
&.red {
background: #ffebee;
color: #ff3b30;
&.tag-horse {
background: #fff1f0;
color: #ff4d4f;
}
&.tag-ink {
background: #f0f5ff;
color: #2f54eb;
}
&.tag-draft {
background: #f5f5f5;
color: #bfbfbf;
}
&.tag-spring {
background: #fff1f0;
color: #ff4d4f;
}
}
&.blue {
background: #e3f2fd;
color: #2196f3;
.actions {
display: flex;
gap: 16rpx;
.action-btn {
background: transparent;
border: none;
padding: 0;
margin: 0;
line-height: 1;
display: flex;
align-items: center;
justify-content: center;
outline: none;
&::after {
border: none;
}
.action-emoji {
font-size: 32rpx;
opacity: 0.6;
}
}
}
}
}
}
.card-actions {
.fab-btn {
position: fixed;
bottom: 60rpx;
left: 50%;
transform: translateX(-50%);
z-index: 100;
.fab-content {
background: #ff4d4f;
padding: 20rpx 48rpx;
border-radius: 100rpx;
display: flex;
flex-direction: column;
gap: 16px;
align-items: center;
box-shadow: 0 10rpx 30rpx rgba(255, 77, 79, 0.3);
.action-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
.fab-emoji {
font-size: 36rpx;
margin-right: 12rpx;
}
.icon {
font-size: 20px;
color: #666;
}
&:active {
opacity: 0.6;
}
text {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
}
&:active {
opacity: 0.9;
transform: translateX(-50%) scale(0.95);
}
}
.footer-note {
text-align: center;
padding: 60rpx 0 40rpx;
font-size: 24rpx;
color: #ccc;
}
.loading-state,
.empty-state,
.no-more {
.empty-state {
grid-column: span 2;
text-align: center;
padding: 20px;
padding: 100rpx 0;
color: #999;
font-size: 12px;
font-size: 28rpx;
}
</style>

View File

@@ -28,10 +28,10 @@
</view>
</view>
<view class="row-2" v-if="isLoggedIn">
<text class="arrow-icon"></text>
<text class="stats-text"
<!-- <text class="arrow-icon"></text> -->
<!-- <text class="stats-text"
>已发送 <text class="num">3</text> 条新春祝福</text
>
> -->
</view>
<view class="row-2" v-else>
<text class="stats-text">点击登录解锁更多功能</text>
@@ -136,9 +136,7 @@
<script setup>
import { ref, computed, onMounted } from "vue";
import { useUserStore } from "@/stores/user";
import {
onShareAppMessage,
} from "@dcloudio/uni-app";
import { onShareAppMessage } from "@dcloudio/uni-app";
import LoginPopup from "@/components/LoginPopup/LoginPopup.vue";
const userStore = useUserStore();
@@ -155,6 +153,7 @@ const defaultAvatarUrl =
const userInfo = computed(() => ({
nickName: userStore.userInfo.nickName || "点击登录",
avatarUrl: userStore.userInfo.avatarUrl || defaultAvatarUrl,
isVip: userStore.userInfo.isVip || false,
}));
const isLoggedIn = computed(() => !!userStore.userInfo.nickName);
@@ -166,11 +165,11 @@ onMounted(() => {
navBarHeight.value = 44;
});
onShareAppMessage( () => {
onShareAppMessage(() => {
return {
title: "新年好运已送达 🎊|祝福卡·头像·壁纸",
path: "/pages/index/index",
};
title: "新年好运已送达 🎊|祝福卡·头像·壁纸",
path: "/pages/index/index",
};
});
const handleUserClick = () => {

View File

@@ -1,5 +1,5 @@
<template>
<view class="vip-page" >
<view class="vip-page">
<NavBar title="会员中心" />
<!-- Content -->
@@ -120,10 +120,20 @@ const selectedPlanIndex = ref(1);
const plans = ref([]);
const benefits = [
{ name: "高级模板", icon: "star-filled", color: "#ff3b30", bg: "#fff0f0" },
{ name: "无限制下载", icon: "image-filled", color: "#ff6b00", bg: "#fff7e6" },
{ name: "马年头像框", icon: "medal-filled", color: "#bfa46f", bg: "#fffbe6" },
{ name: "高速渲染", icon: "upload-filled", color: "#ff3b30", bg: "#fff0f0" },
{ name: "纯净无广", icon: "star-filled", color: "#ff3b30", bg: "#fff0f0" },
{ name: "多次下载", icon: "image-filled", color: "#ff6b00", bg: "#fff7e6" },
{
name: "会员精选头像",
icon: "medal-filled",
color: "#bfa46f",
bg: "#fffbe6",
},
{
name: "会员精选模版",
icon: "upload-filled",
color: "#ff3b30",
bg: "#fff0f0",
},
{
name: "数据永久保存",
icon: "paperplane-filled",

View File

@@ -1,8 +1,6 @@
<template>
<view
class="wallpaper-page"
>
<NavBar title="精美壁纸" />
<view class="wallpaper-page">
<NavBar title="新春精美壁纸" />
<!-- Category Tabs -->
<view class="category-tabs">
@@ -37,18 +35,12 @@
:key="index"
>
<image
:src="getThumbUrl(item.imageUrl)"
:src="getThumbUrl(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>
<button
class="action-btn share"
open-type="share"
@@ -57,6 +49,12 @@
>
<text class="icon"></text>
</button>
<view
class="action-btn download"
@tap.stop="downloadWallpaper(item)"
>
<text class="icon"></text>
</view>
</view>
</view>
</view>
@@ -82,7 +80,6 @@
<script setup>
import { ref, onMounted, computed } from "vue";
import { getBavBarHeight } from "@/utils/system";
import { getWallpaperList, getWallpaperCategoryList } from "@/api/wallpaper.js";
import {
saveRemoteImageToLocal,
@@ -122,6 +119,8 @@ onShareAppMessage(async (options) => {
return {
title: "新春祝福",
path: `/pages/index/index?shareToken=${shareTokenRes.shareToken}`,
imageUrl:
"https://file.lihailezzc.com/resource/cfed2edbfa19250b836a87a4bbf0d5ad.png",
};
}
});
@@ -131,7 +130,7 @@ onMounted(async () => {
});
const getThumbUrl = (url) => {
return `${url}?imageView2/1/w/340/h/600/q/80`;
return `${url}?imageView2/1/w/340/h/600/q/80`;
};
const fetchCategories = async () => {
@@ -245,46 +244,21 @@ const shareWallpaper = (item) => {};
<style lang="scss" scoped>
.wallpaper-page {
height: 100vh;
background-color: #7a0909; /* Dark Red Background */
background-color: #ffffff;
display: flex;
flex-direction: column;
box-sizing: border-box;
}
.nav-bar {
display: flex;
align-items: center;
padding: 16rpx 24rpx;
/* background: #7A0909; */
.category-tabs {
padding: 0;
background-color: #ffffff;
border-bottom: 1rpx solid #eeeeee;
position: sticky;
top: 0;
z-index: 100;
}
.back {
font-size: 50rpx;
margin-right: 24rpx;
line-height: 1;
color: #ffd700; /* Gold */
/* 增大点击区域 */
padding: 20rpx;
margin-left: -20rpx;
}
.nav-title {
font-size: 34rpx;
font-weight: 600;
color: #ffd700; /* Gold */
flex: 1;
text-align: center;
margin-right: 50rpx; /* Balance back button */
}
.category-tabs {
padding: 20rpx 0;
/* background-color: #7A0909; */
}
.tabs-scroll {
white-space: nowrap;
width: 100%;
@@ -292,51 +266,54 @@ const shareWallpaper = (item) => {};
.tabs-content {
display: inline-flex;
padding: 0 24rpx;
gap: 20rpx;
padding: 0 30rpx;
}
.tab-item {
padding: 12rpx 32rpx;
border-radius: 999rpx;
font-size: 28rpx;
color: #ffd700;
background: rgba(0, 0, 0, 0.3);
border: 2rpx solid transparent;
padding: 24rpx 30rpx;
font-size: 30rpx;
color: #999999;
position: relative;
transition: all 0.3s;
font-weight: 500;
}
.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);
color: #e60012;
font-weight: bold;
&::after {
content: "";
position: absolute;
bottom: 0;
left: 50%;
transform: translateX(-50%);
width: 80rpx;
height: 4rpx;
background-color: #e60012;
border-radius: 2rpx;
}
}
.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;
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30rpx;
padding: 30rpx;
}
.grid-item {
width: 340rpx;
height: 600rpx;
border-radius: 24rpx;
border-radius: 32rpx;
overflow: hidden;
margin-bottom: 24rpx;
position: relative;
box-shadow: 0 8rpx 16rpx rgba(0, 0, 0, 0.3);
background: #333;
background: #f5f5f5;
}
.wallpaper-img {
@@ -357,22 +334,29 @@ const shareWallpaper = (item) => {};
width: 64rpx;
height: 64rpx;
border-radius: 50%;
background: rgba(0, 0, 0, 0.5);
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(4px);
display: flex;
align-items: center;
justify-content: center;
border: 1rpx solid rgba(255, 255, 255, 0.3);
border: 1rpx solid rgba(255, 255, 255, 0.2);
padding: 0;
margin: 0;
line-height: 1;
&::after {
border: none;
}
}
.action-btn.share .icon {
font-size: 30rpx;
}
.action-btn .icon {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
.action-btn.share .icon {
font-size: 28rpx;
font-size: 36rpx;
font-weight: normal;
}
.loading-state,
@@ -380,7 +364,7 @@ const shareWallpaper = (item) => {};
.no-more {
text-align: center;
padding: 40rpx;
color: rgba(255, 255, 255, 0.6);
color: #999999;
font-size: 24rpx;
}
</style>

BIN
static/.DS_Store vendored Normal file

Binary file not shown.

BIN
static/images/.DS_Store vendored Normal file

Binary file not shown.

Binary file not shown.

Before

Width:  |  Height:  |  Size: 58 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 72 KiB

View File

@@ -21,8 +21,12 @@ export const uploadImage = (filePath) => {
if (res.statusCode < 400) {
try {
const keyJson = JSON.parse(res.data);
const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
resolve(url);
if (keyJson?.data?.result === "auditFailed") {
reject("图片不符合发布规范,请稍作修改后再试");
} else {
const url = `https://file.lihailezzc.com/${keyJson?.data.key}`;
resolve(url);
}
} catch (e) {
reject(e);
}