完善宠物管理功能

 新增功能:
- 宠物详情页面:展示完整的宠物信息和最近记录
- 添加宠物页面:支持头像上传、基本信息录入、表单验证
- 添加记录页面:支持多种记录类型、照片上传、日期选择
- 本地存储集成:宠物数据和记录数据持久化存储

🎨 界面优化:
- 使用uView组件构建现代化表单界面
- 支持图片选择和预览功能
- 响应式网格布局展示信息
- 完善的空状态和加载状态

📱 用户体验:
- 表单验证和错误提示
- 图片上传和预览
- 日期选择器集成
- 数据持久化存储

🔧 技术改进:
- 页面路由配置完善
- 自定义导航栏样式
- 组件化开发模式
This commit is contained in:
yvan 2025-08-12 10:33:15 +08:00
parent 3c7238a45c
commit 63bf142c55
5 changed files with 703 additions and 25 deletions

View File

@ -32,6 +32,33 @@
"navigationBarTextStyle": "black"
}
},
{
"path": "pages/pets/pet-detail",
"style": {
"navigationBarTitleText": "宠物详情",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/add-pet",
"style": {
"navigationBarTitleText": "添加宠物",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/pets/add-record",
"style": {
"navigationBarTitleText": "添加记录",
"navigationBarBackgroundColor": "#ffffff",
"navigationBarTextStyle": "black",
"navigationStyle": "custom"
}
},
{
"path": "pages/index/index",
"style": {

209
pages/pets/add-pet.vue Normal file
View File

@ -0,0 +1,209 @@
<template>
<view class="add-pet-container">
<u-navbar title="添加宠物" left-icon="arrow-left" @left-click="goBack"></u-navbar>
<u-form :model="petForm" ref="petFormRef" :rules="rules" label-width="120">
<u-card :padding="20" margin="20">
<view class="avatar-section">
<u-avatar :src="petForm.avatar || '/static/default-pet.png'" size="80" shape="circle" @click="chooseAvatar"></u-avatar>
<u-text text="点击上传头像" type="tips" size="12"></u-text>
</view>
</u-card>
<u-card :padding="20" margin="20">
<u-form-item label="宠物姓名" prop="name" required>
<u-input v-model="petForm.name" placeholder="请输入宠物姓名"></u-input>
</u-form-item>
<u-form-item label="品种" prop="breed" required>
<u-input v-model="petForm.breed" placeholder="请输入宠物品种"></u-input>
</u-form-item>
<u-form-item label="性别" prop="gender" required>
<u-radio-group v-model="petForm.gender" placement="row">
<u-radio label="公" name="公"></u-radio>
<u-radio label="母" name="母"></u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="生日" prop="birthday" required>
<u-input v-model="petForm.birthday" placeholder="请选择生日" readonly @click="showDatePicker = true"></u-input>
</u-form-item>
<u-form-item label="体重" prop="weight">
<u-input v-model="petForm.weight" placeholder="请输入体重4.5kg"></u-input>
</u-form-item>
<u-form-item label="备注" prop="notes">
<u-textarea v-model="petForm.notes" placeholder="记录一些特殊信息..." maxlength="200"></u-textarea>
</u-form-item>
</u-card>
</u-form>
<view class="submit-section">
<u-button type="primary" text="保存宠物信息" @click="submitForm" :loading="loading"></u-button>
</view>
<!-- 日期选择器 -->
<u-datetime-picker
:show="showDatePicker"
v-model="selectedDate"
mode="date"
@confirm="confirmDate"
@cancel="showDatePicker = false">
</u-datetime-picker>
</view>
</template>
<script>
export default {
data() {
return {
loading: false,
showDatePicker: false,
selectedDate: new Date().getTime(),
petForm: {
name: '',
breed: '',
gender: '公',
birthday: '',
weight: '',
notes: '',
avatar: ''
},
rules: {
name: [
{
required: true,
message: '请输入宠物姓名',
trigger: 'blur'
}
],
breed: [
{
required: true,
message: '请输入宠物品种',
trigger: 'blur'
}
],
gender: [
{
required: true,
message: '请选择宠物性别',
trigger: 'change'
}
],
birthday: [
{
required: true,
message: '请选择宠物生日',
trigger: 'blur'
}
]
}
}
},
methods: {
goBack() {
uni.navigateBack()
},
chooseAvatar() {
uni.chooseImage({
count: 1,
sizeType: ['compressed'],
sourceType: ['album', 'camera'],
success: (res) => {
this.petForm.avatar = res.tempFilePaths[0]
},
fail: (err) => {
console.error('选择图片失败', err)
}
})
},
confirmDate(e) {
const date = new Date(e.value)
this.petForm.birthday = `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
this.showDatePicker = false
},
submitForm() {
this.$refs.petFormRef.validate().then(valid => {
if (valid) {
this.savePet()
}
}).catch(errors => {
console.log('表单验证失败', errors)
})
},
savePet() {
this.loading = true
//
const birthday = new Date(this.petForm.birthday)
const today = new Date()
const companionDays = Math.floor((today - birthday) / (1000 * 60 * 60 * 24))
//
const age = Math.floor(companionDays / 365)
const petData = {
...this.petForm,
id: Date.now(), // ID
age: age,
companionDays: companionDays,
createTime: new Date().toISOString()
}
//
setTimeout(() => {
try {
let pets = uni.getStorageSync('pets') || []
pets.push(petData)
uni.setStorageSync('pets', pets)
this.loading = false
uni.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
this.loading = false
uni.showToast({
title: '保存失败',
icon: 'error'
})
}
}, 1000)
}
}
}
</script>
<style lang="scss" scoped>
.add-pet-container {
background-color: #f8f9fa;
min-height: 100vh;
}
.avatar-section {
display: flex;
flex-direction: column;
align-items: center;
gap: 20rpx;
}
.submit-section {
padding: 40rpx 30rpx;
}
/deep/ .u-form-item {
margin-bottom: 30rpx;
}
</style>

244
pages/pets/add-record.vue Normal file
View File

@ -0,0 +1,244 @@
<template>
<view class="add-record-container">
<u-navbar title="添加记录" left-icon="arrow-left" @left-click="goBack"></u-navbar>
<u-form :model="recordForm" ref="recordFormRef" :rules="rules" label-width="120">
<u-card :padding="20" margin="20">
<u-form-item label="记录类型" prop="type" required>
<u-radio-group v-model="recordForm.type" placement="row">
<u-radio label="日常" name="daily"></u-radio>
<u-radio label="喂食" name="feeding"></u-radio>
<u-radio label="健康" name="health"></u-radio>
<u-radio label="其他" name="other"></u-radio>
</u-radio-group>
</u-form-item>
<u-form-item label="记录日期" prop="date" required>
<u-input v-model="recordForm.date" placeholder="请选择日期" readonly @click="showDatePicker = true"></u-input>
</u-form-item>
<u-form-item label="记录内容" prop="content" required>
<u-textarea v-model="recordForm.content" placeholder="记录宠物的状态、行为或其他信息..." maxlength="500" :count="true"></u-textarea>
</u-form-item>
<u-form-item label="添加照片" prop="photos">
<view class="photo-section">
<view class="photo-list">
<view class="photo-item" v-for="(photo, index) in recordForm.photos" :key="index">
<image :src="photo" mode="aspectFill" @click="previewImage(photo)"></image>
<u-icon name="close-circle-fill" color="#ff4757" size="16" @click="removePhoto(index)"></u-icon>
</view>
<view class="add-photo-btn" @click="choosePhotos" v-if="recordForm.photos.length < 9">
<u-icon name="camera" size="24" color="#999"></u-icon>
<text class="add-text">添加照片</text>
</view>
</view>
</view>
</u-form-item>
</u-card>
</u-form>
<view class="submit-section">
<u-button type="primary" text="保存记录" @click="submitForm" :loading="loading"></u-button>
</view>
<!-- 日期选择器 -->
<u-datetime-picker
:show="showDatePicker"
v-model="selectedDate"
mode="date"
@confirm="confirmDate"
@cancel="showDatePicker = false">
</u-datetime-picker>
</view>
</template>
<script>
export default {
data() {
return {
petId: '',
loading: false,
showDatePicker: false,
selectedDate: new Date().getTime(),
recordForm: {
type: 'daily',
date: this.formatDate(new Date()),
content: '',
photos: []
},
rules: {
type: [
{
required: true,
message: '请选择记录类型',
trigger: 'change'
}
],
date: [
{
required: true,
message: '请选择记录日期',
trigger: 'blur'
}
],
content: [
{
required: true,
message: '请输入记录内容',
trigger: 'blur'
}
]
}
}
},
onLoad(options) {
this.petId = options.petId
},
methods: {
formatDate(date) {
return `${date.getFullYear()}-${(date.getMonth() + 1).toString().padStart(2, '0')}-${date.getDate().toString().padStart(2, '0')}`
},
goBack() {
uni.navigateBack()
},
confirmDate(e) {
const date = new Date(e.value)
this.recordForm.date = this.formatDate(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)
}
})
},
removePhoto(index) {
this.recordForm.photos.splice(index, 1)
},
previewImage(src) {
uni.previewImage({
urls: this.recordForm.photos,
current: src
})
},
submitForm() {
this.$refs.recordFormRef.validate().then(valid => {
if (valid) {
this.saveRecord()
}
}).catch(errors => {
console.log('表单验证失败', errors)
})
},
saveRecord() {
this.loading = true
const recordData = {
...this.recordForm,
id: Date.now(),
petId: this.petId,
createTime: new Date().toISOString()
}
//
setTimeout(() => {
try {
let records = uni.getStorageSync('petRecords') || []
records.unshift(recordData) //
uni.setStorageSync('petRecords', records)
this.loading = false
uni.showToast({
title: '保存成功',
icon: 'success'
})
setTimeout(() => {
uni.navigateBack()
}, 1500)
} catch (error) {
this.loading = false
uni.showToast({
title: '保存失败',
icon: 'error'
})
}
}, 1000)
}
}
}
</script>
<style lang="scss" scoped>
.add-record-container {
background-color: #f8f9fa;
min-height: 100vh;
}
.photo-section {
.photo-list {
display: flex;
flex-wrap: wrap;
gap: 20rpx;
.photo-item {
position: relative;
width: 150rpx;
height: 150rpx;
image {
width: 100%;
height: 100%;
border-radius: 10rpx;
}
/deep/ .u-icon {
position: absolute;
top: -8rpx;
right: -8rpx;
}
}
.add-photo-btn {
width: 150rpx;
height: 150rpx;
border: 2rpx dashed #ddd;
border-radius: 10rpx;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 10rpx;
.add-text {
font-size: 24rpx;
color: #999;
}
}
}
}
.submit-section {
padding: 40rpx 30rpx;
}
/deep/ .u-form-item {
margin-bottom: 30rpx;
}
</style>

210
pages/pets/pet-detail.vue Normal file
View File

@ -0,0 +1,210 @@
<template>
<view class="pet-detail-container">
<u-navbar :title="petInfo.name || '宠物详情'" left-icon="arrow-left" @left-click="goBack">
<template #right>
<u-icon name="edit-pen" size="20" @click="editPet"></u-icon>
</template>
</u-navbar>
<view class="pet-header">
<u-avatar :src="petInfo.avatar || '/static/default-pet.png'" size="80" shape="circle"></u-avatar>
<view class="pet-basic-info">
<u-text :text="petInfo.name" type="primary" size="18" bold></u-text>
<u-text :text="`${petInfo.breed} · ${petInfo.gender}`" type="info" size="14"></u-text>
<u-text :text="`${petInfo.age}岁 · ${petInfo.weight}`" type="tips" size="12"></u-text>
</view>
</view>
<u-gap height="20"></u-gap>
<u-card title="基本信息" :padding="20">
<view class="info-grid">
<view class="info-item">
<u-text text="生日" type="tips" size="12"></u-text>
<u-text :text="petInfo.birthday" type="primary" size="14"></u-text>
</view>
<view class="info-item">
<u-text text="陪伴天数" type="tips" size="12"></u-text>
<u-text :text="`${petInfo.companionDays}天`" type="primary" size="14"></u-text>
</view>
<view class="info-item">
<u-text text="体重" type="tips" size="12"></u-text>
<u-text :text="petInfo.weight" type="primary" size="14"></u-text>
</view>
<view class="info-item">
<u-text text="性别" type="tips" size="12"></u-text>
<u-text :text="petInfo.gender" type="primary" size="14"></u-text>
</view>
</view>
</u-card>
<u-gap height="20"></u-gap>
<u-card title="最近记录" :padding="20">
<view class="records-list" v-if="recentRecords.length > 0">
<view class="record-item" v-for="record in recentRecords" :key="record.id">
<view class="record-date">{{ record.date }}</view>
<view class="record-content">{{ record.content }}</view>
</view>
</view>
<u-empty v-else mode="data" text="暂无记录" :show="true"></u-empty>
</u-card>
<u-gap height="20"></u-gap>
<view class="action-buttons">
<u-button type="primary" text="添加记录" @click="addRecord"></u-button>
<u-button type="success" text="健康档案" @click="viewHealth"></u-button>
</view>
</view>
</template>
<script>
export default {
data() {
return {
petId: '',
petInfo: {},
recentRecords: [
{
id: 1,
date: '2024-01-15',
content: '今天小橘很活泼,食欲很好'
},
{
id: 2,
date: '2024-01-14',
content: '带小橘去公园散步,玩得很开心'
}
]
}
},
onLoad(options) {
this.petId = options.id
this.loadPetInfo()
},
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'
}
]
this.petInfo = mockPets.find(pet => pet.id == this.petId) || {}
},
goBack() {
uni.navigateBack()
},
editPet() {
uni.navigateTo({
url: `/pages/pets/edit-pet?id=${this.petId}`
})
},
addRecord() {
uni.navigateTo({
url: `/pages/pets/add-record?petId=${this.petId}`
})
},
viewHealth() {
uni.navigateTo({
url: `/pages/pets/health-record?petId=${this.petId}`
})
}
}
}
</script>
<style lang="scss" scoped>
.pet-detail-container {
background-color: #f8f9fa;
min-height: 100vh;
}
.pet-header {
background-color: white;
padding: 30rpx;
display: flex;
align-items: center;
.pet-basic-info {
margin-left: 30rpx;
flex: 1;
/deep/ .u-text {
margin-bottom: 8rpx;
}
}
}
.info-grid {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 30rpx;
.info-item {
display: flex;
flex-direction: column;
/deep/ .u-text:first-child {
margin-bottom: 8rpx;
}
}
}
.records-list {
.record-item {
padding: 20rpx 0;
border-bottom: 1rpx solid #f0f0f0;
&:last-child {
border-bottom: none;
}
.record-date {
font-size: 24rpx;
color: #999;
margin-bottom: 8rpx;
}
.record-content {
font-size: 28rpx;
color: #333;
}
}
}
.action-buttons {
padding: 30rpx;
display: flex;
gap: 20rpx;
/deep/ .u-button {
flex: 1;
}
}
</style>

View File

@ -34,34 +34,22 @@
export default {
data() {
return {
petsList: [
//
{
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'
}
]
petsList: []
}
},
onShow() {
this.loadPets()
},
methods: {
loadPets() {
try {
const pets = uni.getStorageSync('pets') || []
this.petsList = pets
} catch (error) {
console.error('加载宠物列表失败', error)
this.petsList = []
}
},
addPet() {
uni.navigateTo({
url: '/pages/pets/add-pet'