705 lines
16 KiB
Vue
705 lines
16 KiB
Vue
<template>
|
|
<view class="edit-page">
|
|
<scroll-view class="edit-scroll" scroll-y :scroll-with-animation="true">
|
|
<view class="edit-container">
|
|
|
|
<!-- 头像区域 -->
|
|
<view class="avatar-section" @click="handleChooseAvatar">
|
|
<view class="avatar-wrap">
|
|
<TnAvatar
|
|
:url="displayAvatar"
|
|
:icon="displayAvatar ? '' : 'camera-fill'"
|
|
:size="180"
|
|
shape="circle"
|
|
:shadow="true"
|
|
bg-color="#FFF7ED"
|
|
/>
|
|
<view class="avatar-badge">
|
|
<TnIcon name="camera-fill" :size="22" color="#fff" />
|
|
</view>
|
|
</view>
|
|
<text class="avatar-hint">点击上传头像</text>
|
|
</view>
|
|
|
|
<!-- 基本信息 -->
|
|
<view class="form-section">
|
|
<text class="section-title">基本信息</text>
|
|
<view class="form-card">
|
|
<!-- 宠物名称 -->
|
|
<view class="form-row">
|
|
<text class="form-label required">名称</text>
|
|
<TnInput
|
|
v-model="form.name"
|
|
placeholder="给你的毛球取个名字"
|
|
:border="false"
|
|
text-align="right"
|
|
:max-length="20"
|
|
custom-class="form-tn-input"
|
|
/>
|
|
</view>
|
|
<view class="form-divider" />
|
|
|
|
<!-- 物种 -->
|
|
<view class="form-row" @click="showSpeciesPicker = true">
|
|
<text class="form-label">物种</text>
|
|
<text class="form-value" :class="{ 'value-placeholder': !form.species }">
|
|
{{ getSpeciesLabel(form.species) || '请选择' }}
|
|
</text>
|
|
<TnIcon name="right" :size="28" color="#CBD5E1" />
|
|
</view>
|
|
<view class="form-divider" />
|
|
|
|
<!-- 品种 -->
|
|
<view class="form-row">
|
|
<text class="form-label">品种</text>
|
|
<TnInput
|
|
v-model="form.breed"
|
|
placeholder="如:金毛、英短蓝猫"
|
|
:border="false"
|
|
text-align="right"
|
|
:max-length="30"
|
|
/>
|
|
</view>
|
|
<view class="form-divider" />
|
|
|
|
<!-- 性别 -->
|
|
<view class="form-row">
|
|
<text class="form-label">性别</text>
|
|
<view class="gender-wrap">
|
|
<TnRadioGroup v-model="form.gender" class="gender-group">
|
|
<view class="gender-option" :class="{ 'gender-active': form.gender === 'male' }">
|
|
<TnRadio label="male" size="lg" active-color="#3B82F6">
|
|
<view class="radio-inner">
|
|
<TnIcon name="male" :size="26" :color="form.gender === 'male' ? '#3B82F6' : '#94A3B8'" />
|
|
<text class="radio-text" :class="{ 'radio-text-active': form.gender === 'male' }">公</text>
|
|
</view>
|
|
</TnRadio>
|
|
</view>
|
|
<view class="gender-option" :class="{ 'gender-active-female': form.gender === 'female' }">
|
|
<TnRadio label="female" size="lg" active-color="#EC4899">
|
|
<view class="radio-inner">
|
|
<TnIcon name="female" :size="26" :color="form.gender === 'female' ? '#EC4899' : '#94A3B8'" />
|
|
<text class="radio-text" :class="{ 'radio-text-female': form.gender === 'female' }">母</text>
|
|
</view>
|
|
</TnRadio>
|
|
</view>
|
|
</TnRadioGroup>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 详细信息 -->
|
|
<view class="form-section">
|
|
<text class="section-title">详细信息</text>
|
|
<view class="form-card">
|
|
<!-- 生日 -->
|
|
<view class="form-row" @click="showDatePicker = true">
|
|
<text class="form-label">生日</text>
|
|
<text class="form-value" :class="{ 'value-placeholder': !form.birthday }">
|
|
{{ form.birthday || '请选择' }}
|
|
</text>
|
|
<TnIcon name="calendar" :size="28" color="#CBD5E1" />
|
|
</view>
|
|
<view class="form-divider" />
|
|
|
|
<!-- 体重 -->
|
|
<view class="form-row">
|
|
<text class="form-label">体重</text>
|
|
<view class="weight-row">
|
|
<TnInput
|
|
v-model="form.weight"
|
|
type="digit"
|
|
placeholder="0.0"
|
|
:border="false"
|
|
text-align="right"
|
|
custom-class="weight-tn-input"
|
|
/>
|
|
<text class="weight-unit">kg</text>
|
|
</view>
|
|
</view>
|
|
<view class="form-divider" />
|
|
|
|
<!-- 毛色 -->
|
|
<view class="form-row">
|
|
<text class="form-label">毛色</text>
|
|
<TnInput
|
|
v-model="form.color"
|
|
placeholder="如:橘色、黑白"
|
|
:border="false"
|
|
text-align="right"
|
|
:max-length="20"
|
|
/>
|
|
</view>
|
|
<view class="form-divider" />
|
|
|
|
<!-- 是否绝育 -->
|
|
<view class="form-row">
|
|
<text class="form-label">绝育</text>
|
|
<view class="switch-row">
|
|
<text class="switch-label">{{ form.neutered ? '已绝育' : '未绝育' }}</text>
|
|
<TnSwitch v-model="form.neutered" active-color="#F97316" />
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
|
|
<!-- 删除按钮(编辑模式) -->
|
|
<view v-if="isEdit" class="delete-section">
|
|
<TnButton
|
|
bg-color="#FEF2F2"
|
|
text-color="#EF4444"
|
|
shape="round"
|
|
:bold="true"
|
|
width="280"
|
|
height="84"
|
|
@click="handleDelete"
|
|
>
|
|
<TnIcon name="delete-fill" />
|
|
<text class="delete-btn-text">删除宠物</text>
|
|
</TnButton>
|
|
</view>
|
|
|
|
</view>
|
|
</scroll-view>
|
|
|
|
<!-- 底部保存按钮 -->
|
|
<view class="bottom-bar" :style="{ paddingBottom: safeBottom + 'px' }">
|
|
<TnButton
|
|
text-color="#FFFFFF"
|
|
shape="round"
|
|
:bold="true"
|
|
:custom-style="primaryButtonStyle"
|
|
:loading="submitting"
|
|
:disabled="submitting || !form.name"
|
|
width="100%"
|
|
height="96"
|
|
:font-size="30"
|
|
@click="handleSave"
|
|
>
|
|
{{ submitting ? '保存中...' : (isEdit ? '保存修改' : '添加宠物') }}
|
|
</TnButton>
|
|
</view>
|
|
|
|
<!-- 物种选择弹窗 -->
|
|
<TnPopup v-model="showSpeciesPicker" open-direction="bottom" :radius="32" :safe-area-inset-bottom="true">
|
|
<view class="species-panel">
|
|
<view class="panel-header">
|
|
<text class="panel-title">选择物种</text>
|
|
<view class="panel-close" @click="showSpeciesPicker = false">
|
|
<TnIcon name="close" :size="32" color="#64748B" />
|
|
</view>
|
|
</view>
|
|
<view class="species-grid">
|
|
<view
|
|
v-for="item in speciesOptions"
|
|
:key="item.value"
|
|
class="species-item"
|
|
:class="{ 'species-selected': form.species === item.value }"
|
|
@click="selectSpecies(item.value)"
|
|
>
|
|
<text class="species-emoji">{{ item.emoji }}</text>
|
|
<text class="species-label" :class="{ 'species-label-active': form.species === item.value }">
|
|
{{ item.label }}
|
|
</text>
|
|
</view>
|
|
</view>
|
|
</view>
|
|
</TnPopup>
|
|
|
|
<!-- 日期选择器 -->
|
|
<TnDateTimePicker
|
|
v-model="form.birthday"
|
|
v-model:open="showDatePicker"
|
|
mode="date"
|
|
:max-time="todayStr"
|
|
@confirm="showDatePicker = false"
|
|
@cancel="showDatePicker = false"
|
|
/>
|
|
|
|
</view>
|
|
</template>
|
|
|
|
<script setup lang="ts">
|
|
import { ref, reactive, computed, onMounted } from 'vue'
|
|
import { onLoad } from '@dcloudio/uni-app'
|
|
import TnInput from '@/uni_modules/tuniaoui-vue3/components/input/src/input.vue'
|
|
import TnButton from '@/uni_modules/tuniaoui-vue3/components/button/src/button.vue'
|
|
import TnAvatar from '@/uni_modules/tuniaoui-vue3/components/avatar/src/avatar.vue'
|
|
import TnSwitch from '@/uni_modules/tuniaoui-vue3/components/switch/src/switch.vue'
|
|
import TnDateTimePicker from '@/uni_modules/tuniaoui-vue3/components/date-time-picker/src/date-time-picker.vue'
|
|
import TnPopup from '@/uni_modules/tuniaoui-vue3/components/popup/src/popup.vue'
|
|
import TnRadioGroup from '@/uni_modules/tuniaoui-vue3/components/radio/src/radio-group.vue'
|
|
import TnRadio from '@/uni_modules/tuniaoui-vue3/components/radio/src/radio.vue'
|
|
import TnIcon from '@/uni_modules/tuniaoui-vue3/components/icon/src/icon.vue'
|
|
|
|
interface PetForm {
|
|
name: string
|
|
species: string
|
|
breed: string
|
|
gender: string
|
|
birthday: string
|
|
weight: string
|
|
avatarUrl: string
|
|
color: string
|
|
neutered: boolean
|
|
}
|
|
|
|
const speciesOptions = [
|
|
{ value: 'dog', label: '狗狗', emoji: '🐶' },
|
|
{ value: 'cat', label: '猫咪', emoji: '🐱' },
|
|
{ value: 'bird', label: '鸟类', emoji: '🐦' },
|
|
{ value: 'hamster', label: '仓鼠', emoji: '🐹' },
|
|
{ value: 'rabbit', label: '兔子', emoji: '🐰' },
|
|
{ value: 'fish', label: '鱼类', emoji: '🐟' },
|
|
]
|
|
const primaryButtonStyle = {
|
|
background: 'linear-gradient(135deg, #F97316, #FB923C)',
|
|
}
|
|
|
|
const todayStr = computed(() => {
|
|
const d = new Date()
|
|
return `${d.getFullYear()}/${String(d.getMonth() + 1).padStart(2, '0')}/${String(d.getDate()).padStart(2, '0')} 23:59:59`
|
|
})
|
|
|
|
const isEdit = ref(false)
|
|
const petId = ref('')
|
|
const submitting = ref(false)
|
|
const showSpeciesPicker = ref(false)
|
|
const showDatePicker = ref(false)
|
|
const safeBottom = ref(0)
|
|
const displayAvatar = ref('')
|
|
|
|
const form = reactive<PetForm>({
|
|
name: '',
|
|
species: 'dog',
|
|
breed: '',
|
|
gender: 'male',
|
|
birthday: '',
|
|
weight: '',
|
|
avatarUrl: '',
|
|
color: '',
|
|
neutered: false,
|
|
})
|
|
|
|
onLoad((options: any) => {
|
|
if (options?.id) {
|
|
isEdit.value = true
|
|
petId.value = options.id
|
|
uni.setNavigationBarTitle({ title: '编辑宠物' })
|
|
loadPetDetail(options.id)
|
|
} else {
|
|
uni.setNavigationBarTitle({ title: '添加宠物' })
|
|
}
|
|
})
|
|
|
|
onMounted(() => {
|
|
const sys = uni.getSystemInfoSync()
|
|
safeBottom.value = sys.safeAreaInsets?.bottom || 0
|
|
})
|
|
|
|
function getSpeciesLabel(species: string) {
|
|
return speciesOptions.find(s => s.value === species)?.label || ''
|
|
}
|
|
|
|
function selectSpecies(value: string) {
|
|
form.species = value
|
|
showSpeciesPicker.value = false
|
|
}
|
|
|
|
async function loadPetDetail(id: string) {
|
|
try {
|
|
const uid = uni.getStorageSync('uid')
|
|
const res = await uniCloud.callFunction({
|
|
name: 'petManager',
|
|
data: { action: 'detail', uid, petId: id },
|
|
})
|
|
if (res.result?.code === 0 && res.result.data) {
|
|
const d = res.result.data
|
|
form.name = d.name || ''
|
|
form.species = d.species || 'dog'
|
|
form.breed = d.breed || ''
|
|
form.gender = d.gender || 'male'
|
|
form.birthday = d.birthday || ''
|
|
form.weight = d.weight ? String(d.weight) : ''
|
|
form.avatarUrl = d.avatarUrl || ''
|
|
form.color = d.color || ''
|
|
form.neutered = d.neutered || false
|
|
await resolveAvatar()
|
|
}
|
|
} catch (e) {
|
|
console.warn('加载宠物详情失败', e)
|
|
uni.showToast({ title: '加载失败', icon: 'none' })
|
|
}
|
|
}
|
|
|
|
async function resolveAvatar() {
|
|
if (!form.avatarUrl) { displayAvatar.value = ''; return }
|
|
if (form.avatarUrl.startsWith('cloud://')) {
|
|
try {
|
|
const res = await uniCloud.getTempFileURL({ fileList: [form.avatarUrl] })
|
|
displayAvatar.value = res.fileList?.[0]?.tempFileURL || ''
|
|
} catch (e) { displayAvatar.value = '' }
|
|
} else {
|
|
displayAvatar.value = form.avatarUrl
|
|
}
|
|
}
|
|
|
|
function handleChooseAvatar() {
|
|
uni.chooseImage({
|
|
count: 1,
|
|
sizeType: ['compressed'],
|
|
sourceType: ['album', 'camera'],
|
|
success: async (res) => {
|
|
const tempPath = res.tempFilePaths[0]
|
|
displayAvatar.value = tempPath
|
|
try {
|
|
const uploadRes = await uniCloud.uploadFile({
|
|
filePath: tempPath,
|
|
cloudPath: `pets/avatar_${Date.now()}.jpg`,
|
|
})
|
|
form.avatarUrl = uploadRes.fileID
|
|
} catch (e) {
|
|
console.warn('头像上传失败', e)
|
|
uni.showToast({ title: '头像上传失败', icon: 'none' })
|
|
}
|
|
},
|
|
})
|
|
}
|
|
|
|
async function handleSave() {
|
|
if (!form.name.trim()) {
|
|
uni.showToast({ title: '请输入宠物名称', icon: 'none' })
|
|
return
|
|
}
|
|
if (submitting.value) return
|
|
submitting.value = true
|
|
try {
|
|
const uid = uni.getStorageSync('uid')
|
|
const payload: any = {
|
|
uid,
|
|
name: form.name.trim(),
|
|
species: form.species,
|
|
breed: form.breed.trim(),
|
|
gender: form.gender,
|
|
birthday: form.birthday,
|
|
weight: form.weight ? parseFloat(form.weight) : null,
|
|
avatarUrl: form.avatarUrl,
|
|
color: form.color.trim(),
|
|
neutered: form.neutered,
|
|
action: isEdit.value ? 'update' : 'add',
|
|
}
|
|
if (isEdit.value) payload.petId = petId.value
|
|
|
|
const res = await uniCloud.callFunction({ name: 'petManager', data: payload })
|
|
if (res.result?.code === 0) {
|
|
uni.showToast({ title: isEdit.value ? '修改成功' : '添加成功', icon: 'success' })
|
|
setTimeout(() => uni.navigateBack(), 1200)
|
|
} else {
|
|
uni.showToast({ title: res.result?.message || '操作失败', icon: 'none' })
|
|
}
|
|
} catch (e) {
|
|
console.error('保存宠物失败', e)
|
|
uni.showToast({ title: '保存失败,请重试', icon: 'none' })
|
|
} finally {
|
|
submitting.value = false
|
|
}
|
|
}
|
|
|
|
function handleDelete() {
|
|
uni.showModal({
|
|
title: '确认删除',
|
|
content: `确定要删除「${form.name}」吗?删除后不可恢复。`,
|
|
confirmColor: '#EF4444',
|
|
success: async (res) => {
|
|
if (res.confirm) {
|
|
try {
|
|
const uid = uni.getStorageSync('uid')
|
|
const res2 = await uniCloud.callFunction({
|
|
name: 'petManager',
|
|
data: { action: 'delete', uid, petId: petId.value },
|
|
})
|
|
if (res2.result?.code === 0) {
|
|
uni.showToast({ title: '已删除', icon: 'success' })
|
|
setTimeout(() => uni.navigateBack(), 1200)
|
|
} else {
|
|
uni.showToast({ title: '删除失败', icon: 'none' })
|
|
}
|
|
} catch (e) {
|
|
uni.showToast({ title: '删除失败', icon: 'none' })
|
|
}
|
|
}
|
|
},
|
|
})
|
|
}
|
|
</script>
|
|
|
|
<style lang="scss" scoped>
|
|
$primary: #F97316;
|
|
$primary-light: #FB923C;
|
|
$primary-bg: #FFF7ED;
|
|
$text-dark: #1E293B;
|
|
$text-body: #475569;
|
|
$text-muted: #94A3B8;
|
|
$card-bg: #FFFFFF;
|
|
$shadow: 0 4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
$radius-lg: 24rpx;
|
|
|
|
.edit-page {
|
|
height: 100vh;
|
|
background: #F8FAFC;
|
|
display: flex;
|
|
flex-direction: column;
|
|
}
|
|
|
|
.edit-scroll {
|
|
flex: 1;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.edit-container {
|
|
padding: 24rpx 28rpx 220rpx;
|
|
}
|
|
|
|
.avatar-section {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
padding: 40rpx 0 24rpx;
|
|
}
|
|
|
|
.avatar-wrap {
|
|
position: relative;
|
|
}
|
|
|
|
.avatar-badge {
|
|
position: absolute;
|
|
bottom: 0;
|
|
right: 0;
|
|
width: 48rpx;
|
|
height: 48rpx;
|
|
border-radius: 50%;
|
|
background: $primary;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
border: 4rpx solid #FFFFFF;
|
|
box-shadow: 0 4rpx 8rpx rgba(249, 115, 22, 0.3);
|
|
}
|
|
|
|
.avatar-hint {
|
|
margin-top: 12rpx;
|
|
font-size: 24rpx;
|
|
color: $text-muted;
|
|
}
|
|
|
|
.form-section {
|
|
margin-top: 32rpx;
|
|
}
|
|
|
|
.section-title {
|
|
font-size: 28rpx;
|
|
font-weight: 700;
|
|
color: $text-dark;
|
|
margin-bottom: 16rpx;
|
|
padding-left: 4rpx;
|
|
}
|
|
|
|
.form-card {
|
|
background: $card-bg;
|
|
border-radius: $radius-lg;
|
|
box-shadow: $shadow;
|
|
padding: 0 28rpx;
|
|
overflow: hidden;
|
|
}
|
|
|
|
.form-row {
|
|
display: flex;
|
|
align-items: center;
|
|
padding: 24rpx 0;
|
|
gap: 12rpx;
|
|
min-height: 88rpx;
|
|
}
|
|
|
|
.form-divider {
|
|
height: 1rpx;
|
|
background: #F1F5F9;
|
|
}
|
|
|
|
.form-label {
|
|
font-size: 28rpx;
|
|
font-weight: 600;
|
|
color: $text-dark;
|
|
width: 112rpx;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.required::before {
|
|
content: '*';
|
|
color: #EF4444;
|
|
margin-right: 4rpx;
|
|
}
|
|
|
|
.form-value {
|
|
flex: 1;
|
|
font-size: 28rpx;
|
|
color: $text-dark;
|
|
text-align: right;
|
|
}
|
|
|
|
.value-placeholder {
|
|
color: #CBD5E1;
|
|
}
|
|
|
|
.gender-wrap {
|
|
flex: 1;
|
|
display: flex;
|
|
justify-content: flex-end;
|
|
}
|
|
|
|
.gender-group {
|
|
display: flex;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.gender-option {
|
|
padding: 10rpx 20rpx;
|
|
border-radius: 40rpx;
|
|
background: #F8FAFC;
|
|
border: 2rpx solid #E2E8F0;
|
|
|
|
&.gender-active {
|
|
background: #EFF6FF;
|
|
border-color: #BFDBFE;
|
|
}
|
|
|
|
&.gender-active-female {
|
|
background: #FDF2F8;
|
|
border-color: #FBCFE8;
|
|
}
|
|
}
|
|
|
|
.radio-inner {
|
|
display: flex;
|
|
align-items: center;
|
|
gap: 6rpx;
|
|
padding: 0 4rpx;
|
|
}
|
|
|
|
.radio-text {
|
|
font-size: 26rpx;
|
|
color: $text-muted;
|
|
|
|
&.radio-text-active { color: #3B82F6; font-weight: 600; }
|
|
&.radio-text-female { color: #EC4899; font-weight: 600; }
|
|
}
|
|
|
|
.weight-row {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 8rpx;
|
|
}
|
|
|
|
.weight-unit {
|
|
font-size: 26rpx;
|
|
color: $text-muted;
|
|
flex-shrink: 0;
|
|
}
|
|
|
|
.switch-row {
|
|
flex: 1;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: flex-end;
|
|
gap: 16rpx;
|
|
}
|
|
|
|
.switch-label {
|
|
font-size: 26rpx;
|
|
color: $text-body;
|
|
}
|
|
|
|
.delete-section {
|
|
margin-top: 48rpx;
|
|
display: flex;
|
|
justify-content: center;
|
|
}
|
|
|
|
.delete-btn-text {
|
|
margin-left: 8rpx;
|
|
}
|
|
|
|
.bottom-bar {
|
|
padding: 16rpx 28rpx;
|
|
background: #FFFFFF;
|
|
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
|
|
}
|
|
|
|
.species-panel {
|
|
padding: 32rpx 28rpx 32rpx;
|
|
}
|
|
|
|
.panel-header {
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: space-between;
|
|
margin-bottom: 28rpx;
|
|
}
|
|
|
|
.panel-title {
|
|
font-size: 32rpx;
|
|
font-weight: 700;
|
|
color: $text-dark;
|
|
}
|
|
|
|
.panel-close {
|
|
width: 56rpx;
|
|
height: 56rpx;
|
|
display: flex;
|
|
align-items: center;
|
|
justify-content: center;
|
|
}
|
|
|
|
.species-grid {
|
|
display: flex;
|
|
flex-wrap: wrap;
|
|
gap: 20rpx;
|
|
}
|
|
|
|
.species-item {
|
|
display: flex;
|
|
flex-direction: column;
|
|
align-items: center;
|
|
gap: 8rpx;
|
|
width: calc(33.33% - 14rpx);
|
|
padding: 24rpx 0;
|
|
border-radius: 20rpx;
|
|
background: #F8FAFC;
|
|
border: 2rpx solid #E2E8F0;
|
|
transition: all 0.15s ease;
|
|
|
|
&.species-selected {
|
|
background: $primary-bg;
|
|
border-color: #FED7AA;
|
|
}
|
|
}
|
|
|
|
.species-emoji {
|
|
font-size: 48rpx;
|
|
line-height: 1;
|
|
}
|
|
|
|
.species-label {
|
|
font-size: 26rpx;
|
|
color: $text-body;
|
|
font-weight: 500;
|
|
|
|
&.species-label-active {
|
|
color: $primary;
|
|
font-weight: 700;
|
|
}
|
|
}
|
|
</style>
|