This commit is contained in:
2025-08-12 17:12:37 +08:00
parent 715f117b64
commit 3f46e57c54
12 changed files with 3620 additions and 627 deletions
+643
View File
@@ -0,0 +1,643 @@
<template>
<view class="add-record-container">
<u-navbar title="添加记录" left-icon="arrow-left" @left-click="goBack">
<template #right>
<u-text text="保存" color="#ff6b6b" @click="saveRecord"></u-text>
</template>
</u-navbar>
<u-form ref="recordFormRef" :model="recordForm" :rules="rules" label-width="80">
<!-- 记录类型选择 -->
<u-card title="记录类型" :padding="20" margin="20">
<u-radio-group v-model="recordForm.category" @change="onCategoryChange">
<view class="category-grid">
<view
class="category-item"
v-for="category in recordCategories"
:key="category.value"
@click="selectCategory(category.value)"
>
<view class="category-icon" :class="{ active: recordForm.category === category.value }">
<u-icon :name="category.icon" size="24" :color="recordForm.category === category.value ? '#ffffff' : '#666666'"></u-icon>
</view>
<u-text :text="category.label" size="12" :color="recordForm.category === category.value ? '#ff6b6b' : '#666666'"></u-text>
</view>
</view>
</u-radio-group>
</u-card>
<!-- 基础信息 -->
<u-card title="基础信息" :padding="20" margin="20">
<u-form-item label="记录时间" prop="recordTime">
<u-input
v-model="recordForm.recordTime"
placeholder="选择记录时间"
readonly
@click="showDatePicker = true"
></u-input>
</u-form-item>
<u-form-item label="记录内容" prop="content">
<u-textarea
v-model="recordForm.content"
placeholder="描述一下具体情况..."
maxlength="500"
count
></u-textarea>
</u-form-item>
</u-card>
<!-- 动态表单区域 -->
<u-card :title="currentCategoryInfo.title" :padding="20" margin="20" v-if="currentCategoryInfo.fields.length > 0">
<view v-for="field in currentCategoryInfo.fields" :key="field.key">
<!-- 数字输入 -->
<u-form-item :label="field.label" v-if="field.type === 'number'">
<u-input
v-model="recordForm.details[field.key]"
:placeholder="field.placeholder"
type="number"
>
<template #suffix v-if="field.unit">
<u-text :text="field.unit" size="14" color="#999"></u-text>
</template>
</u-input>
</u-form-item>
<!-- 选择器 -->
<u-form-item :label="field.label" v-if="field.type === 'select'">
<u-input
v-model="recordForm.details[field.key]"
:placeholder="field.placeholder"
readonly
@click="showPicker(field)"
></u-input>
</u-form-item>
<!-- 多选标签 -->
<u-form-item :label="field.label" v-if="field.type === 'tags'">
<view class="tags-container">
<u-tag
v-for="option in field.options"
:key="option.value"
:text="option.label"
:type="isTagSelected(field.key, option.value) ? 'primary' : 'info'"
size="mini"
@click="toggleTag(field.key, option.value)"
></u-tag>
</view>
</u-form-item>
<!-- 文本输入 -->
<u-form-item :label="field.label" v-if="field.type === 'text'">
<u-input
v-model="recordForm.details[field.key]"
:placeholder="field.placeholder"
></u-input>
</u-form-item>
</view>
</u-card>
<!-- 图片上传 -->
<u-card title="添加图片" :padding="20" margin="20">
<view class="photo-upload-area">
<view class="photo-grid">
<view class="photo-item" v-for="(photo, index) in recordForm.photos" :key="index">
<u-image :src="photo" width="80px" height="80px" border-radius="8px"></u-image>
<view class="photo-delete" @click="removePhoto(index)">
<u-icon name="close-circle-fill" size="16" color="#ff6b6b"></u-icon>
</view>
</view>
<view class="photo-add" @click="choosePhotos" v-if="recordForm.photos.length < 9">
<u-icon name="camera-fill" size="24" color="#cccccc"></u-icon>
<u-text text="添加图片" size="12" color="#cccccc"></u-text>
</view>
</view>
<u-text :text="`${recordForm.photos.length}/9`" size="12" color="#999" style="margin-top: 10px;"></u-text>
</view>
</u-card>
<!-- 分享设置 -->
<u-card title="分享设置" :padding="20" margin="20">
<u-radio-group v-model="recordForm.shareLevel">
<view class="share-options">
<view class="share-option" v-for="option in shareOptions" :key="option.value">
<u-radio :name="option.value" :disabled="false">
<template #icon="{ checked }">
<u-icon :name="checked ? 'checkmark-circle-fill' : 'circle'" :color="checked ? '#ff6b6b' : '#c8c9cc'" size="18"></u-icon>
</template>
</u-radio>
<view class="share-info">
<u-text :text="option.label" size="14" bold></u-text>
<u-text :text="option.desc" size="12" color="#999"></u-text>
</view>
</view>
</view>
</u-radio-group>
</u-card>
</u-form>
<!-- 日期选择器 -->
<u-datetime-picker
:show="showDatePicker"
v-model="selectedDate"
mode="datetime"
@confirm="confirmDate"
@cancel="showDatePicker = false"
></u-datetime-picker>
<!-- 选择器弹窗 -->
<u-picker
:show="showPickerModal"
:columns="pickerColumns"
@confirm="confirmPicker"
@cancel="showPickerModal = false"
></u-picker>
</view>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
export default {
name: 'AddRecordEnhanced',
setup() {
// 响应式数据
const state = reactive({
petId: '',
showDatePicker: false,
showPickerModal: false,
selectedDate: new Date().getTime(),
currentPickerField: null,
pickerColumns: [],
recordForm: {
category: 'daily',
recordTime: formatDateTime(new Date()),
content: '',
details: {},
photos: [],
shareLevel: 'family'
}
})
// 记录分类配置
const recordCategories = [
{ value: 'daily', label: '随手记', icon: 'edit-pen' },
{ value: 'milestone', label: '大事记', icon: 'star' },
{ value: 'health', label: '健康记录', icon: 'heart' },
{ value: 'grooming', label: '洗护记录', icon: 'flower' },
{ value: 'cleaning', label: '清洁记录', icon: 'home' },
{ value: 'expense', label: '消费记录', icon: 'rmb-circle' }
]
// 分享选项
const shareOptions = [
{ value: 'public', label: '公开', desc: '所有用户可见' },
{ value: 'family', label: '家人', desc: '仅家庭成员可见' },
{ value: 'private', label: '私有', desc: '仅自己可见' }
]
// 记录类型字段配置
const categoryFields = {
daily: {
title: '随手记详情',
fields: []
},
milestone: {
title: '大事记详情',
fields: [
{
key: 'milestoneType',
label: '里程碑类型',
type: 'select',
placeholder: '选择里程碑类型',
options: [
{ label: '绝育', value: 'neuter' },
{ label: '第一次吃奶', value: 'first_milk' },
{ label: '第一次吃猫粮', value: 'first_food' },
{ label: '第一次用猫砂', value: 'first_litter' },
{ label: '第一次出门', value: 'first_outside' },
{ label: '生日', value: 'birthday' },
{ label: '到家纪念日', value: 'adoption_day' }
]
}
]
},
health: {
title: '健康记录详情',
fields: [
{
key: 'healthType',
label: '健康类型',
type: 'select',
placeholder: '选择健康记录类型',
options: [
{ label: '疫苗接种', value: 'vaccine' },
{ label: '驱虫', value: 'deworming' },
{ label: '体检', value: 'checkup' },
{ label: '看病', value: 'treatment' },
{ label: '给药', value: 'medication' },
{ label: '手术', value: 'surgery' }
]
},
{
key: 'weight',
label: '体重',
type: 'number',
placeholder: '输入体重',
unit: 'kg'
},
{
key: 'temperature',
label: '体温',
type: 'number',
placeholder: '输入体温',
unit: '°C'
},
{
key: 'symptoms',
label: '症状',
type: 'tags',
options: [
{ label: '食欲不振', value: 'loss_appetite' },
{ label: '精神萎靡', value: 'lethargy' },
{ label: '呕吐', value: 'vomiting' },
{ label: '腹泻', value: 'diarrhea' },
{ label: '发热', value: 'fever' },
{ label: '咳嗽', value: 'cough' }
]
}
]
},
grooming: {
title: '洗护记录详情',
fields: [
{
key: 'groomingType',
label: '洗护类型',
type: 'tags',
options: [
{ label: '洗澡', value: 'bath' },
{ label: '剪指甲', value: 'nail_trim' },
{ label: '洗耳朵', value: 'ear_clean' },
{ label: '刷牙', value: 'teeth_clean' },
{ label: '梳毛', value: 'brush' },
{ label: '美容', value: 'grooming' }
]
},
{
key: 'duration',
label: '耗时',
type: 'number',
placeholder: '输入耗时',
unit: '分钟'
}
]
},
cleaning: {
title: '清洁记录详情',
fields: [
{
key: 'cleaningType',
label: '清洁类型',
type: 'tags',
options: [
{ label: '换猫砂', value: 'litter_change' },
{ label: '洗猫砂盆', value: 'litter_box_clean' },
{ label: '洗食盆', value: 'food_bowl_clean' },
{ label: '洗水盆', value: 'water_bowl_clean' },
{ label: '清洁笼子', value: 'cage_clean' },
{ label: '消毒', value: 'disinfect' }
]
}
]
},
expense: {
title: '消费记录详情',
fields: [
{
key: 'amount',
label: '金额',
type: 'number',
placeholder: '输入消费金额',
unit: '元'
},
{
key: 'expenseType',
label: '消费类型',
type: 'select',
placeholder: '选择消费类型',
options: [
{ label: '食物', value: 'food' },
{ label: '医疗', value: 'medical' },
{ label: '用品', value: 'supplies' },
{ label: '服务', value: 'service' },
{ label: '其他', value: 'other' }
]
},
{
key: 'store',
label: '购买地点',
type: 'text',
placeholder: '输入购买地点'
}
]
}
}
// 计算属性
const currentCategoryInfo = computed(() => {
return categoryFields[state.recordForm.category] || { title: '', fields: [] }
})
// 表单验证规则
const rules = {
recordTime: [
{ required: true, message: '请选择记录时间', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入记录内容', trigger: 'blur' }
]
}
// 生命周期
onMounted(() => {
initPage()
})
// 初始化页面
const initPage = () => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
state.petId = currentPage.options.petId || '1'
}
// 选择记录类型
const selectCategory = (category) => {
state.recordForm.category = category
state.recordForm.details = {} // 清空详情数据
}
// 记录类型变化
const onCategoryChange = (value) => {
state.recordForm.details = {} // 清空详情数据
}
// 确认日期
const confirmDate = (e) => {
const date = new Date(e.value)
state.recordForm.recordTime = formatDateTime(date)
state.showDatePicker = false
}
// 显示选择器
const showPicker = (field) => {
state.currentPickerField = field
state.pickerColumns = [field.options.map(option => option.label)]
state.showPickerModal = true
}
// 确认选择器
const confirmPicker = (e) => {
if (state.currentPickerField) {
const selectedIndex = e.indexs[0]
const selectedOption = state.currentPickerField.options[selectedIndex]
state.recordForm.details[state.currentPickerField.key] = selectedOption.label
}
state.showPickerModal = false
}
// 标签选择
const isTagSelected = (fieldKey, value) => {
const currentValue = state.recordForm.details[fieldKey]
if (!currentValue) return false
return Array.isArray(currentValue) ? currentValue.includes(value) : currentValue === value
}
const toggleTag = (fieldKey, value) => {
if (!state.recordForm.details[fieldKey]) {
state.recordForm.details[fieldKey] = []
}
const currentValue = state.recordForm.details[fieldKey]
if (Array.isArray(currentValue)) {
const index = currentValue.indexOf(value)
if (index > -1) {
currentValue.splice(index, 1)
} else {
currentValue.push(value)
}
} else {
state.recordForm.details[fieldKey] = [value]
}
}
// 选择图片
const choosePhotos = () => {
const remainingCount = 9 - state.recordForm.photos.length
uni.chooseImage({
count: remainingCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
state.recordForm.photos.push(...res.tempFilePaths)
},
fail: (err) => {
console.error('选择图片失败', err)
uni.showToast({
title: '选择图片失败',
icon: 'error'
})
}
})
}
// 删除图片
const removePhoto = (index) => {
state.recordForm.photos.splice(index, 1)
}
// 保存记录
const saveRecord = () => {
// 表单验证
if (!state.recordForm.recordTime) {
uni.showToast({
title: '请选择记录时间',
icon: 'error'
})
return
}
if (!state.recordForm.content.trim()) {
uni.showToast({
title: '请输入记录内容',
icon: 'error'
})
return
}
// 构建记录数据
const recordData = {
id: Date.now().toString(),
petId: state.petId,
category: state.recordForm.category,
recordTime: state.recordForm.recordTime,
content: state.recordForm.content,
details: state.recordForm.details,
photos: state.recordForm.photos,
shareLevel: state.recordForm.shareLevel,
createTime: new Date().toISOString()
}
// 模拟保存到本地存储
try {
let records = uni.getStorageSync('petRecords') || []
records.unshift(recordData)
uni.setStorageSync('petRecords', records)
uni.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('保存记录失败', error)
uni.showToast({
title: '保存失败',
icon: 'error'
})
}
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
return {
...state,
recordCategories,
shareOptions,
currentCategoryInfo,
rules,
selectCategory,
onCategoryChange,
confirmDate,
showPicker,
confirmPicker,
isTagSelected,
toggleTag,
choosePhotos,
removePhoto,
saveRecord,
goBack
}
}
}
// 工具函数
function formatDateTime(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
}
</script>
<style lang="scss" scoped>
.add-record-container {
background-color: #f8f9fa;
min-height: 100vh;
}
.category-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 10px;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
}
.category-icon {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.active {
background-color: #ff6b6b;
}
}
.tags-container {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.photo-upload-area {
.photo-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.photo-item {
position: relative;
.photo-delete {
position: absolute;
top: -5px;
right: -5px;
background-color: #ffffff;
border-radius: 50%;
}
}
.photo-add {
width: 80px;
height: 80px;
border: 2px dashed #cccccc;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
}
}
.share-options {
.share-option {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.share-info {
flex: 1;
}
}
}
</style>
+328
View File
@@ -0,0 +1,328 @@
<template>
<view class="add-record-container">
<u-navbar title="添加记录" left-icon="arrow-left" @left-click="goBack">
<template #right>
<u-text text="保存" color="#ff6b6b" @click="saveRecord"></u-text>
</template>
</u-navbar>
<u-form ref="recordFormRef" :model="recordForm" :rules="rules" label-width="80">
<!-- 记录类型选择 -->
<u-card title="记录类型" :padding="20" margin="20">
<view class="category-grid">
<view
class="category-item"
v-for="category in recordCategories"
:key="category.value"
@click="selectCategory(category.value)"
>
<view class="category-icon" :class="{ active: recordForm.category === category.value }">
<u-icon :name="category.icon" size="24" :color="recordForm.category === category.value ? '#ffffff' : '#666666'"></u-icon>
</view>
<u-text :text="category.label" size="12" :color="recordForm.category === category.value ? '#ff6b6b' : '#666666'"></u-text>
</view>
</view>
</u-card>
<!-- 基础信息 -->
<u-card title="基础信息" :padding="20" margin="20">
<u-form-item label="记录时间" prop="recordTime">
<u-input
v-model="recordForm.recordTime"
placeholder="选择记录时间"
readonly
@click="showDatePicker = true"
></u-input>
</u-form-item>
<u-form-item label="记录内容" prop="content">
<u-textarea
v-model="recordForm.content"
placeholder="描述一下具体情况..."
maxlength="500"
count
></u-textarea>
</u-form-item>
</u-card>
<!-- 图片上传 -->
<u-card title="添加图片" :padding="20" margin="20">
<view class="photo-upload-area">
<view class="photo-grid">
<view class="photo-item" v-for="(photo, index) in recordForm.photos" :key="index">
<u-image :src="photo" width="80px" height="80px" border-radius="8px"></u-image>
<view class="photo-delete" @click="removePhoto(index)">
<u-icon name="close-circle-fill" size="16" color="#ff6b6b"></u-icon>
</view>
</view>
<view class="photo-add" @click="choosePhotos" v-if="recordForm.photos.length < 9">
<u-icon name="camera-fill" size="24" color="#cccccc"></u-icon>
<u-text text="添加图片" size="12" color="#cccccc"></u-text>
</view>
</view>
<u-text :text="`${recordForm.photos.length}/9`" size="12" color="#999" style="margin-top: 10px;"></u-text>
</view>
</u-card>
<!-- 分享设置 -->
<u-card title="分享设置" :padding="20" margin="20">
<u-radio-group v-model="recordForm.shareLevel">
<view class="share-options">
<view class="share-option" v-for="option in shareOptions" :key="option.value">
<u-radio :name="option.value" :disabled="false">
<template #icon="{ checked }">
<u-icon :name="checked ? 'checkmark-circle-fill' : 'circle'" :color="checked ? '#ff6b6b' : '#c8c9cc'" size="18"></u-icon>
</template>
</u-radio>
<view class="share-info">
<u-text :text="option.label" size="14" bold></u-text>
<u-text :text="option.desc" size="12" color="#999"></u-text>
</view>
</view>
</view>
</u-radio-group>
</u-card>
</u-form>
<!-- 日期选择器 -->
<u-datetime-picker
:show="showDatePicker"
v-model="selectedDate"
mode="datetime"
@confirm="confirmDate"
@cancel="showDatePicker = false"
></u-datetime-picker>
</view>
</template>
<script>
export default {
name: 'AddRecordSimple',
data() {
return {
petId: '',
showDatePicker: false,
selectedDate: new Date().getTime(),
recordForm: {
category: 'daily',
recordTime: '',
content: '',
photos: [],
shareLevel: 'family'
},
rules: {
recordTime: [
{ required: true, message: '请选择记录时间', trigger: 'blur' }
],
content: [
{ required: true, message: '请输入记录内容', trigger: 'blur' }
]
},
recordCategories: [
{ value: 'daily', label: '随手记', icon: 'edit-pen' },
{ value: 'milestone', label: '大事记', icon: 'star' },
{ value: 'health', label: '健康记录', icon: 'heart' },
{ value: 'grooming', label: '洗护记录', icon: 'flower' },
{ value: 'cleaning', label: '清洁记录', icon: 'home' },
{ value: 'expense', label: '消费记录', icon: 'rmb-circle' }
],
shareOptions: [
{ value: 'public', label: '公开', desc: '所有用户可见' },
{ value: 'family', label: '家人', desc: '仅家庭成员可见' },
{ value: 'private', label: '私有', desc: '仅自己可见' }
]
}
},
onLoad(options) {
this.petId = options.petId || '1'
this.recordForm.recordTime = this.formatDateTime(new Date())
},
methods: {
formatDateTime(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
const hours = date.getHours().toString().padStart(2, '0')
const minutes = date.getMinutes().toString().padStart(2, '0')
return `${year}-${month}-${day} ${hours}:${minutes}`
},
selectCategory(category) {
this.recordForm.category = category
},
confirmDate(e) {
const date = new Date(e.value)
this.recordForm.recordTime = this.formatDateTime(date)
this.showDatePicker = false
},
choosePhotos() {
const remainingCount = 9 - this.recordForm.photos.length
uni.chooseImage({
count: remainingCount,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.recordForm.photos.push(...res.tempFilePaths)
},
fail: (err) => {
console.error('选择图片失败', err)
uni.showToast({
title: '选择图片失败',
icon: 'error'
})
}
})
},
removePhoto(index) {
this.recordForm.photos.splice(index, 1)
},
saveRecord() {
// 表单验证
if (!this.recordForm.recordTime) {
uni.showToast({
title: '请选择记录时间',
icon: 'error'
})
return
}
if (!this.recordForm.content.trim()) {
uni.showToast({
title: '请输入记录内容',
icon: 'error'
})
return
}
// 构建记录数据
const recordData = {
id: Date.now().toString(),
petId: this.petId,
category: this.recordForm.category,
recordTime: this.recordForm.recordTime,
content: this.recordForm.content,
photos: this.recordForm.photos,
shareLevel: this.recordForm.shareLevel,
createTime: new Date().toISOString()
}
// 模拟保存到本地存储
try {
let records = uni.getStorageSync('petRecords') || []
records.unshift(recordData)
uni.setStorageSync('petRecords', records)
uni.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
console.error('保存记录失败', error)
uni.showToast({
title: '保存失败',
icon: 'error'
})
}
},
goBack() {
uni.navigateBack()
}
}
}
</script>
<style lang="scss" scoped>
.add-record-container {
background-color: #f8f9fa;
min-height: 100vh;
}
.category-grid {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 15px;
margin-top: 10px;
}
.category-item {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
cursor: pointer;
}
.category-icon {
width: 50px;
height: 50px;
border-radius: 25px;
background-color: #f5f5f5;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
&.active {
background-color: #ff6b6b;
}
}
.photo-upload-area {
.photo-grid {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 10px;
}
.photo-item {
position: relative;
.photo-delete {
position: absolute;
top: -5px;
right: -5px;
background-color: #ffffff;
border-radius: 50%;
}
}
.photo-add {
width: 80px;
height: 80px;
border: 2px dashed #cccccc;
border-radius: 8px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 5px;
}
}
.share-options {
.share-option {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 0;
border-bottom: 1px solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.share-info {
flex: 1;
}
}
}
</style>
+442
View File
@@ -0,0 +1,442 @@
<template>
<view class="pet-chat-container">
<u-navbar :title="`与${petInfo.name}聊天`" left-icon="arrow-left" @left-click="goBack">
<template #right>
<u-icon name="more-circle" size="20" @click="showMenu = true"></u-icon>
</template>
</u-navbar>
<!-- 宠物信息卡片 -->
<view class="pet-info-card">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="40" shape="circle"></u-avatar>
<view class="pet-info-text">
<u-text :text="petInfo.name" type="primary" size="14" bold></u-text>
<u-text :text="petInfo.personality" type="tips" size="12"></u-text>
</view>
<view class="chat-status">
<u-tag text="在线" type="success" size="mini"></u-tag>
</view>
</view>
<!-- 聊天消息列表 -->
<scroll-view
class="chat-messages"
scroll-y
:scroll-top="scrollTop"
scroll-with-animation
>
<view class="message-list">
<!-- 消息项 -->
<view
class="message-item"
v-for="message in messageList"
:key="message.id"
:class="message.type"
>
<view class="message-avatar" v-if="message.type === 'ai'">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
</view>
<view class="message-content">
<view class="message-bubble" :class="message.type">
<u-text :text="message.content" size="14" :color="message.type === 'user' ? '#ffffff' : '#333333'"></u-text>
</view>
<view class="message-time">
<u-text :text="formatTime(message.timestamp)" type="tips" size="10"></u-text>
</view>
</view>
<view class="message-avatar" v-if="message.type === 'user'">
<u-avatar src="/static/user-avatar.png" size="32" shape="circle"></u-avatar>
</view>
</view>
<!-- AI正在输入提示 -->
<view class="message-item ai" v-if="aiTyping">
<view class="message-avatar">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
</view>
<view class="message-content">
<view class="message-bubble ai typing">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input-area">
<view class="input-container">
<u-input
v-model="inputMessage"
placeholder="和你的宠物说点什么..."
:border="false"
:custom-style="inputStyle"
@confirm="sendMessage"
confirm-type="send"
></u-input>
<view class="input-actions">
<u-button
type="primary"
size="mini"
text="发送"
@click="sendMessage"
:disabled="!inputMessage.trim()"
></u-button>
</view>
</view>
</view>
<!-- 聊天菜单弹窗 -->
<u-popup v-model="showMenu" mode="bottom" border-radius="20">
<view class="chat-menu">
<view class="menu-title">
<u-text text="聊天设置" type="primary" size="16" bold></u-text>
</view>
<view class="menu-items">
<view class="menu-item" @click="clearHistory">
<u-icon name="delete" size="20" color="#ff6b6b"></u-icon>
<u-text text="清空聊天记录" size="14"></u-text>
</view>
</view>
<view class="menu-cancel" @click="showMenu = false">
<u-text text="取消" type="info" size="14"></u-text>
</view>
</view>
</u-popup>
</view>
</template>
<script>
export default {
name: 'PetChatSimple',
data() {
return {
petId: '',
petInfo: {},
messageList: [],
inputMessage: '',
scrollTop: 0,
aiTyping: false,
showMenu: false,
inputStyle: {
backgroundColor: '#f8f9fa',
borderRadius: '20px',
padding: '10px 15px'
}
}
},
onLoad(options) {
this.petId = options.petId || '1'
this.loadPetInfo()
this.loadChatHistory()
},
methods: {
// 加载宠物信息
loadPetInfo() {
const mockPets = [
{
id: '1',
name: '小橘',
breed: '橘猫',
personality: '活泼好动,喜欢撒娇',
avatar: '/static/cat-avatar.jpg'
},
{
id: '2',
name: '小白',
breed: '金毛',
personality: '温顺友好,聪明伶俐',
avatar: '/static/dog-avatar.jpg'
}
]
this.petInfo = mockPets.find(pet => pet.id === this.petId) || mockPets[0]
},
// 加载聊天历史
loadChatHistory() {
const mockMessages = [
{
id: '1',
type: 'ai',
content: `你好主人!我是${this.petInfo.name},今天感觉特别开心呢!有什么想和我聊的吗?`,
timestamp: Date.now() - 3600000
},
{
id: '2',
type: 'user',
content: '小橘今天吃饭怎么样?',
timestamp: Date.now() - 3500000
},
{
id: '3',
type: 'ai',
content: '今天的小鱼干特别香!我吃得可开心了,还想要更多呢~主人你也要记得按时吃饭哦!',
timestamp: Date.now() - 3400000
}
]
this.messageList = mockMessages
this.scrollToBottom()
},
// 发送消息
sendMessage() {
if (!this.inputMessage.trim()) return
const userMessage = {
id: Date.now().toString(),
type: 'user',
content: this.inputMessage.trim(),
timestamp: Date.now()
}
this.messageList.push(userMessage)
const messageContent = this.inputMessage.trim()
this.inputMessage = ''
this.scrollToBottom()
this.simulateAIReply(messageContent)
},
// 模拟AI回复
simulateAIReply(userMessage) {
this.aiTyping = true
this.scrollToBottom()
setTimeout(() => {
const aiResponse = this.generateAIResponse(userMessage)
const aiMessage = {
id: (Date.now() + 1).toString(),
type: 'ai',
content: aiResponse,
timestamp: Date.now()
}
this.messageList.push(aiMessage)
this.aiTyping = false
this.scrollToBottom()
}, 1500)
},
// 生成AI回复内容
generateAIResponse(userMessage) {
const petName = this.petInfo.name
const responses = [
`${petName}最喜欢和主人聊天了!今天过得怎么样呀?`,
`主人好!${petName}今天心情特别好呢~`,
`哇!主人来找我聊天了,${petName}好开心!`,
`${petName}听不太懂,但是很开心能和主人聊天!`,
`主人说的话${petName}都会认真听的!`
]
return responses[Math.floor(Math.random() * responses.length)]
},
// 滚动到底部
scrollToBottom() {
this.$nextTick(() => {
this.scrollTop = 99999
})
},
// 格式化时间
formatTime(timestamp) {
const date = new Date(timestamp)
const now = new Date()
const diff = now - date
if (diff < 60000) return '刚刚'
if (diff < 3600000) return `${Math.floor(diff / 60000)}分钟前`
if (diff < 86400000) return `${Math.floor(diff / 3600000)}小时前`
return `${date.getMonth() + 1}月${date.getDate()}日`
},
// 清空聊天记录
clearHistory() {
uni.showModal({
title: '确认清空',
content: '确定要清空所有聊天记录吗?',
success: (res) => {
if (res.confirm) {
this.messageList = []
this.showMenu = false
uni.showToast({
title: '已清空聊天记录',
icon: 'success'
})
}
}
})
},
// 返回上一页
goBack() {
uni.navigateBack()
}
}
}
</script>
<style lang="scss" scoped>
.pet-chat-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
.pet-info-card {
display: flex;
align-items: center;
padding: 15px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
.pet-info-text {
flex: 1;
margin-left: 10px;
}
.chat-status {
margin-left: 10px;
}
}
.chat-messages {
flex: 1;
padding: 0 15px;
}
.message-list {
padding: 20px 0;
}
.message-item {
display: flex;
margin-bottom: 20px;
&.user {
flex-direction: row-reverse;
.message-content {
align-items: flex-end;
}
.message-bubble {
background-color: #FF8A80;
color: #ffffff;
}
}
&.ai {
.message-bubble {
background-color: #ffffff;
border: 1px solid #f0f0f0;
}
}
}
.message-avatar {
margin: 0 10px;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
.message-bubble {
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
&.typing {
padding: 16px;
}
}
.message-time {
margin-top: 5px;
text-align: center;
}
.typing-dots {
display: flex;
gap: 4px;
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #ccc;
animation: typing 1.4s infinite ease-in-out;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input-area {
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
padding: 15px 20px;
padding-bottom: calc(15px + env(safe-area-inset-bottom));
}
.input-container {
display: flex;
align-items: center;
gap: 10px;
}
.input-actions {
flex-shrink: 0;
}
.chat-menu {
padding: 20px;
.menu-title {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.menu-items {
padding: 20px 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 0;
}
.menu-cancel {
text-align: center;
padding: 15px 0;
border-top: 1px solid #f0f0f0;
}
}
</style>
+639
View File
@@ -0,0 +1,639 @@
<template>
<view class="pet-chat-container">
<u-navbar :title="`与${petInfo.name}聊天`" left-icon="arrow-left" @left-click="goBack">
<template #right>
<u-icon name="more-circle" size="20" @click="showChatMenu"></u-icon>
</template>
</u-navbar>
<!-- 宠物信息卡片 -->
<view class="pet-info-card">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="40" shape="circle"></u-avatar>
<view class="pet-info-text">
<u-text :text="petInfo.name" type="primary" size="14" bold></u-text>
<u-text :text="petInfo.personality" type="tips" size="12"></u-text>
</view>
<view class="chat-status">
<u-tag text="在线" type="success" size="mini"></u-tag>
</view>
</view>
<!-- 聊天消息列表 -->
<scroll-view
class="chat-messages"
scroll-y
:scroll-top="scrollTop"
scroll-with-animation
@scrolltolower="loadMoreMessages"
>
<view class="message-list">
<!-- 加载更多提示 -->
<view class="load-more" v-if="hasMoreMessages">
<u-loading-icon v-if="loadingMore"></u-loading-icon>
<u-text text="上拉加载更多历史消息" type="tips" size="12" v-else></u-text>
</view>
<!-- 消息项 -->
<view
class="message-item"
v-for="message in messageList"
:key="message.id"
:class="message.type"
>
<view class="message-avatar" v-if="message.type === 'ai'">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
</view>
<view class="message-content">
<view class="message-bubble" :class="message.type">
<u-text :text="message.content" size="14" :color="message.type === 'user' ? '#ffffff' : '#333333'"></u-text>
</view>
<view class="message-time">
<u-text :text="formatTime(message.timestamp)" type="tips" size="10"></u-text>
</view>
</view>
<view class="message-avatar" v-if="message.type === 'user'">
<u-avatar src="/static/user-avatar.png" size="32" shape="circle"></u-avatar>
</view>
</view>
<!-- AI正在输入提示 -->
<view class="message-item ai" v-if="aiTyping">
<view class="message-avatar">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="32" shape="circle"></u-avatar>
</view>
<view class="message-content">
<view class="message-bubble ai typing">
<view class="typing-dots">
<view class="dot"></view>
<view class="dot"></view>
<view class="dot"></view>
</view>
</view>
</view>
</view>
</view>
</scroll-view>
<!-- 输入区域 -->
<view class="chat-input-area">
<view class="input-container">
<u-input
v-model="inputMessage"
placeholder="和你的宠物说点什么..."
:border="false"
:custom-style="inputStyle"
@keyboardheightchange="onKeyboardHeightChange"
@confirm="sendMessage"
confirm-type="send"
></u-input>
<view class="input-actions">
<u-icon name="mic" size="20" color="#999" @click="startVoiceInput" v-if="!inputMessage"></u-icon>
<u-button
type="primary"
size="mini"
text="发送"
@click="sendMessage"
:disabled="!inputMessage.trim()"
v-else
></u-button>
</view>
</view>
</view>
<!-- 聊天菜单弹窗 -->
<u-popup v-model="showMenu" mode="bottom" border-radius="20">
<view class="chat-menu">
<view class="menu-title">
<u-text text="聊天设置" type="primary" size="16" bold></u-text>
</view>
<view class="menu-items">
<view class="menu-item" @click="clearHistory">
<u-icon name="delete" size="20" color="#ff6b6b"></u-icon>
<u-text text="清空聊天记录" size="14"></u-text>
</view>
<view class="menu-item" @click="exportHistory">
<u-icon name="download" size="20" color="#64B5F6"></u-icon>
<u-text text="导出聊天记录" size="14"></u-text>
</view>
<view class="menu-item" @click="togglePersonality">
<u-icon name="setting" size="20" color="#81C784"></u-icon>
<u-text text="个性化设置" size="14"></u-text>
</view>
</view>
<view class="menu-cancel" @click="showMenu = false">
<u-text text="取消" type="info" size="14"></u-text>
</view>
</view>
</u-popup>
</view>
</template>
<script>
import { ref, reactive, onMounted, nextTick } from 'vue'
export default {
name: 'PetChat',
setup() {
// 响应式数据
const state = reactive({
petId: '',
petInfo: {},
messageList: [],
inputMessage: '',
scrollTop: 0,
aiTyping: false,
showMenu: false,
hasMoreMessages: true,
loadingMore: false,
keyboardHeight: 0
})
// 样式配置
const inputStyle = {
backgroundColor: '#f8f9fa',
borderRadius: '20px',
padding: '10px 15px'
}
// 生命周期
onMounted(() => {
initPage()
})
// 初始化页面
const initPage = () => {
// 获取宠物ID(从路由参数)
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
state.petId = currentPage.options.petId || '1'
loadPetInfo()
loadChatHistory()
}
// 加载宠物信息
const loadPetInfo = () => {
// 模拟从本地存储获取宠物信息
const mockPets = [
{
id: '1',
name: '小橘',
breed: '橘猫',
personality: '活泼好动,喜欢撒娇',
avatar: '/static/cat-avatar.jpg'
},
{
id: '2',
name: '小白',
breed: '金毛',
personality: '温顺友好,聪明伶俐',
avatar: '/static/dog-avatar.jpg'
}
]
state.petInfo = mockPets.find(pet => pet.id === state.petId) || mockPets[0]
}
// 加载聊天历史
const loadChatHistory = () => {
// 模拟聊天历史数据
const mockMessages = [
{
id: '1',
type: 'ai',
content: `你好主人!我是${state.petInfo.name},今天感觉特别开心呢!有什么想和我聊的吗?`,
timestamp: Date.now() - 3600000
},
{
id: '2',
type: 'user',
content: '小橘今天吃饭怎么样?',
timestamp: Date.now() - 3500000
},
{
id: '3',
type: 'ai',
content: '今天的小鱼干特别香!我吃得可开心了,还想要更多呢~主人你也要记得按时吃饭哦!',
timestamp: Date.now() - 3400000
},
{
id: '4',
type: 'user',
content: '那你今天有没有调皮捣蛋?',
timestamp: Date.now() - 3300000
},
{
id: '5',
type: 'ai',
content: '嘿嘿,我今天把你的拖鞋藏起来了!不过是因为太想你了,想要你的味道陪着我~',
timestamp: Date.now() - 3200000
}
]
state.messageList = mockMessages
scrollToBottom()
}
// 发送消息
const sendMessage = () => {
if (!state.inputMessage.trim()) return
const userMessage = {
id: Date.now().toString(),
type: 'user',
content: state.inputMessage.trim(),
timestamp: Date.now()
}
state.messageList.push(userMessage)
const messageContent = state.inputMessage.trim()
state.inputMessage = ''
// 滚动到底部
scrollToBottom()
// 模拟AI回复
simulateAIReply(messageContent)
}
// 模拟AI回复
const simulateAIReply = (userMessage) => {
state.aiTyping = true
scrollToBottom()
setTimeout(() => {
const aiResponse = generateAIResponse(userMessage)
const aiMessage = {
id: (Date.now() + 1).toString(),
type: 'ai',
content: aiResponse,
timestamp: Date.now()
}
state.messageList.push(aiMessage)
state.aiTyping = false
scrollToBottom()
}, 1500 + Math.random() * 1000) // 1.5-2.5秒随机延迟
}
// 生成AI回复内容
const generateAIResponse = (userMessage) => {
const petName = state.petInfo.name
const responses = {
// 问候类
greeting: [
`${petName}最喜欢和主人聊天了!今天过得怎么样呀?`,
`主人好!${petName}今天心情特别好呢~`,
`哇!主人来找我聊天了,${petName}好开心!`
],
// 食物相关
food: [
`说到吃的,${petName}就兴奋了!今天的小鱼干特别香呢~`,
`${petName}最爱吃主人准备的美食了!什么时候再给我加餐呀?`,
`嘿嘿,${petName}今天偷偷多吃了一点,不要告诉其他人哦!`
],
// 健康相关
health: [
`${petName}今天身体棒棒的!谢谢主人这么关心我~`,
`${petName}觉得自己越来越健康了,都是主人照顾得好!`,
`主人真贴心!${petName}会好好保护自己的身体的!`
],
// 玩耍相关
play: [
`${petName}最喜欢和主人一起玩了!我们什么时候再玩游戏呀?`,
`玩耍时间到!${petName}已经准备好了,主人快来陪我!`,
`${petName}今天学会了新的小把戏,想不想看看?`
],
// 情感相关
emotion: [
`${petName}最爱主人了!每天都想和你在一起~`,
`主人不在的时候,${petName}会想念你的味道呢!`,
`${petName}觉得自己是世界上最幸福的小宝贝!`
],
// 默认回复
default: [
`${petName}听不太懂,但是很开心能和主人聊天!`,
`主人说的话${petName}都会认真听的!`,
`${petName}觉得主人说得很有道理呢!`,
`虽然${petName}不太明白,但是感觉很厉害的样子!`
]
}
// 简单的关键词匹配
const message = userMessage.toLowerCase()
let category = 'default'
if (message.includes('你好') || message.includes('hi') || message.includes('hello')) {
category = 'greeting'
} else if (message.includes('吃') || message.includes('食') || message.includes('饭') || message.includes('零食')) {
category = 'food'
} else if (message.includes('健康') || message.includes('身体') || message.includes('生病') || message.includes('医生')) {
category = 'health'
} else if (message.includes('玩') || message.includes('游戏') || message.includes('运动') || message.includes('散步')) {
category = 'play'
} else if (message.includes('爱') || message.includes('想') || message.includes('喜欢') || message.includes('亲亲')) {
category = 'emotion'
}
const categoryResponses = responses[category]
return categoryResponses[Math.floor(Math.random() * categoryResponses.length)]
}
// 滚动到底部
const scrollToBottom = () => {
nextTick(() => {
state.scrollTop = 99999
})
}
// 格式化时间
const formatTime = (timestamp) => {
const date = new Date(timestamp)
const now = new Date()
const diff = now - date
if (diff < 60000) { // 1分钟内
return '刚刚'
} else if (diff < 3600000) { // 1小时内
return `${Math.floor(diff / 60000)}分钟前`
} else if (diff < 86400000) { // 24小时内
return `${Math.floor(diff / 3600000)}小时前`
} else {
return `${date.getMonth() + 1}月${date.getDate()}日`
}
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
// 显示聊天菜单
const showChatMenu = () => {
state.showMenu = true
}
// 加载更多消息
const loadMoreMessages = () => {
if (state.loadingMore || !state.hasMoreMessages) return
state.loadingMore = true
// 模拟加载历史消息
setTimeout(() => {
const moreMessages = [
{
id: `history_${Date.now()}`,
type: 'ai',
content: `主人,${state.petInfo.name}想你了!`,
timestamp: Date.now() - 7200000
}
]
state.messageList.unshift(...moreMessages)
state.loadingMore = false
// 模拟没有更多消息
if (state.messageList.length > 20) {
state.hasMoreMessages = false
}
}, 1000)
}
// 清空聊天记录
const clearHistory = () => {
uni.showModal({
title: '确认清空',
content: '确定要清空所有聊天记录吗?此操作不可恢复。',
success: (res) => {
if (res.confirm) {
state.messageList = []
state.showMenu = false
uni.showToast({
title: '已清空聊天记录',
icon: 'success'
})
}
}
})
}
// 导出聊天记录
const exportHistory = () => {
uni.showToast({
title: '导出功能开发中',
icon: 'none'
})
state.showMenu = false
}
// 个性化设置
const togglePersonality = () => {
uni.showToast({
title: '个性化设置开发中',
icon: 'none'
})
state.showMenu = false
}
// 语音输入
const startVoiceInput = () => {
uni.showToast({
title: '语音功能开发中',
icon: 'none'
})
}
// 键盘高度变化
const onKeyboardHeightChange = (e) => {
state.keyboardHeight = e.detail.height
if (e.detail.height > 0) {
scrollToBottom()
}
}
return {
...state,
inputStyle,
sendMessage,
formatTime,
goBack,
showChatMenu,
loadMoreMessages,
clearHistory,
exportHistory,
togglePersonality,
startVoiceInput,
onKeyboardHeightChange
}
}
}
</script>
<style lang="scss" scoped>
.pet-chat-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
.pet-info-card {
display: flex;
align-items: center;
padding: 15px 20px;
background-color: #ffffff;
border-bottom: 1px solid #f0f0f0;
.pet-info-text {
flex: 1;
margin-left: 10px;
}
.chat-status {
margin-left: 10px;
}
}
.chat-messages {
flex: 1;
padding: 0 15px;
}
.message-list {
padding: 20px 0;
}
.load-more {
text-align: center;
padding: 20px 0;
}
.message-item {
display: flex;
margin-bottom: 20px;
&.user {
flex-direction: row-reverse;
.message-content {
align-items: flex-end;
}
.message-bubble {
background-color: #FF8A80;
color: #ffffff;
}
}
&.ai {
.message-bubble {
background-color: #ffffff;
border: 1px solid #f0f0f0;
}
}
}
.message-avatar {
margin: 0 10px;
}
.message-content {
display: flex;
flex-direction: column;
max-width: 70%;
}
.message-bubble {
padding: 12px 16px;
border-radius: 18px;
word-wrap: break-word;
&.typing {
padding: 16px;
}
}
.message-time {
margin-top: 5px;
text-align: center;
}
.typing-dots {
display: flex;
gap: 4px;
.dot {
width: 6px;
height: 6px;
border-radius: 50%;
background-color: #ccc;
animation: typing 1.4s infinite ease-in-out;
&:nth-child(1) { animation-delay: -0.32s; }
&:nth-child(2) { animation-delay: -0.16s; }
}
}
@keyframes typing {
0%, 80%, 100% {
transform: scale(0.8);
opacity: 0.5;
}
40% {
transform: scale(1);
opacity: 1;
}
}
.chat-input-area {
background-color: #ffffff;
border-top: 1px solid #f0f0f0;
padding: 15px 20px;
padding-bottom: calc(15px + env(safe-area-inset-bottom));
}
.input-container {
display: flex;
align-items: center;
gap: 10px;
}
.input-actions {
flex-shrink: 0;
}
.chat-menu {
padding: 20px;
.menu-title {
text-align: center;
padding-bottom: 20px;
border-bottom: 1px solid #f0f0f0;
}
.menu-items {
padding: 20px 0;
}
.menu-item {
display: flex;
align-items: center;
gap: 15px;
padding: 15px 0;
border-bottom: 1px solid #f8f9fa;
&:last-child {
border-bottom: none;
}
}
.menu-cancel {
text-align: center;
padding: 15px 0;
border-top: 1px solid #f0f0f0;
}
}
</style>
+32 -30
View File
@@ -54,7 +54,7 @@
<view class="action-buttons">
<u-button type="primary" text="添加记录" @click="addRecord"></u-button>
<u-button type="success" text="健康档案" @click="viewHealth"></u-button>
<u-button type="warning" text="AI聊天" @click="chatWithPet"></u-button>
</view>
</view>
</template>
@@ -85,33 +85,29 @@ export default {
},
methods: {
loadPetInfo() {
// 模拟从本地存储或API获取宠物信息
const mockPets = [
{
id: 1,
name: '小橘',
breed: '橘猫',
age: 2,
companionDays: 365,
avatar: '/static/cat-avatar.jpg',
gender: '公',
weight: '4.5kg',
birthday: '2022-01-15'
},
{
id: 2,
name: '小白',
breed: '金毛',
age: 3,
companionDays: 1095,
avatar: '/static/dog-avatar.jpg',
gender: '母',
weight: '25kg',
birthday: '2021-03-20'
try {
// 从本地存储获取宠物信息
const pets = uni.getStorageSync('pets') || []
this.petInfo = pets.find(pet => pet.id == this.petId) || {}
// 如果没有找到宠物信息,使用默认数据
if (!this.petInfo.id) {
this.petInfo = {
id: this.petId,
name: '未知宠物',
breed: '未知品种',
age: 0,
companionDays: 0,
avatar: '/static/default-pet.png',
gender: '未知',
weight: '0kg',
birthday: '未知'
}
}
]
this.petInfo = mockPets.find(pet => pet.id == this.petId) || {}
} catch (error) {
console.error('加载宠物信息失败', error)
this.petInfo = {}
}
},
goBack() {
@@ -126,10 +122,16 @@ export default {
addRecord() {
uni.navigateTo({
url: `/pages/pets/add-record?petId=${this.petId}`
url: `/pages/pets/add-record-simple?petId=${this.petId}`
})
},
chatWithPet() {
uni.navigateTo({
url: `/pages/pets/pet-chat-simple?petId=${this.petId}`
})
},
viewHealth() {
uni.navigateTo({
url: `/pages/pets/health-record?petId=${this.petId}`
@@ -202,7 +204,7 @@ export default {
padding: 30rpx;
display: flex;
gap: 20rpx;
:deep(.u-button) {
flex: 1;
}
+587
View File
@@ -0,0 +1,587 @@
<template>
<view class="pet-records-container">
<u-navbar :title="`${petInfo.name}的记录`" left-icon="arrow-left" @left-click="goBack">
<template #right>
<u-icon name="plus-circle" size="20" @click="addRecord"></u-icon>
</template>
</u-navbar>
<!-- 筛选栏 -->
<view class="filter-bar">
<u-tabs
:list="filterTabs"
v-model="activeFilter"
@change="onFilterChange"
:scrollable="true"
active-color="#ff6b6b"
inactive-color="#666666"
></u-tabs>
</view>
<!-- 记录列表 -->
<scroll-view class="records-scroll" scroll-y @scrolltolower="loadMoreRecords">
<view class="records-list">
<!-- 时间分组 -->
<view v-for="group in groupedRecords" :key="group.date" class="record-group">
<view class="group-header">
<u-text :text="group.date" size="14" color="#999" bold></u-text>
<view class="group-line"></view>
</view>
<!-- 记录项 -->
<view v-for="record in group.records" :key="record.id" class="record-item" @click="viewRecordDetail(record)">
<view class="record-icon">
<u-icon :name="getCategoryIcon(record.category)" size="20" :color="getCategoryColor(record.category)"></u-icon>
</view>
<view class="record-content">
<view class="record-header">
<u-text :text="getCategoryName(record.category)" size="14" bold></u-text>
<u-text :text="formatTime(record.recordTime)" size="12" color="#999"></u-text>
</view>
<view class="record-text">
<u-text :text="record.content" size="13" color="#666" :lines="2"></u-text>
</view>
<!-- 记录详情标签 -->
<view class="record-tags" v-if="getRecordTags(record).length > 0">
<u-tag
v-for="tag in getRecordTags(record)"
:key="tag"
:text="tag"
type="info"
size="mini"
></u-tag>
</view>
<!-- 图片预览 -->
<view class="record-photos" v-if="record.photos && record.photos.length > 0">
<u-image
v-for="(photo, index) in record.photos.slice(0, 3)"
:key="index"
:src="photo"
width="40px"
height="40px"
border-radius="4px"
></u-image>
<view class="more-photos" v-if="record.photos.length > 3">
<u-text :text="`+${record.photos.length - 3}`" size="12" color="#999"></u-text>
</view>
</view>
</view>
<!-- 分享状态 -->
<view class="record-share">
<u-icon :name="getShareIcon(record.shareLevel)" size="16" :color="getShareColor(record.shareLevel)"></u-icon>
</view>
</view>
</view>
<!-- 加载更多 -->
<view class="load-more" v-if="hasMoreRecords">
<u-loading-icon v-if="loadingMore"></u-loading-icon>
<u-text text="上拉加载更多" type="tips" size="12" v-else></u-text>
</view>
<!-- 空状态 -->
<u-empty v-if="recordsList.length === 0 && !loadingMore" mode="data" text="还没有记录哦">
<template #bottom>
<u-button type="primary" text="添加第一条记录" @click="addRecord"></u-button>
</template>
</u-empty>
</view>
</scroll-view>
<!-- 浮动添加按钮 -->
<view class="fab-button" @click="addRecord">
<u-icon name="plus" size="24" color="#ffffff"></u-icon>
</view>
</view>
</template>
<script>
import { ref, reactive, computed, onMounted } from 'vue'
export default {
name: 'PetRecords',
setup() {
// 响应式数据
const state = reactive({
petId: '',
petInfo: {},
recordsList: [],
activeFilter: 0,
hasMoreRecords: true,
loadingMore: false,
page: 1,
pageSize: 20
})
// 筛选标签
const filterTabs = [
{ name: '全部' },
{ name: '随手记' },
{ name: '大事记' },
{ name: '健康记录' },
{ name: '洗护记录' },
{ name: '清洁记录' },
{ name: '消费记录' }
]
// 记录分类配置
const categoryConfig = {
daily: { name: '随手记', icon: 'edit-pen', color: '#64B5F6' },
milestone: { name: '大事记', icon: 'star', color: '#FFB74D' },
health: { name: '健康记录', icon: 'heart', color: '#81C784' },
grooming: { name: '洗护记录', icon: 'flower', color: '#F06292' },
cleaning: { name: '清洁记录', icon: 'home', color: '#9575CD' },
expense: { name: '消费记录', icon: 'rmb-circle', color: '#FF8A80' }
}
// 生命周期
onMounted(() => {
initPage()
})
// 初始化页面
const initPage = () => {
const pages = getCurrentPages()
const currentPage = pages[pages.length - 1]
state.petId = currentPage.options.petId || '1'
loadPetInfo()
loadRecords()
}
// 加载宠物信息
const loadPetInfo = () => {
// 模拟从本地存储获取宠物信息
const mockPets = [
{
id: '1',
name: '小橘',
breed: '橘猫',
avatar: '/static/cat-avatar.jpg'
},
{
id: '2',
name: '小白',
breed: '金毛',
avatar: '/static/dog-avatar.jpg'
}
]
state.petInfo = mockPets.find(pet => pet.id === state.petId) || mockPets[0]
}
// 加载记录
const loadRecords = () => {
// 模拟记录数据
const mockRecords = [
{
id: '1',
petId: state.petId,
category: 'daily',
recordTime: '2024-01-15 14:30',
content: '小橘今天特别活泼,一直在客厅里跑来跑去,看起来心情很好!',
details: {},
photos: ['/static/cat1.jpg', '/static/cat2.jpg'],
shareLevel: 'family',
createTime: '2024-01-15T14:30:00.000Z'
},
{
id: '2',
petId: state.petId,
category: 'health',
recordTime: '2024-01-14 10:00',
content: '带小橘去宠物医院打疫苗,医生说身体很健康',
details: {
healthType: '疫苗接种',
weight: '4.2',
temperature: '38.5'
},
photos: ['/static/vaccine.jpg'],
shareLevel: 'family',
createTime: '2024-01-14T10:00:00.000Z'
},
{
id: '3',
petId: state.petId,
category: 'milestone',
recordTime: '2024-01-13 16:20',
content: '小橘第一次学会用猫砂盆,真是个聪明的小家伙!',
details: {
milestoneType: '第一次用猫砂'
},
photos: [],
shareLevel: 'public',
createTime: '2024-01-13T16:20:00.000Z'
},
{
id: '4',
petId: state.petId,
category: 'grooming',
recordTime: '2024-01-12 09:15',
content: '给小橘洗澡和剪指甲,全程很乖很配合',
details: {
groomingType: ['洗澡', '剪指甲'],
duration: '45'
},
photos: ['/static/grooming.jpg'],
shareLevel: 'family',
createTime: '2024-01-12T09:15:00.000Z'
},
{
id: '5',
petId: state.petId,
category: 'expense',
recordTime: '2024-01-11 18:30',
content: '购买了新的猫粮和猫砂',
details: {
amount: '268',
expenseType: '用品',
store: '宠物用品店'
},
photos: [],
shareLevel: 'private',
createTime: '2024-01-11T18:30:00.000Z'
}
]
state.recordsList = mockRecords
}
// 按日期分组记录
const groupedRecords = computed(() => {
const filtered = getFilteredRecords()
const groups = {}
filtered.forEach(record => {
const date = formatDate(record.recordTime)
if (!groups[date]) {
groups[date] = []
}
groups[date].push(record)
})
return Object.keys(groups)
.sort((a, b) => new Date(b) - new Date(a))
.map(date => ({
date: formatDateLabel(date),
records: groups[date].sort((a, b) => new Date(b.recordTime) - new Date(a.recordTime))
}))
})
// 获取筛选后的记录
const getFilteredRecords = () => {
if (state.activeFilter === 0) {
return state.recordsList
}
const filterMap = {
1: 'daily',
2: 'milestone',
3: 'health',
4: 'grooming',
5: 'cleaning',
6: 'expense'
}
const targetCategory = filterMap[state.activeFilter]
return state.recordsList.filter(record => record.category === targetCategory)
}
// 获取分类名称
const getCategoryName = (category) => {
return categoryConfig[category]?.name || '未知类型'
}
// 获取分类图标
const getCategoryIcon = (category) => {
return categoryConfig[category]?.icon || 'file-text'
}
// 获取分类颜色
const getCategoryColor = (category) => {
return categoryConfig[category]?.color || '#999999'
}
// 获取记录标签
const getRecordTags = (record) => {
const tags = []
if (record.details) {
// 健康记录标签
if (record.category === 'health') {
if (record.details.healthType) tags.push(record.details.healthType)
if (record.details.weight) tags.push(`${record.details.weight}kg`)
if (record.details.symptoms && Array.isArray(record.details.symptoms)) {
tags.push(...record.details.symptoms.slice(0, 2))
}
}
// 洗护记录标签
if (record.category === 'grooming') {
if (record.details.groomingType && Array.isArray(record.details.groomingType)) {
tags.push(...record.details.groomingType.slice(0, 3))
}
if (record.details.duration) tags.push(`${record.details.duration}分钟`)
}
// 消费记录标签
if (record.category === 'expense') {
if (record.details.amount) tags.push(`¥${record.details.amount}`)
if (record.details.expenseType) tags.push(record.details.expenseType)
}
// 大事记标签
if (record.category === 'milestone') {
if (record.details.milestoneType) tags.push(record.details.milestoneType)
}
// 清洁记录标签
if (record.category === 'cleaning') {
if (record.details.cleaningType && Array.isArray(record.details.cleaningType)) {
tags.push(...record.details.cleaningType.slice(0, 3))
}
}
}
return tags.slice(0, 3) // 最多显示3个标签
}
// 获取分享图标
const getShareIcon = (shareLevel) => {
const iconMap = {
public: 'globe',
family: 'home',
private: 'lock'
}
return iconMap[shareLevel] || 'lock'
}
// 获取分享颜色
const getShareColor = (shareLevel) => {
const colorMap = {
public: '#81C784',
family: '#64B5F6',
private: '#FFB74D'
}
return colorMap[shareLevel] || '#999999'
}
// 格式化时间
const formatTime = (dateTime) => {
const time = dateTime.split(' ')[1]
return time ? time.substring(0, 5) : ''
}
// 筛选变化
const onFilterChange = (index) => {
state.activeFilter = index
}
// 查看记录详情
const viewRecordDetail = (record) => {
// 这里可以跳转到记录详情页面
uni.showToast({
title: '记录详情功能开发中',
icon: 'none'
})
}
// 添加记录
const addRecord = () => {
uni.navigateTo({
url: `/pages/pets/add-record-enhanced?petId=${state.petId}`
})
}
// 加载更多记录
const loadMoreRecords = () => {
if (state.loadingMore || !state.hasMoreRecords) return
state.loadingMore = true
// 模拟加载更多数据
setTimeout(() => {
state.loadingMore = false
// 模拟没有更多数据
if (state.recordsList.length >= 10) {
state.hasMoreRecords = false
}
}, 1000)
}
// 返回上一页
const goBack = () => {
uni.navigateBack()
}
return {
...state,
filterTabs,
groupedRecords,
getFilteredRecords,
getCategoryName,
getCategoryIcon,
getCategoryColor,
getRecordTags,
getShareIcon,
getShareColor,
formatTime,
onFilterChange,
viewRecordDetail,
addRecord,
loadMoreRecords,
goBack
}
}
}
// 工具函数
function formatDate(dateTime) {
return dateTime.split(' ')[0]
}
function formatDateLabel(date) {
const today = new Date()
const recordDate = new Date(date)
const diffTime = today - recordDate
const diffDays = Math.floor(diffTime / (1000 * 60 * 60 * 24))
if (diffDays === 0) return '今天'
if (diffDays === 1) return '昨天'
if (diffDays === 2) return '前天'
const month = recordDate.getMonth() + 1
const day = recordDate.getDate()
return `${month}月${day}日`
}
</script>
<style lang="scss" scoped>
.pet-records-container {
height: 100vh;
display: flex;
flex-direction: column;
background-color: #f8f9fa;
}
.filter-bar {
background-color: #ffffff;
padding: 0 20px;
border-bottom: 1px solid #f0f0f0;
}
.records-scroll {
flex: 1;
padding: 0 20px;
}
.records-list {
padding: 20px 0 100px 0;
}
.record-group {
margin-bottom: 30px;
.group-header {
display: flex;
align-items: center;
margin-bottom: 15px;
.group-line {
flex: 1;
height: 1px;
background-color: #f0f0f0;
margin-left: 15px;
}
}
}
.record-item {
display: flex;
align-items: flex-start;
gap: 15px;
padding: 15px;
background-color: #ffffff;
border-radius: 12px;
margin-bottom: 10px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
.record-icon {
width: 40px;
height: 40px;
border-radius: 20px;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.record-content {
flex: 1;
.record-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 8px;
}
.record-text {
margin-bottom: 10px;
}
.record-tags {
display: flex;
flex-wrap: wrap;
gap: 5px;
margin-bottom: 10px;
}
.record-photos {
display: flex;
gap: 5px;
align-items: center;
.more-photos {
width: 40px;
height: 40px;
border-radius: 4px;
background-color: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
}
}
}
.record-share {
flex-shrink: 0;
}
}
.load-more {
text-align: center;
padding: 20px 0;
}
.fab-button {
position: fixed;
bottom: 100px;
right: 30px;
width: 56px;
height: 56px;
border-radius: 28px;
background-color: #ff6b6b;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(255, 107, 107, 0.3);
z-index: 999;
}
</style>
+604
View File
@@ -0,0 +1,604 @@
<template>
<view class="pet-timeline-container">
<u-navbar :title="`${petInfo.name}的成长时光`" left-icon="arrow-left" @left-click="goBack">
<template #right>
<u-icon name="calendar" size="20" @click="showCalendar"></u-icon>
</template>
</u-navbar>
<!-- 宠物信息卡片 -->
<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-color: #f8f9fa;
}
.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>
+258 -26
View File
@@ -1,32 +1,137 @@
<template>
<view class="pets-container">
<u-navbar title="我的宠物" :border="false" bg-color="#ffffff">
<template #right>
<u-icon name="plus-circle-fill" color="#ff6b6b" size="24" @click="addPet"></u-icon>
</template>
</u-navbar>
<view class="pets-list" v-if="petsList.length > 0">
<u-card v-for="pet in petsList" :key="pet.id" :margin="20" :padding="20" @click="viewPetDetail(pet)">
<view class="pet-card-content">
<u-avatar :src="pet.avatar || '/static/default-pet.png'" size="60" shape="circle"></u-avatar>
<view class="pet-info">
<u-text :text="pet.name" type="primary" size="16" bold></u-text>
<u-text :text="pet.breed" type="info" size="14"></u-text>
<u-text :text="`${pet.age}岁 · 陪伴${pet.companionDays}天`" type="tips" size="12"></u-text>
<!-- 自定义导航栏 -->
<view class="custom-navbar">
<view class="navbar-content">
<view class="navbar-title">
<text class="title-text">🐾 我的宠物</text>
<text class="subtitle-text">{{ petsList.length }}只小可爱陪伴着你</text>
</view>
<view class="navbar-actions">
<view class="action-btn" @click="showQuickActions = true">
<u-icon name="apps" color="#FF8A80" size="20"></u-icon>
</view>
<view class="pet-actions">
<u-button type="primary" size="mini" @click.stop="addRecord(pet)">记录</u-button>
<view class="action-btn add-btn" @click="addPet">
<u-icon name="plus" color="#ffffff" size="18"></u-icon>
</view>
</view>
</u-card>
</view>
</view>
<u-empty v-else mode="data" text="还没有添加宠物哦">
<template #bottom>
<u-button type="primary" text="添加第一只宠物" @click="addPet"></u-button>
</template>
</u-empty>
<!-- 统计卡片 -->
<view class="stats-card" v-if="petsList.length > 0">
<view class="stats-item">
<view class="stats-number">{{ totalRecords }}</view>
<view class="stats-label">记录</view>
</view>
<view class="stats-item">
<view class="stats-number">{{ totalDays }}</view>
<view class="stats-label">陪伴天数</view>
</view>
<view class="stats-item">
<view class="stats-number">{{ upcomingReminders }}</view>
<view class="stats-label">待办提醒</view>
</view>
</view>
<!-- 宠物列表 -->
<view class="pets-list" v-if="petsList.length > 0">
<view class="pet-card" v-for="pet in petsList" :key="pet.id" @click="viewPetDetail(pet)">
<!-- 宠物头像和基本信息 -->
<view class="pet-header">
<view class="pet-avatar-container">
<u-avatar :src="pet.avatar || '/static/default-pet.png'" size="70" shape="circle"></u-avatar>
<view class="pet-status" :class="pet.healthStatus || 'healthy'">
<u-icon :name="getHealthIcon(pet.healthStatus)" size="12" color="#ffffff"></u-icon>
</view>
</view>
<view class="pet-basic-info">
<view class="pet-name-row">
<text class="pet-name">{{ pet.name }}</text>
<view class="pet-gender" :class="pet.gender === '公' ? 'male' : 'female'">
<u-icon :name="pet.gender === '公' ? 'man' : 'woman'" size="12" color="#ffffff"></u-icon>
</view>
</view>
<text class="pet-breed">{{ pet.breed }}</text>
<text class="pet-age">{{ pet.age }}岁 · 陪伴{{ pet.companionDays }}天</text>
</view>
</view>
<!-- 宠物特征标签 -->
<view class="pet-tags" v-if="pet.personality && pet.personality.length > 0">
<u-tag
v-for="tag in pet.personality.slice(0, 3)"
:key="tag"
:text="tag"
type="primary"
size="mini"
plain
></u-tag>
</view>
<!-- 快捷操作按钮 -->
<view class="pet-actions">
<view class="action-item" @click.stop="addRecord(pet)">
<u-icon name="edit-pen" size="16" color="#81C784"></u-icon>
<text class="action-text">记录</text>
</view>
<view class="action-item" @click.stop="chatWithPet(pet)">
<u-icon name="chat" size="16" color="#64B5F6"></u-icon>
<text class="action-text">聊天</text>
</view>
<view class="action-item" @click.stop="viewHealth(pet)">
<u-icon name="heart" size="16" color="#FF8A80"></u-icon>
<text class="action-text">健康</text>
</view>
<view class="action-item" @click.stop="viewTimeline(pet)">
<u-icon name="clock" size="16" color="#FFB74D"></u-icon>
<text class="action-text">时光</text>
</view>
</view>
</view>
</view>
<!-- 空状态 -->
<view class="empty-state" v-else>
<image class="empty-image" src="/static/empty-pets.png" mode="aspectFit"></image>
<text class="empty-title">还没有小伙伴呢</text>
<text class="empty-desc">添加你的第一只宠物,开始记录美好时光吧~</text>
<u-button
class="add-first-pet-btn"
type="primary"
text="添加第一只宠物"
shape="round"
@click="addPet"
></u-button>
</view>
<!-- 快捷操作弹窗 -->
<u-popup v-model="showQuickActions" mode="bottom" border-radius="20">
<view class="quick-actions-popup">
<view class="popup-header">
<text class="popup-title">快捷操作</text>
<u-icon name="close" size="20" @click="showQuickActions = false"></u-icon>
</view>
<view class="quick-actions-grid">
<view class="quick-action-item" @click="navigateToRecords">
<u-icon name="list" size="24" color="#81C784"></u-icon>
<text class="quick-action-text">所有记录</text>
</view>
<view class="quick-action-item" @click="navigateToStats">
<u-icon name="bar-chart" size="24" color="#64B5F6"></u-icon>
<text class="quick-action-text">数据统计</text>
</view>
<view class="quick-action-item" @click="navigateToReminders">
<u-icon name="bell" size="24" color="#FFB74D"></u-icon>
<text class="quick-action-text">提醒事项</text>
</view>
<view class="quick-action-item" @click="exportData">
<u-icon name="share" size="24" color="#FF8A80"></u-icon>
<text class="quick-action-text">数据导出</text>
</view>
</view>
</view>
</u-popup>
</view>
</template>
@@ -34,35 +139,159 @@
export default {
data() {
return {
petsList: []
petsList: [],
showQuickActions: false,
totalRecords: 0,
totalDays: 0,
upcomingReminders: 0
}
},
onShow() {
this.loadPets()
this.loadStatistics()
},
computed: {
// 计算总陪伴天数
totalCompanionDays() {
return this.petsList.reduce((total, pet) => total + (pet.companionDays || 0), 0)
}
},
methods: {
loadPets() {
try {
const pets = uni.getStorageSync('pets') || []
let pets = uni.getStorageSync('pets') || []
// 如果没有宠物数据,添加一些模拟数据
if (pets.length === 0) {
pets = [
{
id: 1,
name: '小橘',
breed: '橘猫',
age: 2,
companionDays: 365,
avatar: '/static/default-pet.png',
gender: '公',
weight: '4.5kg',
birthday: '2022-01-15',
healthStatus: 'healthy',
personality: ['活泼', '粘人', '贪吃'],
lastRecord: '2024-01-15'
},
{
id: 2,
name: '小白',
breed: '金毛',
age: 3,
companionDays: 1095,
avatar: '/static/default-pet.png',
gender: '母',
weight: '25kg',
birthday: '2021-03-20',
healthStatus: 'healthy',
personality: ['温顺', '聪明', '忠诚'],
lastRecord: '2024-01-14'
}
]
// 保存模拟数据到本地存储
uni.setStorageSync('pets', pets)
}
this.petsList = pets
} catch (error) {
console.error('加载宠物列表失败', error)
this.petsList = []
}
},
loadStatistics() {
try {
// 加载统计数据
const records = uni.getStorageSync('petRecords') || []
this.totalRecords = records.length
// 计算总陪伴天数
this.totalDays = this.petsList.reduce((total, pet) => total + (pet.companionDays || 0), 0)
// 模拟待办提醒数量
this.upcomingReminders = Math.floor(Math.random() * 5) + 1
} catch (error) {
console.error('加载统计数据失败', error)
}
},
getHealthIcon(status) {
const iconMap = {
healthy: 'checkmark',
warning: 'warning',
sick: 'close'
}
return iconMap[status] || 'checkmark'
},
addPet() {
uni.navigateTo({
url: '/pages/pets/add-pet'
})
},
viewPetDetail(pet) {
uni.navigateTo({
url: `/pages/pets/pet-detail?id=${pet.id}`
})
},
chatWithPet(pet) {
uni.navigateTo({
url: `/pages/pets/pet-chat-simple?petId=${pet.id}`
})
},
addRecord(pet) {
uni.navigateTo({
url: `/pages/pets/add-record?petId=${pet.id}`
url: `/pages/pets/add-record-simple?petId=${pet.id}`
})
},
viewHealth(pet) {
uni.navigateTo({
url: `/pages/pets/health-charts?petId=${pet.id}`
})
},
viewTimeline(pet) {
uni.navigateTo({
url: `/pages/pets/pet-timeline?petId=${pet.id}`
})
},
// 快捷操作方法
navigateToRecords() {
this.showQuickActions = false
uni.navigateTo({
url: '/pages/pets/all-records'
})
},
navigateToStats() {
this.showQuickActions = false
uni.navigateTo({
url: '/pages/pets/statistics'
})
},
navigateToReminders() {
this.showQuickActions = false
uni.navigateTo({
url: '/pages/pets/reminders'
})
},
exportData() {
this.showQuickActions = false
uni.showToast({
title: '数据导出功能开发中',
icon: 'none'
})
}
}
@@ -90,6 +319,9 @@ export default {
.pet-actions {
margin-left: 20rpx;
display: flex;
gap: 8px;
flex-direction: column;
}
}