pet/pages/pets/pet-timeline.vue

644 lines
17 KiB
Vue

<template>
<view class="pet-timeline-container">
<!-- 头部操作栏 -->
<view class="header-actions">
<text class="page-title">{{ petInfo.name }}的成长时光</text>
<view class="calendar-btn" @click="showCalendar">
<text class="calendar-text">日历</text>
</view>
</view>
<!-- 宠物信息卡片 -->
<view class="pet-info-card">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="60" shape="circle"></u-avatar>
<view class="pet-info">
<u-text :text="petInfo.name" type="primary" size="16" bold></u-text>
<u-text :text="`陪伴了 ${petInfo.companionDays} 天`" type="info" size="14"></u-text>
<view class="growth-stats">
<view class="stat-item">
<u-text text="记录" type="tips" size="12"></u-text>
<u-text :text="totalRecords" type="primary" size="14" bold></u-text>
</view>
<view class="stat-item">
<u-text text="里程碑" type="tips" size="12"></u-text>
<u-text :text="milestoneCount" type="primary" size="14" bold></u-text>
</view>
<view class="stat-item">
<u-text text="照片" type="tips" size="12"></u-text>
<u-text :text="photoCount" type="primary" size="14" bold></u-text>
</view>
</view>
</view>
</view>
<!-- 时间轴 -->
<scroll-view class="timeline-scroll" scroll-y @scrolltolower="loadMoreTimeline">
<view class="timeline-container">
<view v-for="(item, index) in timelineData" :key="item.id" class="timeline-item">
<!-- 时间轴线条 -->
<view class="timeline-line" v-if="index < timelineData.length - 1"></view>
<!-- 时间轴节点 -->
<view class="timeline-node" :class="getNodeClass(item.type)">
<u-icon :name="getNodeIcon(item.type)" size="16" color="#ffffff"></u-icon>
</view>
<!-- 时间轴内容 -->
<view class="timeline-content">
<view class="timeline-header">
<u-text :text="item.title" size="14" bold></u-text>
<u-text :text="formatTimelineDate(item.date)" size="12" color="#999"></u-text>
</view>
<view class="timeline-body">
<u-text :text="item.content" size="13" color="#666" :lines="3"></u-text>
<!-- 图片展示 -->
<view class="timeline-photos" v-if="item.photos && item.photos.length > 0">
<u-image
v-for="(photo, photoIndex) in item.photos.slice(0, 4)"
:key="photoIndex"
:src="photo"
:width="item.photos.length === 1 ? '120px' : '60px'"
:height="item.photos.length === 1 ? '120px' : '60px'"
border-radius="8px"
@click="previewPhoto(item.photos, photoIndex)"
></u-image>
<view class="more-photos" v-if="item.photos.length > 4">
<u-text :text="`+${item.photos.length - 4}`" size="12" color="#999"></u-text>
</view>
</view>
<!-- 特殊数据展示 -->
<view class="timeline-data" v-if="item.data">
<view class="data-item" v-for="(value, key) in item.data" :key="key">
<u-tag :text="`${key}: ${value}`" type="info" size="mini"></u-tag>
</view>
</view>
</view>
<!-- 时间轴操作 -->
<view class="timeline-actions">
<u-icon name="heart" size="16" :color="item.liked ? '#ff6b6b' : '#cccccc'" @click="toggleLike(item)"></u-icon>
<u-icon name="share" size="16" color="#cccccc" @click="shareTimeline(item)"></u-icon>
<u-icon name="more-dot-fill" size="16" color="#cccccc" @click="showTimelineMenu(item)"></u-icon>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMoreTimeline">
<u-loading-icon v-if="loadingMore"></u-loading-icon>
<u-text text="上拉查看更多回忆" type="tips" size="12" v-else></u-text>
</view>
<!-- 时间轴底部 -->
<view class="timeline-end" v-if="!hasMoreTimeline">
<view class="end-node">
<u-icon name="home" size="20" color="#ff6b6b"></u-icon>
</view>
<u-text :text="`${petInfo.name}来到这个家的第一天`" size="14" color="#999" style="margin-top: 10px;"></u-text>
<u-text :text="formatTimelineDate(petInfo.adoptionDate || '2023-01-01')" size="12" color="#ccc"></u-text>
</view>
</view>
</scroll-view>
<!-- 快速添加按钮 -->
<view class="quick-actions">
<view class="action-button" @click="addMilestone">
<u-icon name="star" size="20" color="#ffffff"></u-icon>
<u-text text="里程碑" size="12" color="#ffffff"></u-text>
</view>
<view class="action-button" @click="addMemory">
<u-icon name="camera" size="20" color="#ffffff"></u-icon>
<u-text text="记忆" size="12" color="#ffffff"></u-text>
</view>
</view>
</view>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
export default {
name: 'PetTimeline',
setup() {
// 响应式数据
const state = reactive({
petId: '',
petInfo: {},
timelineData: [],
hasMoreTimeline: true,
loadingMore: false,
page: 1,
pageSize: 20
})
// 计算属性
const totalRecords = computed(() => {
return state.timelineData.length
})
const milestoneCount = computed(() => {
return state.timelineData.filter(item => item.type === 'milestone').length
})
const photoCount = computed(() => {
return state.timelineData.reduce((count, item) => {
return count + (item.photos ? item.photos.length : 0)
}, 0)
})
// 生命周期
onMounted(() => {
initPage()
})
// 初始化页面
const initPage = () => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
state.petId = currentPage.options.petId || '1'
loadPetInfo()
loadTimelineData()
}
// 加载宠物信息
const loadPetInfo = () => {
// 模拟从本地存储获取宠物信息
const mockPets = [
{
id: '1',
name: '小橘',
breed: '橘猫',
companionDays: 365,
avatar: '/static/cat-avatar.jpg',
adoptionDate: '2023-01-15'
},
{
id: '2',
name: '小白',
breed: '金毛',
companionDays: 1095,
avatar: '/static/dog-avatar.jpg',
adoptionDate: '2021-03-20'
}
]
state.petInfo = mockPets.find(pet => pet.id === state.petId) || mockPets[0]
}
// 加载时间轴数据
const loadTimelineData = () => {
// 模拟时间轴数据
const mockTimeline = [
{
id: '1',
type: 'milestone',
title: '第一次学会用猫砂',
content: '小橘今天终于学会了用猫砂盆!看着它小心翼翼地刨砂子的样子真是太可爱了,从此告别到处乱拉的日子。',
date: '2024-01-15',
photos: ['/static/milestone1.jpg', '/static/milestone2.jpg'],
data: null,
liked: true
},
{
id: '2',
type: 'health',
title: '疫苗接种',
content: '带小橘去宠物医院接种疫苗,医生说它身体很健康,体重也在正常范围内。',
date: '2024-01-10',
photos: ['/static/vaccine.jpg'],
data: {
'体重': '4.2kg',
'体温': '38.5°C',
'疫苗': '三联疫苗'
},
liked: false
},
{
id: '3',
type: 'daily',
title: '阳光午后',
content: '小橘最喜欢趴在阳台上晒太阳,懒洋洋的样子特别治愈。今天阳光特别好,它一直眯着眼睛享受温暖。',
date: '2024-01-08',
photos: ['/static/sunshine.jpg'],
data: null,
liked: true
},
{
id: '4',
type: 'milestone',
title: '第一次洗澡',
content: '小橘第一次洗澡,虽然有点紧张但表现得很乖。洗完后毛发变得特别柔软,香香的。',
date: '2024-01-05',
photos: ['/static/bath1.jpg', '/static/bath2.jpg', '/static/bath3.jpg'],
data: {
'耗时': '30分钟',
'水温': '38°C'
},
liked: true
},
{
id: '5',
type: 'growth',
title: '体重增长',
content: '小橘的体重从刚来时的3.2kg增长到了4.0kg,看起来更加健康强壮了。',
date: '2024-01-01',
photos: [],
data: {
'当前体重': '4.0kg',
'增长': '+0.8kg'
},
liked: false
}
]
state.timelineData = mockTimeline
}
// 获取节点样式类
const getNodeClass = (type) => {
return type
}
// 获取节点图标
const getNodeIcon = (type) => {
const iconMap = {
milestone: 'star',
health: 'heart',
daily: 'edit-pen',
growth: 'trending-up'
}
return iconMap[type] || 'circle'
}
// 格式化时间轴日期
const formatTimelineDate = (date) => {
const timelineDate = new Date(date)
const now = new Date()
const diffTime = now - timelineDate
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 0) return '今天'
if (diffDays === 1) return '昨天'
if (diffDays < 7) return `${diffDays}天前`
if (diffDays < 30) return `${Math.floor(diffDays / 7)}周前`
if (diffDays < 365) return `${Math.floor(diffDays / 30)}个月前`
const year = timelineDate.getFullYear()
const month = timelineDate.getMonth() + 1
const day = timelineDate.getDate()
return `${year}${month}${day}`
}
// 预览图片
const previewPhoto = (photos, index) => {
uni.previewImage({
urls: photos,
current: index
})
}
// 切换喜欢状态
const toggleLike = (item) => {
item.liked = !item.liked
// 这里可以调用API保存喜欢状态
uni.showToast({
title: item.liked ? '已收藏' : '已取消收藏',
icon: 'success',
duration: 1000
})
}
// 分享时间轴
const shareTimeline = (item) => {
uni.showActionSheet({
itemList: ['分享到微信', '分享到朋友圈', '复制链接'],
success: (res) => {
const actions = ['微信好友', '朋友圈', '复制链接']
uni.showToast({
title: `分享到${actions[res.tapIndex]}`,
icon: 'success'
})
}
})
}
// 显示时间轴菜单
const showTimelineMenu = (item) => {
uni.showActionSheet({
itemList: ['编辑', '删除', '设为封面'],
success: (res) => {
const actions = ['编辑记录', '删除记录', '设为封面']
if (res.tapIndex === 1) {
// 删除确认
uni.showModal({
title: '确认删除',
content: '确定要删除这条记录吗?',
success: (modalRes) => {
if (modalRes.confirm) {
const index = state.timelineData.findIndex(t => t.id === item.id)
if (index > -1) {
state.timelineData.splice(index, 1)
uni.showToast({
title: '删除成功',
icon: 'success'
})
}
}
}
})
} else {
uni.showToast({
title: actions[res.tapIndex],
icon: 'none'
})
}
}
})
}
// 显示日历
const showCalendar = () => {
uni.showToast({
title: '日历功能开发中',
icon: 'none'
})
}
// 加载更多时间轴
const loadMoreTimeline = () => {
if (state.loadingMore || !state.hasMoreTimeline) return
state.loadingMore = true
// 模拟加载更多数据
setTimeout(() => {
state.loadingMore = false
// 模拟没有更多数据
if (state.timelineData.length >= 10) {
state.hasMoreTimeline = false
}
}, 1000)
}
// 添加里程碑
const addMilestone = () => {
uni.navigateTo({
url: `/pages/pets/add-record-enhanced?petId=${state.petId}&category=milestone`
})
}
// 添加记忆
const addMemory = () => {
uni.navigateTo({
url: `/pages/pets/add-record-enhanced?petId=${state.petId}&category=daily`
})
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
return {
...state,
totalRecords,
milestoneCount,
photoCount,
getNodeClass,
getNodeIcon,
formatTimelineDate,
previewPhoto,
toggleLike,
shareTimeline,
showTimelineMenu,
showCalendar,
loadMoreTimeline,
addMilestone,
addMemory,
goBack
}
}
}
</script>
<style lang="scss" scoped>
.pet-timeline-container {
height: 100vh;
display: flex;
flex-direction: column;
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
}
.header-actions {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
margin: 20rpx 30rpx;
border-radius: 24rpx;
padding: 24rpx;
display: flex;
justify-content: space-between;
align-items: center;
box-shadow: 0 8rpx 32rpx rgba(255, 138, 128, 0.2);
border: 1rpx solid rgba(255, 255, 255, 0.3);
.page-title {
font-size: 32rpx;
font-weight: 600;
color: #FF8A80;
}
.calendar-btn {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 100%);
border-radius: 20rpx;
padding: 16rpx 24rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.4);
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
.calendar-text {
font-size: 28rpx;
color: #ffffff;
font-weight: 500;
}
}
}
.pet-info-card {
display: flex;
align-items: center;
padding: 20px;
background-color: #ffffff;
margin: 20px;
border-radius: 12px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.pet-info {
flex: 1;
margin-left: 15px;
.growth-stats {
display: flex;
gap: 20px;
margin-top: 10px;
.stat-item {
text-align: center;
}
}
}
}
.timeline-scroll {
flex: 1;
padding: 0 20px;
}
.timeline-container {
padding: 20px 0 120px 0;
}
.timeline-item {
position: relative;
display: flex;
margin-bottom: 30px;
.timeline-line {
position: absolute;
left: 20px;
top: 40px;
bottom: -30px;
width: 2px;
background-color: #e0e0e0;
}
.timeline-node {
width: 40px;
height: 40px;
border-radius: 20px;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
z-index: 1;
&.milestone {
background-color: #FFB74D;
}
&.health {
background-color: #81C784;
}
&.daily {
background-color: #64B5F6;
}
&.growth {
background-color: #F06292;
}
}
.timeline-content {
flex: 1;
margin-left: 15px;
background-color: #ffffff;
border-radius: 12px;
padding: 15px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.timeline-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.timeline-body {
margin-bottom: 15px;
.timeline-photos {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
.more-photos {
width: 60px;
height: 60px;
border-radius: 8px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
}
.timeline-data {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
}
.timeline-actions {
display: flex;
gap: 20px;
padding-top: 10px;
border-top: 1px solid #f0f0f0;
}
}
}
.timeline-end {
text-align: center;
padding: 40px 0;
.end-node {
width: 60px;
height: 60px;
border-radius: 30px;
background-color: #ff6b6b;
display: flex;
align-items: center;
justify-content: center;
margin: 0 auto 15px;
}
}
.load-more {
text-align: center;
padding: 20px 0;
}
.quick-actions {
position: fixed;
bottom: 30px;
right: 30px;
display: flex;
flex-direction: column;
gap: 15px;
.action-button {
width: 60px;
height: 60px;
border-radius: 30px;
background-color: #ff6b6b;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 2px;
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
}
}
</style>