Parcourir la source

新增二维码重置功能,优化二维码生成与下载,更新依赖包,调整环境配置

wuyi il y a 1 mois
Parent
commit
a33b0e6914
7 fichiers modifiés avec 758 ajouts et 86 suppressions
  1. 3 1
      .env
  2. 2 0
      package.json
  3. 9 0
      src/services/api.js
  4. 239 0
      src/utils/qrcode.js
  5. 258 83
      src/views/QrCodeManageView.vue
  6. 1 0
      vite.config.js
  7. 246 2
      yarn.lock

+ 3 - 1
.env

@@ -1 +1,3 @@
-VITE_API_URL=http://localhost:3010/api
+VITE_API_URL=http://192.168.6.125:3010/api
+
+VITE_QR_CODE_URL=http://192.168.6.125:5180

+ 2 - 0
package.json

@@ -19,11 +19,13 @@
     "axios": "^1.8.4",
     "chart.js": "^4.4.8",
     "decimal.js": "^10.5.0",
+    "jszip": "^3.10.1",
     "less": "^4.2.2",
     "pinia": "^3.0.1",
     "primeflex": "^4.0.0",
     "primeicons": "^7.0.0",
     "primevue": "^4.3.3",
+    "qrcode": "^1.5.4",
     "tailwindcss-primeui": "^0.6.1",
     "url": "^0.11.4",
     "v-viewer": "^3.0.22",

+ 9 - 0
src/services/api.js

@@ -185,6 +185,15 @@ export const verifyMaintenanceCode = async (qrCode, maintenanceCode) => {
   return response.data
 }
 
+// 重置维护码
+export const resetMaintenanceCode = async (qrCode, maintenanceCode) => {
+  const response = await api.post('/qr/reset', {
+    qrCode,
+    maintenanceCode
+  })
+  return response.data
+}
+
 // ==================== 人员信息相关API ====================
 
 // 创建人员信息

+ 239 - 0
src/utils/qrcode.js

@@ -0,0 +1,239 @@
+import QRCode from 'qrcode'
+import JSZip from 'jszip'
+
+/**
+ * 二维码工具类
+ */
+class QRCodeUtil {
+  /**
+   * 生成二维码的默认配置
+   */
+  static defaultOptions = {
+    width: 300,
+    margin: 2,
+    color: {
+      dark: '#000000',
+      light: '#FFFFFF'
+    },
+    errorCorrectionLevel: 'M'
+  }
+
+  /**
+   * 生成二维码 Data URL
+   * @param {string} text - 要编码的文本或URL
+   * @param {object} options - 可选配置项
+   * @returns {Promise<string>} 返回 base64 格式的图片 Data URL
+   */
+  static async toDataURL(text, options = {}) {
+    try {
+      const mergedOptions = { ...this.defaultOptions, ...options }
+      return await QRCode.toDataURL(text, mergedOptions)
+    } catch (error) {
+      console.error('生成二维码失败:', error)
+      throw new Error('生成二维码失败')
+    }
+  }
+
+  /**
+   * 生成二维码并渲染到 Canvas 元素
+   * @param {HTMLCanvasElement} canvas - Canvas 元素
+   * @param {string} text - 要编码的文本或URL
+   * @param {object} options - 可选配置项
+   * @returns {Promise<void>}
+   */
+  static async toCanvas(canvas, text, options = {}) {
+    try {
+      const mergedOptions = { ...this.defaultOptions, ...options }
+      await QRCode.toCanvas(canvas, text, mergedOptions)
+    } catch (error) {
+      console.error('生成二维码失败:', error)
+      throw new Error('生成二维码失败')
+    }
+  }
+
+  /**
+   * 生成二维码 SVG 字符串
+   * @param {string} text - 要编码的文本或URL
+   * @param {object} options - 可选配置项
+   * @returns {Promise<string>} 返回 SVG 字符串
+   */
+  static async toString(text, options = {}) {
+    try {
+      const mergedOptions = { ...this.defaultOptions, ...options }
+      return await QRCode.toString(text, mergedOptions)
+    } catch (error) {
+      console.error('生成二维码失败:', error)
+      throw new Error('生成二维码失败')
+    }
+  }
+
+  /**
+   * 根据二维码编号生成完整的二维码 URL
+   * @param {string} qrCode - 二维码编号
+   * @param {string} baseUrl - 可选的基础 URL,默认从环境变量读取
+   * @returns {string} 完整的二维码 URL
+   */
+  static generateQrCodeUrl(qrCode, baseUrl = null) {
+    const base = baseUrl || import.meta.env.VITE_QR_CODE_URL
+    return `${base}/${qrCode}`
+  }
+
+  /**
+   * 根据二维码编号生成二维码图片
+   * @param {string} qrCode - 二维码编号
+   * @param {string} baseUrl - 可选的基础 URL
+   * @param {object} options - 可选配置项
+   * @returns {Promise<string>} 返回 base64 格式的图片 Data URL
+   */
+  static async generateQrCodeImage(qrCode, baseUrl = null, options = {}) {
+    const url = this.generateQrCodeUrl(qrCode, baseUrl)
+    return await this.toDataURL(url, options)
+  }
+
+  /**
+   * 下载二维码图片
+   * @param {string} dataUrl - 二维码的 Data URL
+   * @param {string} filename - 文件名(不含扩展名)
+   */
+  static downloadQrCode(dataUrl, filename = 'qrcode') {
+    const link = document.createElement('a')
+    link.href = dataUrl
+    link.download = `${filename}.png`
+    document.body.appendChild(link)
+    link.click()
+    document.body.removeChild(link)
+  }
+
+  /**
+   * 批量生成二维码
+   * @param {Array<string>} qrCodes - 二维码编号数组
+   * @param {string} baseUrl - 可选的基础 URL
+   * @param {object} options - 可选配置项
+   * @returns {Promise<Array<{qrCode: string, url: string, image: string}>>}
+   */
+  static async generateBatchQrCodes(qrCodes, baseUrl = null, options = {}) {
+    const promises = qrCodes.map(async (qrCode) => {
+      const url = this.generateQrCodeUrl(qrCode, baseUrl)
+      const image = await this.toDataURL(url, options)
+      return { qrCode, url, image }
+    })
+    return await Promise.all(promises)
+  }
+
+  /**
+   * 自定义样式的二维码配置预设
+   */
+  static presets = {
+    // 小尺寸
+    small: {
+      width: 150,
+      margin: 1
+    },
+    // 中等尺寸(默认)
+    medium: {
+      width: 300,
+      margin: 2
+    },
+    // 大尺寸
+    large: {
+      width: 500,
+      margin: 3
+    },
+    // 高容错率(适合添加 logo)
+    highError: {
+      width: 300,
+      margin: 2,
+      errorCorrectionLevel: 'H'
+    },
+    // 彩色二维码
+    colored: {
+      width: 300,
+      margin: 2,
+      color: {
+        dark: '#1976D2',
+        light: '#F5F5F5'
+      }
+    }
+  }
+
+  /**
+   * 使用预设配置生成二维码
+   * @param {string} text - 要编码的文本或URL
+   * @param {string} presetName - 预设名称
+   * @returns {Promise<string>}
+   */
+  static async toDataURLWithPreset(text, presetName = 'medium') {
+    const preset = this.presets[presetName] || this.presets.medium
+    return await this.toDataURL(text, preset)
+  }
+
+  /**
+   * 批量生成二维码并打包成 ZIP 文件下载
+   * @param {Array<{qrCode: string, maintenanceCode: string}>} qrCodeList - 二维码列表
+   * @param {string} folderName - 文件夹名称(通常是日期)
+   * @param {string} baseUrl - 可选的基础 URL
+   * @param {object} options - 可选配置项
+   * @returns {Promise<void>}
+   */
+  static async downloadBatchQrCodesAsZip(qrCodeList, folderName, baseUrl = null, options = {}) {
+    try {
+      const zip = new JSZip()
+      const folder = zip.folder(folderName)
+
+      // 批量生成二维码图片
+      const promises = qrCodeList.map(async (item) => {
+        const url = this.generateQrCodeUrl(item.qrCode, baseUrl)
+        const dataUrl = await this.toDataURL(url, options)
+
+        // 将 base64 转换为 blob
+        const base64Data = dataUrl.split(',')[1]
+        const binaryData = atob(base64Data)
+        const arrayBuffer = new ArrayBuffer(binaryData.length)
+        const uint8Array = new Uint8Array(arrayBuffer)
+        for (let i = 0; i < binaryData.length; i++) {
+          uint8Array[i] = binaryData.charCodeAt(i)
+        }
+
+        // 添加到 zip 文件,文件名格式:qrCode_maintenanceCode.png
+        const filename = `${item.qrCode}_${item.maintenanceCode}.png`
+        folder.file(filename, uint8Array, { binary: true })
+      })
+
+      await Promise.all(promises)
+
+      // 生成 zip 文件
+      const zipBlob = await zip.generateAsync({ type: 'blob' })
+
+      // 下载 zip 文件
+      const link = document.createElement('a')
+      link.href = URL.createObjectURL(zipBlob)
+      link.download = `${folderName}.zip`
+      document.body.appendChild(link)
+      link.click()
+      document.body.removeChild(link)
+      URL.revokeObjectURL(link.href)
+    } catch (error) {
+      console.error('批量下载二维码失败:', error)
+      throw new Error('批量下载二维码失败')
+    }
+  }
+
+  /**
+   * 将 Data URL 转换为 Blob
+   * @param {string} dataUrl - Data URL
+   * @returns {Blob}
+   */
+  static dataURLToBlob(dataUrl) {
+    const arr = dataUrl.split(',')
+    const mime = arr[0].match(/:(.*?);/)[1]
+    const bstr = atob(arr[1])
+    let n = bstr.length
+    const u8arr = new Uint8Array(n)
+    while (n--) {
+      u8arr[n] = bstr.charCodeAt(n)
+    }
+    return new Blob([u8arr], { type: mime })
+  }
+}
+
+export default QRCodeUtil

+ 258 - 83
src/views/QrCodeManageView.vue

@@ -2,9 +2,9 @@
 import {
   generateQrCodes,
   queryQrCodes,
-  downloadQrCodesByDate,
   getQrCodeScanRecords,
-  getQrCodeInfo
+  getQrCodeInfo,
+  resetMaintenanceCode
 } from '@/services/api'
 import { getQrCodeTypeConfig } from '@/enums'
 import { useDateFormat } from '@vueuse/core'
@@ -22,6 +22,7 @@ import DatePicker from 'primevue/datepicker'
 import Tag from 'primevue/tag'
 import { useToast } from 'primevue/usetoast'
 import { onMounted, ref } from 'vue'
+import QRCodeUtil from '@/utils/qrcode'
 
 const toast = useToast()
 
@@ -76,12 +77,28 @@ const selectedQrCode = ref(null)
 // 批量下载对话框
 const batchDownloadDialog = ref(false)
 const downloadDate = ref(null)
+const batchDownloadLoading = ref(false)
 
 // 详情对话框
 const detailDialog = ref(false)
 const detailLoading = ref(false)
 const qrCodeDetail = ref(null)
 
+// 重置维护码对话框
+const resetDialog = ref(false)
+const resetForm = ref({
+  qrCode: '',
+  maintenanceCode: '',
+  codeLength: 10
+})
+const resetLoading = ref(false)
+
+// 展示二维码对话框
+const showQrCodeDialog = ref(false)
+const currentQrCodeUrl = ref('')
+const currentQrCodeNumber = ref('')
+const qrCodeImage = ref('')
+
 // 获取二维码类型配置(使用枚举)
 const getTypeConfig = (type) => {
   return getQrCodeTypeConfig(type)
@@ -186,8 +203,56 @@ const handleBatchDownload = async () => {
     })
     return
   }
-  await downloadByDate(downloadDate.value)
-  batchDownloadDialog.value = false
+
+  batchDownloadLoading.value = true
+  try {
+    const dateStr = formatDateShort(downloadDate.value)
+
+    // 查询该日期的所有二维码
+    const response = await queryQrCodes({
+      startDate: dateStr,
+      endDate: dateStr,
+      page: 0,
+      pageSize: 10000
+    })
+
+    if (!response.content || response.content.length === 0) {
+      toast.add({
+        severity: 'warn',
+        summary: '警告',
+        detail: '该日期没有二维码数据',
+        life: 3000
+      })
+      return
+    }
+
+    // 准备二维码列表数据
+    const qrCodeList = response.content.map(item => ({
+      qrCode: item.qrCode,
+      maintenanceCode: item.maintenanceCode || 'unknown'
+    }))
+
+    // 批量生成并下载为 ZIP
+    await QRCodeUtil.downloadBatchQrCodesAsZip(qrCodeList, dateStr)
+
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: `成功下载 ${qrCodeList.length} 个二维码`,
+      life: 3000
+    })
+
+    batchDownloadDialog.value = false
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '批量下载失败',
+      life: 3000
+    })
+  } finally {
+    batchDownloadLoading.value = false
+  }
 }
 
 // 生成二维码
@@ -205,13 +270,13 @@ const handleGenerate = async () => {
   generateLoading.value = true
   try {
     const response = await generateQrCodes(generateForm.value.qrType, generateForm.value.quantity)
-    generatedCodes.value = response.data
     toast.add({
       severity: 'success',
       summary: '成功',
       detail: `成功生成 ${response.data.length} 个二维码`,
       life: 3000
     })
+    generateDialog.value = false
     fetchData()
   } catch (error) {
     toast.add({
@@ -225,55 +290,8 @@ const handleGenerate = async () => {
   }
 }
 
-// 导出生成的二维码
-const exportGeneratedCodes = () => {
-  if (generatedCodes.value.length === 0) return
-
-  const csvContent = [
-    ['二维码编号', '维护码', '类型', '生成时间'].join(','),
-    ...generatedCodes.value.map((code) =>
-      [
-        code.qrCode,
-        code.maintenanceCode,
-        getTypeConfig(generateForm.value.qrType).label,
-        formatDate(new Date())
-      ].join(',')
-    )
-  ].join('\n')
-
-  const blob = new Blob(['\ufeff' + csvContent], { type: 'text/csv;charset=utf-8;' })
-  const link = document.createElement('a')
-  link.href = URL.createObjectURL(blob)
-  link.download = `二维码列表_${formatDateShort(new Date())}.csv`
-  link.click()
-}
-
-// 按日期下载二维码
-const downloadByDate = async (date) => {
-  try {
-    const dateStr = formatDateShort(date)
-    const blob = await downloadQrCodesByDate(dateStr)
 
-    const link = document.createElement('a')
-    link.href = URL.createObjectURL(blob)
-    link.download = `二维码_${dateStr}.csv`
-    link.click()
 
-    toast.add({
-      severity: 'success',
-      summary: '成功',
-      detail: '下载成功',
-      life: 3000
-    })
-  } catch (error) {
-    toast.add({
-      severity: 'error',
-      summary: '错误',
-      detail: error.message || '下载失败',
-      life: 3000
-    })
-  }
-}
 
 // 查看扫描记录
 const viewScanRecords = async (qrCode) => {
@@ -326,6 +344,119 @@ const viewDetail = async (qrCode) => {
   }
 }
 
+// 打开重置维护码对话框
+const openResetDialog = (qrCode) => {
+  resetForm.value = {
+    qrCode: qrCode.qrCode,
+    maintenanceCode: '',
+    codeLength: 10
+  }
+  resetDialog.value = true
+}
+
+// 生成随机维护码(根据选择的位数)
+const generateRandomCode = () => {
+  const length = resetForm.value.codeLength || 10
+  const chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789'
+  let code = ''
+  for (let i = 0; i < length; i++) {
+    code += chars.charAt(Math.floor(Math.random() * chars.length))
+  }
+  resetForm.value.maintenanceCode = code
+}
+
+// 确认重置维护码
+const handleResetMaintenanceCode = async () => {
+  if (!resetForm.value.maintenanceCode) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '请输入维护码',
+      life: 3000
+    })
+    return
+  }
+
+  const code = resetForm.value.maintenanceCode.trim()
+  if (code.length < 8 || code.length > 20) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '维护码长度必须在8-20位之间',
+      life: 3000
+    })
+    return
+  }
+
+  if (!/^[A-Za-z0-9]+$/.test(code)) {
+    toast.add({
+      severity: 'warn',
+      summary: '警告',
+      detail: '维护码只能包含字母和数字',
+      life: 3000
+    })
+    return
+  }
+
+  resetLoading.value = true
+  try {
+    await resetMaintenanceCode(resetForm.value.qrCode, code)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '维护码重置成功',
+      life: 3000
+    })
+    resetDialog.value = false
+    fetchData()
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '重置维护码失败',
+      life: 3000
+    })
+  } finally {
+    resetLoading.value = false
+  }
+}
+
+// 展示二维码
+const showQrCode = async (qrCode) => {
+  try {
+    // 保存二维码编号
+    currentQrCodeNumber.value = qrCode
+
+    // 生成二维码 URL
+    currentQrCodeUrl.value = QRCodeUtil.generateQrCodeUrl(qrCode)
+
+    // 生成二维码图片
+    qrCodeImage.value = await QRCodeUtil.generateQrCodeImage(qrCode)
+
+    showQrCodeDialog.value = true
+  } catch (error) {
+    toast.add({
+      severity: 'error',
+      summary: '错误',
+      detail: error.message || '生成二维码失败',
+      life: 3000
+    })
+  }
+}
+
+// 下载当前二维码
+const downloadCurrentQrCode = () => {
+  if (qrCodeImage.value && currentQrCodeNumber.value) {
+    QRCodeUtil.downloadQrCode(qrCodeImage.value, `${currentQrCodeNumber.value}`)
+    toast.add({
+      severity: 'success',
+      summary: '成功',
+      detail: '二维码下载成功',
+      life: 2000
+    })
+  }
+}
+
 onMounted(() => {
   fetchData()
 })
@@ -370,6 +501,15 @@ onMounted(() => {
           </div>
         </template>
       </Column>
+      <Column field="maintenanceCode" header="维护码" style="min-width: 180px">
+        <template #body="slotProps">
+          <div class="flex items-center gap-2">
+            <code class="text-sm">{{ slotProps.data.maintenanceCode || '-' }}</code>
+            <Button v-if="slotProps.data.maintenanceCode" icon="pi pi-copy" size="small" text rounded
+              @click="copyToClipboard(slotProps.data.maintenanceCode)" />
+          </div>
+        </template>
+      </Column>
       <Column field="qrType" header="类型" style="min-width: 120px">
         <template #body="slotProps">
           <Tag :severity="getTypeConfig(slotProps.data.qrType).severity">
@@ -384,9 +524,12 @@ onMounted(() => {
             :severity="getActivatedTag(slotProps.data.isActivated).severity" />
         </template>
       </Column>
-      <Column field="scanCount" header="扫描次数" style="min-width: 100px">
+      <Column field="scanCount" header="扫描次数" style="min-width: 100px"
+        :pt="{ columnHeaderContent: { class: 'justify-center' } }">
         <template #body="slotProps">
-          <span class="font-semibold">{{ slotProps.data.scanCount || 0 }}</span>
+          <div class="text-center">
+            <span class="font-semibold">{{ slotProps.data.scanCount || 0 }}</span>
+          </div>
         </template>
       </Column>
       <Column field="createdAt" header="生成时间" style="min-width: 180px">
@@ -394,22 +537,24 @@ onMounted(() => {
           {{ formatDate(slotProps.data.createdAt) }}
         </template>
       </Column>
-      <Column header="操作" style="min-width: 200px">
+      <Column header="操作" style="min-width: 350px" :pt="{ columnHeaderContent: { class: 'justify-center' } }">
         <template #body="slotProps">
-          <div class="flex gap-1">
+          <div class="flex gap-1 justify-center">
             <Button icon="pi pi-eye" severity="info" size="small" text rounded v-tooltip.top="'查看详情'"
               @click="viewDetail(slotProps.data)" />
+            <Button icon="pi pi-qrcode" severity="success" size="small" text rounded v-tooltip.top="'展示二维码'"
+              @click="showQrCode(slotProps.data.qrCode)" />
             <Button icon="pi pi-chart-line" label="扫描记录" size="small" text style="white-space: nowrap"
               @click="viewScanRecords(slotProps.data)" />
-            <Button icon="pi pi-download" severity="danger" size="small" text rounded v-tooltip.top="'下载'"
-              @click="downloadByDate(slotProps.data.createdAt)" />
+            <Button icon="pi pi-key" label="重置维护码" severity="warn" size="small" text style="white-space: nowrap"
+              @click="openResetDialog(slotProps.data)" />
           </div>
         </template>
       </Column>
     </DataTable>
 
     <!-- 生成二维码对话框 -->
-    <Dialog v-model:visible="generateDialog" :modal="true" header="生成二维码" :style="{ width: '550px' }" position="center">
+    <Dialog v-model:visible="generateDialog" :modal="true" header="生成二维码" :style="{ width: '450px' }" position="center">
       <div class="space-y-4">
         <div class="field" style="margin-top: 10px;">
           <FloatLabel variant="on">
@@ -428,25 +573,6 @@ onMounted(() => {
             <label for="quantity">生成数量 (1-1000)</label>
           </FloatLabel>
         </div>
-
-        <!-- 已生成的二维码列表 -->
-        <div v-if="generatedCodes.length > 0" class="mt-4">
-          <div class="flex justify-between items-center mb-2">
-            <h4 class="font-semibold">生成结果 ({{ generatedCodes.length }} 个)</h4>
-            <Button icon="pi pi-download" label="导出CSV" size="small" @click="exportGeneratedCodes" />
-          </div>
-          <div class="max-h-60 overflow-y-auto border rounded p-2">
-            <div v-for="(code, index) in generatedCodes" :key="index"
-              class="flex justify-between items-center py-2 border-b last:border-b-0">
-              <div>
-                <div class="text-sm font-mono">{{ code.qrCode }}</div>
-                <div class="text-xs text-gray-500">维护码: {{ code.maintenanceCode }}</div>
-              </div>
-              <Button icon="pi pi-copy" size="small" text
-                @click="copyToClipboard(`${code.qrCode},${code.maintenanceCode}`)" />
-            </div>
-          </div>
-        </div>
       </div>
 
       <template #footer>
@@ -536,20 +662,69 @@ onMounted(() => {
     </Dialog>
 
     <!-- 批量下载二维码对话框 -->
-    <Dialog v-model:visible="batchDownloadDialog" :modal="true" header="批量下载二维码" :style="{ width: '450px' }"
+    <Dialog v-model:visible="batchDownloadDialog" :modal="true" header="批量下载二维码" :style="{ width: '500px' }"
       position="center">
       <div class="space-y-4">
         <div class="field" style="margin-top: 10px;">
           <FloatLabel variant="on">
-            <DatePicker id="downloadDate" v-model="downloadDate" dateFormat="yy-mm-dd" fluid />
-            <label for="downloadDate">下载日期</label>
+            <DatePicker id="downloadDate" v-model="downloadDate" dateFormat="yy-mm-dd" fluid
+              :disabled="batchDownloadLoading" />
+            <label for="downloadDate">选择日期</label>
           </FloatLabel>
         </div>
+
+        <div class="text-sm text-gray-600 bg-blue-50 dark:bg-blue-900/20 p-3 rounded">
+          <i class="pi pi-info-circle mr-2"></i>
+          将下载该日期生成的所有二维码,打包为 ZIP 文件
+        </div>
+      </div>
+
+      <template #footer>
+        <Button label="取消" severity="secondary" @click="batchDownloadDialog = false" :disabled="batchDownloadLoading" />
+        <Button icon="pi pi-download" label="下载" @click="handleBatchDownload" :loading="batchDownloadLoading" />
+      </template>
+    </Dialog>
+
+    <!-- 重置维护码对话框 -->
+    <Dialog v-model:visible="resetDialog" :modal="true" header="重置维护码" :style="{ width: '500px' }" position="center">
+      <div class="space-y-4">
+        <div class="field" style="margin-top: 10px;">
+          <label class="block mb-2 text-sm font-medium">二维码编号</label>
+          <InputText :value="resetForm.qrCode" disabled fluid />
+        </div>
+
+        <div class="field" style="margin-top: 20px;">
+          <label class="block mb-2 text-sm font-medium">新维护码 *</label>
+          <div class="flex gap-2 items-stretch">
+            <InputText id="maintenanceCode" v-model="resetForm.maintenanceCode" placeholder="8-20位字母和数字"
+              class="flex-1" />
+            <InputNumber v-model="resetForm.codeLength" :min="8" :max="20" showButtons :step="1"
+              :inputStyle="{ minWidth: '45px', textAlign: 'center' }" style="width: 100px" v-tooltip.top="'位数'" />
+            <Button icon="pi pi-refresh" severity="secondary" @click="generateRandomCode" v-tooltip.top="'随机生成'"
+              style="width: 2.8rem; padding: 0" />
+          </div>
+          <small class="text-gray-500 mt-1 block">维护码长度为8-20位,只能包含字母和数字</small>
+        </div>
       </div>
 
       <template #footer>
-        <Button label="取消" severity="secondary" @click="batchDownloadDialog = false" />
-        <Button label="下载" @click="handleBatchDownload" />
+        <Button label="取消" severity="secondary" @click="resetDialog = false" :disabled="resetLoading" />
+        <Button label="确认重置" @click="handleResetMaintenanceCode" :loading="resetLoading" />
+      </template>
+    </Dialog>
+
+    <!-- 展示二维码对话框 -->
+    <Dialog v-model:visible="showQrCodeDialog" :modal="true" header="二维码展示" :style="{ width: '450px' }"
+      position="center">
+      <div class="flex flex-col items-center gap-4 py-4">
+        <!-- 二维码图片 -->
+        <div v-if="qrCodeImage" class="bg-white p-4 rounded-lg shadow-md">
+          <img :src="qrCodeImage" alt="二维码" class="w-[300px] h-[300px]" />
+        </div>
+      </div>
+      <template #footer>
+        <Button icon="pi pi-download" label="下载二维码" @click="downloadCurrentQrCode" />
+        <Button label="关闭" severity="secondary" @click="showQrCodeDialog = false" />
       </template>
     </Dialog>
 

+ 1 - 0
vite.config.js

@@ -11,6 +11,7 @@ export default defineConfig({
   plugins: [vue(), vueJsx(), vueDevTools(), tailwindcss()],
   base: '/admin/',
   server: {
+    host: '0.0.0.0',
     port: 5175
   },
   resolve: {

+ 246 - 2
yarn.lock

@@ -1102,7 +1102,12 @@ alien-signals@^1.0.3:
   resolved "https://registry.npmjs.org/alien-signals/-/alien-signals-1.0.13.tgz"
   integrity sha512-OGj9yyTnJEttvzhTUWuscOvtqxq5vrhF7vL9oS0xJ2mK0ItPYP1/y+vCFebfxoEyAz0++1AIwJ5CMr+Fk3nDmg==
 
-ansi-styles@^4.1.0:
+ansi-regex@^5.0.1:
+  version "5.0.1"
+  resolved "https://registry.yarnpkg.com/ansi-regex/-/ansi-regex-5.0.1.tgz#082cb2c89c9fe8659a311a53bd6a4dc5301db304"
+  integrity sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==
+
+ansi-styles@^4.0.0, ansi-styles@^4.1.0:
   version "4.3.0"
   resolved "https://registry.npmmirror.com/ansi-styles/-/ansi-styles-4.3.0.tgz"
   integrity sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==
@@ -1196,6 +1201,11 @@ callsites@^3.0.0:
   resolved "https://registry.npmmirror.com/callsites/-/callsites-3.1.0.tgz"
   integrity sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==
 
+camelcase@^5.0.0:
+  version "5.3.1"
+  resolved "https://registry.yarnpkg.com/camelcase/-/camelcase-5.3.1.tgz#e3c9b31569e106811df242f715725a1f4c494320"
+  integrity sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==
+
 caniuse-lite@^1.0.30001688:
   version "1.0.30001707"
   resolved "https://registry.npmmirror.com/caniuse-lite/-/caniuse-lite-1.0.30001707.tgz"
@@ -1216,6 +1226,15 @@ chart.js@^4.4.8:
   dependencies:
     "@kurkle/color" "^0.3.0"
 
+cliui@^6.0.0:
+  version "6.0.0"
+  resolved "https://registry.yarnpkg.com/cliui/-/cliui-6.0.0.tgz#511d702c0c4e41ca156d7d0e96021f23e13225b1"
+  integrity sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==
+  dependencies:
+    string-width "^4.2.0"
+    strip-ansi "^6.0.0"
+    wrap-ansi "^6.2.0"
+
 color-convert@^2.0.1:
   version "2.0.1"
   resolved "https://registry.npmmirror.com/color-convert/-/color-convert-2.0.1.tgz"
@@ -1259,6 +1278,11 @@ copy-anything@^3.0.2:
   dependencies:
     is-what "^4.1.8"
 
+core-util-is@~1.0.0:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/core-util-is/-/core-util-is-1.0.3.tgz#a6042d3634c2b27e9328f837b965fac83808db85"
+  integrity sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==
+
 cross-spawn@^7.0.3, cross-spawn@^7.0.6:
   version "7.0.6"
   resolved "https://registry.npmmirror.com/cross-spawn/-/cross-spawn-7.0.6.tgz"
@@ -1290,6 +1314,11 @@ debug@^4.1.0, debug@^4.3.1, debug@^4.3.2, debug@^4.3.7:
   dependencies:
     ms "^2.1.3"
 
+decamelize@^1.2.0:
+  version "1.2.0"
+  resolved "https://registry.yarnpkg.com/decamelize/-/decamelize-1.2.0.tgz#f6534d15148269b20352e7bee26f501f9a191290"
+  integrity sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==
+
 decimal.js@^10.5.0:
   version "10.6.0"
   resolved "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz"
@@ -1328,6 +1357,11 @@ detect-libc@^2.0.3:
   resolved "https://registry.npmmirror.com/detect-libc/-/detect-libc-2.0.3.tgz"
   integrity sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==
 
+dijkstrajs@^1.0.1:
+  version "1.0.3"
+  resolved "https://registry.yarnpkg.com/dijkstrajs/-/dijkstrajs-1.0.3.tgz#4c8dbdea1f0f6478bff94d9c49c784d623e4fc23"
+  integrity sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==
+
 dunder-proto@^1.0.1:
   version "1.0.1"
   resolved "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz"
@@ -1342,6 +1376,11 @@ electron-to-chromium@^1.5.73:
   resolved "https://registry.npmmirror.com/electron-to-chromium/-/electron-to-chromium-1.5.128.tgz"
   integrity sha512-bo1A4HH/NS522Ws0QNFIzyPcyUUNV/yyy70Ho1xqfGYzPUme2F/xr4tlEOuM6/A538U1vDA7a4XfCd1CKRegKQ==
 
+emoji-regex@^8.0.0:
+  version "8.0.0"
+  resolved "https://registry.yarnpkg.com/emoji-regex/-/emoji-regex-8.0.0.tgz#e818fd69ce5ccfcb404594f842963bf53164cc37"
+  integrity sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==
+
 enhanced-resolve@^5.18.1:
   version "5.18.1"
   resolved "https://registry.npmmirror.com/enhanced-resolve/-/enhanced-resolve-5.18.1.tgz"
@@ -1614,6 +1653,14 @@ file-entry-cache@^8.0.0:
   dependencies:
     flat-cache "^4.0.0"
 
+find-up@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/find-up/-/find-up-4.1.0.tgz#97afe7d6cdc0bc5928584b7c8d7b16e8a9aa5d19"
+  integrity sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==
+  dependencies:
+    locate-path "^5.0.0"
+    path-exists "^4.0.0"
+
 find-up@^5.0.0:
   version "5.0.0"
   resolved "https://registry.npmmirror.com/find-up/-/find-up-5.0.0.tgz"
@@ -1675,6 +1722,11 @@ gensync@^1.0.0-beta.2:
   resolved "https://registry.npmmirror.com/gensync/-/gensync-1.0.0-beta.2.tgz"
   integrity sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==
 
+get-caller-file@^2.0.1:
+  version "2.0.5"
+  resolved "https://registry.yarnpkg.com/get-caller-file/-/get-caller-file-2.0.5.tgz#4f94412a82db32f36e3b0b9741f8a97feb031f7e"
+  integrity sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==
+
 get-intrinsic@^1.2.5, get-intrinsic@^1.2.6, get-intrinsic@^1.3.0:
   version "1.3.0"
   resolved "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz"
@@ -1795,6 +1847,11 @@ image-size@~0.5.0:
   resolved "https://registry.npmmirror.com/image-size/-/image-size-0.5.5.tgz"
   integrity sha512-6TDAlDPZxUFCv+fuOkIoXT/V/f3Qbq8e37p+YOiYrUv3v9cc3/6x78VdfPgFVaB9dZYeLUfKgHRebpkm/oP2VQ==
 
+immediate@~3.0.5:
+  version "3.0.6"
+  resolved "https://registry.yarnpkg.com/immediate/-/immediate-3.0.6.tgz#9db1dbd0faf8de6fbe0f5dd5e56bb606280de69b"
+  integrity sha512-XXOFtyqDjNDAQxVfYxuF7g9Il/IbWmmlQg2MYKOH8ExIT1qg6xc4zyS3HaEEATgs1btfzxq15ciUiY7gjSXRGQ==
+
 import-fresh@^3.2.1:
   version "3.3.1"
   resolved "https://registry.npmmirror.com/import-fresh/-/import-fresh-3.3.1.tgz"
@@ -1808,6 +1865,11 @@ imurmurhash@^0.1.4:
   resolved "https://registry.npmmirror.com/imurmurhash/-/imurmurhash-0.1.4.tgz"
   integrity sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==
 
+inherits@~2.0.3:
+  version "2.0.4"
+  resolved "https://registry.yarnpkg.com/inherits/-/inherits-2.0.4.tgz#0fa2c64f932917c3433a0ded55363aae37416b7c"
+  integrity sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==
+
 is-docker@^3.0.0:
   version "3.0.0"
   resolved "https://registry.npmmirror.com/is-docker/-/is-docker-3.0.0.tgz"
@@ -1818,6 +1880,11 @@ is-extglob@^2.1.1:
   resolved "https://registry.npmmirror.com/is-extglob/-/is-extglob-2.1.1.tgz"
   integrity sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==
 
+is-fullwidth-code-point@^3.0.0:
+  version "3.0.0"
+  resolved "https://registry.yarnpkg.com/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz#f116f8064fe90b3f7844a38997c0b75051269f1d"
+  integrity sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==
+
 is-glob@^4.0.0, is-glob@^4.0.3:
   version "4.0.3"
   resolved "https://registry.npmmirror.com/is-glob/-/is-glob-4.0.3.tgz"
@@ -1864,6 +1931,11 @@ is-wsl@^3.1.0:
   dependencies:
     is-inside-container "^1.0.0"
 
+isarray@~1.0.0:
+  version "1.0.0"
+  resolved "https://registry.yarnpkg.com/isarray/-/isarray-1.0.0.tgz#bb935d48582cba168c06834957a54a3e07124f11"
+  integrity sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==
+
 isexe@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/isexe/-/isexe-2.0.0.tgz"
@@ -1920,6 +1992,16 @@ jsonfile@^6.0.1:
   optionalDependencies:
     graceful-fs "^4.1.6"
 
+jszip@^3.10.1:
+  version "3.10.1"
+  resolved "https://registry.yarnpkg.com/jszip/-/jszip-3.10.1.tgz#34aee70eb18ea1faec2f589208a157d1feb091c2"
+  integrity sha512-xXDvecyTpGLrqFrvkrUSoxxfJI5AH7U8zxxtVclpsUtMCq4JQ290LY8AW5c7Ggnr/Y/oK+bQMbqK2qmtk3pN4g==
+  dependencies:
+    lie "~3.3.0"
+    pako "~1.0.2"
+    readable-stream "~2.3.6"
+    setimmediate "^1.0.5"
+
 keyv@^4.5.4:
   version "4.5.4"
   resolved "https://registry.npmmirror.com/keyv/-/keyv-4.5.4.tgz"
@@ -1957,6 +2039,13 @@ levn@^0.4.1:
     prelude-ls "^1.2.1"
     type-check "~0.4.0"
 
+lie@~3.3.0:
+  version "3.3.0"
+  resolved "https://registry.yarnpkg.com/lie/-/lie-3.3.0.tgz#dcf82dee545f46074daf200c7c1c5a08e0f40f6a"
+  integrity sha512-UaiMJzeWRlEujzAuw5LokY1L5ecNQYZKfmyZ9L7wDHb/p5etKaxXhohBcrw0EYby+G/NA52vRSN4N39dxHAIwQ==
+  dependencies:
+    immediate "~3.0.5"
+
 lightningcss-darwin-arm64@1.29.2:
   version "1.29.2"
   resolved "https://registry.npmmirror.com/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.29.2.tgz"
@@ -2025,6 +2114,13 @@ lightningcss@1.29.2:
     lightningcss-win32-arm64-msvc "1.29.2"
     lightningcss-win32-x64-msvc "1.29.2"
 
+locate-path@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/locate-path/-/locate-path-5.0.0.tgz#1afba396afd676a6d42504d0a67a3a7eb9f62aa0"
+  integrity sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==
+  dependencies:
+    p-locate "^4.1.0"
+
 locate-path@^6.0.0:
   version "6.0.0"
   resolved "https://registry.npmmirror.com/locate-path/-/locate-path-6.0.0.tgz"
@@ -2190,6 +2286,13 @@ optionator@^0.9.3:
     type-check "^0.4.0"
     word-wrap "^1.2.5"
 
+p-limit@^2.2.0:
+  version "2.3.0"
+  resolved "https://registry.yarnpkg.com/p-limit/-/p-limit-2.3.0.tgz#3dd33c647a214fdfffd835933eb086da0dc21db1"
+  integrity sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==
+  dependencies:
+    p-try "^2.0.0"
+
 p-limit@^3.0.2:
   version "3.1.0"
   resolved "https://registry.npmmirror.com/p-limit/-/p-limit-3.1.0.tgz"
@@ -2197,6 +2300,13 @@ p-limit@^3.0.2:
   dependencies:
     yocto-queue "^0.1.0"
 
+p-locate@^4.1.0:
+  version "4.1.0"
+  resolved "https://registry.yarnpkg.com/p-locate/-/p-locate-4.1.0.tgz#a3428bb7088b3a60292f66919278b7c297ad4f07"
+  integrity sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==
+  dependencies:
+    p-limit "^2.2.0"
+
 p-locate@^5.0.0:
   version "5.0.0"
   resolved "https://registry.npmmirror.com/p-locate/-/p-locate-5.0.0.tgz"
@@ -2204,6 +2314,16 @@ p-locate@^5.0.0:
   dependencies:
     p-limit "^3.0.2"
 
+p-try@^2.0.0:
+  version "2.2.0"
+  resolved "https://registry.yarnpkg.com/p-try/-/p-try-2.2.0.tgz#cb2868540e313d61de58fafbe35ce9004d5540e6"
+  integrity sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==
+
+pako@~1.0.2:
+  version "1.0.11"
+  resolved "https://registry.yarnpkg.com/pako/-/pako-1.0.11.tgz#6c9599d340d54dfd3946380252a35705a6b992bf"
+  integrity sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==
+
 parent-module@^1.0.0:
   version "1.0.1"
   resolved "https://registry.npmmirror.com/parent-module/-/parent-module-1.0.1.tgz"
@@ -2273,6 +2393,11 @@ pinia@^3.0.1:
   dependencies:
     "@vue/devtools-api" "^7.7.2"
 
+pngjs@^5.0.0:
+  version "5.0.0"
+  resolved "https://registry.yarnpkg.com/pngjs/-/pngjs-5.0.0.tgz#e79dd2b215767fd9c04561c01236df960bce7fbb"
+  integrity sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==
+
 postcss-selector-parser@^6.0.15:
   version "6.1.2"
   resolved "https://registry.npmmirror.com/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz"
@@ -2340,6 +2465,11 @@ primevue@^4.3.3:
     "@primevue/core" "4.3.3"
     "@primevue/icons" "4.3.3"
 
+process-nextick-args@~2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/process-nextick-args/-/process-nextick-args-2.0.1.tgz#7820d9b16120cc55ca9ae7792680ae7dba6d7fe2"
+  integrity sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==
+
 proxy-from-env@^1.1.0:
   version "1.1.0"
   resolved "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz"
@@ -2360,6 +2490,15 @@ punycode@^2.1.0:
   resolved "https://registry.npmmirror.com/punycode/-/punycode-2.3.1.tgz"
   integrity sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==
 
+qrcode@^1.5.4:
+  version "1.5.4"
+  resolved "https://registry.yarnpkg.com/qrcode/-/qrcode-1.5.4.tgz#5cb81d86eb57c675febb08cf007fff963405da88"
+  integrity sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==
+  dependencies:
+    dijkstrajs "^1.0.1"
+    pngjs "^5.0.0"
+    yargs "^15.3.1"
+
 qs@^6.12.3:
   version "6.14.0"
   resolved "https://registry.npmjs.org/qs/-/qs-6.14.0.tgz"
@@ -2367,6 +2506,29 @@ qs@^6.12.3:
   dependencies:
     side-channel "^1.1.0"
 
+readable-stream@~2.3.6:
+  version "2.3.8"
+  resolved "https://registry.yarnpkg.com/readable-stream/-/readable-stream-2.3.8.tgz#91125e8042bba1b9887f49345f6277027ce8be9b"
+  integrity sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==
+  dependencies:
+    core-util-is "~1.0.0"
+    inherits "~2.0.3"
+    isarray "~1.0.0"
+    process-nextick-args "~2.0.0"
+    safe-buffer "~5.1.1"
+    string_decoder "~1.1.1"
+    util-deprecate "~1.0.1"
+
+require-directory@^2.1.1:
+  version "2.1.1"
+  resolved "https://registry.yarnpkg.com/require-directory/-/require-directory-2.1.1.tgz#8c64ad5fd30dab1c976e2344ffe7f792a6a6df42"
+  integrity sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==
+
+require-main-filename@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/require-main-filename/-/require-main-filename-2.0.0.tgz#d0b329ecc7cc0f61649f62215be69af54aa8989b"
+  integrity sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==
+
 resolve-from@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/resolve-from/-/resolve-from-4.0.0.tgz"
@@ -2411,6 +2573,11 @@ run-applescript@^7.0.0:
   resolved "https://registry.npmmirror.com/run-applescript/-/run-applescript-7.0.0.tgz"
   integrity sha512-9by4Ij99JUr/MCFBUkDKLWK3G9HVXmabKz9U5MlIAIuvuzkiOicRYs8XJLxX+xahD+mLiiCYDqF9dKAgtzKP1A==
 
+safe-buffer@~5.1.0, safe-buffer@~5.1.1:
+  version "5.1.2"
+  resolved "https://registry.yarnpkg.com/safe-buffer/-/safe-buffer-5.1.2.tgz#991ec69d296e0313747d59bdfd2b745c35f8828d"
+  integrity sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==
+
 "safer-buffer@>= 2.1.2 < 3.0.0":
   version "2.1.2"
   resolved "https://registry.npmmirror.com/safer-buffer/-/safer-buffer-2.1.2.tgz"
@@ -2436,6 +2603,16 @@ semver@^7.6.3:
   resolved "https://registry.npmmirror.com/semver/-/semver-7.7.1.tgz"
   integrity sha512-hlq8tAfn0m/61p4BVRcPzIGr6LKiMwo4VM6dGi6pt4qcRkmNzTcWq6eCEjEh+qXjkMDvPlOFFSGwQjoEa6gyMA==
 
+set-blocking@^2.0.0:
+  version "2.0.0"
+  resolved "https://registry.yarnpkg.com/set-blocking/-/set-blocking-2.0.0.tgz#045f9782d011ae9a6803ddd382b24392b3d890f7"
+  integrity sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==
+
+setimmediate@^1.0.5:
+  version "1.0.5"
+  resolved "https://registry.yarnpkg.com/setimmediate/-/setimmediate-1.0.5.tgz#290cbb232e306942d7d7ea9b83732ab7856f8285"
+  integrity sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==
+
 shebang-command@^2.0.0:
   version "2.0.0"
   resolved "https://registry.npmmirror.com/shebang-command/-/shebang-command-2.0.0.tgz"
@@ -2517,6 +2694,29 @@ speakingurl@^14.0.1:
   resolved "https://registry.npmmirror.com/speakingurl/-/speakingurl-14.0.1.tgz"
   integrity sha512-1POYv7uv2gXoyGFpBCmpDVSNV74IfsWlDW216UPjbWufNf+bSU6GdbDsxdcxtfwb4xlI3yxzOTKClUosxARYrQ==
 
+string-width@^4.1.0, string-width@^4.2.0:
+  version "4.2.3"
+  resolved "https://registry.yarnpkg.com/string-width/-/string-width-4.2.3.tgz#269c7117d27b05ad2e536830a8ec895ef9c6d010"
+  integrity sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==
+  dependencies:
+    emoji-regex "^8.0.0"
+    is-fullwidth-code-point "^3.0.0"
+    strip-ansi "^6.0.1"
+
+string_decoder@~1.1.1:
+  version "1.1.1"
+  resolved "https://registry.yarnpkg.com/string_decoder/-/string_decoder-1.1.1.tgz#9cf1611ba62685d7030ae9e4ba34149c3af03fc8"
+  integrity sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==
+  dependencies:
+    safe-buffer "~5.1.0"
+
+strip-ansi@^6.0.0, strip-ansi@^6.0.1:
+  version "6.0.1"
+  resolved "https://registry.yarnpkg.com/strip-ansi/-/strip-ansi-6.0.1.tgz#9e26c63d30f53443e9489495b2105d37b67a85d9"
+  integrity sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==
+  dependencies:
+    ansi-regex "^5.0.1"
+
 strip-final-newline@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/strip-final-newline/-/strip-final-newline-4.0.0.tgz"
@@ -2627,7 +2827,7 @@ url@^0.11.4:
     punycode "^1.4.1"
     qs "^6.12.3"
 
-util-deprecate@^1.0.2:
+util-deprecate@^1.0.2, util-deprecate@~1.0.1:
   version "1.0.2"
   resolved "https://registry.npmmirror.com/util-deprecate/-/util-deprecate-1.0.2.tgz"
   integrity sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==
@@ -2738,6 +2938,11 @@ vue@^3.5.13:
     "@vue/server-renderer" "3.5.13"
     "@vue/shared" "3.5.13"
 
+which-module@^2.0.0:
+  version "2.0.1"
+  resolved "https://registry.yarnpkg.com/which-module/-/which-module-2.0.1.tgz#776b1fe35d90aebe99e8ac15eb24093389a4a409"
+  integrity sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==
+
 which@^2.0.1:
   version "2.0.2"
   resolved "https://registry.npmmirror.com/which/-/which-2.0.2.tgz"
@@ -2750,16 +2955,55 @@ word-wrap@^1.2.5:
   resolved "https://registry.npmmirror.com/word-wrap/-/word-wrap-1.2.5.tgz"
   integrity sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA==
 
+wrap-ansi@^6.2.0:
+  version "6.2.0"
+  resolved "https://registry.yarnpkg.com/wrap-ansi/-/wrap-ansi-6.2.0.tgz#e9393ba07102e6c91a3b221478f0257cd2856e53"
+  integrity sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==
+  dependencies:
+    ansi-styles "^4.0.0"
+    string-width "^4.1.0"
+    strip-ansi "^6.0.0"
+
 xml-name-validator@^4.0.0:
   version "4.0.0"
   resolved "https://registry.npmmirror.com/xml-name-validator/-/xml-name-validator-4.0.0.tgz"
   integrity sha512-ICP2e+jsHvAj2E2lIHxa5tjXRlKDJo4IdvPvCXbXQGdzSfmSpNVyIKMvoZHjDY9DP0zV17iI85o90vRFXNccRw==
 
+y18n@^4.0.0:
+  version "4.0.3"
+  resolved "https://registry.yarnpkg.com/y18n/-/y18n-4.0.3.tgz#b5f259c82cd6e336921efd7bfd8bf560de9eeedf"
+  integrity sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==
+
 yallist@^3.0.2:
   version "3.1.1"
   resolved "https://registry.npmmirror.com/yallist/-/yallist-3.1.1.tgz"
   integrity sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==
 
+yargs-parser@^18.1.2:
+  version "18.1.3"
+  resolved "https://registry.yarnpkg.com/yargs-parser/-/yargs-parser-18.1.3.tgz#be68c4975c6b2abf469236b0c870362fab09a7b0"
+  integrity sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==
+  dependencies:
+    camelcase "^5.0.0"
+    decamelize "^1.2.0"
+
+yargs@^15.3.1:
+  version "15.4.1"
+  resolved "https://registry.yarnpkg.com/yargs/-/yargs-15.4.1.tgz#0d87a16de01aee9d8bec2bfbf74f67851730f4f8"
+  integrity sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==
+  dependencies:
+    cliui "^6.0.0"
+    decamelize "^1.2.0"
+    find-up "^4.1.0"
+    get-caller-file "^2.0.1"
+    require-directory "^2.1.1"
+    require-main-filename "^2.0.0"
+    set-blocking "^2.0.0"
+    string-width "^4.2.0"
+    which-module "^2.0.0"
+    y18n "^4.0.0"
+    yargs-parser "^18.1.2"
+
 yocto-queue@^0.1.0:
   version "0.1.0"
   resolved "https://registry.npmmirror.com/yocto-queue/-/yocto-queue-0.1.0.tgz"