This commit is contained in:
yvan 2025-08-12 17:12:37 +08:00
parent 715f117b64
commit 3f46e57c54
12 changed files with 3620 additions and 627 deletions

View File

@ -50,7 +50,7 @@
"quickapp" : {},
/* */
"mp-weixin" : {
"appid" : "",
"appid" : "wxdf340fe2d9aff21d",
"setting" : {
"urlCheck" : false
},

View File

@ -5,7 +5,8 @@
"style": {
"navigationBarTitleText": "我的宠物",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black"
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
@ -69,6 +70,60 @@
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/pet-chat",
"style": {
"navigationBarTitleText": "AI聊天",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/pet-chat-simple",
"style": {
"navigationBarTitleText": "AI聊天",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/add-record-enhanced",
"style": {
"navigationBarTitleText": "添加记录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/add-record-simple",
"style": {
"navigationBarTitleText": "添加记录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/pet-records",
"style": {
"navigationBarTitleText": "宠物记录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/pet-timeline",
"style": {
"navigationBarTitleText": "成长时光",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/index/index",
"style": {

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>

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>

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
pages/pets/pet-chat.vue Normal file
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>

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
pages/pets/pet-records.vue Normal file
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
pages/pets/pet-timeline.vue Normal file
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>

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;
}
}

View File

@ -1,152 +0,0 @@
# 宠物管家小程序技术栈选择建议
## 推荐方案uni-app + Vue3
### 选择理由
#### 1. 功能复杂度匹配
- 你的项目包含8个主要功能模块功能复杂度较高
- uni-app的组件化开发模式非常适合复杂业务场景
- Vue3的组合式API便于组织复杂的业务逻辑
#### 2. 开发效率优势
- **快速开发**丰富的UI组件库uView、uni-ui
- **代码复用**:组件可以在不同页面间复用
- **开发工具**HBuilderX提供可视化开发支持
#### 3. 跨平台能力
- 一套代码多端发布微信小程序、支付宝小程序、H5、App
- 为后续业务扩展提供技术基础
#### 4. 生态系统
- 插件市场丰富有现成的图表、AI对话等插件
- 社区活跃,问题解决方案多
## 具体技术栈配置
### 前端技术栈
```
核心框架uni-app + Vue3 + TypeScript
状态管理Pinia
UI组件库uView UI 2.0
图表库uCharts
工具库lodash、dayjs
```
### 后端技术栈
```
运行环境Node.js 16+
Web框架Express.js
数据库MongoDB + Redis
文件存储腾讯云COS
AI服务腾讯云AI或百度文心一言
推送服务:微信小程序模板消息
```
### 开发工具
```
IDEHBuilderX 或 VS Code + uni-app插件
版本控制Git
API测试Postman
数据库管理MongoDB Compass
```
## 项目结构建议
```
pet-miniprogram/
├── pages/ # 页面文件
│ ├── index/ # 首页
│ ├── pet/ # 宠物相关页面
│ │ ├── list/ # 宠物列表
│ │ ├── detail/ # 宠物详情
│ │ └── add/ # 添加宠物
│ ├── record/ # 记录相关页面
│ │ ├── list/ # 记录列表
│ │ ├── add/ # 添加记录
│ │ └── detail/ # 记录详情
│ ├── statistics/ # 统计页面
│ ├── family/ # 家庭管理
│ ├── adoption/ # 领养专区
│ ├── ai-assistant/ # AI助手
│ └── user/ # 用户中心
├── components/ # 公共组件
│ ├── pet-card/ # 宠物卡片
│ ├── record-item/ # 记录项
│ ├── chart/ # 图表组件
│ └── upload/ # 上传组件
├── static/ # 静态资源
├── store/ # 状态管理
│ ├── modules/
│ │ ├── user.js # 用户状态
│ │ ├── pet.js # 宠物状态
│ │ └── record.js # 记录状态
│ └── index.js
├── utils/ # 工具函数
│ ├── request.js # 网络请求
│ ├── auth.js # 认证相关
│ ├── upload.js # 文件上传
│ └── common.js # 通用工具
├── api/ # API接口
│ ├── user.js
│ ├── pet.js
│ ├── record.js
│ └── adoption.js
├── manifest.json # 应用配置
├── pages.json # 页面配置
└── App.vue # 应用入口
```
## 关键技术实现要点
### 1. 数据管理
- 使用Pinia进行状态管理
- 本地存储用户登录状态和常用数据
- 实现数据的离线缓存机制
### 2. 图片处理
- 集成腾讯云COS SDK
- 实现图片压缩和上传
- 支持多图上传和预览
### 3. 图表统计
- 使用uCharts实现消费统计图表
- 支持柱状图、饼图、折线图
- 响应式图表适配不同屏幕
### 4. AI对话
- 集成AI API腾讯云或百度
- 实现对话历史存储
- 支持语义识别和智能提醒
### 5. 消息推送
- 集成微信小程序模板消息
- 实现家庭成员消息通知
- 支持定时提醒功能
## 开发周期预估
基于uni-app + Vue3的开发效率预计开发周期
- **项目搭建**1周
- **核心功能开发**10-12周
- **测试优化**2-3周
- **总计**13-16周
## 学习资源推荐
1. **uni-app官方文档**https://uniapp.dcloud.net.cn/
2. **Vue3官方文档**https://cn.vuejs.org/
3. **uView UI文档**https://www.uviewui.com/
4. **微信小程序开发文档**https://developers.weixin.qq.com/miniprogram/dev/framework/
## 总结
对于你的宠物管家小程序项目,**uni-app + Vue3** 是最佳选择:
**开发效率高** - 适合复杂功能快速开发
**技术成熟** - 生态完善,社区支持好
**扩展性强** - 支持跨平台发布
**维护成本低** - 代码结构清晰,易于维护
建议立即开始使用这个技术栈进行项目开发!

View File

@ -1,419 +1,32 @@
# 宠物管家小程序第一版本功能拆分详细说明
## 1. 功能模块总览
### 1.1 核心功能模块
1. **用户系统** - 微信登录、用户信息管理
2. **宠物档案管理** - 宠物信息录入、展示、编辑
3. **日常记录系统** - 多类型记录的添加、查看、管理
4. **记账统计功能** - 消费记录、统计分析、图表展示
5. **分享权限管理** - 记录分享设置、隐私控制
6. **家庭共养功能** - 家庭创建、成员邀请、权限管理
7. **AI助手基础版** - 简单对话、知识查询、智能提醒
8. **领养专区** - 发布领养、浏览申请、沟通管理
## 2. 详细功能拆分
### 2.1 用户系统
#### 2.1.1 微信登录
**功能描述**:用户通过微信授权登录小程序
**具体实现**
- 调用微信小程序登录API获取code
- 后端通过code换取openid和session_key
- 生成JWT token返回给前端
- 前端存储token用于后续API调用
**页面设计**
- 登录页面:显示微信登录按钮
- 授权页面:获取用户基本信息授权
**数据字段**
- openid微信用户唯一标识
- nickname用户昵称
- avatar用户头像
- phone手机号可选
#### 2.1.2 用户信息管理
**功能描述**:用户可以查看和编辑个人信息
**具体实现**
- 个人中心页面展示用户信息
- 支持修改昵称、头像、手机号
- 头像支持拍照或从相册选择
### 2.2 宠物档案管理
#### 2.2.1 宠物信息录入
**功能描述**:用户可以添加宠物的基本信息
**必填字段**
- 宠物名字:文本输入
- 品种:下拉选择或文本输入
- 性别:单选(公/母)
- 生日:日期选择器
- 到家日期:日期选择器
**可选字段**
- 体重数字输入单位kg
- 毛色:文本输入
- 性格标签:多选标签
- 特殊说明:文本输入
- 头像:图片上传
**自动计算字段**
- 年龄:根据生日自动计算
- 陪伴天数:根据到家日期自动计算
- 星座:根据生日自动计算
#### 2.2.2 宠物信息展示
**首页宠物卡片**
- 宠物头像(圆形)
- 宠物名字
- 年龄和陪伴天数
- 快捷操作按钮(添加记录、查看详情)
**宠物详情页**
- 完整的宠物信息展示
- 编辑按钮
- 最近记录列表
- 统计数据概览
#### 2.2.3 多宠物管理
**功能描述**:支持用户管理多只宠物
**具体实现**
- 宠物列表页面
- 宠物切换功能
- 每只宠物独立的记录和统计
### 2.3 日常记录系统
#### 2.3.1 记录分类定义
**随手记**
- 内容:文字描述
- 图片可选最多9张
- 时间:默认当前时间,可修改
**大事记**
- 预设选项:绝育、第一次吃奶、第一次吃猫粮、第一次用猫砂、第一次搞破坏、第一次出门、第一次发情、生日、到家
- 自定义:用户可以添加自定义大事记
- 内容:文字描述
- 图片:可选
- 时间:必填
**日常记录**
- 体重数值输入kg+ 备注
- 尿便:次数 + 状态描述 + 图片
- 饮食:食物类型 + 分量 + 时间 + 图片
- 喝水水量ml+ 时间
- 记账:金额 + 类别 + 备注
**健康记录**
- 喂保健品:保健品名称 + 剂量 + 时间
- 驱虫:驱虫药名称 + 时间 + 下次提醒
- 疫苗:疫苗类型 + 时间 + 下次提醒
- 抗体水平:检测结果 + 时间
- 体检:体检项目 + 结果 + 图片
- 看病:症状 + 诊断 + 治疗方案 + 费用
- 给药:药品名称 + 剂量 + 频次 + 时间
- 住院:入院时间 + 出院时间 + 原因 + 费用
- 手术:手术类型 + 时间 + 费用 + 恢复情况
**洗护记录**
- 洗澡:时间 + 使用产品 + 图片
- 剪指甲:时间 + 备注
- 洗耳朵:时间 + 使用产品
- 刷牙:时间 + 使用产品
- 梳毛:时间 + 备注
- 剃脚毛:时间 + 备注
- 美容:时间 + 项目 + 费用 + 图片
- 挤肛门腺:时间 + 备注
- 擦眼屎:时间 + 备注
**清洁记录**
- 消毒:消毒区域 + 使用产品 + 时间
- 清洁:清洁项目 + 时间
- 换猫砂:时间 + 猫砂类型
- 洗猫砂盆:时间 + 清洁产品
- 洗食盆:时间
- 洗水盆:时间
- 洗玩具:时间 + 清洁方式
- 洗笼子:时间 + 清洁产品
- 换滤芯:设备名称 + 时间
- 换干燥剂:时间 + 位置
**自定义类型**
- 用户可以创建自定义记录类型
- 自定义字段配置
- 保存为模板供后续使用
#### 2.3.2 记录添加流程
1. 选择宠物(如果有多只)
2. 选择记录类型
3. 填写记录内容
4. 上传图片(可选)
5. 设置分享权限
6. 保存记录
#### 2.3.3 记录展示和管理
**时间线展示**
- 按时间倒序显示所有记录
- 不同类型用不同图标和颜色区分
- 支持下拉刷新和上拉加载
**分类筛选**
- 按记录类型筛选
- 按时间范围筛选
- 按宠物筛选(多宠物情况)
**记录详情**
- 完整的记录信息展示
- 图片查看(支持放大)
- 编辑和删除功能
### 2.4 记账统计功能
#### 2.4.1 消费记录
**记录字段**
- 金额:数字输入(精确到分)
- 类别:食物、医疗、用品、服务、其他
- 备注:文字描述
- 关联宠物:选择宠物(支持多选)
- 时间:默认当前时间,可修改
**快捷记录**
- 常用金额快捷按钮
- 常用类别快捷选择
- 语音输入金额
#### 2.4.2 统计分析
**宠物维度统计**
- 可以切换查看不同宠物的消费
- 支持查看所有宠物的总消费
**时间维度统计**
- 按月份统计:当月消费总额和分类占比
- 按年份统计:全年消费趋势
- 自定义时间范围统计
**图表展示**
- 柱状图:月度消费趋势
- 饼图:消费类别占比
- 折线图:消费变化趋势
#### 2.4.3 数据导出
- 支持导出Excel格式的消费记录
- 按时间范围和宠物筛选导出
- 包含详细的消费明细和统计汇总
### 2.5 分享权限管理
#### 2.5.1 分享设置
**三种分享模式**
- 公开:所有用户可见
- 仅家人:只有家庭成员可见
- 私有:只有自己可见
**设置方式**
- 添加记录时设置
- 记录详情页修改
- 批量修改多条记录
#### 2.5.2 默认设置
- 用户可以设置默认的分享权限
- 新记录自动使用默认设置
- 可以为不同类型的记录设置不同的默认权限
### 2.6 家庭共养功能
#### 2.6.1 家庭创建
**创建流程**
1. 用户点击创建家庭
2. 填写家庭名称
3. 选择要共享的宠物
4. 生成邀请码
**家庭信息**
- 家庭名称
- 创建时间
- 成员数量
- 共享宠物列表
#### 2.6.2 成员邀请
**邀请方式**
- 邀请码分享生成6位数字邀请码
- 微信直接邀请:通过微信分享邀请链接
**邀请流程**
1. 家庭主人发送邀请
2. 被邀请人点击链接或输入邀请码
3. 确认加入家庭
4. 设置成员权限
#### 2.6.3 权限管理
**角色定义**
- 主人:所有权限,可以邀请成员、设置权限、删除家庭
- 家人:可以查看和添加记录,不能删除其他人的记录
- 观察者:只能查看记录,不能添加或修改
**权限细分**
- 查看记录:所有角色都有
- 添加记录:主人和家人
- 编辑记录:只能编辑自己添加的记录
- 删除记录:只能删除自己添加的记录
- 管理宠物:只有主人可以
#### 2.6.4 消息推送
**推送场景**
- 新成员加入家庭
- 重要健康记录(生病、疫苗、体检等)
- 紧急情况记录
**推送方式**
- 微信小程序模板消息
- 站内消息通知
### 2.7 AI助手基础版
#### 2.7.1 基础对话功能
**对话界面**
- 聊天气泡样式
- 支持文字输入
- 显示AI回复
**知识问答**
- 宠物生病:常见疾病症状和处理建议
- 宠物训练:基础训练方法和技巧
- 宠物喂养:不同年龄段的喂养指导
- 紧急情况:紧急情况的处理步骤
#### 2.7.2 智能提醒功能
**语义识别**
- 识别时间表达明天、下周、9点等
- 识别任务内容:疫苗、体检、喂药等
- 生成结构化提醒
**提醒管理**
- 自动生成提醒记录
- 设置推送时间
- 提醒后询问完成状态
- 更新任务状态
#### 2.7.3 对话历史
**历史记录**
- 保存所有对话内容
- 按日期分组显示
- 支持搜索历史对话
**分页加载**
- 每页显示20条对话
- 支持上拉加载更多
- 新对话自动滚动到底部
#### 2.7.4 每日总结
**总结功能**
- 每天晚上自动总结当天对话
- 识别重要信息(健康问题、异常情况)
- 生成关注提醒
**主动关怀**
- 如果提到宠物生病,第二天主动询问情况
- 定期询问宠物状况
- 提供个性化建议
### 2.8 领养专区
#### 2.8.1 发布领养信息
**宠物信息**
- 基本信息:名字、性别、种类、年龄
- 健康状况:绝育、疫苗、驱虫、疾病情况
- 性格描述:文字描述宠物性格和习惯
- 图片上传最多9张宠物照片
**领养要求**
- 基础要求:年龄、居住条件、经济能力
- 特殊要求:养宠经验、是否同意绝育等
- 筛选问题:自定义问题列表
**发布设置**
- 有效期:可设置领养信息的有效期
- 联系方式:手机号或微信号
- 地区:选择所在城市和区域
#### 2.8.2 浏览和筛选
**列表展示**
- 宠物照片和基本信息
- 发布时间和地区
- 快速筛选标签
**筛选功能**
- 地区筛选:按省市区筛选
- 类型筛选:按宠物种类筛选
- 条件筛选:按年龄、性别、绝育状态筛选
**搜索功能**
- 关键词搜索
- 按品种搜索
- 按发布者搜索
#### 2.8.3 申请和沟通
**申请流程**
1. 查看领养详情
2. 填写申请信息
3. 回答筛选问题
4. 提交申请
5. 等待审核
**申请信息**
- 个人基本信息
- 养宠经验描述
- 居住环境说明
- 承诺和保证
**沟通功能**
- 简单的聊天界面
- 支持文字和图片
- 消息推送通知
#### 2.8.4 管理和审核
**发布者管理**
- 查看所有申请
- 筛选合适的申请者
- 与申请者沟通
- 选择最终领养者
**管理员审核**
- 审核发布的领养信息
- 处理举报和投诉
- 维护平台秩序
**状态管理**
- 发布中、已暂停、已完成
- 申请中、已通过、已拒绝
- 自动状态更新
## 3. 技术实现要点
### 3.1 数据存储
- 使用MongoDB存储所有数据
- 图片存储在腾讯云COS
- Redis缓存热点数据
### 3.2 API设计
- RESTful API设计
- JWT token认证
- 统一的错误处理和返回格式
### 3.3 性能优化
- 图片压缩和懒加载
- 分页加载减少数据量
- 缓存常用数据
### 3.4 安全考虑
- 用户数据加密存储
- API接口权限验证
- 图片内容安全检查
---
**文档版本**v1.0
**创建日期**2025年1月
**负责人**:开发团队
- 宠物
- 我的宠物记录(有多只宠物)
- 我的宠物信息(有多只宠物)
- 宠物信息详情
- 体重图表等等一些宠物健康相关的图形化记录
- 宠物信息修改
- 宠物性格预设
- 我的宠物智能体
- 宠物智能体聊天
- 宠物智能体互动通过live2d或者其他方式进行互动
- 我的宠物记录
- 添加宠物
- 创建记录
- 助手
- AI对话上半部分有这个助手的基本介绍介绍助手能做什么以及加一个宠物医生助手的动图头像之类的上面有几个常见问题点了就发送然后AI回答或者自己编辑询问等待回答。
- 测评
- 测评列表TAB上面选择猫或者狗默认为猫然后左边有猫粮/猫罐头/猫砂/等等,狗狗同理
- 测评详情
- 领养
- 领养宠物列表(地区筛选,种类筛选猫或者狗)
- 点击详情可以进行填写信息进行申请
- 我的
- 我的个人信息管理
- 登录
- 我的家庭
- 我的宠物概括(多少只,多少记录,多少提醒)
- 领养管理
- 进去显示已发布的管理列表
- 点进去显示申请领养人填写的申请记录
- 或者点击发布申请,你看一下放下右下角的悬浮按钮或者右上角