Files
spring-festival-greetings/pages/mine/greeting.vue
2026-02-03 11:01:58 +08:00

517 lines
11 KiB
Vue
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<template>
<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 class="sparkle-emoji"></text>
</view>
<view class="stats-info">
<text class="label">已累计创作</text>
<view class="value-wrap">
<text class="value">{{ totalCount }}</text>
<text class="unit">份祝福</text>
</view>
</view>
</view>
<view class="divider"></view>
<view class="stats-right">
<text class="label">马年运势</text>
<text class="value red-text">一马当先</text>
</view>
</view>
</view>
<!-- List Section -->
<view class="list-section">
<view class="section-header">
<text class="section-title">祝福库</text>
</view>
<view class="list-grid">
<view
v-for="item in list"
:key="item.id"
class="card-item"
@tap="onDetail(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>
</view>
<view class="card-info">
<view class="card-title"
>{{ item.blessingTo || "祝全家" }}福寿双全</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">
<view class="action-btn" @tap.stop="onShare(item)">
<text class="action-emoji">🔗</text>
</view>
<!-- <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="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>
</template>
<script setup>
import { ref, onMounted } from "vue";
import { onPullDownRefresh, onReachBottom } from "@dcloudio/uni-app";
import { getMyCard } from "@/api/mine.js";
import NavBar from "@/components/NavBar/NavBar.vue";
const list = ref([]);
const page = ref(1);
const loading = ref(false);
const hasMore = ref(true);
const totalCount = ref(0);
onMounted(() => {
fetchList(true);
});
onPullDownRefresh(() => {
fetchList(true);
});
onReachBottom(() => {
if (hasMore.value && !loading.value) {
fetchList();
}
});
const fetchList = async (reset = false) => {
if (loading.value) return;
if (reset) {
page.value = 1;
hasMore.value = true;
}
if (!hasMore.value) return;
loading.value = true;
try {
const res = await getMyCard(page.value);
const dataList = res?.list || [];
totalCount.value = res?.totalCount || 0;
if (reset) {
list.value = dataList;
} else {
list.value = [...list.value, ...dataList];
}
hasMore.value = res.hasNext;
if (hasMore.value) {
page.value++;
}
} catch (e) {
console.error("Failed to fetch greeting list", e);
} finally {
loading.value = false;
uni.stopPullDownRefresh();
}
};
const formatDate = (dateStr) => {
if (!dateStr) return "";
const date = new Date(dateStr);
const y = date.getFullYear();
const m = String(date.getMonth() + 1).padStart(2, "0");
const d = String(date.getDate()).padStart(2, "0");
return `${y}-${m}-${d}`;
};
const getTagText = (item) => {
// if (item.status === "draft") return "草稿";
return item?.title?.name || item.festival || "新春快乐";
};
const getTitle = (item) => {
// if (item.status === "draft") return "草稿";
return item.blessingTo + (item?.content?.content || "新春快乐");
};
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 onDetail = (item) => {
uni.navigateTo({
url: `/pages/detail/index?id=${item.id}`,
});
};
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) {
list.value = list.value.filter((i) => i.id !== item.id);
totalCount.value = Math.max(0, totalCount.value - 1);
uni.showToast({ title: "删除成功", icon: "none" });
}
},
});
};
</script>
<style lang="scss" scoped>
.greeting-page {
min-height: 100vh;
background: #fbf9f2;
padding-bottom: 120rpx;
box-sizing: border-box;
}
.header-stats {
padding: 30rpx 40rpx;
margin-top: 20rpx;
.stats-card {
background: #fff;
border-radius: 40rpx;
padding: 40rpx;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.02);
.stats-left {
display: flex;
align-items: center;
.icon-circle {
width: 100rpx;
height: 100rpx;
border-radius: 50%;
background: #fff5f5;
display: flex;
align-items: center;
justify-content: center;
margin-right: 24rpx;
.sparkle-emoji {
font-size: 50rpx;
}
}
.stats-info {
display: flex;
flex-direction: column;
.label {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.value-wrap {
display: flex;
align-items: baseline;
.value {
font-size: 40rpx;
font-weight: bold;
color: #333;
margin-right: 8rpx;
}
.unit {
font-size: 24rpx;
color: #666;
}
}
}
}
.divider {
width: 1rpx;
height: 80rpx;
background: #f0f0f0;
}
.stats-right {
display: flex;
flex-direction: column;
align-items: center;
padding-left: 20rpx;
.label {
font-size: 24rpx;
color: #999;
margin-bottom: 12rpx;
}
.value {
font-size: 32rpx;
font-weight: bold;
&.red-text {
color: #ff4d4f;
}
}
}
}
}
.list-section {
padding: 0 40rpx;
.section-header {
margin: 40rpx 0 24rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #7c6d5d;
}
}
.list-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 30rpx;
}
}
.card-item {
background: #fff;
border-radius: 48rpx;
overflow: hidden;
box-shadow: 0 10rpx 30rpx rgba(0, 0, 0, 0.03);
.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-info {
padding: 24rpx;
.card-title {
font-size: 28rpx;
font-weight: bold;
color: #333;
margin-bottom: 8rpx;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.card-date {
font-size: 22rpx;
color: #999;
margin-bottom: 20rpx;
}
.card-footer {
display: flex;
justify-content: space-between;
align-items: center;
.tag {
font-size: 20rpx;
padding: 4rpx 16rpx;
border-radius: 20rpx;
&.tag-gold {
background: #fff8e6;
color: #ffb800;
}
&.tag-horse {
background: #fff1f0;
color: #ff4d4f;
}
&.tag-ink {
background: #f0f5ff;
color: #2f54eb;
}
&.tag-draft {
background: #f5f5f5;
color: #bfbfbf;
}
&.tag-spring {
background: #fff1f0;
color: #ff4d4f;
}
}
.actions {
display: flex;
gap: 16rpx;
.action-btn {
.action-emoji {
font-size: 32rpx;
opacity: 0.6;
}
}
}
}
}
}
.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;
align-items: center;
box-shadow: 0 10rpx 30rpx rgba(255, 77, 79, 0.3);
.fab-emoji {
font-size: 36rpx;
margin-right: 12rpx;
}
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 {
grid-column: span 2;
text-align: center;
padding: 100rpx 0;
color: #999;
font-size: 28rpx;
}
</style>