573 lines
15 KiB
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>
|