浏览代码

人员,宠物,物品列表展示,详情页优化

wuyi 1 月之前
父节点
当前提交
aaf19703ce

+ 25 - 13
src/enums/index.js

@@ -1,10 +1,6 @@
 export const UserRole = {
   user: '普通用户',
-  admin: '管理员',
-  channel: '渠道',
-  operator: '运营',
-  mss: 'MSS',
-  show: 'SHOW'
+  admin: '管理员'
 }
 
 export const ConfigType = {
@@ -18,16 +14,32 @@ export const ConfigType = {
   range: '范围'
 }
 
-export const ChainType = {
-  btc: 'btc',
-  eth: 'eth',
-  bsc: 'bsc',
-  tron: 'tron',
-  sol: 'sol'
-}
-
 export const PlatformType = {
   Android: 'Android',
   iOS: 'iOS',
   PC: 'PC'
 }
+
+// 二维码类型
+export const QrCodeType = {
+  person: {
+    label: '人员',
+    icon: 'pi-user',
+    severity: 'info'
+  },
+  pet: {
+    label: '宠物',
+    icon: 'pi-heart',
+    severity: 'success'
+  },
+  goods: {
+    label: '物品',
+    icon: 'pi-box',
+    severity: 'warn'
+  }
+}
+
+// 获取二维码类型配置
+export const getQrCodeTypeConfig = (type) => {
+  return QrCodeType[type] || { label: type, icon: 'pi-qrcode', severity: 'secondary' }
+}

+ 6 - 0
src/router/index.js

@@ -57,6 +57,12 @@ const router = createRouter({
           component: () => import('@/views/PetInfoManageView.vue'),
           meta: { roles: ['admin'] }
         },
+        {
+          path: 'goods-info-manage',
+          name: 'goods-info-manage',
+          component: () => import('@/views/GoodsInfoManageView.vue'),
+          meta: { roles: ['admin'] }
+        },
         {
           path: 'scan-record',
           name: 'scan-record',

+ 62 - 4
src/services/api.js

@@ -152,9 +152,9 @@ export const queryQrCodes = async (params) => {
 }
 
 // 获取二维码信息
-export const getQrCodeInfo = async (qrCode) => {
+export const getQrCodeInfo = async (id) => {
   const response = await api.get('/qr/info', {
-    params: { qrCode }
+    params: { id }
   })
   return response.data
 }
@@ -215,7 +215,15 @@ export const adminUpdatePersonInfo = async (data) => {
 
 // 查询人员信息列表(管理员)
 export const queryPersonInfo = async (params) => {
-  const response = await api.get('/person/list', { params })
+  const response = await api.get('/person/admin/list', { params })
+  return response.data
+}
+
+// 管理员查询人员详情
+export const getPersonAdminDetail = async (qrCodeId) => {
+  const response = await api.get('/person/admin/detail', {
+    params: { qrCodeId }
+  })
   return response.data
 }
 
@@ -249,7 +257,57 @@ export const adminUpdatePetInfo = async (data) => {
 
 // 查询宠物信息列表(管理员)
 export const queryPetInfo = async (params) => {
-  const response = await api.get('/pet/list', { params })
+  const response = await api.get('/pet/admin/list', { params })
+  return response.data
+}
+
+// 管理员查询宠物详情
+export const getPetAdminDetail = async (qrCodeId) => {
+  const response = await api.get('/pet/admin/detail', {
+    params: { qrCodeId }
+  })
+  return response.data
+}
+
+// ==================== 物品信息相关API ====================
+
+// 创建物品信息
+export const createGoodsInfo = async (data) => {
+  const response = await api.post('/goods/create', data)
+  return response.data
+}
+
+// 获取物品信息
+export const getGoodsInfo = async (qrCode) => {
+  const response = await api.get('/goods/get', {
+    params: { qrCode }
+  })
+  return response.data
+}
+
+// 更新物品信息(前台,需要维护码)
+export const updateGoodsInfo = async (data) => {
+  const response = await api.put('/goods/update', data)
+  return response.data
+}
+
+// 管理员更新物品信息
+export const adminUpdateGoodsInfo = async (data) => {
+  const response = await api.post('/goods/admin/update', data)
+  return response.data
+}
+
+// 查询物品信息列表(管理员)
+export const queryGoodsInfo = async (params) => {
+  const response = await api.get('/goods/admin/list', { params })
+  return response.data
+}
+
+// 管理员查询物品详情
+export const getGoodsAdminDetail = async (qrCodeId) => {
+  const response = await api.get('/goods/admin/detail', {
+    params: { qrCodeId }
+  })
   return response.data
 }
 

+ 505 - 0
src/views/GoodsInfoManageView.vue

@@ -0,0 +1,505 @@
+<script setup>
+import { queryGoodsInfo, adminUpdateGoodsInfo, getQrCodeInfo, uploadFile } from '@/services/api'
+import { Form } from '@primevue/forms'
+import { zodResolver } from '@primevue/forms/resolvers/zod'
+import { useDateFormat } from '@vueuse/core'
+import Button from 'primevue/button'
+import Column from 'primevue/column'
+import DataTable from 'primevue/datatable'
+import Dialog from 'primevue/dialog'
+import FloatLabel from 'primevue/floatlabel'
+import IconField from 'primevue/iconfield'
+import InputIcon from 'primevue/inputicon'
+import InputText from 'primevue/inputtext'
+import Textarea from 'primevue/textarea'
+import FileUpload from 'primevue/fileupload'
+import Image from 'primevue/image'
+import Message from 'primevue/message'
+import Tag from 'primevue/tag'
+import { useToast } from 'primevue/usetoast'
+import { computed, onMounted, ref } from 'vue'
+import { z } from 'zod'
+
+const toast = useToast()
+
+// 表格数据
+const tableData = ref({
+  content: [],
+  metadata: {
+    page: 0,
+    size: 20,
+    total: 0
+  }
+})
+
+// 筛选条件
+const filters = ref({
+  name: '',
+  contactName: '',
+  contactPhone: ''
+})
+
+// 格式化日期
+const formatDate = (date) => {
+  if (!date) return '-'
+  return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
+}
+
+// 获取数据
+const fetchData = async () => {
+  try {
+    const params = {
+      page: tableData.value.metadata.page,
+      size: tableData.value.metadata.size
+    }
+
+    if (filters.value.name) params.name = filters.value.name
+    if (filters.value.contactName) params.contactName = filters.value.contactName
+    if (filters.value.contactPhone) params.contactPhone = filters.value.contactPhone
+
+    const response = await queryGoodsInfo(params)
+    const content = response?.content ?? []
+    const metadata = response?.metadata ?? {}
+
+    tableData.value.content = content
+    tableData.value.metadata = {
+      page: metadata.page ?? params.page,
+      size: metadata.size ?? params.size,
+      total: metadata.total ?? content.length
+    }
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '获取数据失败',
+      life: 3000
+    })
+  }
+}
+
+// 分页
+const handlePageChange = (event) => {
+  tableData.value.metadata.page = event.page
+  tableData.value.metadata.size = event.rows
+  fetchData()
+}
+
+// 重置筛选并刷新
+const resetAndRefresh = () => {
+  filters.value = {
+    name: '',
+    contactName: '',
+    contactPhone: ''
+  }
+  tableData.value.metadata.page = 0
+  fetchData()
+}
+
+// 编辑对话框
+const editDialog = ref(false)
+const editForm = ref({
+  qrCodeId: null,
+  photoUrl: '',
+  name: '',
+  contactName: '',
+  contactPhone: '',
+  contactEmail: '',
+  remark: ''
+})
+const editFormLoading = ref(false)
+const uploadedPhotoUrl = ref('')
+
+// 表单验证
+const editFormResolver = zodResolver(
+  z.object({
+    name: z.string().min(1, { message: '物品名称不能为空' }),
+    contactName: z.string().min(1, { message: '联系人姓名不能为空' }),
+    contactPhone: z.string().min(1, { message: '联系电话不能为空' }),
+    contactEmail: z.string().email({ message: '邮箱格式不正确' }).optional().or(z.literal('')),
+    remark: z.string().optional()
+  })
+)
+
+// 打开编辑对话框
+const openEditDialog = async (goods) => {
+  editForm.value = {
+    qrCodeId: goods.qrCodeId,
+    photoUrl: goods.photoUrl || '',
+    name: goods.name,
+    contactName: goods.contactName,
+    contactPhone: goods.contactPhone,
+    contactEmail: goods.contactEmail || '',
+    remark: goods.remark || ''
+  }
+  uploadedPhotoUrl.value = goods.photoUrl || ''
+  editDialog.value = true
+}
+
+// 照片上传
+const handlePhotoUpload = async (event) => {
+  const file = event.files[0]
+  if (!file) return
+
+  try {
+    const response = await uploadFile(file)
+    uploadedPhotoUrl.value = response.url
+    editForm.value.photoUrl = response.url
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '照片上传成功',
+      life: 3000
+    })
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '照片上传失败',
+      life: 3000
+    })
+  }
+}
+
+// 保存编辑
+const saveEdit = async ({ valid, values }) => {
+  if (!valid) return
+
+  editFormLoading.value = true
+  try {
+    const submitData = {
+      qrCodeId: editForm.value.qrCodeId,
+      photoUrl: uploadedPhotoUrl.value || editForm.value.photoUrl || '',
+      name: values.name,
+      contactName: values.contactName,
+      contactPhone: values.contactPhone,
+      contactEmail: values.contactEmail || '',
+      remark: values.remark || ''
+    }
+
+    await adminUpdateGoodsInfo(submitData)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '物品信息更新成功',
+      life: 3000
+    })
+    editDialog.value = false
+    fetchData()
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '更新失败',
+      life: 3000
+    })
+  } finally {
+    editFormLoading.value = false
+  }
+}
+
+// 查看详情对话框
+const detailDialog = ref(false)
+const selectedGoods = ref(null)
+const qrCodeDetail = ref(null)
+const detailLoading = ref(false)
+
+const resolveQrCodeValue = (record) => record?.qrCode ?? record?.qrCodeId ?? ''
+const resolveQrCodeId = (record) => record?.qrCodeId ?? null
+const resolveRemark = (record) => record?.remark ?? record?.remarks ?? ''
+
+const viewDetail = async (goods) => {
+  try {
+    detailDialog.value = true
+    detailLoading.value = true
+    selectedGoods.value = null
+    qrCodeDetail.value = null
+
+    const qrCodeId = resolveQrCodeId(goods)
+    const qrInfo = qrCodeId ? await getQrCodeInfo(qrCodeId) : null
+    const normalizedDetail = qrInfo?.info
+      ? {
+        ...qrInfo.info,
+        qrCode: qrInfo.info.qrCode ?? qrInfo.qrCode,
+        qrCodeId: qrInfo.info.qrCodeId ?? qrInfo.id ?? qrInfo.qrCodeId
+      }
+      : null
+    selectedGoods.value = normalizedDetail || goods
+    qrCodeDetail.value = qrInfo
+  } catch (error) {
+    detailDialog.value = false
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '获取详情失败',
+      life: 3000
+    })
+  } finally {
+    detailLoading.value = false
+  }
+}
+
+// 复制到剪贴板
+const copyToClipboard = (text) => {
+  navigator.clipboard.writeText(String(text ?? '')).then(() => {
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '已复制到剪贴板',
+      life: 2000
+    })
+  })
+}
+
+onMounted(() => {
+  fetchData()
+})
+</script>
+
+<template>
+  <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
+    <DataTable :value="tableData.content" :paginator="true"
+      paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
+      currentPageReportTemplate="{totalRecords} 条记录" :rows="tableData.metadata.size"
+      :rowsPerPageOptions="[10, 20, 50, 100]" :totalRecords="tableData.metadata.total" @page="handlePageChange" lazy
+      scrollable>
+      <template #header>
+        <div class="flex flex-wrap items-center gap-2">
+          <!-- 筛选条件 - 左侧 -->
+          <IconField>
+            <InputIcon>
+              <i class="pi pi-box" />
+            </InputIcon>
+            <InputText v-model="filters.name" placeholder="物品名称" size="small" class="w-32" />
+          </IconField>
+          <IconField>
+            <InputIcon>
+              <i class="pi pi-user" />
+            </InputIcon>
+            <InputText v-model="filters.contactName" placeholder="联系人" size="small" class="w-32" />
+          </IconField>
+          <IconField>
+            <InputIcon>
+              <i class="pi pi-phone" />
+            </InputIcon>
+            <InputText v-model="filters.contactPhone" placeholder="联系电话" size="small" class="w-32" />
+          </IconField>
+          <Button icon="pi pi-search" @click="fetchData" label="查询" size="small" />
+          <Button icon="pi pi-refresh" @click="resetAndRefresh" label="刷新" size="small" />
+        </div>
+      </template>
+
+      <Column field="qrCodeId" header="二维码ID" style="min-width: 160px">
+        <template #body="slotProps">
+          <div class="flex items-center gap-2">
+            <code class="text-xs">{{ resolveQrCodeValue(slotProps.data) || '-' }}</code>
+            <Button icon="pi pi-copy" size="small" text rounded
+              @click="copyToClipboard(resolveQrCodeValue(slotProps.data))"
+              :disabled="!resolveQrCodeValue(slotProps.data)" />
+          </div>
+        </template>
+      </Column>
+      <Column field="name" header="物品名称" style="min-width: 150px"></Column>
+      <Column field="contactName" header="联系人" style="min-width: 100px"></Column>
+      <Column field="contactPhone" header="联系电话" style="min-width: 130px">
+        <template #body="slotProps">
+          <a :href="`tel:${slotProps.data.contactPhone}`" class="text-blue-600 hover:underline">
+            {{ slotProps.data.contactPhone }}
+          </a>
+        </template>
+      </Column>
+      <Column field="contactEmail" header="邮箱" style="min-width: 180px">
+        <template #body="slotProps">
+          <a v-if="slotProps.data.contactEmail" :href="`mailto:${slotProps.data.contactEmail}`"
+            class="text-blue-600 hover:underline">
+            {{ slotProps.data.contactEmail }}
+          </a>
+          <span v-else class="text-gray-400">-</span>
+        </template>
+      </Column>
+      <Column field="remark" header="备注" style="min-width: 220px">
+        <template #body="slotProps">
+          <span class="whitespace-pre-line break-words">{{ resolveRemark(slotProps.data) || '-' }}</span>
+        </template>
+      </Column>
+      <Column field="createdAt" header="创建时间" style="min-width: 180px">
+        <template #body="slotProps">
+          {{ formatDate(slotProps.data.createdAt) }}
+        </template>
+      </Column>
+      <Column field="updatedAt" header="更新时间" style="min-width: 180px">
+        <template #body="slotProps">
+          {{ formatDate(slotProps.data.updatedAt) }}
+        </template>
+      </Column>
+      <Column header="操作" style="min-width: 150px" frozen alignFrozen="right">
+        <template #body="slotProps">
+          <div class="flex gap-1">
+            <Button icon="pi pi-eye" severity="info" size="small" text rounded v-tooltip.top="'查看详情'"
+              @click="viewDetail(slotProps.data)" />
+            <Button icon="pi pi-pencil" severity="warn" size="small" text rounded v-tooltip.top="'编辑'"
+              @click="openEditDialog(slotProps.data)" />
+          </div>
+        </template>
+      </Column>
+    </DataTable>
+
+    <!-- 编辑对话框 -->
+    <Dialog v-model:visible="editDialog" :modal="true" header="编辑物品信息" :style="{ width: '550px' }" position="center">
+      <Form v-slot="$form" :resolver="editFormResolver" :initialValues="editForm" @submit="saveEdit" class="p-fluid">
+        <!-- 照片上传 -->
+        <div class="field mb-4">
+          <label class="block mb-2">物品照片</label>
+          <div class="flex gap-4 items-start">
+            <Image v-if="uploadedPhotoUrl" :src="uploadedPhotoUrl" alt="照片预览" width="120" preview />
+            <FileUpload mode="basic" accept="image/*" :maxFileSize="5000000" chooseLabel="上传照片"
+              @select="handlePhotoUpload" :auto="true" />
+          </div>
+        </div>
+
+        <div class="field">
+          <FloatLabel variant="on">
+            <InputText id="name" name="name" v-model="editForm.name" fluid />
+            <label for="name">物品名称 *</label>
+          </FloatLabel>
+          <Message v-if="$form.name?.invalid" severity="error" size="small" variant="simple">
+            {{ $form.name.error?.message }}
+          </Message>
+        </div>
+
+        <div class="field mt-4">
+          <FloatLabel variant="on">
+            <InputText id="contactName" name="contactName" v-model="editForm.contactName" fluid />
+            <label for="contactName">联系人姓名 *</label>
+          </FloatLabel>
+          <Message v-if="$form.contactName?.invalid" severity="error" size="small" variant="simple">
+            {{ $form.contactName.error?.message }}
+          </Message>
+        </div>
+
+        <div class="field mt-4">
+          <FloatLabel variant="on">
+            <InputText id="contactPhone" name="contactPhone" v-model="editForm.contactPhone" fluid />
+            <label for="contactPhone">联系电话 *</label>
+          </FloatLabel>
+          <Message v-if="$form.contactPhone?.invalid" severity="error" size="small" variant="simple">
+            {{ $form.contactPhone.error?.message }}
+          </Message>
+        </div>
+
+        <div class="field mt-4">
+          <FloatLabel variant="on">
+            <InputText id="contactEmail" name="contactEmail" v-model="editForm.contactEmail" fluid />
+            <label for="contactEmail">联系邮箱</label>
+          </FloatLabel>
+          <Message v-if="$form.contactEmail?.invalid" severity="error" size="small" variant="simple">
+            {{ $form.contactEmail.error?.message }}
+          </Message>
+        </div>
+
+        <div class="field mt-4">
+          <FloatLabel variant="on">
+            <Textarea id="remark" name="remark" v-model="editForm.remark" rows="3" autoResize fluid />
+            <label for="remark">备注</label>
+          </FloatLabel>
+          <Message v-if="$form.remark?.invalid" severity="error" size="small" variant="simple">
+            {{ $form.remark.error?.message }}
+          </Message>
+        </div>
+
+        <div class="flex justify-end gap-2 mt-4">
+          <Button label="取消" severity="secondary" type="button" @click="editDialog = false"
+            :disabled="editFormLoading" />
+          <Button label="保存" type="submit" :loading="editFormLoading" />
+        </div>
+      </Form>
+    </Dialog>
+
+    <!-- 详情对话框 -->
+    <Dialog v-model:visible="detailDialog" :modal="true" header="物品信息详情" :style="{ width: '750px' }" position="center">
+      <div v-if="detailLoading" class="py-10 text-center text-gray-500">详情加载中...</div>
+      <div v-else-if="selectedGoods" class="space-y-4">
+        <!-- 照片 -->
+        <div v-if="selectedGoods.photoUrl" class="text-center">
+          <Image :src="selectedGoods.photoUrl" alt="物品照片" width="150" preview />
+        </div>
+
+        <!-- 基本信息 -->
+        <div class="border rounded p-4">
+          <h4 class="font-semibold mb-3">基本信息</h4>
+          <div class="grid grid-cols-2 gap-4">
+            <div>
+              <div class="text-sm text-gray-500">物品名称</div>
+              <div class="font-medium">{{ selectedGoods.name || '-' }}</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">联系人</div>
+              <div class="font-medium">{{ selectedGoods.contactName || '-' }}</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">联系电话</div>
+              <div>
+                <a v-if="selectedGoods.contactPhone" :href="`tel:${selectedGoods.contactPhone}`"
+                  class="text-blue-600 hover:underline">
+                  {{ selectedGoods.contactPhone }}
+                </a>
+                <span v-else>-</span>
+              </div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">联系邮箱</div>
+              <div>
+                <a v-if="selectedGoods.contactEmail" :href="`mailto:${selectedGoods.contactEmail}`"
+                  class="text-blue-600 hover:underline">
+                  {{ selectedGoods.contactEmail }}
+                </a>
+                <span v-else>-</span>
+              </div>
+            </div>
+            <div v-if="resolveRemark(selectedGoods)" class="col-span-2">
+              <div class="text-sm text-gray-500">备注</div>
+              <div class="whitespace-pre-wrap">{{ resolveRemark(selectedGoods) }}</div>
+            </div>
+          </div>
+        </div>
+
+        <!-- 二维码信息 -->
+        <div v-if="qrCodeDetail" class="border rounded p-4">
+          <h4 class="font-semibold mb-3">二维码信息</h4>
+          <div class="grid grid-cols-2 gap-4">
+            <div>
+              <div class="text-sm text-gray-500">二维码编号</div>
+              <div class="font-mono text-sm break-all">{{ qrCodeDetail.qrCode || '-' }}</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">类型</div>
+              <div class="font-medium">{{ qrCodeDetail.qrType || '-' }}</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">激活状态</div>
+              <div>
+                <Tag :value="qrCodeDetail.isActivated ? '已激活' : '未激活'"
+                  :severity="qrCodeDetail.isActivated ? 'success' : 'secondary'" />
+              </div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">扫描次数</div>
+              <div class="font-semibold text-lg text-blue-600">{{ qrCodeDetail.scanCount || 0 }} 次</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">创建时间</div>
+              <div>{{ formatDate(selectedGoods.createdAt) }}</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">更新时间</div>
+              <div>{{ formatDate(selectedGoods.updatedAt) }}</div>
+            </div>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <Button label="关闭" @click="detailDialog = false" />
+      </template>
+    </Dialog>
+  </div>
+</template>

+ 6 - 0
src/views/MainView.vue

@@ -60,6 +60,12 @@ const allNavItems = [
         name: 'pet-info-manage',
         roles: ['admin']
       },
+      {
+        label: '物品信息管理',
+        icon: 'pi pi-fw pi-box',
+        name: 'goods-info-manage',
+        roles: ['admin']
+      },
       {
         label: '扫描记录',
         icon: 'pi pi-fw pi-chart-line',

+ 119 - 60
src/views/PersonInfoManageView.vue

@@ -1,10 +1,5 @@
 <script setup>
-import {
-  queryPersonInfo,
-  adminUpdatePersonInfo,
-  getQrCodeInfo,
-  uploadFile
-} from '@/services/api'
+import { queryPersonInfo, adminUpdatePersonInfo, getQrCodeInfo, uploadFile } from '@/services/api'
 import { Form } from '@primevue/forms'
 import { zodResolver } from '@primevue/forms/resolvers/zod'
 import { useDateFormat } from '@vueuse/core'
@@ -75,7 +70,7 @@ const fetchData = async () => {
   try {
     const params = {
       page: tableData.value.metadata.page,
-      pageSize: tableData.value.metadata.size
+      size: tableData.value.metadata.size
     }
 
     if (filters.value.name) params.name = filters.value.name
@@ -84,8 +79,15 @@ const fetchData = async () => {
     if (filters.value.gender) params.gender = filters.value.gender
 
     const response = await queryPersonInfo(params)
-    tableData.value.content = response.data
-    tableData.value.metadata.total = response.total
+    const content = response?.content ?? []
+    const metadata = response?.metadata ?? {}
+
+    tableData.value.content = content
+    tableData.value.metadata = {
+      page: metadata.page ?? params.page,
+      size: metadata.size ?? params.size,
+      total: metadata.total ?? content.length
+    }
   } catch (error) {
     toast.add({
       severity: 'error',
@@ -126,7 +128,8 @@ const editForm = ref({
   specialNote: '',
   emergencyContactName: '',
   emergencyContactPhone: '',
-  emergencyContactEmail: ''
+  emergencyContactEmail: '',
+  remark: ''
 })
 const editFormLoading = ref(false)
 const uploadedPhotoUrl = ref('')
@@ -139,7 +142,8 @@ const editFormResolver = zodResolver(
     phone: z.string().min(1, { message: '电话不能为空' }),
     emergencyContactName: z.string().min(1, { message: '紧急联系人姓名不能为空' }),
     emergencyContactPhone: z.string().min(1, { message: '紧急联系人电话不能为空' }),
-    emergencyContactEmail: z.string().email({ message: '邮箱格式不正确' }).optional().or(z.literal(''))
+    emergencyContactEmail: z.string().email({ message: '邮箱格式不正确' }).optional().or(z.literal('')),
+    remark: z.string().optional()
   })
 )
 
@@ -154,7 +158,8 @@ const openEditDialog = async (person) => {
     specialNote: person.specialNote || '',
     emergencyContactName: person.emergencyContactName,
     emergencyContactPhone: person.emergencyContactPhone,
-    emergencyContactEmail: person.emergencyContactEmail || ''
+    emergencyContactEmail: person.emergencyContactEmail || '',
+    remark: person.remark || ''
   }
   uploadedPhotoUrl.value = person.photoUrl || ''
   editDialog.value = true
@@ -193,14 +198,15 @@ const saveEdit = async ({ valid, values }) => {
   try {
     const submitData = {
       qrCodeId: editForm.value.qrCodeId,
-      photoUrl: uploadedPhotoUrl.value,
+      photoUrl: uploadedPhotoUrl.value || editForm.value.photoUrl || '',
       name: values.name,
       gender: values.gender,
       phone: values.phone,
       specialNote: values.specialNote || '',
       emergencyContactName: values.emergencyContactName,
       emergencyContactPhone: values.emergencyContactPhone,
-      emergencyContactEmail: values.emergencyContactEmail || ''
+      emergencyContactEmail: values.emergencyContactEmail || '',
+      remark: values.remark || ''
     }
 
     await adminUpdatePersonInfo(submitData)
@@ -228,26 +234,50 @@ const saveEdit = async ({ valid, values }) => {
 const detailDialog = ref(false)
 const selectedPerson = ref(null)
 const qrCodeDetail = ref(null)
+const detailLoading = ref(false)
+
+const resolveQrCodeValue = (record) => record?.qrCode ?? record?.qrCodeId ?? ''
+const resolveQrCodeId = (record) => record?.qrCodeId ?? null
+const resolveRemark = (record) => record?.remark ?? record?.remarks ?? ''
+const resolveAddress = (record) => record?.address ?? record?.contactAddress ?? ''
+const resolveEmail = (record) => record?.email ?? record?.contactEmail ?? ''
+const resolveEmergencyName = (record) => record?.emergencyContactName ?? record?.emergencyContact ?? ''
+const resolveEmergencyPhone = (record) => record?.emergencyContactPhone ?? record?.emergencyPhone ?? ''
+const resolveEmergencyEmail = (record) => record?.emergencyContactEmail ?? record?.emergencyEmail ?? ''
 
 const viewDetail = async (person) => {
   try {
-    selectedPerson.value = person
-    const response = await getQrCodeInfo(person.qrCode)
-    qrCodeDetail.value = response
     detailDialog.value = true
+    detailLoading.value = true
+    selectedPerson.value = null
+    qrCodeDetail.value = null
+    const qrCodeId = resolveQrCodeId(person)
+    const qrInfo = qrCodeId ? await getQrCodeInfo(qrCodeId) : null
+    const normalizedDetail = qrInfo?.info
+      ? {
+        ...qrInfo.info,
+        qrCode: qrInfo.info.qrCode ?? qrInfo.qrCode,
+        qrCodeId: qrInfo.info.qrCodeId ?? qrInfo.id ?? qrInfo.qrCodeId
+      }
+      : null
+    selectedPerson.value = normalizedDetail || person
+    qrCodeDetail.value = qrInfo
   } catch (error) {
+    detailDialog.value = false
     toast.add({
       severity: 'error',
       summary: '错误',
       detail: error.message || '获取详情失败',
       life: 3000
     })
+  } finally {
+    detailLoading.value = false
   }
 }
 
 // 复制到剪贴板
 const copyToClipboard = (text) => {
-  navigator.clipboard.writeText(text).then(() => {
+  navigator.clipboard.writeText(String(text ?? '')).then(() => {
     toast.add({
       severity: 'success',
       summary: '成功',
@@ -297,20 +327,16 @@ onMounted(() => {
         </div>
       </template>
 
-      <Column field="qrCode" header="二维码编号" style="min-width: 180px">
+      <Column field="qrCodeId" header="二维码ID" style="min-width: 160px">
         <template #body="slotProps">
           <div class="flex items-center gap-2">
-            <code class="text-xs">{{ slotProps.data.qrCode }}</code>
-            <Button icon="pi pi-copy" size="small" text rounded @click="copyToClipboard(slotProps.data.qrCode)" />
+            <code class="text-xs">{{ resolveQrCodeValue(slotProps.data) || '-' }}</code>
+            <Button icon="pi pi-copy" size="small" text rounded
+              @click="copyToClipboard(resolveQrCodeValue(slotProps.data))"
+              :disabled="!resolveQrCodeValue(slotProps.data)" />
           </div>
         </template>
       </Column>
-      <Column field="photoUrl" header="照片" style="min-width: 80px">
-        <template #body="slotProps">
-          <Image v-if="slotProps.data.photoUrl" :src="slotProps.data.photoUrl" alt="照片" width="50" preview />
-          <span v-else class="text-gray-400">-</span>
-        </template>
-      </Column>
       <Column field="name" header="姓名" style="min-width: 100px"></Column>
       <Column field="gender" header="性别" style="min-width: 80px">
         <template #body="slotProps">
@@ -332,11 +358,26 @@ onMounted(() => {
           </a>
         </template>
       </Column>
+      <Column field="specialNote" header="特别说明" style="min-width: 200px">
+        <template #body="slotProps">
+          <span class="whitespace-pre-line break-words">{{ slotProps.data.specialNote || '-' }}</span>
+        </template>
+      </Column>
+      <Column field="remark" header="备注" style="min-width: 200px">
+        <template #body="slotProps">
+          <span class="whitespace-pre-line break-words">{{ resolveRemark(slotProps.data) || '-' }}</span>
+        </template>
+      </Column>
       <Column field="createdAt" header="创建时间" style="min-width: 180px">
         <template #body="slotProps">
           {{ formatDate(slotProps.data.createdAt) }}
         </template>
       </Column>
+      <Column field="updatedAt" header="更新时间" style="min-width: 180px">
+        <template #body="slotProps">
+          {{ formatDate(slotProps.data.updatedAt) }}
+        </template>
+      </Column>
       <Column header="操作" style="min-width: 150px" frozen alignFrozen="right">
         <template #body="slotProps">
           <div class="flex gap-1">
@@ -405,6 +446,16 @@ onMounted(() => {
           </FloatLabel>
         </div>
 
+        <div class="field mt-4">
+          <FloatLabel variant="on">
+            <Textarea id="remark" name="remark" v-model="editForm.remark" rows="3" autoResize fluid />
+            <label for="remark">备注</label>
+          </FloatLabel>
+          <Message v-if="$form.remark?.invalid" severity="error" size="small" variant="simple">
+            {{ $form.remark.error?.message }}
+          </Message>
+        </div>
+
         <div class="field mt-4">
           <FloatLabel variant="on">
             <InputText id="emergencyContactName" name="emergencyContactName" v-model="editForm.emergencyContactName"
@@ -447,8 +498,9 @@ onMounted(() => {
     </Dialog>
 
     <!-- 详情对话框 -->
-    <Dialog v-model:visible="detailDialog" :modal="true" header="人员信息详情" :style="{ width: '600px' }" position="center">
-      <div v-if="selectedPerson" class="space-y-4">
+    <Dialog v-model:visible="detailDialog" :modal="true" header="人员信息详情" :style="{ width: '750px' }" position="center">
+      <div v-if="detailLoading" class="py-10 text-center text-gray-500">详情加载中...</div>
+      <div v-else-if="selectedPerson" class="space-y-4">
         <!-- 照片 -->
         <div v-if="selectedPerson.photoUrl" class="text-center">
           <Image :src="selectedPerson.photoUrl" alt="照片" width="150" preview />
@@ -457,18 +509,14 @@ onMounted(() => {
         <!-- 基本信息 -->
         <div class="border rounded p-4">
           <h4 class="font-semibold mb-3">基本信息</h4>
-          <div class="grid grid-cols-2 gap-3">
-            <div>
-              <div class="text-sm text-gray-500">二维码</div>
-              <div class="font-mono">{{ selectedPerson.qrCode }}</div>
-            </div>
+          <div class="grid grid-cols-2 gap-4">
             <div>
               <div class="text-sm text-gray-500">姓名</div>
-              <div>{{ selectedPerson.name }}</div>
+              <div class="font-medium">{{ selectedPerson.name }}</div>
             </div>
             <div>
               <div class="text-sm text-gray-500">性别</div>
-              <div>{{ getGenderName(selectedPerson.gender) }}</div>
+              <div class="font-medium">{{ getGenderName(selectedPerson.gender) }}</div>
             </div>
             <div>
               <div class="text-sm text-gray-500">电话</div>
@@ -478,52 +526,63 @@ onMounted(() => {
                 </a>
               </div>
             </div>
-          </div>
-          <div v-if="selectedPerson.specialNote" class="mt-3">
-            <div class="text-sm text-gray-500">特别说明</div>
-            <div class="whitespace-pre-wrap">{{ selectedPerson.specialNote }}</div>
-          </div>
-        </div>
-
-        <!-- 紧急联系人信息 -->
-        <div class="border rounded p-4">
-          <h4 class="font-semibold mb-3">紧急联系人</h4>
-          <div class="space-y-2">
             <div>
-              <div class="text-sm text-gray-500">姓名</div>
-              <div>{{ selectedPerson.emergencyContactName }}</div>
+              <div class="text-sm text-gray-500">紧急联系人</div>
+              <div class="font-medium">{{ selectedPerson.emergencyContactName || '-' }}</div>
             </div>
             <div>
-              <div class="text-sm text-gray-500">电话</div>
+              <div class="text-sm text-gray-500">紧急联系人电话</div>
               <div>
-                <a :href="`tel:${selectedPerson.emergencyContactPhone}`" class="text-blue-600 hover:underline">
+                <a v-if="selectedPerson.emergencyContactPhone" :href="`tel:${selectedPerson.emergencyContactPhone}`"
+                  class="text-blue-600 hover:underline">
                   {{ selectedPerson.emergencyContactPhone }}
                 </a>
+                <span v-else>-</span>
               </div>
             </div>
-            <div v-if="selectedPerson.emergencyContactEmail">
-              <div class="text-sm text-gray-500">邮箱</div>
+            <div>
+              <div class="text-sm text-gray-500">紧急联系人邮箱</div>
               <div>
-                <a :href="`mailto:${selectedPerson.emergencyContactEmail}`" class="text-blue-600 hover:underline">
+                <a v-if="selectedPerson.emergencyContactEmail" :href="`mailto:${selectedPerson.emergencyContactEmail}`"
+                  class="text-blue-600 hover:underline">
                   {{ selectedPerson.emergencyContactEmail }}
                 </a>
+                <span v-else>-</span>
               </div>
             </div>
+            <div v-if="selectedPerson.specialNote" class="col-span-2">
+              <div class="text-sm text-gray-500">特别说明</div>
+              <div class="whitespace-pre-wrap">{{ selectedPerson.specialNote }}</div>
+            </div>
+            <div v-if="resolveRemark(selectedPerson)" class="col-span-2">
+              <div class="text-sm text-gray-500">备注</div>
+              <div class="whitespace-pre-wrap">{{ resolveRemark(selectedPerson) }}</div>
+            </div>
           </div>
         </div>
 
-        <!-- 二维码统计信息 -->
+        <!-- 二维码信息 -->
         <div v-if="qrCodeDetail" class="border rounded p-4">
-          <h4 class="font-semibold mb-3">统计信息</h4>
-          <div class="grid grid-cols-2 gap-3">
+          <h4 class="font-semibold mb-3">二维码信息</h4>
+          <div class="grid grid-cols-2 gap-4">
+            <div>
+              <div class="text-sm text-gray-500">二维码编号</div>
+              <div class="font-mono text-sm break-all">{{ qrCodeDetail.qrCode || '-' }}</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">类型</div>
+              <div class="font-medium">{{ qrCodeDetail.qrType || '-' }}</div>
+            </div>
             <div>
               <div class="text-sm text-gray-500">激活状态</div>
-              <Tag :value="qrCodeDetail.isActivated ? '已激活' : '未激活'"
-                :severity="qrCodeDetail.isActivated ? 'success' : 'secondary'" />
+              <div>
+                <Tag :value="qrCodeDetail.isActivated ? '已激活' : '未激活'"
+                  :severity="qrCodeDetail.isActivated ? 'success' : 'secondary'" />
+              </div>
             </div>
             <div>
               <div class="text-sm text-gray-500">扫描次数</div>
-              <div class="font-semibold text-lg">{{ qrCodeDetail.scanCount || 0 }} 次</div>
+              <div class="font-semibold text-lg text-blue-600">{{ qrCodeDetail.scanCount || 0 }} 次</div>
             </div>
             <div>
               <div class="text-sm text-gray-500">创建时间</div>

+ 121 - 69
src/views/PetInfoManageView.vue

@@ -1,5 +1,5 @@
 <script setup>
-import { queryPetInfo, adminUpdatePetInfo, getQrCodeInfo, uploadFile } from '@/services/api'
+import { queryPetInfo, adminUpdatePetInfo, uploadFile, getQrCodeInfo } from '@/services/api'
 import { Form } from '@primevue/forms'
 import { zodResolver } from '@primevue/forms/resolvers/zod'
 import { useDateFormat } from '@vueuse/core'
@@ -11,12 +11,13 @@ import FloatLabel from 'primevue/floatlabel'
 import IconField from 'primevue/iconfield'
 import InputIcon from 'primevue/inputicon'
 import InputText from 'primevue/inputtext'
+import Textarea from 'primevue/textarea'
 import FileUpload from 'primevue/fileupload'
 import Image from 'primevue/image'
 import Message from 'primevue/message'
 import Tag from 'primevue/tag'
 import { useToast } from 'primevue/usetoast'
-import { computed, onMounted, ref } from 'vue'
+import { onMounted, ref } from 'vue'
 import { z } from 'zod'
 
 const toast = useToast()
@@ -34,8 +35,7 @@ const tableData = ref({
 // 筛选条件
 const filters = ref({
   name: '',
-  contactName: '',
-  qrCode: ''
+  contactName: ''
 })
 
 // 格式化日期
@@ -49,16 +49,22 @@ const fetchData = async () => {
   try {
     const params = {
       page: tableData.value.metadata.page,
-      pageSize: tableData.value.metadata.size
+      size: tableData.value.metadata.size
     }
 
     if (filters.value.name) params.name = filters.value.name
     if (filters.value.contactName) params.contactName = filters.value.contactName
-    if (filters.value.qrCode) params.qrCode = filters.value.qrCode
 
     const response = await queryPetInfo(params)
-    tableData.value.content = response.data
-    tableData.value.metadata.total = response.total
+    const content = response?.content ?? []
+    const metadata = response?.metadata ?? {}
+
+    tableData.value.content = content
+    tableData.value.metadata = {
+      page: metadata.page ?? params.page,
+      size: metadata.size ?? params.size,
+      total: metadata.total ?? content.length
+    }
   } catch (error) {
     toast.add({
       severity: 'error',
@@ -80,8 +86,7 @@ const handlePageChange = (event) => {
 const resetAndRefresh = () => {
   filters.value = {
     name: '',
-    contactName: '',
-    qrCode: ''
+    contactName: ''
   }
   tableData.value.metadata.page = 0
   fetchData()
@@ -95,7 +100,8 @@ const editForm = ref({
   name: '',
   contactName: '',
   contactPhone: '',
-  contactEmail: ''
+  contactEmail: '',
+  remark: ''
 })
 const editFormLoading = ref(false)
 const uploadedPhotoUrl = ref('')
@@ -105,8 +111,13 @@ const editFormResolver = zodResolver(
   z.object({
     name: z.string().min(1, { message: '名称不能为空' }),
     contactName: z.string().min(1, { message: '联系人姓名不能为空' }),
-    contactPhone: z.string().min(1, { message: '联系人电话不能为空' }),
-    contactEmail: z.string().email({ message: '邮箱格式不正确' }).optional().or(z.literal(''))
+    contactPhone: z.string().min(1, { message: '联系电话不能为空' }),
+    contactEmail: z
+      .string()
+      .email({ message: '邮箱格式不正确' })
+      .optional()
+      .or(z.literal('')),
+    remark: z.string().optional()
   })
 )
 
@@ -115,10 +126,11 @@ const openEditDialog = async (pet) => {
   editForm.value = {
     qrCodeId: pet.qrCodeId,
     photoUrl: pet.photoUrl || '',
-    name: pet.name,
-    contactName: pet.contactName,
-    contactPhone: pet.contactPhone,
-    contactEmail: pet.contactEmail || ''
+    name: pet.name || '',
+    contactName: pet.contactName || '',
+    contactPhone: pet.contactPhone || '',
+    contactEmail: pet.contactEmail || '',
+    remark: pet.remark || ''
   }
   uploadedPhotoUrl.value = pet.photoUrl || ''
   editDialog.value = true
@@ -157,11 +169,12 @@ const saveEdit = async ({ valid, values }) => {
   try {
     const submitData = {
       qrCodeId: editForm.value.qrCodeId,
-      photoUrl: uploadedPhotoUrl.value,
+      photoUrl: uploadedPhotoUrl.value || editForm.value.photoUrl || '',
       name: values.name,
       contactName: values.contactName,
       contactPhone: values.contactPhone,
-      contactEmail: values.contactEmail || ''
+      contactEmail: values.contactEmail || '',
+      remark: values.remark || ''
     }
 
     await adminUpdatePetInfo(submitData)
@@ -189,26 +202,45 @@ const saveEdit = async ({ valid, values }) => {
 const detailDialog = ref(false)
 const selectedPet = ref(null)
 const qrCodeDetail = ref(null)
+const detailLoading = ref(false)
+const resolveQrCodeValue = (record) => record?.qrCode ?? record?.qrCodeId ?? ''
+const resolveQrCodeId = (record) => record?.qrCodeId ?? null
+const resolveRemark = (record) => record?.remark ?? record?.remarks ?? ''
 
 const viewDetail = async (pet) => {
   try {
-    selectedPet.value = pet
-    const response = await getQrCodeInfo(pet.qrCode)
-    qrCodeDetail.value = response
     detailDialog.value = true
+    detailLoading.value = true
+    selectedPet.value = null
+    qrCodeDetail.value = null
+    const qrCodeId = resolveQrCodeId(pet)
+    const qrInfo = qrCodeId ? await getQrCodeInfo(qrCodeId) : null
+    const normalizedDetail = qrInfo?.info
+      ? {
+        ...qrInfo.info,
+        qrCode: qrInfo.info.qrCode ?? qrInfo.qrCode,
+        qrCodeId: qrInfo.info.qrCodeId ?? qrInfo.id ?? qrInfo.qrCodeId
+      }
+      : null
+    selectedPet.value = normalizedDetail || pet
+    qrCodeDetail.value = qrInfo
   } catch (error) {
+    detailDialog.value = false
     toast.add({
       severity: 'error',
       summary: '错误',
       detail: error.message || '获取详情失败',
       life: 3000
     })
+  } finally {
+    detailLoading.value = false
   }
 }
 
 // 复制到剪贴板
 const copyToClipboard = (text) => {
-  navigator.clipboard.writeText(text).then(() => {
+  if (text === undefined || text === null) return
+  navigator.clipboard.writeText(String(text)).then(() => {
     toast.add({
       severity: 'success',
       summary: '成功',
@@ -245,41 +277,33 @@ onMounted(() => {
             </InputIcon>
             <InputText v-model="filters.contactName" placeholder="联系人" size="small" class="w-32" />
           </IconField>
-          <IconField>
-            <InputIcon>
-              <i class="pi pi-qrcode" />
-            </InputIcon>
-            <InputText v-model="filters.qrCode" placeholder="二维码编号" size="small" class="w-32" />
-          </IconField>
           <Button icon="pi pi-search" @click="fetchData" label="查询" size="small" />
           <Button icon="pi pi-refresh" @click="resetAndRefresh" label="刷新" size="small" />
         </div>
       </template>
 
-      <Column field="qrCode" header="二维码编号" style="min-width: 180px">
+      <Column field="qrCodeId" header="二维码ID" style="min-width: 120px">
         <template #body="slotProps">
           <div class="flex items-center gap-2">
-            <code class="text-xs">{{ slotProps.data.qrCode }}</code>
-            <Button icon="pi pi-copy" size="small" text rounded @click="copyToClipboard(slotProps.data.qrCode)" />
+            <code class="text-xs">{{ resolveQrCodeValue(slotProps.data) }}</code>
+            <Button icon="pi pi-copy" size="small" text rounded
+              @click="copyToClipboard(resolveQrCodeValue(slotProps.data))"
+              :disabled="!resolveQrCodeValue(slotProps.data)" />
           </div>
         </template>
       </Column>
-      <Column field="photoUrl" header="照片" style="min-width: 80px">
-        <template #body="slotProps">
-          <Image v-if="slotProps.data.photoUrl" :src="slotProps.data.photoUrl" alt="照片" width="50" preview />
-          <span v-else class="text-gray-400">-</span>
-        </template>
-      </Column>
       <Column field="name" header="名称" style="min-width: 120px"></Column>
-      <Column field="contactName" header="联系人" style="min-width: 100px"></Column>
-      <Column field="contactPhone" header="联系电话" style="min-width: 130px">
+      <Column field="contactName" header="联系人" style="min-width: 120px"></Column>
+      <Column field="contactPhone" header="联系电话" style="min-width: 150px">
         <template #body="slotProps">
-          <a :href="`tel:${slotProps.data.contactPhone}`" class="text-blue-600 hover:underline">
+          <a v-if="slotProps.data.contactPhone" :href="`tel:${slotProps.data.contactPhone}`"
+            class="text-blue-600 hover:underline">
             {{ slotProps.data.contactPhone }}
           </a>
+          <span v-else class="text-gray-400">-</span>
         </template>
       </Column>
-      <Column field="contactEmail" header="邮箱" style="min-width: 180px">
+      <Column field="contactEmail" header="联系邮箱" style="min-width: 200px">
         <template #body="slotProps">
           <a v-if="slotProps.data.contactEmail" :href="`mailto:${slotProps.data.contactEmail}`"
             class="text-blue-600 hover:underline">
@@ -288,11 +312,21 @@ onMounted(() => {
           <span v-else class="text-gray-400">-</span>
         </template>
       </Column>
+      <Column field="remark" header="备注" style="min-width: 220px">
+        <template #body="slotProps">
+          <span class="whitespace-pre-line break-words">{{ resolveRemark(slotProps.data) || '-' }}</span>
+        </template>
+      </Column>
       <Column field="createdAt" header="创建时间" style="min-width: 180px">
         <template #body="slotProps">
           {{ formatDate(slotProps.data.createdAt) }}
         </template>
       </Column>
+      <Column field="updatedAt" header="更新时间" style="min-width: 180px">
+        <template #body="slotProps">
+          {{ formatDate(slotProps.data.updatedAt) }}
+        </template>
+      </Column>
       <Column header="操作" style="min-width: 150px" frozen alignFrozen="right">
         <template #body="slotProps">
           <div class="flex gap-1">
@@ -358,6 +392,16 @@ onMounted(() => {
           </Message>
         </div>
 
+        <div class="field mt-4">
+          <FloatLabel variant="on">
+            <Textarea id="remark" name="remark" v-model="editForm.remark" autoResize rows="3" fluid />
+            <label for="remark">备注</label>
+          </FloatLabel>
+          <Message v-if="$form.remark?.invalid" severity="error" size="small" variant="simple">
+            {{ $form.remark.error?.message }}
+          </Message>
+        </div>
+
         <div class="flex justify-end gap-2 mt-4">
           <Button label="取消" severity="secondary" type="button" @click="editDialog = false"
             :disabled="editFormLoading" />
@@ -367,9 +411,10 @@ onMounted(() => {
     </Dialog>
 
     <!-- 详情对话框 -->
-    <Dialog v-model:visible="detailDialog" :modal="true" header="宠物/物品信息详情" :style="{ width: '550px' }"
+    <Dialog v-model:visible="detailDialog" :modal="true" header="宠物/物品信息详情" :style="{ width: '750px' }"
       position="center">
-      <div v-if="selectedPet" class="space-y-4">
+      <div v-if="detailLoading" class="py-10 text-center text-gray-500">详情加载中...</div>
+      <div v-else-if="selectedPet" class="space-y-4">
         <!-- 照片 -->
         <div v-if="selectedPet.photoUrl" class="text-center">
           <Image :src="selectedPet.photoUrl" alt="照片" width="150" preview />
@@ -378,57 +423,64 @@ onMounted(() => {
         <!-- 基本信息 -->
         <div class="border rounded p-4">
           <h4 class="font-semibold mb-3">基本信息</h4>
-          <div class="space-y-2">
-            <div>
-              <div class="text-sm text-gray-500">二维码</div>
-              <div class="font-mono">{{ selectedPet.qrCode }}</div>
-            </div>
+          <div class="grid grid-cols-2 gap-4">
             <div>
               <div class="text-sm text-gray-500">名称</div>
-              <div>{{ selectedPet.name }}</div>
+              <div class="font-medium">{{ selectedPet.name || '-' }}</div>
             </div>
-          </div>
-        </div>
-
-        <!-- 联系人信息 -->
-        <div class="border rounded p-4">
-          <h4 class="font-semibold mb-3">联系人信息</h4>
-          <div class="space-y-2">
             <div>
               <div class="text-sm text-gray-500">联系人</div>
-              <div>{{ selectedPet.contactName }}</div>
+              <div class="font-medium">{{ selectedPet.contactName || '-' }}</div>
             </div>
             <div>
-              <div class="text-sm text-gray-500">电话</div>
+              <div class="text-sm text-gray-500">联系电话</div>
               <div>
-                <a :href="`tel:${selectedPet.contactPhone}`" class="text-blue-600 hover:underline">
+                <a v-if="selectedPet.contactPhone" :href="`tel:${selectedPet.contactPhone}`"
+                  class="text-blue-600 hover:underline">
                   {{ selectedPet.contactPhone }}
                 </a>
+                <span v-else>-</span>
               </div>
             </div>
-            <div v-if="selectedPet.contactEmail">
-              <div class="text-sm text-gray-500">邮箱</div>
+            <div>
+              <div class="text-sm text-gray-500">联系邮箱</div>
               <div>
-                <a :href="`mailto:${selectedPet.contactEmail}`" class="text-blue-600 hover:underline">
+                <a v-if="selectedPet.contactEmail" :href="`mailto:${selectedPet.contactEmail}`"
+                  class="text-blue-600 hover:underline">
                   {{ selectedPet.contactEmail }}
                 </a>
+                <span v-else>-</span>
               </div>
             </div>
+            <div v-if="resolveRemark(selectedPet)" class="col-span-2">
+              <div class="text-sm text-gray-500">备注</div>
+              <div class="whitespace-pre-wrap">{{ resolveRemark(selectedPet) }}</div>
+            </div>
           </div>
         </div>
 
-        <!-- 二维码统计信息 -->
+        <!-- 二维码信息 -->
         <div v-if="qrCodeDetail" class="border rounded p-4">
-          <h4 class="font-semibold mb-3">统计信息</h4>
-          <div class="grid grid-cols-2 gap-3">
+          <h4 class="font-semibold mb-3">二维码信息</h4>
+          <div class="grid grid-cols-2 gap-4">
+            <div>
+              <div class="text-sm text-gray-500">二维码编号</div>
+              <div class="font-mono text-sm break-all">{{ qrCodeDetail.qrCode || '-' }}</div>
+            </div>
+            <div>
+              <div class="text-sm text-gray-500">类型</div>
+              <div class="font-medium">{{ qrCodeDetail.qrType || '-' }}</div>
+            </div>
             <div>
               <div class="text-sm text-gray-500">激活状态</div>
-              <Tag :value="qrCodeDetail.isActivated ? '已激活' : '未激活'"
-                :severity="qrCodeDetail.isActivated ? 'success' : 'secondary'" />
+              <div>
+                <Tag :value="qrCodeDetail.isActivated ? '已激活' : '未激活'"
+                  :severity="qrCodeDetail.isActivated ? 'success' : 'secondary'" />
+              </div>
             </div>
             <div>
               <div class="text-sm text-gray-500">扫描次数</div>
-              <div class="font-semibold text-lg">{{ qrCodeDetail.scanCount || 0 }} 次</div>
+              <div class="font-semibold text-lg text-blue-600">{{ qrCodeDetail.scanCount || 0 }} 次</div>
             </div>
             <div>
               <div class="text-sm text-gray-500">创建时间</div>

+ 14 - 9
src/views/QrCodeManageView.vue

@@ -5,6 +5,7 @@ import {
   downloadQrCodesByDate,
   getQrCodeScanRecords
 } from '@/services/api'
+import { getQrCodeTypeConfig } from '@/enums'
 import { useDateFormat } from '@vueuse/core'
 import Button from 'primevue/button'
 import Column from 'primevue/column'
@@ -19,7 +20,7 @@ import InputNumber from 'primevue/inputnumber'
 import DatePicker from 'primevue/datepicker'
 import Tag from 'primevue/tag'
 import { useToast } from 'primevue/usetoast'
-import { computed, onMounted, ref } from 'vue'
+import { onMounted, ref } from 'vue'
 
 const toast = useToast()
 
@@ -46,7 +47,8 @@ const filters = ref({
 const qrTypeOptions = [
   { label: '全部', value: null },
   { label: '人员', value: 'person' },
-  { label: '宠物|物品', value: 'pet' }
+  { label: '宠物', value: 'pet' },
+  { label: '物品', value: 'goods' }
 ]
 
 // 激活状态选项
@@ -74,9 +76,9 @@ const selectedQrCode = ref(null)
 const batchDownloadDialog = ref(false)
 const downloadDate = ref(null)
 
-// 获取二维码类型名称
-const getQrTypeName = (type) => {
-  return type === 'person' ? '人员' : '宠物|物品'
+// 获取二维码类型配置(使用枚举)
+const getTypeConfig = (type) => {
+  return getQrCodeTypeConfig(type)
 }
 
 // 获取激活状态标签
@@ -227,7 +229,7 @@ const exportGeneratedCodes = () => {
       [
         code.qrCode,
         code.maintenanceCode,
-        getQrTypeName(generateForm.value.qrType),
+        getTypeConfig(generateForm.value.qrType).label,
         formatDate(new Date())
       ].join(',')
     )
@@ -342,8 +344,10 @@ onMounted(() => {
       </Column>
       <Column field="qrType" header="类型" style="min-width: 120px">
         <template #body="slotProps">
-          <Tag :value="getQrTypeName(slotProps.data.qrType)"
-            :severity="slotProps.data.qrType === 'person' ? 'info' : 'warn'" />
+          <Tag :severity="getTypeConfig(slotProps.data.qrType).severity">
+            <i :class="`pi ${getTypeConfig(slotProps.data.qrType).icon} mr-1`"></i>
+            {{ getTypeConfig(slotProps.data.qrType).label }}
+          </Tag>
         </template>
       </Column>
       <Column field="isActivated" header="状态" style="min-width: 100px">
@@ -381,7 +385,8 @@ onMounted(() => {
           <FloatLabel variant="on">
             <Select id="qrType" v-model="generateForm.qrType" :options="[
               { label: '人员', value: 'person' },
-              { label: '宠物|物品', value: 'pet' }
+              { label: '宠物', value: 'pet' },
+              { label: '物品', value: 'goods' }
             ]" optionLabel="label" optionValue="value" fluid />
             <label for="qrType">二维码类型</label>
           </FloatLabel>