Files
spring-festival-greetings/pages/mine/greeting.vue
2026-01-28 22:44:47 +08:00

506 lines
11 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" :style="{ paddingTop: navBarTop + 'px' }">
<!-- Navbar -->
<view
class="nav-bar"
:style="{ height: navBarHeight + 'px', paddingTop: navBarTop + 'px' }"
>
<view class="nav-content">
<view class="back-btn" @tap="goBack">
<text class="back-arrow"></text>
</view>
<text class="title">我的新春祝福</text>
</view>
</view>
<!-- 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">
<!-- Swipe Action Wrapper would go here, for now simulating simple layout -->
<!-- Since uni-swipe-action requires specific component structure,
we'll implement the visual part first as per design.
For "Left swipe to delete", we can use uni-swipe-action.
-->
<uni-swipe-action>
<uni-swipe-action-item
:right-options="deleteOptions"
@click="onSwipeClick($event, item)"
>
<view class="card-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>
</uni-swipe-action-item>
</uni-swipe-action>
</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 { getBavBarHeight } from "@/utils/system";
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",
},
},
]);
onMounted(() => {
const sysInfo = uni.getSystemInfoSync();
navBarTop.value = sysInfo.statusBarHeight;
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 onSwipeClick = (e, item) => {
if (e.index === 0) {
// Delete action
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" });
}
},
});
}
};
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;
}
.nav-bar {
position: fixed;
top: 0;
left: 0;
width: 100%;
z-index: 100;
background-color: #f9f9f9;
.nav-content {
height: 44px;
display: flex;
align-items: center;
justify-content: center;
position: relative;
.back-btn {
position: absolute;
left: 16px;
height: 100%;
display: flex;
align-items: center;
padding: 0 10px;
.back-arrow {
font-size: 32px;
color: #333;
font-weight: 300;
}
}
.title {
font-size: 18px;
font-weight: 600;
color: #333;
}
}
}
.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);
}
.card-item {
background: #fff;
padding: 16px;
display: flex;
align-items: center;
.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>