Browse Source

新增维护码缓存功能,允许用户在二维码管理和验证过程中自动填充维护码,提升用户体验。同时优化了路由守卫逻辑,确保二维码参数的有效性和处理。更新了多个视图以支持维护码的缓存和提示功能。

wuyi 2 tuần trước cách đây
mục cha
commit
757118bb90

+ 7 - 6
src/router/index.js

@@ -64,8 +64,9 @@ router.beforeEach(async (to) => {
   if (to.meta?.guestOnly) {
     if (userStore.token) {
       // 如果带有二维码参数,跳转到二维码页面
-      if (to.query.qrCode) {
-        return { name: 'scan', params: { qrCode: to.query.qrCode } }
+      const qrCode = to.query?.qrCode?.toString()
+      if (qrCode && qrCode !== 'undefined') {
+        return { name: 'scan', params: { qrCode } }
       }
       return { name: 'qrmanagerHome' }
     }
@@ -77,8 +78,8 @@ router.beforeEach(async (to) => {
 
   if (!userStore.token) {
     // 如果目标路由带有二维码参数,传递到登录页
-    const qrCode = to.params?.qrCode || to.query?.qrCode
-    if (qrCode) {
+    const qrCode = to.params?.qrCode?.toString() || to.query?.qrCode?.toString()
+    if (qrCode && qrCode !== 'undefined') {
       return { name: 'qrmanagerAuth', query: { qrCode, redirect: to.fullPath } }
     }
     return { name: 'qrmanagerAuth', query: { redirect: to.fullPath } }
@@ -90,8 +91,8 @@ router.beforeEach(async (to) => {
       await userStore.sync()
     } catch (e) {
       userStore.logout()
-      const qrCode = to.params?.qrCode || to.query?.qrCode
-      if (qrCode) {
+      const qrCode = to.params?.qrCode?.toString() || to.query?.qrCode?.toString()
+      if (qrCode && qrCode !== 'undefined') {
         return { name: 'qrmanagerAuth', query: { qrCode, redirect: to.fullPath } }
       }
       return { name: 'qrmanagerAuth', query: { redirect: to.fullPath } }

+ 126 - 0
src/utils/maintenanceCodeCache.js

@@ -0,0 +1,126 @@
+/**
+ * 维护码缓存工具
+ * 将二维码和维护码存储到 localStorage,10分钟有效期
+ */
+
+const STORAGE_KEY = 'qr_maintenance_codes'
+const EXPIRY_MINUTES = 10
+
+/**
+ * 获取所有缓存的维护码
+ * @returns {Object} { qrCode: { maintenanceCode, expiresAt } }
+ */
+function getCachedCodes() {
+  try {
+    const cached = localStorage.getItem(STORAGE_KEY)
+    if (!cached) return {}
+    return JSON.parse(cached)
+  } catch {
+    return {}
+  }
+}
+
+/**
+ * 保存所有缓存的维护码
+ * @param {Object} codes - { qrCode: { maintenanceCode, expiresAt } }
+ */
+function saveCachedCodes(codes) {
+  try {
+    localStorage.setItem(STORAGE_KEY, JSON.stringify(codes))
+  } catch (error) {
+    console.error('Failed to save maintenance codes to cache:', error)
+  }
+}
+
+/**
+ * 清理过期的维护码
+ */
+function cleanExpiredCodes() {
+  const codes = getCachedCodes()
+  const now = Date.now()
+  const cleaned = {}
+  
+  for (const [qrCode, data] of Object.entries(codes)) {
+    if (data.expiresAt && data.expiresAt > now) {
+      cleaned[qrCode] = data
+    }
+  }
+  
+  if (Object.keys(cleaned).length !== Object.keys(codes).length) {
+    saveCachedCodes(cleaned)
+  }
+  
+  return cleaned
+}
+
+/**
+ * 保存维护码到缓存
+ * @param {string} qrCode - 二维码编号
+ * @param {string} maintenanceCode - 维护码
+ */
+export function saveMaintenanceCode(qrCode, maintenanceCode) {
+  if (!qrCode || !maintenanceCode) return
+  
+  const codes = cleanExpiredCodes()
+  const expiresAt = Date.now() + EXPIRY_MINUTES * 60 * 1000
+  
+  codes[qrCode] = {
+    maintenanceCode: maintenanceCode.trim(),
+    expiresAt
+  }
+  
+  saveCachedCodes(codes)
+}
+
+/**
+ * 从缓存获取维护码
+ * @param {string} qrCode - 二维码编号
+ * @returns {string|null} 维护码,如果不存在或已过期则返回 null
+ */
+export function getMaintenanceCode(qrCode) {
+  if (!qrCode) return null
+  
+  const codes = cleanExpiredCodes()
+  const data = codes[qrCode]
+  
+  if (!data) return null
+  
+  // 再次检查是否过期(防止在清理和获取之间过期)
+  if (data.expiresAt && data.expiresAt <= Date.now()) {
+    // 已过期,删除
+    delete codes[qrCode]
+    saveCachedCodes(codes)
+    return null
+  }
+  
+  return data.maintenanceCode || null
+}
+
+/**
+ * 删除指定二维码的维护码缓存
+ * @param {string} qrCode - 二维码编号
+ */
+export function removeMaintenanceCode(qrCode) {
+  if (!qrCode) return
+  
+  const codes = getCachedCodes()
+  if (codes[qrCode]) {
+    delete codes[qrCode]
+    saveCachedCodes(codes)
+  }
+}
+
+/**
+ * 清除所有维护码缓存
+ */
+export function clearAllMaintenanceCodes() {
+  try {
+    localStorage.removeItem(STORAGE_KEY)
+  } catch (error) {
+    console.error('Failed to clear maintenance codes cache:', error)
+  }
+}
+
+// 初始化时清理过期数据
+cleanExpiredCodes()
+

+ 54 - 4
src/views/JumpView.vue

@@ -9,6 +9,7 @@ import {
     verifyMaintenanceCodeApi,
     fetchRecentScanRecordsApi
 } from '@/services/api'
+import { saveMaintenanceCode, getMaintenanceCode } from '@/utils/maintenanceCodeCache'
 
 const route = useRoute()
 const router = useRouter()
@@ -34,6 +35,7 @@ const infoStatus = reactive({
 const maintenanceCode = ref('')
 const maintenancePassed = ref(false)
 const showMaintenanceDialog = ref(false)
+const hasCachedMaintenanceCode = ref(false)
 const isEditing = ref(false)
 const showScanRecordsDialog = ref(false)
 const showScanRecordsMaintenanceDialog = ref(false)
@@ -119,7 +121,19 @@ const handleQrSubmit = () => {
 }
 
 const handleVerifyMaintenance = async () => {
-    if (!qrCode.value || !maintenanceCode.value) {
+    if (!qrCode.value) {
+        toast.add({ severity: 'warn', summary: 'Notice', detail: 'QR code is required.', life: 2400 })
+        return
+    }
+    // 如果有缓存的维护码,直接使用;否则使用用户输入的
+    let codeToUse = maintenanceCode.value.trim()
+    if (!codeToUse && hasCachedMaintenanceCode.value) {
+        const cachedCode = getMaintenanceCode(qrCode.value)
+        if (cachedCode) {
+            codeToUse = cachedCode
+        }
+    }
+    if (!codeToUse) {
         toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
         return
     }
@@ -127,8 +141,10 @@ const handleVerifyMaintenance = async () => {
     try {
         await verifyMaintenanceCodeApi({
             qrCode: qrCode.value,
-            maintenanceCode: maintenanceCode.value
+            maintenanceCode: codeToUse
         })
+        // 保存维护码到缓存
+        saveMaintenanceCode(qrCode.value, codeToUse)
         maintenancePassed.value = true
         showMaintenanceDialog.value = false
         isEditing.value = true
@@ -363,6 +379,32 @@ watch(isFirstFill, (value) => {
     }
 })
 
+// 当打开维护码对话框时,自动填充缓存的维护码
+watch(showMaintenanceDialog, (isOpen) => {
+    if (isOpen && qrCode.value) {
+        const cachedCode = getMaintenanceCode(qrCode.value)
+        hasCachedMaintenanceCode.value = !!cachedCode
+        if (cachedCode) {
+            maintenanceCode.value = cachedCode
+        } else {
+            maintenanceCode.value = ''
+        }
+    }
+})
+
+// 当二维码变化时,尝试自动填充维护码
+watch(qrCode, (newQrCode) => {
+    if (newQrCode) {
+        const cachedCode = getMaintenanceCode(newQrCode)
+        hasCachedMaintenanceCode.value = !!cachedCode
+        if (cachedCode) {
+            maintenanceCode.value = cachedCode
+        } else {
+            maintenanceCode.value = ''
+        }
+    }
+})
+
 onMounted(() => {
     if (!qrCode.value) {
         setDocumentTitle()
@@ -621,12 +663,20 @@ onMounted(() => {
             </template>
 
             <div class="space-y-4 py-4">
-                <div>
+                <div v-if="!hasCachedMaintenanceCode">
                     <label class="mb-2 block text-sm font-medium text-slate-700">Maintenance code</label>
                     <input v-model="maintenanceCode" type="text" maxlength="8"
                         class="w-full rounded-xl border border-slate-300 px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
                         placeholder="Enter the maintenance code" @keyup.enter="handleVerifyMaintenance" autofocus />
                 </div>
+                <div v-else class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
+                    <div class="flex items-start gap-2">
+                        <i class="pi pi-check-circle text-sm text-emerald-600" />
+                        <p class="text-xs text-emerald-700">
+                            Maintenance code has been entered. You can click the button to proceed.
+                        </p>
+                    </div>
+                </div>
                 <div v-if="isFirstFill" class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
                     <div class="flex items-start gap-2">
                         <i class="pi pi-info-circle text-sm text-emerald-600" />
@@ -646,7 +696,7 @@ onMounted(() => {
                     </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="loading.verifying || !maintenanceCode" @click="handleVerifyMaintenance">
+                        :disabled="loading.verifying || (!hasCachedMaintenanceCode && !maintenanceCode)" @click="handleVerifyMaintenance">
                         {{ loading.verifying ? 'Verifying...' : 'Verify' }}
                     </button>
                 </div>

+ 68 - 7
src/views/ScanView.vue

@@ -15,10 +15,13 @@ import {
   uploadFile,
   fetchRecentScanRecordsApi
 } from '@/services/api'
+import { saveMaintenanceCode, getMaintenanceCode } from '@/utils/maintenanceCodeCache'
+import { useUserStore } from '@/stores/user'
 
 const route = useRoute()
 const router = useRouter()
 const toast = useToast()
+const userStore = useUserStore()
 
 // Pull QR code from route params
 const qrCode = ref(route.params.qrCode?.toString().trim() || '')
@@ -43,6 +46,7 @@ const maintenancePassed = ref(false)
 const showMaintenancePanel = ref(false)
 const showMaintenanceDialog = ref(false)
 const viewMaintenanceCode = ref('')
+const hasCachedMaintenanceCode = ref(false)
 const loadingViewVerification = ref(false)
 const isEditing = ref(false)
 const showLocationDialog = ref(false)
@@ -239,9 +243,14 @@ const fetchQrDetails = async () => {
       return
     }
     
-    // 如果二维码未激活,跳转到用户管理入口并带着二维码参数
-    if (!data.isActivated) {
-      await router.replace({ name: 'qrmanagerAuth', query: { qrCode: qrCode.value } })
+    // 如果二维码未激活没有 info 信息,且未绑定,跳转到用户管理入口并带着二维码参数
+    if (!data.isActivated && (data.info === null || data.info === undefined) && !data.isBind) {
+      // 如果用户已登录,跳转到管理页面;否则跳转到登录页面
+      if (userStore.token) {
+        await router.replace({ name: 'qrmanagerHome', query: { qrCode: qrCode.value } })
+      } else {
+        await router.replace({ name: 'qrmanagerAuth', query: { qrCode: qrCode.value } })
+      }
       return
     }
     
@@ -274,7 +283,19 @@ const handleQrSubmit = () => {
 }
 
 const handleVerifyMaintenance = async () => {
-  if (!qrCode.value || !maintenanceCode.value) {
+  if (!qrCode.value) {
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'QR code is required.', life: 2400 })
+    return
+  }
+  // 如果有缓存的维护码,直接使用;否则使用用户输入的
+  let codeToUse = maintenanceCode.value.trim()
+  if (!codeToUse && hasCachedMaintenanceCode.value) {
+    const cachedCode = getMaintenanceCode(qrCode.value)
+    if (cachedCode) {
+      codeToUse = cachedCode
+    }
+  }
+  if (!codeToUse) {
     toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
     return
   }
@@ -282,8 +303,10 @@ const handleVerifyMaintenance = async () => {
   try {
     await verifyMaintenanceCodeApi({
       qrCode: qrCode.value,
-      maintenanceCode: maintenanceCode.value
+      maintenanceCode: codeToUse
     })
+    // 保存维护码到缓存
+    saveMaintenanceCode(qrCode.value, codeToUse)
     maintenancePassed.value = true
     showMaintenancePanel.value = false
     showMaintenanceDialog.value = false
@@ -313,6 +336,8 @@ const handleVerifyMaintenanceForView = async () => {
       maintenanceCode: viewMaintenanceCode.value
     })
     if (data.valid) {
+      // 保存维护码到缓存
+      saveMaintenanceCode(qrCode.value, viewMaintenanceCode.value)
       // 更新二维码详情和用户信息
       qrDetail.value = {
         ...qrDetail.value,
@@ -588,6 +613,34 @@ watch(isFirstFill, (value) => {
   }
 })
 
+// 当打开维护码对话框时,自动填充缓存的维护码
+watch(showMaintenanceDialog, (isOpen) => {
+  if (isOpen && qrCode.value) {
+    const cachedCode = getMaintenanceCode(qrCode.value)
+    hasCachedMaintenanceCode.value = !!cachedCode
+    if (cachedCode) {
+      maintenanceCode.value = cachedCode
+    } else {
+      maintenanceCode.value = ''
+    }
+  }
+})
+
+// 当二维码变化时,尝试自动填充维护码
+watch(qrCode, (newQrCode) => {
+  if (newQrCode) {
+    const cachedCode = getMaintenanceCode(newQrCode)
+    hasCachedMaintenanceCode.value = !!cachedCode
+    if (cachedCode) {
+      maintenanceCode.value = cachedCode
+      viewMaintenanceCode.value = cachedCode
+    } else {
+      maintenanceCode.value = ''
+      viewMaintenanceCode.value = ''
+    }
+  }
+})
+
 const handleOpenLocationDialog = () => {
   showLocationDialog.value = true
 }
@@ -1577,7 +1630,7 @@ onMounted(() => {
       </template>
 
       <div class="space-y-4 py-4">
-        <div>
+        <div v-if="!hasCachedMaintenanceCode">
           <label class="mb-2 block text-sm font-medium text-slate-700">Maintenance code</label>
           <input v-model="maintenanceCode" type="text" maxlength="8"
             class="w-full rounded-xl border border-slate-300 px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
@@ -1587,6 +1640,14 @@ onMounted(() => {
             Don't know the maintenance code?
           </button>
         </div>
+        <div v-else class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
+          <div class="flex items-start gap-2">
+            <i class="pi pi-check-circle text-sm text-emerald-600" />
+            <p class="text-xs text-emerald-700">
+              Maintenance code has been entered. You can click the button to proceed.
+            </p>
+          </div>
+        </div>
         <div v-if="isFirstFill" class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
           <div class="flex items-start gap-2">
             <i class="pi pi-info-circle text-sm text-emerald-600" />
@@ -1606,7 +1667,7 @@ onMounted(() => {
           </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="loading.verifying || !maintenanceCode" @click="handleVerifyMaintenance">
+            :disabled="loading.verifying || (!hasCachedMaintenanceCode && !maintenanceCode)" @click="handleVerifyMaintenance">
             {{ loading.verifying ? 'Verifying...' : 'Verify' }}
           </button>
         </div>

+ 46 - 13
src/views/qrmanager/AuthView.vue

@@ -1,6 +1,6 @@
 <script setup>
 import { ref, computed } from 'vue'
-import { useRoute, useRouter } from 'vue-router'
+import { useRoute, useRouter, RouterLink } from 'vue-router'
 import { useToast } from 'primevue/usetoast'
 import { useUserStore } from '@/stores/user'
 
@@ -10,7 +10,9 @@ const toast = useToast()
 const userStore = useUserStore()
 
 // 模式切换:'login' 或 'register'
-const mode = ref('login')
+// 当有二维码参数时,默认显示注册表单
+const qrCode = computed(() => route.query.qrCode?.toString() || '')
+const mode = ref(qrCode.value ? 'register' : 'login')
 
 // 注册表单
 const registerName = ref('')
@@ -26,9 +28,6 @@ const loginPassword = ref('')
 const showLoginPassword = ref(false)
 const loginLoading = ref(false)
 
-// 获取二维码参数
-const qrCode = computed(() => route.query.qrCode?.toString() || '')
-
 // 密码验证
 const validatePassword = (password) => {
   if (password.length < 8) return 'Password must be at least 8 characters'
@@ -65,8 +64,8 @@ const handleRegister = async () => {
     
     // 根据是否有二维码参数决定跳转
     if (qrCode.value) {
-      // 跳转到二维码首次输入信息界面
-      await router.replace({ name: 'scan', params: { qrCode: qrCode.value } })
+      // 跳转到二维码管理主界面,并带上 qrCode 参数以触发绑定弹窗
+      await router.replace({ name: 'qrmanagerHome', query: { qrCode: qrCode.value } })
     } else {
       // 跳转到二维码管理主界面
       await router.replace({ name: 'qrmanagerHome' })
@@ -101,8 +100,8 @@ const handleLogin = async () => {
     
     // 根据是否有二维码参数决定跳转
     if (qrCode.value) {
-      // 跳转到二维码首次输入信息界面
-      await router.replace({ name: 'scan', params: { qrCode: qrCode.value } })
+      // 跳转到二维码管理主界面,并带上 qrCode 参数以触发绑定弹窗
+      await router.replace({ name: 'qrmanagerHome', query: { qrCode: qrCode.value } })
     } else {
       // 跳转到二维码管理主界面
       await router.replace({ name: 'qrmanagerHome' })
@@ -128,13 +127,19 @@ const handleLogin = async () => {
         <div class="mb-6 text-center">
           <div class="text-2xl font-bold text-slate-900">QR Code Management</div>
           <div class="mt-1 text-sm text-slate-500">Register or login to manage your QR codes</div>
-          <div v-if="qrCode" class="mt-2 rounded-lg bg-blue-50 px-3 py-2 text-xs text-blue-700">
-            Detected inactive QR code: <span class="font-mono font-semibold">{{ qrCode }}</span>
+          <!-- 首次扫码提示 -->
+          <div v-if="qrCode" class="mt-4 rounded-2xl bg-slate-50 border border-slate-200 px-4 py-3">
+            <div class="flex items-center gap-2.5">
+              <i class="pi pi-info-circle text-base text-slate-500"></i>
+              <div class="flex-1 text-sm text-slate-700 leading-relaxed">
+                First time activating QR code, please log in to bind your account for future management
+              </div>
+            </div>
           </div>
         </div>
 
-        <!-- 模式切换 -->
-        <div class="mb-6 flex rounded-xl bg-slate-100 p-1">
+        <!-- 模式切换(仅在无二维码参数时显示) -->
+        <div v-if="!qrCode" class="mb-6 flex rounded-xl bg-slate-100 p-1">
           <button
             type="button"
             class="flex-1 rounded-lg px-4 py-2 text-sm font-medium transition-all"
@@ -193,6 +198,20 @@ const handleLogin = async () => {
             {{ loginLoading ? 'Logging in...' : 'Login' }}
           </button>
 
+          <!-- 首次扫码时的注册提示 -->
+          <div v-if="qrCode" class="text-center">
+            <div class="text-sm text-slate-600">
+              Don't have an account?
+              <button
+                type="button"
+                class="ml-1 font-semibold text-blue-600 hover:text-blue-800 transition-colors"
+                @click="mode = 'register'"
+              >
+                Click to register
+              </button>
+            </div>
+          </div>
+
           <div class="text-center text-sm">
             <RouterLink class="text-slate-600 hover:text-slate-900" :to="{ name: 'home' }">
               Return to Scan Page
@@ -259,6 +278,20 @@ const handleLogin = async () => {
             {{ registerLoading ? 'Registering...' : 'Register' }}
           </button>
 
+          <!-- 首次扫码时的登录提示 -->
+          <div v-if="qrCode" class="text-center">
+            <div class="text-sm text-slate-600">
+              Already have an account?
+              <button
+                type="button"
+                class="ml-1 font-semibold text-blue-600 hover:text-blue-800 transition-colors"
+                @click="mode = 'login'"
+              >
+                Click to login
+              </button>
+            </div>
+          </div>
+
           <div class="text-center text-sm">
             <RouterLink class="text-slate-600 hover:text-slate-900" :to="{ name: 'home' }">
               Return to Scan Page

+ 214 - 4
src/views/qrmanager/HomeView.vue

@@ -1,10 +1,12 @@
 <script setup>
-import { ref, onMounted, computed, inject } from 'vue'
-import { useRouter } from 'vue-router'
+import { ref, onMounted, computed, inject, watch } from 'vue'
+import { useRouter, useRoute } from 'vue-router'
 import { fetchMyQrCodesApi, fetchQrInfoApi, resetPasswordApi, bindQrCodeApi } from '@/services/api'
 import { useToast } from 'primevue/usetoast'
+import { saveMaintenanceCode, getMaintenanceCode } from '@/utils/maintenanceCodeCache'
 
 const router = useRouter()
+const route = useRoute()
 const toast = useToast()
 
 // 注入父组件提供的注册方法
@@ -41,6 +43,13 @@ const bindQrCode = ref('')
 const bindMaintenanceCode = ref('')
 const bindLoading = ref(false)
 
+// 首次扫描后的绑定弹窗(从 URL 参数触发)
+const showFirstScanBindDialog = ref(false)
+const firstScanQrCode = ref('')
+const firstScanMaintenanceCode = ref('')
+const firstScanBindLoading = ref(false)
+const showMaintenanceCodeHintDialog = ref(false)
+
 const qrTypeLabel = computed(() => {
   if (!qrLookupData.value?.qrType) return '-'
   return qrLookupData.value.qrType === 'person' ? 'Person' : qrLookupData.value.qrType === 'pet' ? 'Pet/Item' : qrLookupData.value.qrType
@@ -216,10 +225,12 @@ const openScanPage = async () => {
 }
 
 // 绑定二维码相关
+const hasCachedBindMaintenanceCode = ref(false)
 const openBindDialog = () => {
   showBindDialog.value = true
   bindQrCode.value = ''
   bindMaintenanceCode.value = ''
+  hasCachedBindMaintenanceCode.value = false
 }
 
 const closeBindDialog = () => {
@@ -234,7 +245,15 @@ const submitBind = async () => {
     toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter QR code number', life: 2000 })
     return
   }
-  const maintenanceCode = bindMaintenanceCode.value.trim()
+  // 如果有缓存的维护码,直接使用;否则使用用户输入的
+  let maintenanceCode = bindMaintenanceCode.value.trim()
+  if (!maintenanceCode && hasCachedBindMaintenanceCode.value) {
+    // 从缓存中获取
+    const cachedCode = getMaintenanceCode(code)
+    if (cachedCode) {
+      maintenanceCode = cachedCode
+    }
+  }
   if (!maintenanceCode) {
     toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter maintenance code', life: 2000 })
     return
@@ -242,6 +261,8 @@ const submitBind = async () => {
   bindLoading.value = true
   try {
     await bindQrCodeApi({ qrCode: code, maintenanceCode })
+    // 保存维护码到缓存
+    saveMaintenanceCode(code, maintenanceCode)
     toast.add({
       severity: 'success',
       summary: 'Success',
@@ -262,6 +283,83 @@ const submitBind = async () => {
   }
 }
 
+// 首次扫描后的绑定相关
+const hasCachedMaintenanceCode = ref(false)
+const openFirstScanBindDialog = (qrCode) => {
+  firstScanQrCode.value = qrCode
+  // 尝试从缓存中获取维护码
+  const cachedCode = getMaintenanceCode(qrCode)
+  hasCachedMaintenanceCode.value = !!cachedCode
+  firstScanMaintenanceCode.value = cachedCode || ''
+  showFirstScanBindDialog.value = true
+}
+
+const closeFirstScanBindDialog = () => {
+  showFirstScanBindDialog.value = false
+  firstScanQrCode.value = ''
+  firstScanMaintenanceCode.value = ''
+  hasCachedMaintenanceCode.value = false
+  // 清除 URL 中的 qrCode 参数
+  router.replace({ name: 'qrmanagerHome' })
+}
+
+const submitFirstScanBind = async () => {
+  const code = firstScanQrCode.value.trim()
+  if (!code) {
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'QR code number is required', life: 2000 })
+    return
+  }
+  // 如果有缓存的维护码,直接使用;否则使用用户输入的
+  let maintenanceCode = firstScanMaintenanceCode.value.trim()
+  if (!maintenanceCode && hasCachedMaintenanceCode.value) {
+    // 从缓存中获取
+    const cachedCode = getMaintenanceCode(code)
+    if (cachedCode) {
+      maintenanceCode = cachedCode
+    }
+  }
+  if (!maintenanceCode) {
+    toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter maintenance code', life: 2000 })
+    return
+  }
+  firstScanBindLoading.value = true
+  try {
+    await bindQrCodeApi({ qrCode: code, maintenanceCode })
+    // 保存维护码到缓存
+    saveMaintenanceCode(code, maintenanceCode)
+    toast.add({
+      severity: 'success',
+      summary: 'Success',
+      detail: 'Binding successful',
+      life: 2500
+    })
+    closeFirstScanBindDialog()
+    await fetchMyQrCodes(pagination.value.page)
+  } catch (e) {
+    toast.add({
+      severity: 'error',
+      summary: 'Binding Failed',
+      detail: e?.message || e?.error || 'Binding failed, please check if the maintenance code is correct',
+      life: 3000
+    })
+  } finally {
+    firstScanBindLoading.value = false
+  }
+}
+
+// 监听绑定二维码输入,自动填充维护码
+watch(bindQrCode, (newQrCode) => {
+  if (newQrCode && showBindDialog.value) {
+    const cachedCode = getMaintenanceCode(newQrCode.trim())
+    hasCachedBindMaintenanceCode.value = !!cachedCode
+    if (cachedCode) {
+      bindMaintenanceCode.value = cachedCode
+    } else {
+      bindMaintenanceCode.value = ''
+    }
+  }
+})
+
 onMounted(() => {
   fetchMyQrCodes()
   
@@ -269,6 +367,12 @@ onMounted(() => {
   if (registerOpenPasswordDialog) {
     registerOpenPasswordDialog(openPasswordDialog)
   }
+  
+  // 检查 URL 中是否有 qrCode 参数,如果有则显示首次扫描绑定对话框
+  const qrCodeFromQuery = route.query.qrCode?.toString().trim()
+  if (qrCodeFromQuery) {
+    openFirstScanBindDialog(qrCodeFromQuery)
+  }
 })
 </script>
 
@@ -555,7 +659,7 @@ onMounted(() => {
               required
             />
           </div>
-          <div>
+          <div v-if="!hasCachedBindMaintenanceCode">
             <label class="mb-1 block text-sm font-medium text-slate-700">Maintenance Code <span class="text-red-500">*</span></label>
             <input
               v-model="bindMaintenanceCode"
@@ -566,6 +670,14 @@ onMounted(() => {
               required
             />
           </div>
+          <div v-else class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
+            <div class="flex items-start gap-2">
+              <i class="pi pi-check-circle text-sm text-emerald-600" />
+              <p class="text-xs text-emerald-700">
+                Maintenance code has been entered. You can click the button to proceed.
+              </p>
+            </div>
+          </div>
 
           <div class="flex gap-3 pt-2">
             <button
@@ -587,6 +699,104 @@ onMounted(() => {
         </form>
       </div>
     </div>
+
+    <!-- 首次扫描后的绑定对话框 -->
+    <div
+      v-if="showFirstScanBindDialog"
+      class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
+      @click.self="closeFirstScanBindDialog"
+    >
+      <div class="w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-xl">
+        <div class="mb-4">
+          <div class="text-xl font-semibold text-slate-900">Bind QR Code</div>
+          <div class="mt-1 text-sm text-slate-500">Do you want to bind the QR code you just scanned?</div>
+        </div>
+
+        <div class="mb-4 rounded-xl border border-slate-200 bg-slate-50 p-4">
+          <div class="text-xs font-medium text-slate-500 mb-1">QR Code Number</div>
+          <div class="text-base font-mono font-semibold text-slate-900">{{ firstScanQrCode }}</div>
+        </div>
+
+        <form class="space-y-4" @submit.prevent="submitFirstScanBind">
+          <div v-if="!hasCachedMaintenanceCode">
+            <label class="mb-1 block text-sm font-medium text-slate-700">Maintenance Code <span class="text-red-500">*</span></label>
+            <input
+              v-model="firstScanMaintenanceCode"
+              type="text"
+              maxlength="8"
+              class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
+              placeholder="e.g., AB12CD34"
+              required
+              autofocus
+            />
+            <button
+              type="button"
+              @click="showMaintenanceCodeHintDialog = true"
+              class="mt-2 text-sm text-cyan-600 hover:text-cyan-700 hover:underline"
+            >
+              Don't know the maintenance code?
+            </button>
+          </div>
+          <div v-else class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
+            <div class="flex items-start gap-2">
+              <i class="pi pi-check-circle text-sm text-emerald-600" />
+              <p class="text-xs text-emerald-700">
+                Maintenance code has been entered. You can click the button to proceed.
+              </p>
+            </div>
+          </div>
+
+          <div class="flex gap-3 pt-2">
+            <button
+              type="button"
+              class="flex-1 rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
+              @click="closeFirstScanBindDialog"
+              :disabled="firstScanBindLoading"
+            >
+              Skip
+            </button>
+            <button
+              type="submit"
+              class="flex-1 rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 disabled:opacity-60"
+              :disabled="firstScanBindLoading"
+            >
+              {{ firstScanBindLoading ? 'Binding...' : 'Confirm Bind' }}
+            </button>
+          </div>
+        </form>
+      </div>
+    </div>
+
+    <!-- 维护码提示对话框 -->
+    <div
+      v-if="showMaintenanceCodeHintDialog"
+      class="fixed inset-0 z-[60] flex items-center justify-center bg-black/50 backdrop-blur-sm"
+      @click.self="showMaintenanceCodeHintDialog = false"
+    >
+      <div class="w-full max-w-2xl rounded-2xl border border-slate-200 bg-white p-6 shadow-xl">
+        <div class="mb-4 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-info-circle text-lg text-cyan-600" />
+          </div>
+          <h3 class="text-lg font-semibold text-slate-900">Find Maintenance Code</h3>
+        </div>
+
+        <div class="py-4">
+          <img src="/img/Maintenance_code.jpg" alt="Maintenance code location hint"
+            class="w-full rounded-xl border border-slate-200" />
+        </div>
+
+        <div class="flex justify-end">
+          <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"
+            @click="showMaintenanceCodeHintDialog = false"
+          >
+            Got it
+          </button>
+        </div>
+      </div>
+    </div>
   </div>
 </template>