Files
spring-festival-greetings/pages/mine/greeting.vue
2026-01-31 23:31:40 +08:00

537 lines
12 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. 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="我的祝福" />
<!-- Header Stats -->
<view class="header-stats">
<view class="stats-card">
<view class="stats-left">
<view class="icon-circle">
<text></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>
<text class="section-tip">左滑删除记录</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>
<!-- 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 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>
</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>
</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 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();
});
onReachBottom(() => {
loadMore();
});
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);
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);
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 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;
box-sizing: border-box;
}
.header-stats {
padding: 20px;
background: #f9f9f9;
margin-top: 44px; // Initial offset for fixed nav
.stats-card {
background: #fff;
border-radius: 20px;
padding: 24px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4px 16px rgba(0, 0, 0, 0.04);
.stats-left {
display: flex;
align-items: center;
.icon-circle {
width: 48px;
height: 48px;
border-radius: 50%;
background: #fff0f0;
display: flex;
align-items: center;
justify-content: center;
margin-right: 16px;
text {
font-size: 24px;
}
}
.stats-info {
display: flex;
flex-direction: column;
.label {
font-size: 12px;
color: #999;
margin-bottom: 4px;
}
.value-wrap {
display: flex;
align-items: baseline;
.value {
font-size: 24px;
font-weight: bold;
color: #333;
margin-right: 4px;
}
.unit {
font-size: 12px;
color: #666;
}
}
}
}
.divider {
width: 1px;
height: 40px;
background: #eee;
margin: 0 20px;
}
.stats-right {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
.label {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.value {
font-size: 16px;
font-weight: 600;
&.red-text {
color: #ff3b30;
}
}
}
}
}
.list-section {
flex: 1;
display: flex;
flex-direction: column;
padding: 0 20px;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16px;
.section-title {
font-size: 16px;
font-weight: 600;
color: #666;
}
.section-tip {
font-size: 12px;
color: #ccc;
}
}
.list-container {
padding-bottom: 40px;
}
}
.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;
.card-img {
width: 80px;
height: 80px;
border-radius: 12px;
margin-right: 16px;
background: #f5f5f5;
}
.card-content {
flex: 1;
margin-right: 12px;
.card-title {
font-size: 16px;
font-weight: 600;
color: #333;
margin-bottom: 8px;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 1;
overflow: hidden;
}
.card-date {
font-size: 12px;
color: #999;
margin-bottom: 8px;
}
.tags {
display: flex;
gap: 8px;
.tag {
font-size: 10px;
padding: 2px 8px;
border-radius: 8px;
&.yellow {
background: #fff8e1;
color: #ffb300;
}
&.red {
background: #ffebee;
color: #ff3b30;
}
&.blue {
background: #e3f2fd;
color: #2196f3;
}
}
}
}
.card-actions {
display: flex;
flex-direction: column;
gap: 16px;
.action-btn {
width: 32px;
height: 32px;
display: flex;
align-items: center;
justify-content: center;
.icon {
font-size: 20px;
color: #666;
}
&:active {
opacity: 0.6;
}
}
}
}
.loading-state,
.empty-state,
.no-more {
text-align: center;
padding: 20px;
color: #999;
font-size: 12px;
}
</style>