瀏覽代碼

新增扫描记录功能,支持显示最近扫描记录及其详细信息,包括时间、地址和设备信息,并添加相应的对话框和按钮以便用户访问。

wuyi 1 月之前
父節點
當前提交
c8d2901a55
共有 1 個文件被更改,包括 162 次插入3 次删除
  1. 162 3
      src/views/ScanView.vue

+ 162 - 3
src/views/ScanView.vue

@@ -10,7 +10,8 @@ import {
   updatePetProfileApi,
   updateGoodsInfoApi,
   verifyMaintenanceCodeApi,
-  uploadFile
+  uploadFile,
+  fetchRecentScanRecordsApi
 } from '@/services/api'
 
 const route = useRoute()
@@ -42,6 +43,9 @@ const showMaintenanceDialog = ref(false)
 const isEditing = ref(false)
 const showLocationDialog = ref(false)
 const showLocationViewDialog = ref(false)
+const showScanRecordsDialog = ref(false)
+const scanRecords = ref(null)
+const loadingScanRecords = ref(false)
 
 const FORM_KEY_MAP = Object.freeze({
   person: [
@@ -397,6 +401,50 @@ const openGoogleMaps = () => {
   }
 }
 
+const handleOpenScanRecords = async () => {
+  if (!qrCode.value) {
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter a QR code first.', life: 2400 })
+    return
+  }
+  showScanRecordsDialog.value = true
+  await fetchScanRecords()
+}
+
+const fetchScanRecords = async () => {
+  if (!qrCode.value) return
+  loadingScanRecords.value = true
+  try {
+    const data = await fetchRecentScanRecordsApi(qrCode.value, 10)
+    scanRecords.value = data
+  } catch (error) {
+    toast.add({ severity: 'error', summary: 'Failed', detail: parseError(error), life: 3200 })
+    scanRecords.value = null
+  } finally {
+    loadingScanRecords.value = false
+  }
+}
+
+const formatDateTime = (dateString) => {
+  if (!dateString) return '-'
+  const date = new Date(dateString)
+  return date.toLocaleString('en-US', {
+    year: 'numeric',
+    month: '2-digit',
+    day: '2-digit',
+    hour: '2-digit',
+    minute: '2-digit',
+    second: '2-digit'
+  })
+}
+
+const openRecordLocation = (record) => {
+  if (record.latitude && record.longitude) {
+    window.open(`https://www.google.com/maps?q=${record.latitude},${record.longitude}`, '_blank')
+  } else if (record.address) {
+    window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(record.address)}`, '_blank')
+  }
+}
+
 onMounted(() => {
   if (!qrCode.value) {
     setDocumentTitle()
@@ -581,8 +629,14 @@ onMounted(() => {
                         {{ isPerson ? (profile?.specialNote || 'No extra details') : (profile?.remark || 'No extra details') }}
                       </p>
                     </div>
-                    <!-- Edit button -->
-                    <div v-if="!isEditing" class="pt-4">
+                    <!-- Action buttons -->
+                    <div v-if="!isEditing" class="pt-4 space-y-3">
+                      <button type="button"
+                        class="w-full flex items-center justify-center gap-2 rounded-xl border border-cyan-300 bg-cyan-50 px-5 py-2.5 text-sm font-medium text-cyan-700 transition-all duration-200 hover:border-cyan-400 hover:bg-cyan-100 hover:shadow-md"
+                        @click="handleOpenScanRecords">
+                        <i class="pi pi-history text-xs" />
+                        SCAN RECORDS
+                      </button>
                       <button type="button"
                         class="w-full flex items-center justify-center gap-2 rounded-xl border border-slate-300 bg-white px-5 py-2.5 text-sm font-medium text-slate-700 transition-all duration-200 hover:border-slate-400 hover:bg-slate-50 hover:shadow-md"
                         @click="showMaintenanceDialog = true">
@@ -814,6 +868,111 @@ onMounted(() => {
     <LocationPicker v-model="showLocationViewDialog" :initial-location="profile?.location" :closable="true"
       :readonly="true" />
 
+    <!-- Scan records dialog -->
+    <Dialog v-model:visible="showScanRecordsDialog" modal :closable="true" :closeOnEscape="true"
+      :dismissableMask="true" :style="{ width: '90vw', maxWidth: '800px' }" :draggable="false">
+      <template #header>
+        <div class="flex items-center gap-3">
+          <div class="flex h-10 w-10 items-center justify-center rounded-full bg-cyan-100">
+            <i class="pi pi-history text-lg text-cyan-600" />
+          </div>
+          <div>
+            <h3 class="text-lg font-semibold text-slate-900">Recent Scan Records</h3>
+            <p class="text-sm text-slate-500">
+              QR Code: <span class="font-mono">{{ qrCode }}</span>
+            </p>
+          </div>
+        </div>
+      </template>
+
+      <div class="space-y-4 py-4">
+        <div v-if="loadingScanRecords" class="flex items-center justify-center py-8">
+          <i class="pi pi-spin pi-spinner text-3xl text-slate-400" />
+          <span class="ml-3 text-slate-600">Loading...</span>
+        </div>
+
+        <div v-else-if="scanRecords">
+
+          <div v-if="scanRecords.records && scanRecords.records.length > 0" class="space-y-3 max-h-[60vh] overflow-y-auto">
+            <div v-for="record in scanRecords.records" :key="record.id"
+              class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow">
+              <div class="flex items-start justify-between gap-4">
+                <div class="flex-1 space-y-2">
+                  <div class="flex items-center gap-2">
+                    <i class="pi pi-clock text-sm text-slate-500" />
+                    <span class="text-sm font-medium text-slate-700">Scan Time</span>
+                    <span class="text-sm text-slate-600">{{ formatDateTime(record.scanTime) }}</span>
+                  </div>
+
+                  <div v-if="record.address" class="flex items-start gap-2">
+                    <i class="pi pi-map-marker text-sm text-slate-500 mt-0.5" />
+                    <div class="flex-1">
+                      <span class="text-sm font-medium text-slate-700">Address</span>
+                      <p class="text-sm text-slate-600 break-words">{{ record.address }}</p>
+                    </div>
+                    <button v-if="record.latitude && record.longitude" type="button"
+                      class="ml-2 flex-shrink-0 rounded-lg border border-cyan-200 bg-cyan-50 px-2 py-1 text-xs text-cyan-700 hover:bg-cyan-100 transition"
+                      @click="openRecordLocation(record)">
+                      <i class="pi pi-external-link mr-1" />
+                      Map
+                    </button>
+                  </div>
+
+                  <div v-if="record.latitude && record.longitude" class="flex items-center gap-2">
+                    <i class="pi pi-globe text-sm text-slate-500" />
+                    <span class="text-sm text-slate-600">
+                      Coordinates: {{ record.latitude }}, {{ record.longitude }}
+                    </span>
+                  </div>
+
+                  <div v-if="record.ipAddress" class="flex items-center gap-2">
+                    <i class="pi pi-desktop text-sm text-slate-500" />
+                    <span class="text-sm text-slate-600">IP: {{ record.ipAddress }}</span>
+                  </div>
+
+                  <div v-if="record.userAgent" class="flex items-start gap-2">
+                    <i class="pi pi-browser text-sm text-slate-500 mt-0.5" />
+                    <div class="flex-1">
+                      <span class="text-sm font-medium text-slate-700">Device Info</span>
+                      <p class="text-xs text-slate-500 break-words mt-0.5">{{ record.userAgent }}</p>
+                    </div>
+                  </div>
+                </div>
+              </div>
+            </div>
+          </div>
+
+          <div v-else class="rounded-xl border border-slate-200 bg-slate-50 p-8 text-center">
+            <i class="pi pi-inbox text-4xl text-slate-400 mb-3" />
+            <p class="text-sm text-slate-600">No scan records</p>
+          </div>
+        </div>
+
+        <div v-else class="rounded-xl border border-red-200 bg-red-50 p-4">
+          <div class="flex items-center gap-2">
+            <i class="pi pi-exclamation-triangle text-red-600" />
+            <span class="text-sm text-red-700">Failed to load, please try again later</span>
+          </div>
+        </div>
+      </div>
+
+      <template #footer>
+        <div class="flex justify-end gap-3">
+          <button type="button"
+            class="rounded-xl border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
+            @click="showScanRecordsDialog = false">
+            Close
+          </button>
+          <button type="button"
+            class="rounded-xl bg-cyan-600 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-700 disabled:opacity-50"
+            :disabled="loadingScanRecords" @click="fetchScanRecords">
+            <i class="pi pi-refresh mr-2" :class="{ 'pi-spin': loadingScanRecords }" />
+            Refresh
+          </button>
+        </div>
+      </template>
+    </Dialog>
+
     <!-- Maintenance code dialog -->
     <Dialog v-model:visible="showMaintenanceDialog" modal :closable="!isFirstFill" :closeOnEscape="!isFirstFill"
       :dismissableMask="!isFirstFill" :style="{ width: '90vw', maxWidth: '450px' }" :draggable="false">