This commit is contained in:
parent
715f117b64
commit
3f46e57c54
|
|
@ -50,7 +50,7 @@
|
|||
"quickapp" : {},
|
||||
/* 小程序特有相关 */
|
||||
"mp-weixin" : {
|
||||
"appid" : "",
|
||||
"appid" : "wxdf340fe2d9aff21d",
|
||||
"setting" : {
|
||||
"urlCheck" : false
|
||||
},
|
||||
|
|
|
|||
57
pages.json
57
pages.json
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
152
技术栈选择建议.md
152
技术栈选择建议.md
|
|
@ -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或百度文心一言
|
||||
推送服务:微信小程序模板消息
|
||||
```
|
||||
|
||||
### 开发工具
|
||||
```
|
||||
IDE:HBuilderX 或 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** 是最佳选择:
|
||||
|
||||
✅ **开发效率高** - 适合复杂功能快速开发
|
||||
✅ **技术成熟** - 生态完善,社区支持好
|
||||
✅ **扩展性强** - 支持跨平台发布
|
||||
✅ **维护成本低** - 代码结构清晰,易于维护
|
||||
|
||||
建议立即开始使用这个技术栈进行项目开发!
|
||||
447
第一版本功能拆分详细说明.md
447
第一版本功能拆分详细说明.md
|
|
@ -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上面选择猫或者狗,默认为猫),然后左边有猫粮/猫罐头/猫砂/等等,狗狗同理
|
||||
- 测评详情
|
||||
- 领养
|
||||
- 领养宠物列表(地区筛选,种类筛选猫或者狗)
|
||||
- 点击详情可以进行填写信息进行申请
|
||||
- 我的
|
||||
- 我的个人信息管理
|
||||
- 登录
|
||||
- 我的家庭
|
||||
- 我的宠物概括(多少只,多少记录,多少提醒)
|
||||
- 领养管理
|
||||
- 进去显示已发布的管理列表
|
||||
- 点进去显示申请领养人填写的申请记录
|
||||
- 或者点击发布申请,你看一下放下右下角的悬浮按钮或者右上角
|
||||
Loading…
Reference in New Issue