Browse Source

新增维护代码验证功能,支持通过维护代码查看二维码信息,并在界面中添加相应的输入框和按钮以便用户操作。同时,更新了表单状态以包含可见性选项。

wuyi 1 month ago
parent
commit
48bff3bd98
2 changed files with 134 additions and 8 deletions
  1. 5 0
      src/services/api.js
  2. 129 8
      src/views/ScanView.vue

+ 5 - 0
src/services/api.js

@@ -53,6 +53,11 @@ export const verifyMaintenanceCodeApi = async (payload) => {
   return response.data
 }
 
+export const verifyMaintenanceCodeInfoApi = async (payload) => {
+  const response = await api.post('/qr/verify/info', payload)
+  return response.data
+}
+
 export const updatePersonProfileApi = async (profilePayload) => {
   const response = await api.put('/person/update', profilePayload)
   return response.data

+ 129 - 8
src/views/ScanView.vue

@@ -10,6 +10,7 @@ import {
   updatePetProfileApi,
   updateGoodsInfoApi,
   verifyMaintenanceCodeApi,
+  verifyMaintenanceCodeInfoApi,
   uploadFile,
   fetchRecentScanRecordsApi
 } from '@/services/api'
@@ -40,6 +41,8 @@ const maintenanceCode = ref('')
 const maintenancePassed = ref(false)
 const showMaintenancePanel = ref(false)
 const showMaintenanceDialog = ref(false)
+const viewMaintenanceCode = ref('')
+const loadingViewVerification = ref(false)
 const isEditing = ref(false)
 const showLocationDialog = ref(false)
 const showLocationViewDialog = ref(false)
@@ -57,10 +60,11 @@ const FORM_KEY_MAP = Object.freeze({
     'emergencyContactName',
     'emergencyContactPhone',
     'emergencyContactEmail',
-    'location'
+    'location',
+    'isVisible'
   ],
-  pet: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark'],
-  goods: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark']
+  pet: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark', 'isVisible'],
+  goods: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark', 'isVisible']
 })
 
 const DEFAULT_FORM_STATE = Object.freeze({
@@ -76,7 +80,8 @@ const DEFAULT_FORM_STATE = Object.freeze({
   contactName: '',
   contactPhone: '',
   contactEmail: '',
-  location: ''
+  location: '',
+  isVisible: true
 })
 
 const createDefaultFormState = () => ({
@@ -103,7 +108,13 @@ const isPerson = computed(() => qrType.value === 'person')
 const isPet = computed(() => qrType.value === 'pet')
 const isGoods = computed(() => qrType.value === 'goods')
 const hasProfile = computed(() => Boolean(profile.value))
-const isFirstFill = computed(() => Boolean(qrDetail.value) && !hasProfile.value)
+const isFirstFill = computed(() => {
+  // 优先判断 isVisible,如果不可见则不认为是第一次使用
+  if (qrDetail.value?.isVisible === false) {
+    return false
+  }
+  return Boolean(qrDetail.value) && !hasProfile.value
+})
 const heroTitle = computed(() => {
   if (!qrDetail.value) return 'Contact Card'
   if (isPerson.value) return 'Person Card'
@@ -131,10 +142,21 @@ const resetForm = (source = null) => {
   if (source) {
     getActiveFormKeys().forEach((key) => {
       if (Object.prototype.hasOwnProperty.call(source, key)) {
-        nextState[key] = source[key] ?? DEFAULT_FORM_STATE[key] ?? ''
+        // 对于布尔值,直接使用值(包括 false)
+        if (key === 'isVisible') {
+          nextState[key] = source[key] !== undefined ? source[key] : true
+        } else {
+          nextState[key] = source[key] ?? DEFAULT_FORM_STATE[key] ?? ''
+        }
       }
     })
   }
+  // 如果 source 中没有 isVisible,则从 qrDetail 中获取
+  if (!source || !Object.prototype.hasOwnProperty.call(source, 'isVisible')) {
+    if (qrDetail.value && Object.prototype.hasOwnProperty.call(qrDetail.value, 'isVisible')) {
+      nextState.isVisible = qrDetail.value.isVisible !== false
+    }
+  }
   formData.value = nextState
 }
 
@@ -158,6 +180,14 @@ const fetchQrDetails = async () => {
   try {
     const data = await fetchQrInfoApi(qrCode.value)
     qrDetail.value = data
+    if (data.isVisible === false) {
+      infoStatus.state = 'notVisible'
+      infoStatus.message = 'QR code information is not visible'
+      profile.value = null
+      resetForm()
+      setDocumentTitle()
+      return
+    }
     profile.value = data.info || null
     resetForm(profile.value)
     infoStatus.state = 'ready'
@@ -206,12 +236,51 @@ const handleVerifyMaintenance = async () => {
   }
 }
 
+const handleVerifyMaintenanceForView = async () => {
+  if (!qrCode.value || !viewMaintenanceCode.value) {
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
+    return
+  }
+  loadingViewVerification.value = true
+  try {
+    const data = await verifyMaintenanceCodeInfoApi({
+      qrCode: qrCode.value,
+      maintenanceCode: viewMaintenanceCode.value
+    })
+    if (data.valid) {
+      // 更新二维码详情和用户信息
+      qrDetail.value = {
+        ...qrDetail.value,
+        ...data,
+        isVisible: true // 验证成功后视为可见
+      }
+      profile.value = data.info || null
+      resetForm(profile.value)
+      infoStatus.state = 'ready'
+      infoStatus.message = ''
+      viewMaintenanceCode.value = ''
+      setDocumentTitle()
+      toast.add({ severity: 'success', summary: 'Verified', detail: data.message || 'Verification successful.', life: 2600 })
+    } else {
+      throw new Error(data.message || 'Verification failed')
+    }
+  } catch (error) {
+    toast.add({ severity: 'error', summary: 'Verification failed', detail: parseError(error), life: 3200 })
+  } finally {
+    loadingViewVerification.value = false
+  }
+}
+
 const buildProfilePayload = () => {
   const payload = {
     qrCode: qrCode.value
   }
   getActiveFormKeys().forEach((key) => {
-    payload[key] = formData.value[key] ?? ''
+    if (key === 'isVisible') {
+      payload[key] = formData.value[key] !== undefined ? formData.value[key] : true
+    } else {
+      payload[key] = formData.value[key] ?? ''
+    }
   })
   if (maintenanceCode.value) {
     payload.maintenanceCode = maintenanceCode.value
@@ -228,7 +297,6 @@ const handleSaveProfile = async () => {
     await updater(payload)
     toast.add({ severity: 'success', summary: 'Saved', detail: 'Profile has been updated.', life: 3000 })
     await fetchQrDetails()
-    // Exit edit mode after a successful save
     isEditing.value = false
     maintenancePassed.value = false
     maintenanceCode.value = ''
@@ -339,6 +407,7 @@ const resetPageState = () => {
   maintenancePassed.value = false
   showMaintenancePanel.value = false
   showMaintenanceDialog.value = false
+  viewMaintenanceCode.value = ''
   isEditing.value = false
   infoStatus.state = qrCode.value ? 'loading' : 'idle'
   infoStatus.message = ''
@@ -509,6 +578,45 @@ onMounted(() => {
             <p class="mt-2 text-xs text-red-200">Please double-check the QR code or reach out for assistance.</p>
           </div>
 
+          <div v-if="infoStatus.state === 'notVisible'" class="rounded-3xl border border-amber-500/40 bg-amber-500/10 p-6">
+            <div class="flex items-start gap-4">
+              <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500/20 flex-shrink-0">
+                <i class="pi pi-eye-slash text-2xl text-amber-300" />
+              </div>
+              <div class="flex-1">
+                <p class="text-lg font-semibold text-amber-100">{{ infoStatus.message }}</p>
+                <p class="mt-2 text-sm text-amber-200">This QR code information has been set to invisible and cannot be viewed.</p>
+              </div>
+            </div>
+          </div>
+
+          <div v-if="infoStatus.state === 'notVisible'" class="rounded-3xl border border-cyan-500/40 bg-cyan-500/10 p-6">
+            <div class="flex items-start gap-4">
+              <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-cyan-500/20 flex-shrink-0">
+                <i class="pi pi-key text-2xl text-cyan-300" />
+              </div>
+              <div class="flex-1">
+                <p class="text-lg font-semibold text-cyan-100">View with maintenance code</p>
+                <p class="mt-2 text-sm text-cyan-200">Enter the maintenance code to view complete information.</p>
+                <div class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
+                  <div class="flex-1">
+                    <label class="mb-1.5 block text-xs font-medium text-cyan-100">Maintenance code</label>
+                    <input v-model="viewMaintenanceCode" type="text" maxlength="8"
+                      class="w-full rounded-xl border border-cyan-400/40 bg-white/10 px-4 py-2.5 text-sm text-white placeholder:text-cyan-300/60 focus:border-cyan-300 focus:bg-white/15 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
+                      placeholder="Enter maintenance code" @keyup.enter="handleVerifyMaintenanceForView" />
+                  </div>
+                  <button type="button"
+                    class="rounded-xl bg-cyan-500 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-600 disabled:opacity-50 sm:whitespace-nowrap"
+                    :disabled="loadingViewVerification || !viewMaintenanceCode" @click="handleVerifyMaintenanceForView">
+                    <i v-if="loadingViewVerification" class="pi pi-spin pi-spinner mr-2" />
+                    <i v-else class="pi pi-check mr-2" />
+                    {{ loadingViewVerification ? 'Verifying...' : 'Verify & View' }}
+                  </button>
+                </div>
+              </div>
+            </div>
+          </div>
+
           <div v-if="loading.info" class="rounded-3xl border border-white/10 bg-white/5 p-6 text-base text-white">
             Loading QR information...
           </div>
@@ -837,6 +945,19 @@ onMounted(() => {
                 :placeholder="isGoods ? 'e.g. item features, usage notes, handling tips' : 'e.g. pet habits, health info, behavior notes'" />
             </label>
 
+            <div class="rounded-2xl border border-slate-200 bg-slate-50 p-5">
+              <div class="flex items-center justify-between">
+                <div class="flex-1">
+                  <p class="text-sm font-medium text-slate-700">Visibility</p>
+                  <p class="mt-1 text-xs text-slate-500">When disabled, others will not be able to view information when scanning this QR code</p>
+                </div>
+                <label class="relative inline-flex items-center cursor-pointer">
+                  <input v-model="formData.isVisible" type="checkbox" class="sr-only peer" />
+                  <div class="w-11 h-6 bg-slate-300 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-cyan-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-600"></div>
+                </label>
+              </div>
+            </div>
+
             <div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
               <p class="text-sm text-slate-500">
                 Submitted data will sync to the public scan page.