643 lines
17 KiB
Vue
643 lines
17 KiB
Vue
<template>
|
|
<view class="pet-timeline-container page-container-with-bg">
|
|
<!-- 头部操作栏 -->
|
|
<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/select-record-type?petId=${state.petId}&category=milestone`
|
|
})
|
|
}
|
|
|
|
// 添加记忆
|
|
const addMemory = () => {
|
|
uni.navigateTo({
|
|
url: `/pages/pets/select-record-type?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;
|
|
}
|
|
|
|
.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>
|