Files
menu-mini/src/pages/home/home.vue
2025-08-19 16:26:01 +08:00

854 lines
21 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="home-container">
<!-- 自定义头部 -->
<view class="custom-header">
<view class="status-bar"></view>
<view class="header-content">
<view class="header-title">情侣菜谱</view>
<view class="header-actions">
<uni-icons type="more-filled" size="20" color="#fff"></uni-icons>
</view>
</view>
</view>
<!-- 主要内容区域 -->
<view class="main-content">
<!-- 情侣头像和恋爱天数 -->
<view class="couple-info">
<view class="avatar-section">
<view class="avatar left-avatar">
<img
:src="userInfo.avatar"
class="avatar-img"
:key="userInfo.avatar"
alt="用户头像"
></img>
</view>
<view class="heart-icon">
<uni-icons type="heart-filled" size="30" color="#FF6B9D"></uni-icons>
</view>
<view class="avatar right-avatar">
<img
:src="partnerInfo.avatar"
class="avatar-img"
alt="伴侣头像"
></img>
<view v-if="!partnerInfo.avatar" class="add-partner">
<uni-icons type="plus" size="24" color="#FF6B9D"></uni-icons>
</view>
</view>
</view>
<view class="relationship-info">
<text class="relationship-text">我们在一起已经</text>
<view class="days-count">
<text class="days-number">{{ loveDays }}</text>
<text class="days-unit"></text>
</view>
<text class="start-date">{{ startDate }}</text>
</view>
</view>
<!-- 快捷功能卡片 -->
<view class="feature-cards">
<view class="feature-card diary-card" @click="goToDiary">
<view class="card-content">
<text class="card-title">写日志</text>
<uni-icons type="compose" size="32" color="#fff"></uni-icons>
</view>
</view>
<view class="feature-card order-card" @click="goToOrder">
<view class="card-content">
<text class="card-title">点餐下单</text>
<uni-icons type="cart" size="32" color="#fff"></uni-icons>
</view>
</view>
</view>
<!-- 纪念日列表 -->
<view class="anniversary-section">
<view class="section-header">
<text class="section-title">重要纪念日</text>
<view class="add-anniversary" @click="addAnniversary">
<uni-icons type="plus" size="20" color="#FF6B9D"></uni-icons>
</view>
</view>
<view class="anniversary-list">
<view
v-for="(item, index) in anniversaryList"
:key="item.ID || index"
class="anniversary-item"
@click="viewAnniversary(item)"
>
<view class="anniversary-icon no-circle" :data-type="item.type">
<view class="icon-wrapper">
<!-- 生日类型显示图片其他类型显示图标 -->
<img
v-if="item.type === 2"
src="/static/images/birthday.png"
class="birthday-icon"
alt="生日"
/>
<uni-icons
v-else
:type="getIconByType(item.type)"
size="26"
:color="getIconColorByType(item.type)"
></uni-icons>
</view>
</view>
<view class="anniversary-info">
<text class="anniversary-name">{{ item.title }}</text>
<view class="anniversary-meta">
<!-- <text class="anniversary-type" :data-type="item.type">{{ getTypeTextByType(item.type) }}</text> -->
<text class="anniversary-date">{{ item.date }}</text>
</view>
</view>
<view class="anniversary-countdown">
<text class="countdown-text">{{ getCountdownText(item) }}</text>
</view>
</view>
<!-- 空状态 -->
<view v-if="anniversaryList.length === 0" class="empty-anniversary">
<text class="empty-text">还没有添加纪念日</text>
<text class="empty-desc">点击右上角添加第一个纪念日吧</text>
</view>
</view>
</view>
</view>
</view>
</template>
<script setup>
import { ref, computed, onMounted } from 'vue'
import { useUserStore } from '@/store/user.js'
import api from '@/api'
import { onPullDownRefresh } from '@dcloudio/uni-app'
const userStore = useUserStore()
// 响应式数据
const userInfo = ref({
avatar: '',
nickname: ''
})
const partnerInfo = ref({
avatar: '',
nickname: ''
})
const startDate = ref('')
const loveDays = ref(0)
const anniversaryList = ref([])
// 计算属性
const computedLoveDays = computed(() => loveDays.value)
// API: 初始化用户与情侣信息
const initHomeData = async () => {
try {
// 1. 优先从缓存拿本人信息(登录后已缓存到 userInfo
const cached = uni.getStorageSync('userInfo')
if (cached) {
userInfo.value.avatar = cached.avatar || ''
userInfo.value.nickname = cached.nick_name || cached.nickname || ''
}
// 2. 调用情侣关系接口,拿到 loverId 与 startDate
const loverRes = await api.getLoverInfo()
if (loverRes?.code === 0 && loverRes.data) {
const { loverId, startDate: s } = loverRes.data
// 设置开始日期(用于恋爱天数)
startDate.value = s || ''
// 若未缓存本人信息或需要刷新,可用 getUserInfo 再拉一遍(可选)
if (!userInfo.value.avatar || !userInfo.value.nickname) {
const meRes = await api.getUserInfo()
if (meRes?.code === 0 && meRes.data) {
userInfo.value.avatar = meRes.data.avatar || userInfo.value.avatar
userInfo.value.nickname = meRes.data.nick_name || meRes.data.nickname || userInfo.value.nickname
}
}
// 3. 根据 loverId 获取另一半资料
if (loverId) {
const loverInfoRes = await api.getUserInfoById(loverId)
if (loverInfoRes?.code === 0 && loverInfoRes.data) {
partnerInfo.value.avatar = loverInfoRes.data.avatar || ''
partnerInfo.value.nickname = loverInfoRes.data.nick_name || loverInfoRes.data.nickname || ''
}
}
// 4. 计算恋爱天数
computeLoveDays(startDate.value)
}
// 5. 加载纪念日列表
await loadAnniversaryList()
} catch (e) {
console.error('初始化首页数据失败:', e)
}
}
const computeLoveDays = (dateStr) => {
if (!dateStr) {
loveDays.value = 0
return
}
// 支持 yyyy-MM-dd 或 yyyy/MM/dd
const norm = dateStr.replace(/\./g, '-').replace(/\//g, '-')
const start = new Date(norm)
if (isNaN(start.getTime())) {
loveDays.value = 0
return
}
const today = new Date()
const diffTime = today.getTime() - start.getTime()
loveDays.value = Math.max(0, Math.ceil(diffTime / (1000 * 60 * 60 * 24)))
}
// 加载纪念日列表
const loadAnniversaryList = async () => {
try {
const res = await api.getMemorialDayList()
if (res?.code === 0 && res.data?.list) {
anniversaryList.value = res.data.list || []
// 按日期排序,只显示最近的几个
anniversaryList.value.sort((a, b) => {
const dateA = new Date(a.date)
const dateB = new Date(b.date)
return dateA - dateB
})
// 只显示前3个
anniversaryList.value = anniversaryList.value.slice(0, 3)
}
} catch (error) {
console.error('加载纪念日列表失败:', error)
}
}
// 获取纪念日倒计时文本
const getCountdownText = (item) => {
if (!item.date) return ''
const today = new Date()
const anniversary = new Date(item.date)
// 设置今年的纪念日
const thisYear = new Date(today.getFullYear(), anniversary.getMonth(), anniversary.getDate())
// 如果今年的纪念日已过,计算明年的
if (thisYear < today) {
thisYear.setFullYear(thisYear.getFullYear() + 1)
}
const diffTime = thisYear.getTime() - today.getTime()
const days = Math.ceil(diffTime / (1000 * 60 * 60 * 24))
if (days === 0) return '就是今天'
if (days < 0) return '已过'
if (days === 1) return '明天'
return `还有${days}`
}
// 获取纪念日图标
const getIconByType = (type) => {
const iconMap = {
1: 'heart-filled', // 恋爱纪念日 - 使用 heart-filled
2: 'star-filled', // 生日 - 使用 star-filled
3: 'calendar-filled' // 其他 - 使用 calendar-filled
}
return iconMap[type] || 'calendar-filled'
}
// 获取纪念日图标颜色
const getIconColorByType = (type) => {
const colorMap = {
1: '#FF6B9D', // 恋爱纪念日 - 粉色
2: '#FFD700', // 生日 - 金色
3: '#4CAF50' // 其他 - 绿色
}
return colorMap[type] || '#FF6B9D'
}
// 获取纪念日类型文字
const getTypeTextByType = (type) => {
const textMap = {
1: '恋爱纪念日',
2: '生日',
3: '其他'
}
return textMap[type] || '其他'
}
// 方法
const invitePartner = () => {
uni.showToast({
title: '邀请功能开发中',
icon: 'none'
})
}
const goToDiary = () => {
uni.switchTab({ url: '/pages/diary/diary' })
}
const goToOrder = () => {
uni.switchTab({ url: '/pages/order/order' })
}
const addAnniversary = () => {
uni.navigateTo({
url: '/pages/anniversary/anniversary'
})
}
const viewAnniversary = (item) => {
uni.navigateTo({
url: '/pages/anniversary/anniversary'
})
}
// 生命周期
onMounted(() => {
initHomeData()
})
// 下拉刷新
onPullDownRefresh(() => {
// 同时刷新用户信息和纪念日列表
Promise.all([
initHomeData(),
loadAnniversaryList()
]).finally(() => {
// 结束下拉刷新动画
uni.stopPullDownRefresh()
})
})
</script>
<style lang="scss" scoped>
.home-container {
min-height: 100vh;
background: linear-gradient(135deg, #FFE4E1 0%, #FFB6C1 50%, #E6E6FA 100%);
}
.custom-header {
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
padding: 0 20rpx;
.status-bar {
height: var(--status-bar-height);
}
.header-content {
display: flex;
justify-content: space-between;
align-items: center;
padding: 20rpx 0;
.header-title {
color: #fff;
font-size: 36rpx;
font-weight: bold;
}
.header-actions {
display: flex;
gap: 20rpx;
}
}
}
.main-content {
padding: 40rpx 30rpx 60rpx;
}
.couple-info {
text-align: center;
margin-bottom: 60rpx;
.avatar-section {
display: flex;
justify-content: center;
align-items: center;
margin-bottom: 40rpx;
gap: 40rpx;
.avatar {
position: relative;
.avatar-img {
width: 160rpx;
height: 160rpx;
border-radius: 50%;
border: 4rpx solid #fff;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.15);
object-fit: cover;
background: #f0f0f0;
transition: all 0.3s ease;
cursor: pointer;
&:hover {
transform: scale(1.05);
box-shadow: 0 6rpx 25rpx rgba(0, 0, 0, 0.2);
}
}
.add-partner {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
width: 40rpx;
height: 40rpx;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.1);
}
}
.heart-icon {
animation: heartbeat 1.5s ease-in-out infinite;
filter: drop-shadow(0 2rpx 8rpx rgba(255, 107, 157, 0.3));
// background: rgba(255, 255, 255, 0.9);
border-radius: 50%;
padding: 15rpx;
// box-shadow: 0 4rpx 15rpx rgba(255, 107, 157, 0.2);
}
}
.relationship-info {
margin-bottom: 30rpx;
.relationship-text {
display: block;
color: #ff6b9d;
font-size: 31rpx;
margin-bottom: 20rpx;
}
.days-count {
margin-bottom: 20rpx;
.days-number {
font-size: 80rpx;
font-weight: bold;
color: #FF6B9D;
}
.days-unit {
font-size: 32rpx;
color: #FF6B9D;
margin-left: 10rpx;
}
}
.start-date {
color: #ff6b9d;
font-size: 30rpx;
}
}
.invite-button {
display: inline-flex;
align-items: center;
gap: 10rpx;
background: linear-gradient(135deg, #FF6B9D 0%, #FF8E9E 100%);
color: #fff;
padding: 20rpx 40rpx;
border-radius: 50rpx;
font-size: 28rpx;
.invite-text {
color: #fff;
}
}
}
.feature-cards {
display: flex;
gap: 30rpx;
margin-bottom: 60rpx;
.feature-card {
flex: 1;
height: 160rpx;
border-radius: 20rpx;
overflow: hidden;
.card-content {
height: 100%;
display: flex;
justify-content: space-between;
align-items: center;
padding: 0 30rpx;
.card-title {
color: #fff;
font-size: 32rpx;
font-weight: bold;
}
}
&.diary-card {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
}
&.order-card {
background: linear-gradient(135deg, #4CAF50 0%, #45a049 100%);
}
}
}
.anniversary-section {
background: linear-gradient(135deg, #E6E6FA 0%, #DDA0DD 50%, #D8BFD8 100%);
border-radius: 25rpx;
padding: 30rpx;
margin-top: 20rpx;
box-shadow: 0 6rpx 25rpx rgba(221, 160, 221, 0.3);
animation: fadeInScale 0.8s ease-out;
.section-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 30rpx;
.section-title {
font-size: 32rpx;
font-weight: bold;
color: #333;
position: relative;
&::after {
content: '';
position: absolute;
bottom: -8rpx;
left: 0;
width: 60rpx;
height: 3rpx;
background: linear-gradient(90deg, #FF6B9D, #FF8E9E);
border-radius: 2rpx;
}
}
.add-anniversary {
width: 60rpx;
height: 60rpx;
background: #fff;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4rpx 20rpx rgba(0, 0, 0, 0.1);
transition: all 0.3s ease;
cursor: pointer;
&:active {
transform: scale(0.95);
box-shadow: 0 2rpx 10rpx rgba(0, 0, 0, 0.15);
}
&:hover {
transform: scale(1.05);
box-shadow: 0 6rpx 25rpx rgba(0, 0, 0, 0.15);
}
}
}
.anniversary-list {
.anniversary-item {
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.95);
padding: 35rpx;
border-radius: 25rpx;
margin-bottom: 25rpx;
box-shadow: 0 8rpx 30rpx rgba(221, 160, 221, 0.2);
backdrop-filter: blur(15rpx);
border: 2rpx solid rgba(255, 255, 255, 0.6);
transition: all 0.4s cubic-bezier(0.4, 0, 0.2, 1);
position: relative;
overflow: hidden;
animation: slideInUp 0.6s ease-out;
@for $i from 1 through 5 {
&:nth-child(#{$i}) {
animation-delay: #{$i * 0.1}s;
}
}
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.3), transparent);
transition: left 0.6s ease;
}
&::after {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.1) 0%, transparent 50%, rgba(255, 255, 255, 0.1) 100%);
opacity: 0;
transition: opacity 0.3s ease;
}
&:last-child {
margin-bottom: 0;
}
&:active {
transform: translateY(3rpx) scale(0.98);
box-shadow: 0 4rpx 20rpx rgba(221, 160, 221, 0.3);
&::before {
left: 100%;
}
&::after {
opacity: 1;
}
}
&:hover {
transform: translateY(-2rpx);
box-shadow: 0 12rpx 40rpx rgba(221, 160, 221, 0.3);
}
.anniversary-icon {
margin-right: 35rpx;
border-radius: 50%;
padding: 20rpx;
position: relative;
z-index: 1;
transition: all 0.3s ease;
&[data-type="1"],
&[data-type="2"],
&[data-type="3"] {
background: transparent;
box-shadow: none;
}
&::after {
display: none;
}
.icon-wrapper {
background: transparent;
border-radius: 0;
padding: 0;
box-shadow: none;
.birthday-icon {
width: 50rpx;
height: 50rpx;
object-fit: contain;
display: block;
}
}
&:hover {
transform: scale(1.1);
&[data-type="1"] {
box-shadow: 0 6rpx 20rpx rgba(255, 107, 157, 0.4);
}
&[data-type="2"] {
box-shadow: 0 6rpx 20rpx rgba(255, 215, 0, 0.4);
}
&[data-type="3"] {
box-shadow: 0 6rpx 20rpx rgba(76, 175, 80, 0.4);
}
&::after {
opacity: 0.5;
transform: scale(1.1);
}
}
}
.anniversary-info {
flex: 1;
position: relative;
z-index: 1;
.anniversary-name {
display: block;
font-size: 32rpx;
font-weight: 700;
color: #2c3e50;
margin-bottom: 12rpx;
line-height: 1.3;
text-shadow: 0 1rpx 2rpx rgba(0, 0, 0, 0.05);
}
.anniversary-meta {
display: flex;
align-items: center;
gap: 10rpx;
margin-bottom: 12rpx;
.anniversary-type {
font-size: 24rpx;
font-weight: 600;
padding: 6rpx 12rpx;
border-radius: 12rpx;
border: 1rpx solid;
position: relative;
overflow: hidden;
&[data-type="1"] {
color: #FF6B9D;
background: rgba(255, 107, 157, 0.1);
border-color: rgba(255, 107, 157, 0.3);
}
&[data-type="2"] {
color: #FFD700;
background: rgba(255, 215, 0, 0.1);
border-color: rgba(255, 215, 0, 0.3);
}
&[data-type="3"] {
color: #4CAF50;
background: rgba(76, 175, 80, 0.1);
border-color: rgba(76, 175, 80, 0.3);
}
}
.anniversary-date {
font-size: 26rpx;
color: #7f8c8d;
font-weight: 500;
background: rgba(255, 255, 255, 0.8);
padding: 6rpx 12rpx;
border-radius: 15rpx;
display: inline-block;
box-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.08);
}
}
}
.anniversary-countdown {
position: relative;
z-index: 1;
.countdown-text {
color: #FF6B9D;
font-size: 28rpx;
font-weight: 700;
background: linear-gradient(135deg, rgba(255, 255, 255, 0.9) 0%, rgba(255, 255, 255, 0.95) 100%);
padding: 12rpx 20rpx;
border-radius: 25rpx;
box-shadow: 0 4rpx 15rpx rgba(0, 0, 0, 0.1);
border: 2rpx solid rgba(255, 107, 157, 0.2);
position: relative;
overflow: hidden;
&::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 107, 157, 0.1), transparent);
transition: left 0.6s ease;
}
&:hover::before {
left: 100%;
}
}
}
}
.empty-anniversary {
text-align: center;
padding: 80rpx 0;
color: #666;
font-size: 28rpx;
background: rgba(255, 255, 255, 0.7);
border-radius: 20rpx;
backdrop-filter: blur(10rpx);
border: 1rpx solid rgba(255, 255, 255, 0.4);
.empty-text {
display: block;
margin-bottom: 10rpx;
color: #333;
font-weight: 500;
}
.empty-desc {
font-size: 24rpx;
color: #888;
}
}
}
}
@keyframes heartbeat {
0% {
transform: scale(1);
filter: drop-shadow(0 2rpx 8rpx rgba(255, 107, 157, 0.3));
}
25% {
transform: scale(1.1);
filter: drop-shadow(0 4rpx 12rpx rgba(255, 107, 157, 0.4));
}
50% {
transform: scale(1.15);
filter: drop-shadow(0 6rpx 16rpx rgba(255, 107, 157, 0.5));
}
75% {
transform: scale(1.1);
filter: drop-shadow(0 4rpx 12rpx rgba(255, 107, 157, 0.4));
}
100% {
transform: scale(1);
filter: drop-shadow(0 2rpx 8rpx rgba(255, 107, 157, 0.3));
}
}
@keyframes slideInUp {
from {
opacity: 0;
transform: translateY(50px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
@keyframes fadeInScale {
from {
opacity: 0;
transform: scale(0.9);
}
to {
opacity: 1;
transform: scale(1);
}
}
</style>