FurBall/pages/record/detail.vue

573 lines
15 KiB
Vue

<template>
<view class="detail-page">
<scroll-view class="detail-scroll" scroll-y :scroll-with-animation="true">
<view v-if="loading" class="loading-state">
<TnLoading show :animation="true" mode="flower" color="#F97316" :size="80" />
</view>
<template v-else-if="record">
<view class="detail-container">
<!-- 类型头部 -->
<view class="type-header">
<view class="type-icon-wrap" :style="{ backgroundColor: typeStyle.bg }">
<TnIcon :name="typeStyle.icon" :size="52" :color="typeStyle.color" />
</view>
<view class="type-info">
<text class="type-label">{{ typeLabel }}</text>
<text class="type-date">{{ formatFullDate(record.date) }}</text>
</view>
<TnTag :type="typeTagType" size="sm" shape="circleRight">{{ typeLabel }}</TnTag>
</view>
<!-- 宠物信息 -->
<view class="info-section">
<text class="section-title">关联宠物</text>
<view class="info-card">
<view class="pet-info-row">
<view class="pet-avatar-wrap">
<TnAvatar :url="petDisplayAvatar" :icon="petDisplayAvatar ? '' : 'floral'"
:size="80" shape="circle" bg-color="#FFF7ED" />
</view>
<view class="pet-info-text">
<text class="pet-name">{{ petName || '未知宠物' }}</text>
<text class="pet-breed">{{ petBreed || '' }}</text>
</view>
</view>
</view>
</view>
<!-- 记录详情 -->
<view class="info-section">
<text class="section-title">记录信息</text>
<view class="info-card">
<view v-if="record.title" class="info-row">
<text class="info-label">标题</text>
<text class="info-value">{{ record.title }}</text>
</view>
<view v-if="record.title" class="info-divider" />
<view class="info-row">
<text class="info-label">日期</text>
<text class="info-value">{{ formatFullDate(record.date) }}</text>
</view>
<view v-if="record.nextDate" class="info-divider" />
<view v-if="record.nextDate" class="info-row">
<text class="info-label">下次提醒</text>
<text class="info-value next-date">{{ formatFullDate(record.nextDate) }}</text>
</view>
</view>
</view>
<!-- 类型专属信息 -->
<view v-if="record.type === 'vaccine' && (record.vaccineName || record.batchNumber || record.brand)" class="info-section">
<text class="section-title">疫苗信息</text>
<view class="info-card">
<template v-if="record.vaccineName">
<view class="info-row">
<text class="info-label">疫苗名称</text>
<text class="info-value">{{ record.vaccineName }}</text>
</view>
<view class="info-divider" />
</template>
<template v-if="record.batchNumber">
<view class="info-row">
<text class="info-label">批号</text>
<text class="info-value">{{ record.batchNumber }}</text>
</view>
<view class="info-divider" />
</template>
<template v-if="record.brand">
<view class="info-row">
<text class="info-label">品牌</text>
<text class="info-value">{{ record.brand }}</text>
</view>
</template>
</view>
</view>
<view v-if="record.type === 'deworm' && (record.dewormType || record.brand)" class="info-section">
<text class="section-title">驱虫信息</text>
<view class="info-card">
<template v-if="record.dewormType">
<view class="info-row">
<text class="info-label">驱虫类型</text>
<text class="info-value">{{ getDewormLabel(record.dewormType) }}</text>
</view>
<view class="info-divider" />
</template>
<template v-if="record.brand">
<view class="info-row">
<text class="info-label">品牌</text>
<text class="info-value">{{ record.brand }}</text>
</view>
</template>
</view>
</view>
<view v-if="record.type === 'medical' && record.diagnosis" class="info-section">
<text class="section-title">就诊信息</text>
<view class="info-card">
<view class="info-row">
<text class="info-label">诊断</text>
<text class="info-value">{{ record.diagnosis }}</text>
</view>
</view>
</view>
<!-- 补充信息 -->
<view v-if="showExtraInfo" class="info-section">
<text class="section-title">补充信息</text>
<view class="info-card">
<template v-if="record.hospital">
<view class="info-row">
<text class="info-label">医院</text>
<text class="info-value">{{ record.hospital }}</text>
</view>
<view class="info-divider" />
</template>
<template v-if="record.doctor">
<view class="info-row">
<text class="info-label">医生</text>
<text class="info-value">{{ record.doctor }}</text>
</view>
<view class="info-divider" />
</template>
<template v-if="record.cost">
<view class="info-row">
<text class="info-label">费用</text>
<text class="info-value cost-value">¥{{ record.cost }}</text>
</view>
<view class="info-divider" />
</template>
<template v-if="record.description">
<view class="info-row desc-row">
<text class="info-label">描述</text>
<text class="info-value desc-text">{{ record.description }}</text>
</view>
</template>
</view>
</view>
</view>
</template>
<view v-else class="empty-state">
<TnEmpty mode="data" :size="200" color="tn-gray-disabled" />
<text class="empty-text">记录不存在</text>
</view>
</scroll-view>
<!-- 底部操作栏 -->
<view v-if="record" class="bottom-bar" :style="{ paddingBottom: safeBottom + 'px' }">
<TnButton bg-color="#FEF2F2" text-color="#EF4444" shape="round" :bold="true"
width="45%" height="88" @click="handleDelete">
<TnIcon name="delete-fill" :size="28" color="#EF4444" />
<text class="btn-text">删除</text>
</TnButton>
<TnButton text-color="#FFFFFF" shape="round" :bold="true" :custom-style="primaryButtonStyle"
width="45%" height="88" @click="handleEdit">
<TnIcon name="edit-fill" :size="28" />
<text class="btn-text">编辑</text>
</TnButton>
</view>
</view>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import { onLoad, onShow } from '@dcloudio/uni-app'
import TnLoading from '@/uni_modules/tuniaoui-vue3/components/loading/src/loading.vue'
import TnEmpty from '@/uni_modules/tuniaoui-vue3/components/empty/src/empty.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 TnIcon from '@/uni_modules/tuniaoui-vue3/components/icon/src/icon.vue'
import TnTag from '@/uni_modules/tuniaoui-vue3/components/tag/src/tag.vue'
interface HealthRecord {
_id: string
pet_id: string
type: string
title: string
date: number
nextDate: string
hospital: string
doctor: string
description: string
vaccineName: string
batchNumber: string
dewormType: string
brand: string
diagnosis: string
cost: number
}
const typeStyleMap: Record<string, { icon: string; color: string; bg: string }> = {
vaccine: { icon: 'safe-fill', color: '#3B82F6', bg: '#EFF6FF' },
deworm: { icon: 'notice-fill', color: '#8B5CF6', bg: '#F5F3FF' },
medical: { icon: 'write-feather', color: '#F97316', bg: '#FFF7ED' },
checkup: { icon: 'data-fill', color: '#14B8A6', bg: '#F0FDFA' },
other: { icon: 'inventory-fill', color: '#64748B', bg: '#F8FAFC' },
}
const typeLabelMap: Record<string, string> = {
vaccine: '疫苗', deworm: '驱虫', medical: '病历', checkup: '体检', other: '其他',
}
const typeTagTypeMap: Record<string, string> = {
vaccine: 'primary', deworm: 'info', medical: 'warning', checkup: 'success', other: '',
}
const dewormLabelMap: Record<string, string> = {
internal: '体内驱虫', external: '体外驱虫', both: '体内外同驱',
}
const primaryButtonStyle = {
background: 'linear-gradient(135deg, #F97316, #FB923C)',
}
const loading = ref(true)
const record = ref<HealthRecord | null>(null)
const recordId = ref('')
const petName = ref('')
const petBreed = ref('')
const petDisplayAvatar = ref('')
const safeBottom = ref(0)
const hasLoaded = ref(false)
const typeStyle = computed(() => typeStyleMap[record.value?.type || ''] || typeStyleMap.other)
const typeLabel = computed(() => typeLabelMap[record.value?.type || ''] || '')
const typeTagType = computed(() => (typeTagTypeMap[record.value?.type || ''] || 'info') as any)
const showExtraInfo = computed(() => {
if (!record.value) return false
return !!(record.value.hospital || record.value.doctor || record.value.cost || record.value.description)
})
onLoad((options: any) => {
if (options?.id) {
recordId.value = options.id
}
const sys = uni.getSystemInfoSync()
safeBottom.value = sys.safeAreaInsets?.bottom || 0
loadDetail()
})
onShow(() => {
if (hasLoaded.value && recordId.value) {
loadDetail(false)
}
})
function formatFullDate(dateVal: string | number) {
if (!dateVal) return ''
let ms: number
if (typeof dateVal === 'string') {
ms = Date.parse(dateVal)
} else if (typeof dateVal === 'number') {
ms = dateVal < 1e12 ? dateVal * 1000 : dateVal
} else {
return ''
}
if (Number.isNaN(ms)) return ''
const d = new Date(ms)
const y = d.getFullYear()
const m = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return `${y}${m}${day}`
}
function getSpeciesLabel(species: string) {
const map: Record<string, string> = { dog: '狗狗', cat: '猫咪', bird: '鸟类', hamster: '仓鼠', rabbit: '兔子', fish: '鱼类' }
return map[species] || ''
}
function getDewormLabel(type: string) {
return dewormLabelMap[type] || type
}
async function loadDetail(showLoading = true) {
try {
if (showLoading) loading.value = true
const uid = uni.getStorageSync('uid')
if (!uid || !recordId.value) {
loading.value = false
return
}
const res = await uniCloud.callFunction({
name: 'recordManager',
data: { action: 'detail', uid, recordId: recordId.value },
})
if (res.result?.code === 0 && res.result.data) {
record.value = res.result.data as HealthRecord
loadPetInfo(record.value.pet_id)
} else {
loading.value = false
}
} catch (e) {
console.warn('加载记录详情失败', e)
loading.value = false
}
}
async function loadPetInfo(petId: string) {
try {
const uid = uni.getStorageSync('uid')
const res = await uniCloud.callFunction({
name: 'petManager',
data: { action: 'detail', uid, petId },
})
if (res.result?.code === 0 && res.result.data) {
const pet = res.result.data
petName.value = pet.name || ''
if (pet.species) {
const speciesLabel = getSpeciesLabel(pet.species)
petBreed.value = speciesLabel ? `${speciesLabel}` : ''
if (pet.breed) petBreed.value += ` · ${pet.breed}`
} else if (pet.breed) {
petBreed.value = pet.breed
}
if (pet.avatarUrl?.startsWith('cloud://')) {
try {
const fileRes = await uniCloud.getTempFileURL({ fileList: [pet.avatarUrl] })
petDisplayAvatar.value = fileRes.fileList?.[0]?.tempFileURL || ''
} catch (e) { petDisplayAvatar.value = '' }
} else {
petDisplayAvatar.value = pet.avatarUrl || ''
}
}
} catch (e) {
console.warn('加载宠物信息失败', e)
} finally {
loading.value = false
hasLoaded.value = true
}
}
function handleEdit() {
uni.navigateTo({ url: `/pages/record/edit?id=${recordId.value}` })
}
function handleDelete() {
uni.showModal({
title: '确认删除',
content: '确定要删除这条记录吗?删除后不可恢复。',
confirmColor: '#EF4444',
success: async (res) => {
if (res.confirm) {
try {
const uid = uni.getStorageSync('uid')
const res2 = await uniCloud.callFunction({
name: 'recordManager',
data: { action: 'delete', uid, recordId: recordId.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-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;
.detail-page {
height: 100vh;
background: #F8FAFC;
display: flex;
flex-direction: column;
}
.detail-scroll {
flex: 1;
overflow: hidden;
}
.detail-container {
padding: 24rpx 28rpx 180rpx;
}
.loading-state {
display: flex;
justify-content: center;
padding-top: 400rpx;
}
.empty-state {
display: flex;
flex-direction: column;
align-items: center;
padding-top: 200rpx;
}
.empty-text {
margin-top: 20rpx;
font-size: 28rpx;
color: $text-muted;
}
.type-header {
display: flex;
align-items: center;
gap: 20rpx;
padding: 20rpx 24rpx;
background: $card-bg;
border-radius: $radius-lg;
box-shadow: $shadow;
}
.type-icon-wrap {
width: 88rpx;
height: 88rpx;
border-radius: 22rpx;
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.type-info {
flex: 1;
display: flex;
flex-direction: column;
gap: 6rpx;
min-width: 0;
}
.type-label {
font-size: 32rpx;
font-weight: 700;
color: $text-dark;
}
.type-date {
font-size: 24rpx;
color: $text-muted;
}
.info-section {
margin-top: 28rpx;
}
.section-title {
font-size: 28rpx;
font-weight: 700;
color: $text-dark;
margin-bottom: 14rpx;
padding-left: 4rpx;
}
.info-card {
background: $card-bg;
border-radius: $radius-lg;
box-shadow: $shadow;
padding: 0 28rpx;
overflow: hidden;
}
.info-row {
display: flex;
align-items: center;
padding: 24rpx 0;
gap: 12rpx;
min-height: 88rpx;
&.desc-row {
align-items: flex-start;
}
}
.info-divider {
height: 1rpx;
background: #F1F5F9;
}
.info-label {
font-size: 28rpx;
font-weight: 600;
color: $text-dark;
width: 128rpx;
flex-shrink: 0;
}
.info-value {
flex: 1;
font-size: 28rpx;
color: $text-body;
text-align: right;
word-break: break-all;
&.next-date {
color: $primary;
font-weight: 600;
}
&.cost-value {
color: #EF4444;
font-weight: 700;
font-size: 30rpx;
}
&.desc-text {
line-height: 1.6;
}
}
.pet-info-row {
display: flex;
align-items: center;
gap: 20rpx;
padding: 20rpx 0;
}
.pet-avatar-wrap {
flex-shrink: 0;
}
.pet-info-text {
flex: 1;
display: flex;
flex-direction: column;
gap: 4rpx;
min-width: 0;
}
.pet-name {
font-size: 28rpx;
font-weight: 700;
color: $text-dark;
}
.pet-breed {
font-size: 24rpx;
color: $text-muted;
}
.bottom-bar {
display: flex;
justify-content: center;
gap: 24rpx;
padding: 16rpx 28rpx;
background: #FFFFFF;
box-shadow: 0 -4rpx 16rpx rgba(0, 0, 0, 0.04);
}
.btn-text {
margin-left: 8rpx;
}
</style>