Эх сурвалжийг харах

fix:修复宠物档案新增宠物字段不完整的问题,实现宠物图片上传

yangyi 4 сар өмнө
parent
commit
32dacf012e

+ 31 - 0
src/api/image-controller.ts

@@ -0,0 +1,31 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+import type { RString, UploadBody } from "./models";
+import { customAxiosInstance } from "../util/axios-instance";
+
+export const getImageController = () => {
+  /**
+   * @summary 上传图片
+   */
+  const upload = (uploadBody: UploadBody) => {
+    const formData = new FormData();
+    if (uploadBody.imageFile !== undefined) {
+      formData.append("imageFile", uploadBody.imageFile);
+    }
+
+    return customAxiosInstance<RString>({
+      url: `/upload`,
+      method: "POST",
+      headers: { "Content-Type": "multipart/form-data" },
+      data: formData,
+    });
+  };
+  return { upload };
+};
+export type UploadResult = NonNullable<
+  Awaited<ReturnType<ReturnType<typeof getImageController>["upload"]>>
+>;

+ 2 - 0
src/api/models/index.ts

@@ -49,6 +49,7 @@ export * from "./rPageVoAppointmentVo";
 export * from "./rPageVoUserVo";
 export * from "./rPetVo";
 export * from "./rScheduleVo";
+export * from "./rString";
 export * from "./rTodayStatusVo";
 export * from "./rUserVo";
 export * from "./rVaccinationVo";
@@ -64,6 +65,7 @@ export * from "./todayStatusVo";
 export * from "./updateUserAvatarDto";
 export * from "./updateUserPasswordDto";
 export * from "./updateUserStatusDto";
+export * from "./uploadBody";
 export * from "./userDto";
 export * from "./userVo";
 export * from "./vaccinationDto";

+ 12 - 0
src/api/models/rString.ts

@@ -0,0 +1,12 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export interface RString {
+  code?: number;
+  data?: string;
+  message?: string;
+}

+ 10 - 0
src/api/models/uploadBody.ts

@@ -0,0 +1,10 @@
+/**
+ * Generated by orval v7.0.1 🍺
+ * Do not edit manually.
+ * OpenAPI definition
+ * OpenAPI spec version: v0
+ */
+
+export type UploadBody = {
+  imageFile?: Blob;
+};

+ 3 - 0
src/type/index.ts

@@ -14,10 +14,13 @@ export interface LoginUser {
 export interface Pet {
     id: string
     name: string
+    species?: string
     breed: string
+    birthDate?: string
     age: number
     weight: number
     gender: 'MALE' | 'FEMALE'
+    avatar?: string
     notes?: string
 }
 

+ 69 - 12
src/view/pet/PetCreateView.vue

@@ -3,14 +3,40 @@
     <el-button text @click="router.back()">← 返回</el-button>
     <h1>新增宠物</h1>
     <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
+      <el-form-item label="头像">
+        <div class="avatar-uploader">
+          <el-avatar v-if="avatarUrl" :size="80" :src="avatarUrl" />
+          <el-avatar v-else :size="80">+</el-avatar>
+          <el-upload
+            :show-file-list="false"
+            :on-change="handleAvatarChange"
+            :auto-upload="false"
+            accept="image/jpeg,image/png,image/gif,image/webp"
+          >
+            <template #trigger>
+              <el-button size="small" :loading="uploading">选择图片</el-button>
+            </template>
+          </el-upload>
+        </div>
+      </el-form-item>
       <el-form-item label="宠物名" prop="name">
         <el-input v-model="form.name" />
       </el-form-item>
+      <el-form-item label="物种" prop="species">
+        <el-select v-model="form.species" placeholder="请选择物种" style="width: 100%">
+          <el-option label="狗" value="DOG" />
+          <el-option label="猫" value="CAT" />
+          <el-option label="兔子" value="RABBIT" />
+          <el-option label="鸟" value="BIRD" />
+          <el-option label="鱼" value="FISH" />
+          <el-option label="其他" value="OTHER" />
+        </el-select>
+      </el-form-item>
       <el-form-item label="品种" prop="breed">
-        <el-input v-model="form.breed" />
+        <el-input v-model="form.breed" placeholder="如:金毛、布偶猫" />
       </el-form-item>
-      <el-form-item label="年龄" prop="age">
-        <el-input-number v-model="form.age" :min="0" />
+      <el-form-item label="出生日期" prop="birthDate">
+        <el-date-picker v-model="form.birthDate" type="date" placeholder="选择出生日期" value-format="YYYY-MM-DD" style="width: 100%" />
       </el-form-item>
       <el-form-item label="体重(kg)" prop="weight">
         <el-input-number v-model="form.weight" :min="0" :precision="1" :step="0.5" />
@@ -22,7 +48,7 @@
         </el-radio-group>
       </el-form-item>
       <el-form-item label="备注">
-        <el-input v-model="form.notes" type="textarea" />
+        <el-input v-model="form.remark" type="textarea" />
       </el-form-item>
       <el-form-item>
         <el-button type="primary" @click="handleSubmit">保存</el-button>
@@ -36,29 +62,52 @@
 import { reactive, ref } from 'vue'
 import { useRouter } from 'vue-router'
 import { ElMessage } from 'element-plus'
-import type { FormInstance, FormRules } from 'element-plus'
+import type { FormInstance, FormRules, UploadFile } from 'element-plus'
 import { getPetController } from '@/api/pet-controller'
+import { getImageController } from '@/api/image-controller'
 import type { PetDto } from '@/api/models'
 
 const router = useRouter()
 
 const formRef = ref<FormInstance>()
+const avatarUrl = ref('')
+const uploading = ref(false)
 
 const form = reactive({
   name: '',
+  species: '',
   breed: '',
-  age: 0,
+  birthDate: '',
   weight: 0,
   gender: 'MALE' as 'MALE' | 'FEMALE',
-  notes: ''
+  remark: ''
 })
 
 const rules: FormRules = {
   name: [{ required: true, message: '请输入宠物名', trigger: 'blur' }],
-  breed: [{ required: true, message: '请输入品种', trigger: 'blur' }],
+  species: [{ required: true, message: '请选择物种', trigger: 'change' }],
   weight: [{ type: 'number', min: 0, message: '体重必须大于 0', trigger: 'blur' }]
 }
 
+const handleAvatarChange = async (uploadFile: UploadFile) => {
+  const file = uploadFile.raw
+  if (!file) return
+  uploading.value = true
+  try {
+    const api = getImageController()
+    const res = await api.upload({ imageFile: file as Blob })
+    if (res.code === 200 && res.data) {
+      avatarUrl.value = res.data
+    } else {
+      ElMessage.error(res.message || '头像上传失败')
+    }
+  } catch (err: any) {
+    ElMessage.error(err.message || '头像上传失败')
+  } finally {
+    uploading.value = false
+  }
+}
+
 const handleSubmit = async () => {
   const valid = await formRef.value?.validate().catch(() => false)
   if (!valid) return
@@ -66,11 +115,13 @@ const handleSubmit = async () => {
     const api = getPetController()
     const dto: PetDto = {
       name: form.name,
-      species: form.breed,
-      breed: form.breed,
+      species: form.species,
+      breed: form.breed || undefined,
+      birthDate: form.birthDate || undefined,
       gender: form.gender,
-      weight: form.weight,
-      remark: form.notes
+      weight: form.weight > 0 ? form.weight : undefined,
+      remark: form.remark || undefined,
+      avatar: avatarUrl.value || undefined
     }
     const res = await api.add1(dto)
     if (res.code === 200) {
@@ -95,4 +146,10 @@ const handleSubmit = async () => {
 .form {
   margin-top: 20px;
 }
+
+.avatar-uploader {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
 </style>

+ 52 - 11
src/view/pet/PetDetailView.vue

@@ -9,11 +9,16 @@
     </div>
 
     <el-card v-if="pet" class="detail-card">
-      <h2>基本信息</h2>
+      <div class="detail-header">
+        <el-avatar v-if="pet.avatar" :size="64" :src="pet.avatar" />
+        <el-avatar v-else :size="64">{{ pet.name[0] }}</el-avatar>
+        <h2>{{ pet.name }}</h2>
+      </div>
       <el-descriptions :column="2" border>
         <el-descriptions-item label="名称">{{ pet.name }}</el-descriptions-item>
+        <el-descriptions-item label="物种">{{ speciesLabel(pet.species) }}</el-descriptions-item>
         <el-descriptions-item label="品种">{{ pet.breed }}</el-descriptions-item>
-        <el-descriptions-item label="年龄">{{ pet.age }}岁</el-descriptions-item>
+        <el-descriptions-item label="出生日期">{{ pet.birthDate || '-' }}</el-descriptions-item>
         <el-descriptions-item label="体重">{{ pet.weight }}kg</el-descriptions-item>
         <el-descriptions-item label="性别">{{ pet.gender === 'MALE' ? '公' : '母' }}</el-descriptions-item>
         <el-descriptions-item label="备注">{{ pet.notes || '-' }}</el-descriptions-item>
@@ -27,11 +32,15 @@
       </div>
       <el-table v-if="vaccinations.length > 0" :data="vaccinations" stripe style="width: 100%">
         <el-table-column prop="vaccineName" label="疫苗名称" />
-        <el-table-column prop="vaccineDate" label="接种日期" />
+        <el-table-column label="接种日期">
+          <template #default="{ row }">{{ formatDate(row.vaccineDate) }}</template>
+        </el-table-column>
         <el-table-column prop="hospital" label="医院" />
         <el-table-column prop="doctor" label="医生" />
         <el-table-column prop="remark" label="备注" />
-        <el-table-column prop="createdAt" label="创建时间" />
+        <el-table-column label="创建时间">
+          <template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
+        </el-table-column>
       </el-table>
       <el-empty v-else description="暂无疫苗记录" />
     </el-card>
@@ -43,11 +52,15 @@
       </div>
       <el-table v-if="deworming.length > 0" :data="deworming" stripe style="width: 100%">
         <el-table-column prop="dewormingName" label="驱虫名称" />
-        <el-table-column prop="dewormingDate" label="驱虫日期" />
+        <el-table-column label="驱虫日期">
+          <template #default="{ row }">{{ formatDate(row.dewormingDate) }}</template>
+        </el-table-column>
         <el-table-column prop="hospital" label="医院" />
         <el-table-column prop="doctor" label="医生" />
         <el-table-column prop="remark" label="备注" />
-        <el-table-column prop="createdAt" label="创建时间" />
+        <el-table-column label="创建时间">
+          <template #default="{ row }">{{ formatDate(row.createdAt) }}</template>
+        </el-table-column>
       </el-table>
       <el-empty v-else description="暂无驱虫记录" />
     </el-card>
@@ -119,6 +132,16 @@ const deworming = ref<any[]>([])
 const loading = ref(false)
 const submitting = ref(false)
 
+const speciesLabel = (species?: string) => {
+  const map: Record<string, string> = { DOG: '狗', CAT: '猫', RABBIT: '兔子', BIRD: '鸟', FISH: '鱼', OTHER: '其他' }
+  return species ? map[species] || species : '-'
+}
+
+const formatDate = (date?: string) => {
+  if (!date) return '-'
+  return date.replace('T', ' ').substring(0, 16)
+}
+
 const showVaccineDialog = ref(false)
 const vaccineFormRef = ref<FormInstance>()
 const vaccineForm = reactive<VaccinationDto>({
@@ -151,21 +174,28 @@ const fetchPet = async () => {
   loading.value = true
   try {
     const api = getPetController()
-    const res = await api.getById(Number(route.params.id))
-    if (res.code === 200 && res.data) {
-      const p = res.data
+    const [petRes, vacRes, dewRes] = await Promise.all([
+      api.getById(Number(route.params.id)),
+      api.getVaccinations(Number(route.params.id)),
+      api.getDeworming(Number(route.params.id))
+    ])
+    if (petRes.code === 200 && petRes.data) {
+      const p = petRes.data
       pet.value = {
         id: String(p.id ?? ''),
         name: p.name ?? '',
+        species: p.species,
         breed: p.breed ?? '',
+        birthDate: p.birthDate,
         age: p.birthDate ? Math.floor((Date.now() - new Date(p.birthDate).getTime()) / 31557600000) : 0,
         weight: p.weight ?? 0,
         gender: (p.gender as Pet['gender']) ?? 'MALE',
+        avatar: p.avatar,
         notes: p.remark
       }
-      vaccinations.value = p.vaccinations ?? []
-      deworming.value = p.dewormings ?? []
     }
+    vaccinations.value = vacRes.data ?? []
+    deworming.value = dewRes.data ?? []
   } catch (err: any) {
     ElMessage.error(err.message || '加载失败')
   } finally {
@@ -254,6 +284,17 @@ onMounted(fetchPet)
   margin-bottom: 16px;
 }
 
+.detail-header {
+  display: flex;
+  align-items: center;
+  gap: 16px;
+  margin-bottom: 16px;
+}
+
+.detail-header h2 {
+  margin: 0;
+}
+
 .detail-card,
 .vaccine-card,
 .deworming-card {

+ 73 - 13
src/view/pet/PetEditView.vue

@@ -3,14 +3,40 @@
     <el-button text @click="router.back()">← 返回</el-button>
     <h1>编辑宠物</h1>
     <el-form ref="formRef" :model="form" :rules="rules" label-width="100px" class="form">
+      <el-form-item label="头像">
+        <div class="avatar-uploader">
+          <el-avatar v-if="avatarUrl" :size="80" :src="avatarUrl" />
+          <el-avatar v-else :size="80">+</el-avatar>
+          <el-upload
+            :show-file-list="false"
+            :on-change="handleAvatarChange"
+            :auto-upload="false"
+            accept="image/jpeg,image/png,image/gif,image/webp"
+          >
+            <template #trigger>
+              <el-button size="small" :loading="uploading">选择图片</el-button>
+            </template>
+          </el-upload>
+        </div>
+      </el-form-item>
       <el-form-item label="宠物名" prop="name">
         <el-input v-model="form.name" />
       </el-form-item>
+      <el-form-item label="物种" prop="species">
+        <el-select v-model="form.species" placeholder="请选择物种" style="width: 100%">
+          <el-option label="狗" value="DOG" />
+          <el-option label="猫" value="CAT" />
+          <el-option label="兔子" value="RABBIT" />
+          <el-option label="鸟" value="BIRD" />
+          <el-option label="鱼" value="FISH" />
+          <el-option label="其他" value="OTHER" />
+        </el-select>
+      </el-form-item>
       <el-form-item label="品种" prop="breed">
-        <el-input v-model="form.breed" />
+        <el-input v-model="form.breed" placeholder="如:金毛、布偶猫" />
       </el-form-item>
-      <el-form-item label="年龄" prop="age">
-        <el-input-number v-model="form.age" :min="0" />
+      <el-form-item label="出生日期" prop="birthDate">
+        <el-date-picker v-model="form.birthDate" type="date" placeholder="选择出生日期" value-format="YYYY-MM-DD" style="width: 100%" />
       </el-form-item>
       <el-form-item label="体重(kg)" prop="weight">
         <el-input-number v-model="form.weight" :min="0" :precision="1" :step="0.5" />
@@ -22,7 +48,7 @@
         </el-radio-group>
       </el-form-item>
       <el-form-item label="备注">
-        <el-input v-model="form.notes" type="textarea" />
+        <el-input v-model="form.remark" type="textarea" />
       </el-form-item>
       <el-form-item>
         <el-button type="primary" @click="handleSubmit">保存</el-button>
@@ -36,30 +62,53 @@
 import { reactive, ref, onMounted } from 'vue'
 import { useRouter, useRoute } from 'vue-router'
 import { ElMessage } from 'element-plus'
-import type { FormInstance, FormRules } from 'element-plus'
+import type { FormInstance, FormRules, UploadFile } from 'element-plus'
 import { getPetController } from '@/api/pet-controller'
+import { getImageController } from '@/api/image-controller'
 import type { PetDto } from '@/api/models'
 
 const router = useRouter()
 const route = useRoute()
 
 const formRef = ref<FormInstance>()
+const avatarUrl = ref('')
+const uploading = ref(false)
 
 const form = reactive({
   name: '',
+  species: '',
   breed: '',
-  age: 0,
+  birthDate: '',
   weight: 0,
   gender: 'MALE' as 'MALE' | 'FEMALE',
-  notes: ''
+  remark: ''
 })
 
 const rules: FormRules = {
   name: [{ required: true, message: '请输入宠物名', trigger: 'blur' }],
-  breed: [{ required: true, message: '请输入品种', trigger: 'blur' }],
+  species: [{ required: true, message: '请选择物种', trigger: 'change' }],
   weight: [{ type: 'number', min: 0, message: '体重必须大于 0', trigger: 'blur' }]
 }
 
+const handleAvatarChange = async (uploadFile: UploadFile) => {
+  const file = uploadFile.raw
+  if (!file) return
+  uploading.value = true
+  try {
+    const api = getImageController()
+    const res = await api.upload({ imageFile: file as Blob })
+    if (res.code === 200 && res.data) {
+      avatarUrl.value = res.data
+    } else {
+      ElMessage.error(res.message || '头像上传失败')
+    }
+  } catch (err: any) {
+    ElMessage.error(err.message || '头像上传失败')
+  } finally {
+    uploading.value = false
+  }
+}
+
 const fetchPet = async () => {
   try {
     const api = getPetController()
@@ -67,10 +116,13 @@ const fetchPet = async () => {
     if (res.code === 200 && res.data) {
       const p = res.data
       form.name = p.name ?? ''
+      form.species = p.species ?? ''
       form.breed = p.breed ?? ''
+      form.birthDate = p.birthDate ?? ''
       form.weight = p.weight ?? 0
       form.gender = (p.gender as 'MALE' | 'FEMALE') ?? 'MALE'
-      form.notes = p.remark ?? ''
+      form.remark = p.remark ?? ''
+      avatarUrl.value = p.avatar ?? ''
     }
   } catch (err: any) {
     ElMessage.error(err.message || '加载失败')
@@ -84,11 +136,13 @@ const handleSubmit = async () => {
     const api = getPetController()
     const dto: PetDto = {
       name: form.name,
-      species: form.breed,
-      breed: form.breed,
+      species: form.species,
+      breed: form.breed || undefined,
+      birthDate: form.birthDate || undefined,
       gender: form.gender,
-      weight: form.weight,
-      remark: form.notes
+      weight: form.weight > 0 ? form.weight : undefined,
+      remark: form.remark || undefined,
+      avatar: avatarUrl.value || undefined
     }
     const res = await api.update(Number(route.params.id), dto)
     if (res.code === 200) {
@@ -115,4 +169,10 @@ onMounted(fetchPet)
 .form {
   margin-top: 20px;
 }
+
+.avatar-uploader {
+  display: flex;
+  align-items: center;
+  gap: 12px;
+}
 </style>

+ 11 - 2
src/view/pet/PetListView.vue

@@ -12,10 +12,11 @@
     <div v-else class="pet-cards">
       <el-card v-for="pet in pets" :key="pet.id" class="pet-card">
         <div class="pet-info">
-          <el-avatar>{{ pet.name[0] }}</el-avatar>
+          <el-avatar v-if="pet.avatar" :src="pet.avatar" />
+          <el-avatar v-else>{{ pet.name[0] }}</el-avatar>
           <div class="pet-detail">
             <strong>{{ pet.name }}</strong> - {{ pet.breed }}
-            <p>{{ pet.age }}岁 | {{ pet.gender === 'MALE' ? '公' : '母' }}</p>
+            <p>{{ speciesLabel(pet.species) }} | {{ pet.age }}岁 | {{ pet.gender === 'MALE' ? '公' : '母' }}</p>
           </div>
         </div>
         <div class="pet-actions">
@@ -39,6 +40,11 @@ const router = useRouter()
 const pets = ref<Pet[]>([])
 const loading = ref(false)
 
+const speciesLabel = (species?: string) => {
+  const map: Record<string, string> = { DOG: '狗', CAT: '猫', RABBIT: '兔子', BIRD: '鸟', FISH: '鱼', OTHER: '其他' }
+  return species ? map[species] || species : ''
+}
+
 const fetchPets = async () => {
   loading.value = true
   try {
@@ -48,10 +54,13 @@ const fetchPets = async () => {
       pets.value = res.data.map(p => ({
         id: String(p.id ?? ''),
         name: p.name ?? '',
+        species: p.species,
         breed: p.breed ?? '',
+        birthDate: p.birthDate,
         age: p.birthDate ? Math.floor((Date.now() - new Date(p.birthDate).getTime()) / 31557600000) : 0,
         weight: p.weight ?? 0,
         gender: (p.gender as Pet['gender']) ?? 'MALE',
+        avatar: p.avatar,
         notes: p.remark
       }))
     }