pet/pages/pets/pet-records.vue

1121 lines
24 KiB
Vue

<template>
<view class="pet-records-container page-container-with-bg">
<!-- 头部操作栏 -->
<view class="header-section">
<view class="header-info">
<text class="pet-name">{{ petName }}的记录</text>
<text class="record-count">{{ totalRecords }}条记录</text>
</view>
<view class="header-actions">
<view class="search-btn" @click="toggleSearch">
<text class="search-icon">🔍</text>
</view>
<view class="add-btn" @click="addRecord">
<text class="add-text">+ 添加</text>
</view>
</view>
</view>
<!-- 搜索栏 -->
<view class="search-section" v-if="showSearch">
<view class="search-input-wrapper">
<input
class="search-input"
v-model="searchKeyword"
placeholder="搜索记录内容、标签..."
@input="onSearchInput"
@confirm="performSearch"
/>
<view class="search-clear" v-if="searchKeyword" @click="clearSearch">
<text class="clear-icon">✕</text>
</view>
</view>
</view>
<!-- 分类筛选栏 -->
<view class="filter-section">
<view class="primary-filters">
<view
class="filter-tab"
:class="{ active: activeCategory === 'all' }"
@click="switchCategory('all')"
>
<text class="tab-text">全部</text>
</view>
<view
class="filter-tab"
:class="{ active: activeCategory === category.key }"
v-for="category in primaryCategories"
:key="category.key"
@click="switchCategory(category.key)"
>
<text class="tab-icon">{{ category.icon }}</text>
<text class="tab-text">{{ category.name }}</text>
</view>
</view>
<!-- 二级分类筛选 -->
<view class="secondary-filters" v-if="activeCategory !== 'all' && subCategories.length > 0">
<view
class="sub-filter-tab"
:class="{ active: activeSubCategory === 'all' }"
@click="switchSubCategory('all')"
>
<text class="sub-tab-text">全部</text>
</view>
<view
class="sub-filter-tab"
:class="{ active: activeSubCategory === subCategory.key }"
v-for="subCategory in subCategories"
:key="subCategory.key"
@click="switchSubCategory(subCategory.key)"
>
<text class="sub-tab-icon">{{ subCategory.icon }}</text>
<text class="sub-tab-text">{{ subCategory.name }}</text>
</view>
</view>
</view>
<!-- 排序选择 -->
<view class="sort-section">
<view class="sort-options">
<view
class="sort-option"
:class="{ active: sortBy === 'time' }"
@click="changeSortBy('time')"
>
<text class="sort-text">按时间</text>
</view>
<view
class="sort-option"
:class="{ active: sortBy === 'category' }"
@click="changeSortBy('category')"
>
<text class="sort-text">按分类</text>
</view>
<view
class="sort-option"
:class="{ active: sortBy === 'popularity' }"
@click="changeSortBy('popularity')"
>
<text class="sort-text">按热度</text>
</view>
</view>
</view>
<!-- 记录列表 -->
<scroll-view
class="records-scroll"
scroll-y
@scrolltolower="loadMoreRecords"
@refresherrefresh="onRefresh"
:refresher-enabled="true"
:refresher-triggered="refreshing"
>
<view class="records-list">
<!-- 记录项 -->
<view
class="record-item"
v-for="record in displayRecords"
:key="record.id"
@click="viewRecordDetail(record)"
>
<view class="record-header">
<view class="record-category">
<text class="category-icon">{{ getCategoryIcon(record.category, record.subCategory) }}</text>
<text class="category-name">{{ getCategoryName(record.category, record.subCategory) }}</text>
</view>
<view class="record-time">
<text class="time-text">{{ formatTime(record.recordTime) }}</text>
</view>
</view>
<view class="record-content">
<text class="record-title">{{ record.title }}</text>
<text class="record-text">{{ record.content }}</text>
</view>
<!-- 标签 -->
<view class="record-tags" v-if="record.tags && record.tags.length > 0">
<view
class="tag-item"
v-for="tag in record.tags.slice(0, 3)"
:key="tag"
>
<text class="tag-text"># {{ tag }}</text>
</view>
<view class="more-tags" v-if="record.tags.length > 3">
<text class="more-text">+{{ record.tags.length - 3 }}</text>
</view>
</view>
<!-- 图片预览 -->
<view class="record-photos" v-if="record.photos && record.photos.length > 0">
<view
class="photo-item"
v-for="(photo, index) in record.photos.slice(0, 3)"
:key="index"
>
<image class="photo-image" :src="photo" mode="aspectFill" />
</view>
<view class="more-photos" v-if="record.photos.length > 3">
<text class="more-photos-text">+{{ record.photos.length - 3 }}</text>
</view>
</view>
<!-- 社交数据 -->
<view class="record-social">
<view class="social-item">
<text class="social-icon">👁️</text>
<text class="social-count">{{ getSocialData(record.id).views }}</text>
</view>
<view class="social-item">
<text class="social-icon">❤️</text>
<text class="social-count">{{ getSocialData(record.id).likes }}</text>
</view>
<view class="social-item">
<text class="social-icon">💬</text>
<text class="social-count">{{ getSocialData(record.id).comments.length }}</text>
</view>
<view class="share-level">
<text class="share-icon">{{ getShareIcon(record.shareLevel) }}</text>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMoreRecords && !refreshing">
<view class="loading-indicator" v-if="loadingMore">
<text class="loading-text">加载中...</text>
</view>
<view class="load-more-tip" v-else>
<text class="tip-text">上拉加载更多</text>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-if="displayRecords.length === 0 && !loadingMore && !refreshing">
<text class="empty-icon">📝</text>
<text class="empty-title">{{ getEmptyTitle() }}</text>
<text class="empty-subtitle">{{ getEmptySubtitle() }}</text>
<view class="empty-action" @click="addRecord">
<text class="action-text">添加第一条记录</text>
</view>
</view>
</view>
</scroll-view>
<!-- 浮动添加按钮 -->
<view class="fab-button" @click="addRecord">
<text class="fab-icon">+</text>
</view>
</view>
</template>
<script>
// 记录管理器 - 从 utils/recordManager.js 移入
const recordManager = {
storageKey: 'pet_records',
getRecords(petId) {
try {
const allRecords = uni.getStorageSync(this.storageKey) || {}
let records = allRecords[petId] || []
if (records.length === 0) {
records = this.initializeTestData(petId)
allRecords[petId] = records
uni.setStorageSync(this.storageKey, allRecords)
}
return records.sort((a, b) => new Date(b.date) - new Date(a.date))
} catch (error) {
console.error('获取记录失败:', error)
return []
}
},
initializeTestData(petId) {
const now = new Date()
return [
{
id: Date.now() + 1,
petId: petId,
type: 'health',
title: '健康检查',
content: '定期健康检查,一切正常',
date: new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
images: [],
createTime: new Date().toISOString()
},
{
id: Date.now() + 2,
petId: petId,
type: 'feeding',
title: '喂食记录',
content: '今天食欲很好,吃完了所有的猫粮',
date: new Date(now.getTime() - 1 * 24 * 60 * 60 * 1000).toISOString().split('T')[0],
images: [],
createTime: new Date().toISOString()
}
]
},
addRecord(petId, recordData) {
try {
const allRecords = uni.getStorageSync(this.storageKey) || {}
if (!allRecords[petId]) {
allRecords[petId] = []
}
const newRecord = {
id: Date.now(),
petId: petId,
...recordData,
createTime: new Date().toISOString()
}
allRecords[petId].push(newRecord)
uni.setStorageSync(this.storageKey, allRecords)
return true
} catch (error) {
console.error('添加记录失败:', error)
return false
}
},
deleteRecord(petId, recordId) {
try {
const allRecords = uni.getStorageSync(this.storageKey) || {}
if (allRecords[petId]) {
allRecords[petId] = allRecords[petId].filter(record => record.id !== recordId)
uni.setStorageSync(this.storageKey, allRecords)
}
return true
} catch (error) {
console.error('删除记录失败:', error)
return false
}
}
}
export default {
data() {
return {
petId: '',
petName: '',
// 记录数据
allRecords: [],
displayRecords: [],
totalRecords: 0,
// 搜索功能
showSearch: false,
searchKeyword: '',
searchResults: [],
// 分类筛选
activeCategory: 'all',
activeSubCategory: 'all',
primaryCategories: [
{ key: 'health', name: '健康', icon: '🏥' },
{ key: 'care', name: '护理', icon: '🛁' },
{ key: 'behavior', name: '行为', icon: '🎾' },
{ key: 'daily', name: '日常', icon: '📝' },
{ key: 'expense', name: '消费', icon: '💰' }
],
subCategories: [],
// 排序
sortBy: 'time', // time, category, popularity
// 分页和加载
page: 1,
pageSize: 10,
hasMoreRecords: true,
loadingMore: false,
refreshing: false,
// 社交数据缓存
socialDataCache: {}
}
},
onLoad(options) {
this.petId = options.petId || ''
this.petName = options.petName || '宠物'
this.loadRecords()
},
methods: {
loadRecords() {
// 获取所有记录
this.allRecords = recordManager.getRecords(this.petId)
this.totalRecords = this.allRecords.length
// 初始化社交数据缓存
this.initSocialDataCache()
// 应用筛选和排序
this.applyFiltersAndSort()
},
initSocialDataCache() {
this.socialDataCache = {}
this.allRecords.forEach(record => {
this.socialDataCache[record.id] = recordManager.getSocialData(record.id)
})
},
applyFiltersAndSort() {
let filteredRecords = [...this.allRecords]
// 应用搜索筛选
if (this.searchKeyword) {
filteredRecords = recordManager.searchRecords(this.petId, this.searchKeyword)
}
// 应用分类筛选
if (this.activeCategory !== 'all') {
const subCategory = this.activeSubCategory !== 'all' ? this.activeSubCategory : null
filteredRecords = filteredRecords.filter(record => {
if (subCategory) {
return record.category === this.activeCategory && record.subCategory === subCategory
} else {
return record.category === this.activeCategory
}
})
}
// 应用排序
this.sortRecords(filteredRecords)
// 分页显示
this.displayRecords = filteredRecords.slice(0, this.page * this.pageSize)
this.hasMoreRecords = filteredRecords.length > this.displayRecords.length
},
sortRecords(records) {
switch (this.sortBy) {
case 'time':
records.sort((a, b) => new Date(b.recordTime) - new Date(a.recordTime))
break
case 'category':
records.sort((a, b) => {
if (a.category !== b.category) {
return a.category.localeCompare(b.category)
}
return new Date(b.recordTime) - new Date(a.recordTime)
})
break
case 'popularity':
records.sort((a, b) => {
const aPopularity = this.getSocialData(a.id).likes + this.getSocialData(a.id).views
const bPopularity = this.getSocialData(b.id).likes + this.getSocialData(b.id).views
return bPopularity - aPopularity
})
break
}
},
// 搜索功能
toggleSearch() {
this.showSearch = !this.showSearch
if (!this.showSearch) {
this.clearSearch()
}
},
onSearchInput() {
// 实时搜索
this.performSearch()
},
performSearch() {
this.page = 1
this.applyFiltersAndSort()
},
clearSearch() {
this.searchKeyword = ''
this.performSearch()
},
// 分类筛选
switchCategory(category) {
this.activeCategory = category
this.activeSubCategory = 'all'
this.page = 1
// 更新二级分类选项
this.updateSubCategories()
this.applyFiltersAndSort()
},
switchSubCategory(subCategory) {
this.activeSubCategory = subCategory
this.page = 1
this.applyFiltersAndSort()
},
updateSubCategories() {
if (this.activeCategory === 'all') {
this.subCategories = []
return
}
const categoryConfig = recordManager.categories[this.activeCategory]
if (categoryConfig && categoryConfig.subCategories) {
this.subCategories = Object.keys(categoryConfig.subCategories).map(key => ({
key: key,
name: categoryConfig.subCategories[key].name,
icon: categoryConfig.subCategories[key].icon
}))
} else {
this.subCategories = []
}
},
// 排序
changeSortBy(sortType) {
this.sortBy = sortType
this.page = 1
this.applyFiltersAndSort()
},
// 加载更多
loadMoreRecords() {
if (this.hasMoreRecords && !this.loadingMore) {
this.loadingMore = true
setTimeout(() => {
this.page += 1
this.applyFiltersAndSort()
this.loadingMore = false
}, 500)
}
},
// 下拉刷新
onRefresh() {
this.refreshing = true
setTimeout(() => {
this.loadRecords()
this.refreshing = false
}, 1000)
},
// 获取分类信息
getCategoryIcon(category, subCategory = null) {
const categoryInfo = recordManager.getCategoryInfo(category, subCategory)
return categoryInfo.icon
},
getCategoryName(category, subCategory = null) {
const categoryInfo = recordManager.getCategoryInfo(category, subCategory)
return categoryInfo.name
},
getCategoryColor(category) {
const categoryInfo = recordManager.getCategoryInfo(category)
return categoryInfo.color
},
// 获取社交数据
getSocialData(recordId) {
return this.socialDataCache[recordId] || {
likes: 0,
views: 0,
comments: []
}
},
// 分享级别图标
getShareIcon(shareLevel) {
const icons = {
'public': '🌍',
'family': '👨‍👩‍👧‍👦',
'private': '🔒'
}
return icons[shareLevel] || '👨‍👩‍👧‍👦'
},
// 时间格式化
formatTime(timeStr) {
if (!timeStr) return ''
const time = new Date(timeStr)
const now = new Date()
const diff = now - time
const minutes = Math.floor(diff / (1000 * 60))
const hours = Math.floor(diff / (1000 * 60 * 60))
const days = Math.floor(diff / (1000 * 60 * 60 * 24))
if (minutes < 60) {
return minutes <= 0 ? '刚刚' : `${minutes}分钟前`
} else if (hours < 24) {
return `${hours}小时前`
} else if (days < 7) {
return `${days}天前`
} else {
return `${time.getMonth() + 1}-${time.getDate()}`
}
},
// 空状态文案
getEmptyTitle() {
if (this.searchKeyword) {
return '没有找到相关记录'
} else if (this.activeCategory !== 'all') {
const categoryName = this.getCategoryName(this.activeCategory)
return `暂无${categoryName}记录`
} else {
return '还没有记录哦'
}
},
getEmptySubtitle() {
if (this.searchKeyword) {
return '试试其他关键词或清空搜索条件'
} else {
return '记录宠物的美好时光,从第一条开始'
}
},
// 页面跳转
viewRecordDetail(record) {
// 增加浏览量
recordManager.incrementViews(record.id)
this.socialDataCache[record.id].views += 1
uni.navigateTo({
url: `/pages/pets/record-detail?recordId=${record.id}&petId=${this.petId}&petName=${this.petName}`
})
},
addRecord() {
uni.navigateTo({
url: `/pages/pets/select-record-type?petId=${this.petId}&petName=${this.petName}`
})
}
}
}
</script>
<style lang="scss" scoped>
.pet-records-container {
background: linear-gradient(135deg, #FF8A80 0%, #FFB6C1 25%, #FECFEF 50%, #F8BBD9 100%);
min-height: 100vh;
}
/* 头部区域 */
.header-section {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
padding: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
.header-info {
.pet-name {
font-size: 32rpx;
font-weight: bold;
color: #333333;
display: block;
margin-bottom: 8rpx;
}
.record-count {
font-size: 22rpx;
color: #999999;
display: block;
}
}
.header-actions {
display: flex;
align-items: center;
gap: 16rpx;
.search-btn {
width: 64rpx;
height: 64rpx;
background: rgba(255, 138, 128, 0.1);
border-radius: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
background: rgba(255, 138, 128, 0.2);
}
.search-icon {
font-size: 24rpx;
}
}
.add-btn {
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 16rpx;
padding: 16rpx 24rpx;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
.add-text {
font-size: 24rpx;
color: white;
font-weight: 500;
}
}
}
}
/* 搜索区域 */
.search-section {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
padding: 0 32rpx 24rpx 32rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
.search-input-wrapper {
position: relative;
background: rgba(255, 138, 128, 0.05);
border-radius: 20rpx;
padding: 0 20rpx;
.search-input {
width: 100%;
height: 72rpx;
font-size: 24rpx;
color: #333333;
background: transparent;
border: none;
outline: none;
}
.search-clear {
position: absolute;
right: 20rpx;
top: 50%;
transform: translateY(-50%);
width: 32rpx;
height: 32rpx;
background: #CCCCCC;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
.clear-icon {
font-size: 16rpx;
color: white;
}
}
}
}
/* 筛选区域 */
.filter-section {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
padding: 24rpx 32rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
.primary-filters {
display: flex;
gap: 12rpx;
margin-bottom: 20rpx;
overflow-x: auto;
.filter-tab {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 8rpx;
padding: 12rpx 20rpx;
border-radius: 20rpx;
background: rgba(255, 138, 128, 0.1);
transition: all 0.3s ease;
&.active {
background: #FF8A80;
.tab-text {
color: white;
}
.tab-icon {
color: white;
}
}
&:active {
transform: scale(0.95);
}
.tab-icon {
font-size: 20rpx;
}
.tab-text {
font-size: 22rpx;
font-weight: 500;
color: #666666;
}
}
}
.secondary-filters {
display: flex;
gap: 8rpx;
overflow-x: auto;
.sub-filter-tab {
flex-shrink: 0;
display: flex;
align-items: center;
gap: 6rpx;
padding: 8rpx 16rpx;
border-radius: 16rpx;
background: rgba(255, 138, 128, 0.05);
transition: all 0.3s ease;
&.active {
background: rgba(255, 138, 128, 0.2);
.sub-tab-text {
color: #FF8A80;
font-weight: bold;
}
}
&:active {
transform: scale(0.95);
}
.sub-tab-icon {
font-size: 16rpx;
}
.sub-tab-text {
font-size: 20rpx;
color: #666666;
}
}
}
}
/* 排序区域 */
.sort-section {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
padding: 16rpx 32rpx;
border-bottom: 1rpx solid rgba(255, 255, 255, 0.3);
.sort-options {
display: flex;
gap: 24rpx;
.sort-option {
padding: 8rpx 16rpx;
border-radius: 12rpx;
background: rgba(255, 138, 128, 0.05);
transition: all 0.3s ease;
&.active {
background: rgba(255, 138, 128, 0.2);
.sort-text {
color: #FF8A80;
font-weight: bold;
}
}
&:active {
transform: scale(0.95);
}
.sort-text {
font-size: 20rpx;
color: #666666;
}
}
}
}
/* 记录列表区域 */
.records-scroll {
height: calc(100vh - 400rpx);
padding: 0 20rpx;
}
.records-list {
padding-bottom: 120rpx;
}
.record-item {
background: rgba(255, 255, 255, 0.95);
backdrop-filter: blur(20rpx);
border-radius: 20rpx;
padding: 24rpx;
margin-bottom: 16rpx;
box-shadow: 0 4rpx 16rpx rgba(255, 138, 128, 0.1);
border: 1rpx solid rgba(255, 255, 255, 0.3);
transition: all 0.3s ease;
&:active {
transform: scale(0.98);
box-shadow: 0 2rpx 8rpx rgba(255, 138, 128, 0.2);
}
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 16rpx;
.record-category {
display: flex;
align-items: center;
gap: 8rpx;
.category-icon {
font-size: 20rpx;
}
.category-name {
font-size: 22rpx;
font-weight: 500;
color: #666666;
}
}
.record-time {
.time-text {
font-size: 20rpx;
color: #999999;
}
}
}
.record-content {
margin-bottom: 16rpx;
.record-title {
font-size: 28rpx;
font-weight: bold;
color: #333333;
margin-bottom: 8rpx;
display: block;
}
.record-text {
font-size: 24rpx;
color: #666666;
line-height: 1.6;
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
display: block;
}
}
.record-tags {
display: flex;
flex-wrap: wrap;
gap: 8rpx;
margin-bottom: 16rpx;
.tag-item {
background: rgba(255, 138, 128, 0.1);
border-radius: 12rpx;
padding: 4rpx 12rpx;
.tag-text {
font-size: 18rpx;
color: #FF8A80;
}
}
.more-tags {
background: rgba(153, 153, 153, 0.1);
border-radius: 12rpx;
padding: 4rpx 12rpx;
.more-text {
font-size: 18rpx;
color: #999999;
}
}
}
.record-photos {
display: flex;
gap: 8rpx;
margin-bottom: 16rpx;
.photo-item {
width: 120rpx;
height: 120rpx;
border-radius: 12rpx;
overflow: hidden;
.photo-image {
width: 100%;
height: 100%;
}
}
.more-photos {
width: 120rpx;
height: 120rpx;
background: rgba(0, 0, 0, 0.1);
border-radius: 12rpx;
display: flex;
align-items: center;
justify-content: center;
.more-photos-text {
font-size: 20rpx;
color: #666666;
}
}
}
.record-social {
display: flex;
justify-content: space-between;
align-items: center;
.social-item {
display: flex;
align-items: center;
gap: 6rpx;
.social-icon {
font-size: 16rpx;
}
.social-count {
font-size: 18rpx;
color: #999999;
}
}
.share-level {
.share-icon {
font-size: 16rpx;
}
}
}
}
/* 加载更多和空状态 */
.load-more {
text-align: center;
padding: 40rpx 0;
.loading-indicator {
.loading-text {
font-size: 24rpx;
color: #999999;
}
}
.load-more-tip {
.tip-text {
font-size: 24rpx;
color: #CCCCCC;
}
}
}
.empty-state {
text-align: center;
padding: 120rpx 40rpx;
.empty-icon {
font-size: 120rpx;
margin-bottom: 32rpx;
display: block;
}
.empty-title {
font-size: 32rpx;
font-weight: bold;
color: #333333;
margin-bottom: 16rpx;
display: block;
}
.empty-subtitle {
font-size: 26rpx;
color: #999999;
margin-bottom: 40rpx;
display: block;
line-height: 1.5;
}
.empty-action {
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 20rpx;
padding: 16rpx 32rpx;
display: inline-block;
transition: all 0.3s ease;
&:active {
transform: scale(0.95);
}
.action-text {
font-size: 24rpx;
color: white;
font-weight: 500;
}
}
}
/* 浮动添加按钮 */
.fab-button {
position: fixed;
bottom: 120rpx;
right: 40rpx;
width: 112rpx;
height: 112rpx;
background: linear-gradient(135deg, #FF8A80, #FFB6C1);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 8rpx 24rpx rgba(255, 138, 128, 0.3);
z-index: 999;
transition: all 0.3s ease;
&:active {
transform: scale(0.9);
}
.fab-icon {
font-size: 40rpx;
color: white;
font-weight: bold;
}
}
</style>