| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981 |
- <script setup>
- import {
- generateQrCodes,
- queryQrCodes,
- getQrCodeScanRecords,
- getQrCodeInfo,
- resetMaintenanceCode
- } from '@/services/api'
- import { getQrCodeTypeConfig } from '@/enums'
- import { useDateFormat } from '@vueuse/core'
- import Button from 'primevue/button'
- import Column from 'primevue/column'
- import DataTable from 'primevue/datatable'
- import Dialog from 'primevue/dialog'
- import Select from 'primevue/select'
- import FloatLabel from 'primevue/floatlabel'
- import IconField from 'primevue/iconfield'
- import InputIcon from 'primevue/inputicon'
- import InputText from 'primevue/inputtext'
- import InputNumber from 'primevue/inputnumber'
- 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()
- // 表格数据
- const tableData = ref({
- content: [],
- metadata: {
- page: 0,
- size: 20,
- total: 0
- }
- })
- // 筛选条件
- const filters = ref({
- qrType: null,
- isActivated: null,
- startDate: null,
- endDate: null,
- search: ''
- })
- // 二维码类型选项
- const qrTypeOptions = [
- { label: '全部', value: null },
- { label: '人员', value: 'person' },
- { label: '宠物', value: 'pet' },
- { label: '物品', value: 'goods' },
- { label: '链接', value: 'link' }
- ]
- // 激活状态选项
- const activatedOptions = [
- { label: '全部', value: null },
- { label: '已激活', value: true },
- { label: '未激活', value: false }
- ]
- // 生成二维码对话框
- const generateDialog = ref(false)
- const generateForm = ref({
- qrType: 'person',
- quantity: 10
- })
- const generateLoading = ref(false)
- const generatedCodes = ref([])
- // 扫描记录对话框
- const scanRecordDialog = ref(false)
- const scanRecords = ref([])
- 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 currentMaintenanceCode = ref('')
- const qrCodeImage = ref('')
- // 获取二维码类型配置(使用枚举)
- const getTypeConfig = (type) => {
- return getQrCodeTypeConfig(type)
- }
- // 获取激活状态标签
- const getActivatedTag = (isActivated) => {
- return isActivated
- ? { severity: 'success', label: '已激活' }
- : { severity: 'secondary', label: '未激活' }
- }
- // 格式化日期
- const formatDate = (date) => {
- if (!date) return '-'
- return useDateFormat(new Date(date), 'YYYY-MM-DD HH:mm:ss').value
- }
- const formatDateShort = (date) => {
- if (!date) return '-'
- return useDateFormat(new Date(date), 'YYYY-MM-DD').value
- }
- // 获取数据
- const fetchData = async () => {
- try {
- const params = {
- page: tableData.value.metadata.page,
- pageSize: tableData.value.metadata.size
- }
- if (filters.value.qrType) params.qrType = filters.value.qrType
- if (filters.value.isActivated !== null) params.isActivated = filters.value.isActivated
- if (filters.value.startDate)
- params.startDate = useDateFormat(filters.value.startDate, 'YYYY-MM-DD').value
- if (filters.value.endDate) params.endDate = useDateFormat(filters.value.endDate, 'YYYY-MM-DD').value
- if (filters.value.search?.trim()) params.qrCode = filters.value.search.trim()
- const response = await queryQrCodes(params)
- tableData.value.content = response.content || []
- tableData.value.metadata = {
- page: response.metadata?.page ?? tableData.value.metadata.page,
- size: response.metadata?.size ?? tableData.value.metadata.size,
- total: response.metadata?.total ?? 0
- }
- } catch (error) {
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: error.message || '获取数据失败',
- life: 3000
- })
- }
- }
- // 分页
- const handlePageChange = (event) => {
- tableData.value.metadata.page = event.page
- tableData.value.metadata.size = event.rows
- fetchData()
- }
- // 重置筛选并刷新
- const resetAndRefresh = () => {
- filters.value = {
- qrType: null,
- isActivated: null,
- startDate: null,
- endDate: null,
- search: ''
- }
- tableData.value.metadata.page = 0
- fetchData()
- }
- // 打开生成对话框
- const openGenerateDialog = () => {
- generateForm.value = {
- qrType: 'person',
- quantity: 10
- }
- generatedCodes.value = []
- generateDialog.value = true
- }
- // 打开批量下载对话框
- const openBatchDownloadDialog = () => {
- downloadDate.value = null
- batchDownloadDialog.value = true
- }
- // 确认批量下载
- const handleBatchDownload = async () => {
- if (!downloadDate.value) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '请选择下载日期',
- life: 3000
- })
- return
- }
- 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
- }
- }
- // 生成二维码
- const handleGenerate = async () => {
- if (generateForm.value.quantity < 1 || generateForm.value.quantity > 10000) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '生成数量必须在1-10000之间',
- life: 3000
- })
- return
- }
- generateLoading.value = true
- try {
- const response = await generateQrCodes(generateForm.value.qrType, generateForm.value.quantity)
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: `成功生成 ${response.data.length} 个二维码`,
- life: 3000
- })
- generateDialog.value = false
- fetchData()
- } catch (error) {
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: error.message || '生成二维码失败',
- life: 3000
- })
- } finally {
- generateLoading.value = false
- }
- }
- // 查看扫描记录
- const viewScanRecords = async (qrCode) => {
- try {
- selectedQrCode.value = qrCode
- const response = await getQrCodeScanRecords(qrCode.qrCode, 10)
- scanRecords.value = response.records || []
- scanRecordDialog.value = true
- } catch (error) {
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: error.message || '获取扫描记录失败',
- life: 3000
- })
- }
- }
- // 复制到剪贴板
- const copyToClipboard = async (text) => {
- try {
- // 优先使用现代 Clipboard API
- if (navigator.clipboard && window.isSecureContext) {
- await navigator.clipboard.writeText(text)
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: '已复制到剪贴板',
- life: 2000
- })
- } else {
- // 降级方案:使用传统的 document.execCommand
- const textArea = document.createElement('textarea')
- textArea.value = text
- textArea.style.position = 'fixed'
- textArea.style.left = '-999999px'
- textArea.style.top = '-999999px'
- document.body.appendChild(textArea)
- textArea.focus()
- textArea.select()
- const successful = document.execCommand('copy')
- document.body.removeChild(textArea)
- if (successful) {
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: '已复制到剪贴板',
- life: 2000
- })
- } else {
- throw new Error('复制失败')
- }
- }
- } catch (error) {
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: '复制失败,请手动复制',
- life: 3000
- })
- }
- }
- // 查看详情
- const viewDetail = async (qrCode) => {
- try {
- detailDialog.value = true
- detailLoading.value = true
- qrCodeDetail.value = null
- const response = await getQrCodeInfo(qrCode.id)
- qrCodeDetail.value = response
- } catch (error) {
- detailDialog.value = false
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: error.message || '获取详情失败',
- life: 3000
- })
- } finally {
- detailLoading.value = false
- }
- }
- // 打开修改维护码对话框
- 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 (qrCodeData) => {
- try {
- // 保存二维码编号和维护码
- const qrCode = typeof qrCodeData === 'string' ? qrCodeData : qrCodeData.qrCode
- const maintenanceCode = typeof qrCodeData === 'string' ? 'unknown' : (qrCodeData.maintenanceCode || 'unknown')
- currentQrCodeNumber.value = qrCode
- currentMaintenanceCode.value = maintenanceCode
- // 生成二维码 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 && currentMaintenanceCode.value) {
- QRCodeUtil.downloadQrCode(qrCodeImage.value, `${currentQrCodeNumber.value}_${currentMaintenanceCode.value}`)
- toast.add({
- severity: 'success',
- summary: '成功',
- detail: '二维码下载成功',
- life: 2000
- })
- }
- }
- onMounted(() => {
- fetchData()
- })
- </script>
- <template>
- <div class="rounded-lg p-4 bg-[var(--p-content-background)]">
- <!-- 数据表格 -->
- <DataTable :value="tableData.content" :paginator="true"
- paginatorTemplate="CurrentPageReport FirstPageLink PrevPageLink PageLinks NextPageLink LastPageLink RowsPerPageDropdown"
- currentPageReportTemplate="{totalRecords} 条记录" :rows="tableData.metadata.size"
- :rowsPerPageOptions="[10, 20, 50, 100]" :totalRecords="tableData.metadata.total" @page="handlePageChange" lazy
- scrollable>
- <template #header>
- <div class="flex flex-wrap items-center gap-2">
- <!-- 筛选条件 - 左侧 -->
- <IconField>
- <InputIcon>
- <i class="pi pi-search" />
- </InputIcon>
- <InputText v-model="filters.search" placeholder="二维码编号" size="small" class="w-35" />
- </IconField>
- <Select v-model="filters.qrType" :options="qrTypeOptions" optionLabel="label" optionValue="value"
- placeholder="类型" class="w-30" size="small" />
- <Select v-model="filters.isActivated" :options="activatedOptions" optionLabel="label" optionValue="value"
- placeholder="激活状态" class="w-30" size="small" />
- <DatePicker v-model="filters.startDate" placeholder="开始日期" dateFormat="yy-mm-dd" class="w-40" size="small" />
- <DatePicker v-model="filters.endDate" placeholder="结束日期" dateFormat="yy-mm-dd" class="w-40" size="small" />
- <Button icon="pi pi-search" @click="fetchData" label="查询" size="small" />
- <Button icon="pi pi-refresh" @click="resetAndRefresh" label="刷新" size="small" />
- <Button icon="pi pi-plus" @click="openGenerateDialog" label="生成二维码" severity="info" size="small" />
- <Button icon="pi pi-download" @click="openBatchDownloadDialog" label="批量下载二维码" severity="danger"
- size="small" />
- </div>
- </template>
- <Column field="qrCode" header="二维码编号" style="min-width: 200px">
- <template #body="slotProps">
- <div class="flex items-center gap-2">
- <code class="text-sm">{{ slotProps.data.qrCode }}</code>
- <Button icon="pi pi-copy" size="small" text rounded @click="copyToClipboard(slotProps.data.qrCode)" />
- </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">
- <i :class="`pi ${getTypeConfig(slotProps.data.qrType).icon} mr-1`"></i>
- {{ getTypeConfig(slotProps.data.qrType).label }}
- </Tag>
- </template>
- </Column>
- <Column field="isActivated" header="状态" style="min-width: 100px">
- <template #body="slotProps">
- <Tag :value="getActivatedTag(slotProps.data.isActivated).label"
- :severity="getActivatedTag(slotProps.data.isActivated).severity" />
- </template>
- </Column>
- <Column field="scanCount" header="扫描次数" style="min-width: 100px"
- :pt="{ columnHeaderContent: { class: 'justify-center' } }">
- <template #body="slotProps">
- <div class="text-center">
- <span class="font-semibold">{{ slotProps.data.scanCount || 0 }}</span>
- </div>
- </template>
- </Column>
- <Column field="createdAt" header="生成时间" style="min-width: 180px">
- <template #body="slotProps">
- {{ formatDate(slotProps.data.createdAt) }}
- </template>
- </Column>
- <Column header="操作" style="min-width: 350px" :pt="{ columnHeaderContent: { class: 'justify-center' } }">
- <template #body="slotProps">
- <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)" />
- <Button icon="pi pi-chart-line" label="扫描记录" size="small" text style="white-space: nowrap"
- @click="viewScanRecords(slotProps.data)" />
- <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: '450px' }" position="center">
- <div class="space-y-4">
- <div class="field" style="margin-top: 10px;">
- <FloatLabel variant="on">
- <Select id="qrType" v-model="generateForm.qrType" :options="[
- { label: '人员', value: 'person' },
- { label: '宠物', value: 'pet' },
- { label: '物品', value: 'goods' },
- { label: '链接', value: 'link' }
- ]" optionLabel="label" optionValue="value" fluid />
- <label for="qrType">二维码类型</label>
- </FloatLabel>
- </div>
- <div class="field" style="margin-top: 10px;">
- <FloatLabel variant="on">
- <InputNumber id="quantity" v-model="generateForm.quantity" :min="1" :max="10000" fluid />
- <label for="quantity">生成数量 (1-10000)</label>
- </FloatLabel>
- </div>
- </div>
- <template #footer>
- <Button label="取消" severity="secondary" @click="generateDialog = false" :disabled="generateLoading" />
- <Button label="生成" @click="handleGenerate" :loading="generateLoading" />
- </template>
- </Dialog>
- <!-- 扫描记录对话框 -->
- <Dialog v-model:visible="scanRecordDialog" :modal="true" header="扫描记录" :style="{ width: '900px' }"
- position="center">
- <div v-if="selectedQrCode" class="mb-4 p-4 bg-gray-50 dark:bg-gray-800 rounded">
- <div class="grid grid-cols-2 gap-4">
- <div>
- <div class="text-sm text-gray-600 dark:text-gray-400">二维码编号</div>
- <div class="font-mono font-semibold">{{ selectedQrCode.qrCode }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-600 dark:text-gray-400">总扫描次数</div>
- <div class="font-semibold text-lg text-blue-600">{{ selectedQrCode.scanCount || 0 }} 次</div>
- </div>
- </div>
- </div>
- <div v-if="scanRecords.length === 0" class="text-center py-8 text-gray-500">
- <i class="pi pi-inbox text-4xl mb-2"></i>
- <div>暂无扫描记录</div>
- </div>
- <DataTable v-else :value="scanRecords" scrollable scrollHeight="450px" striped>
- <template #header>
- <div class="text-sm text-gray-600">最近 10 条扫描记录</div>
- </template>
- <Column field="scanTime" header="扫描时间" style="min-width: 180px">
- <template #body="slotProps">
- <div class="flex items-center gap-2">
- <i class="pi pi-clock text-gray-400"></i>
- <span>{{ formatDate(slotProps.data.scanTime) }}</span>
- </div>
- </template>
- </Column>
- <Column field="ipAddress" header="IP地址" style="min-width: 140px">
- <template #body="slotProps">
- <div class="flex items-center gap-2">
- <i class="pi pi-globe text-gray-400"></i>
- <code class="text-xs">{{ slotProps.data.ipAddress || '-' }}</code>
- </div>
- </template>
- </Column>
- <Column field="address" header="地址" style="min-width: 200px">
- <template #body="slotProps">
- <div class="flex items-center gap-2">
- <i class="pi pi-map-marker text-gray-400"></i>
- <span class="text-sm">{{ slotProps.data.address || '未获取' }}</span>
- </div>
- </template>
- </Column>
- <Column field="location" header="位置坐标" style="min-width: 150px">
- <template #body="slotProps">
- <a v-if="slotProps.data.latitude && slotProps.data.longitude"
- :href="`https://www.google.com/maps?q=${slotProps.data.latitude},${slotProps.data.longitude}`"
- target="_blank" class="text-blue-600 hover:underline flex items-center gap-1">
- <i class="pi pi-external-link"></i>
- 查看地图
- </a>
- <span v-else class="text-gray-400 text-sm">未获取</span>
- </template>
- </Column>
- <Column field="userAgent" header="设备信息" style="min-width: 250px">
- <template #body="slotProps">
- <div class="text-xs text-gray-600 truncate" :title="slotProps.data.userAgent">
- <i class="pi pi-mobile text-gray-400 mr-1"></i>
- {{ slotProps.data.userAgent || '-' }}
- </div>
- </template>
- </Column>
- </DataTable>
- <template #footer>
- <Button label="关闭" @click="scanRecordDialog = false" />
- </template>
- </Dialog>
- <!-- 批量下载二维码对话框 -->
- <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
- :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="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
- class="font-mono text-xs break-all bg-gray-100 dark:bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
- @click="copyToClipboard(currentQrCodeUrl)" v-tooltip.top="'点击复制'">
- {{ currentQrCodeUrl }}
- </div>
- <div
- v-if="currentMaintenanceCode && currentMaintenanceCode !== 'unknown'"
- class="font-mono text-xs break-all bg-gray-100 dark:bg-gray-800 p-3 rounded cursor-pointer hover:bg-gray-200 dark:hover:bg-gray-700 transition-colors"
- @click="copyToClipboard(currentMaintenanceCode)" v-tooltip.top="'点击复制'">
- 维护码:{{ currentMaintenanceCode }}
- </div>
- </div>
- <template #footer>
- <Button icon="pi pi-download" label="下载二维码" @click="downloadCurrentQrCode" />
- <Button label="关闭" severity="secondary" @click="showQrCodeDialog = false" />
- </template>
- </Dialog>
- <!-- 详情对话框 -->
- <Dialog v-model:visible="detailDialog" :modal="true" header="二维码详情" :style="{ width: '750px' }" position="center">
- <div v-if="detailLoading" class="py-10 text-center text-gray-500">详情加载中...</div>
- <div v-else-if="qrCodeDetail" class="space-y-4">
- <!-- 二维码基本信息 -->
- <div class="border rounded p-4">
- <h4 class="font-semibold mb-3">二维码信息</h4>
- <div class="grid grid-cols-2 gap-4">
- <div>
- <div class="text-sm text-gray-500">二维码编号</div>
- <div class="font-mono text-sm break-all">{{ qrCodeDetail.qrCode || '-' }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">类型</div>
- <div>
- <Tag :severity="getTypeConfig(qrCodeDetail.qrType).severity">
- <i :class="`pi ${getTypeConfig(qrCodeDetail.qrType).icon} mr-1`"></i>
- {{ getTypeConfig(qrCodeDetail.qrType).label }}
- </Tag>
- </div>
- </div>
- <div>
- <div class="text-sm text-gray-500">激活状态</div>
- <div>
- <Tag :value="qrCodeDetail.isActivated ? '已激活' : '未激活'"
- :severity="qrCodeDetail.isActivated ? 'success' : 'secondary'" />
- </div>
- </div>
- <div>
- <div class="text-sm text-gray-500">扫描次数</div>
- <div class="font-semibold text-lg text-blue-600">{{ qrCodeDetail.scanCount || 0 }} 次</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">创建时间</div>
- <div>{{ formatDate(qrCodeDetail.createdAt) }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">更新时间</div>
- <div>{{ formatDate(qrCodeDetail.updatedAt) }}</div>
- </div>
- </div>
- </div>
- <!-- 关联信息 -->
- <div v-if="qrCodeDetail.info" class="border rounded p-4">
- <h4 class="font-semibold mb-3">关联信息</h4>
- <!-- 人员信息 -->
- <div v-if="qrCodeDetail.qrType === 'person'" class="grid grid-cols-2 gap-4">
- <div v-if="qrCodeDetail.info.photoUrl" class="col-span-2 text-center mb-2">
- <img :src="qrCodeDetail.info.photoUrl" alt="照片" class="w-32 h-32 object-cover rounded mx-auto" />
- </div>
- <div>
- <div class="text-sm text-gray-500">姓名</div>
- <div class="font-medium">{{ qrCodeDetail.info.name || '-' }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">性别</div>
- <div class="font-medium">{{ qrCodeDetail.info.gender === 'male' ? '男' : qrCodeDetail.info.gender ===
- 'female'
- ? '女' : '其他' }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">电话</div>
- <div>
- <a v-if="qrCodeDetail.info.phone" :href="`tel:${qrCodeDetail.info.phone}`"
- class="text-blue-600 hover:underline">
- {{ qrCodeDetail.info.phone }}
- </a>
- <span v-else>-</span>
- </div>
- </div>
- <div>
- <div class="text-sm text-gray-500">紧急联系人</div>
- <div class="font-medium">{{ qrCodeDetail.info.emergencyContactName || '-' }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">紧急联系人电话</div>
- <div>
- <a v-if="qrCodeDetail.info.emergencyContactPhone"
- :href="`tel:${qrCodeDetail.info.emergencyContactPhone}`" class="text-blue-600 hover:underline">
- {{ qrCodeDetail.info.emergencyContactPhone }}
- </a>
- <span v-else>-</span>
- </div>
- </div>
- <div>
- <div class="text-sm text-gray-500">紧急联系人邮箱</div>
- <div>
- <a v-if="qrCodeDetail.info.emergencyContactEmail"
- :href="`mailto:${qrCodeDetail.info.emergencyContactEmail}`" class="text-blue-600 hover:underline">
- {{ qrCodeDetail.info.emergencyContactEmail }}
- </a>
- <span v-else>-</span>
- </div>
- </div>
- <div v-if="qrCodeDetail.info.specialNote" class="col-span-2">
- <div class="text-sm text-gray-500">特别说明</div>
- <div class="whitespace-pre-wrap">{{ qrCodeDetail.info.specialNote }}</div>
- </div>
- <div v-if="qrCodeDetail.info.remark" class="col-span-2">
- <div class="text-sm text-gray-500">备注</div>
- <div class="whitespace-pre-wrap">{{ qrCodeDetail.info.remark }}</div>
- </div>
- </div>
- <!-- 宠物/物品信息 -->
- <div v-else-if="qrCodeDetail.qrType === 'pet' || qrCodeDetail.qrType === 'goods'"
- class="grid grid-cols-2 gap-4">
- <div v-if="qrCodeDetail.info.photoUrl" class="col-span-2 text-center mb-2">
- <img :src="qrCodeDetail.info.photoUrl" alt="照片" class="w-32 h-32 object-cover rounded mx-auto" />
- </div>
- <div>
- <div class="text-sm text-gray-500">{{ qrCodeDetail.qrType === 'pet' ? '名称' : '物品名称' }}</div>
- <div class="font-medium">{{ qrCodeDetail.info.name || '-' }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">联系人</div>
- <div class="font-medium">{{ qrCodeDetail.info.contactName || '-' }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">联系电话</div>
- <div>
- <a v-if="qrCodeDetail.info.contactPhone" :href="`tel:${qrCodeDetail.info.contactPhone}`"
- class="text-blue-600 hover:underline">
- {{ qrCodeDetail.info.contactPhone }}
- </a>
- <span v-else>-</span>
- </div>
- </div>
- <div>
- <div class="text-sm text-gray-500">联系邮箱</div>
- <div>
- <a v-if="qrCodeDetail.info.contactEmail" :href="`mailto:${qrCodeDetail.info.contactEmail}`"
- class="text-blue-600 hover:underline">
- {{ qrCodeDetail.info.contactEmail }}
- </a>
- <span v-else>-</span>
- </div>
- </div>
- <div v-if="qrCodeDetail.info.remark">
- <div class="text-sm text-gray-500">备注</div>
- <div class="whitespace-pre-wrap">{{ qrCodeDetail.info.remark }}</div>
- </div>
- <div>
- <div class="text-sm text-gray-500">是否显示</div>
- <div>
- <Tag :value="qrCodeDetail.info.isVisible ? '显示' : '隐藏'"
- :severity="qrCodeDetail.info.isVisible ? 'success' : 'secondary'" />
- </div>
- </div>
- </div>
- <!-- 链接信息 -->
- <div v-else-if="qrCodeDetail.qrType === 'link'" class="grid grid-cols-2 gap-4">
- <div class="col-span-2">
- <div class="text-sm text-gray-500">跳转链接</div>
- <div>
- <a v-if="qrCodeDetail.info.jumpUrl" :href="qrCodeDetail.info.jumpUrl" target="_blank"
- class="text-blue-600 hover:underline break-all">
- {{ qrCodeDetail.info.jumpUrl }}
- </a>
- <span v-else class="text-gray-400">-</span>
- </div>
- </div>
- <div>
- <div class="text-sm text-gray-500">可见性</div>
- <div>
- <Tag :value="qrCodeDetail.info.isVisible ? '可见' : '隐藏'"
- :severity="qrCodeDetail.info.isVisible ? 'success' : 'secondary'" />
- </div>
- </div>
- <div v-if="qrCodeDetail.info.remark" class="col-span-2">
- <div class="text-sm text-gray-500">备注</div>
- <div class="whitespace-pre-wrap">{{ qrCodeDetail.info.remark }}</div>
- </div>
- </div>
- </div>
-
- <!-- 已激活,未绑定信息 -->
- <div v-else-if="qrCodeDetail.isActivated && !qrCodeDetail.info" class="border rounded p-4 text-center text-gray-500">
- <i class="pi pi-info-circle text-2xl mb-2"></i>
- <div>该二维码已激活,但未填写关联信息</div>
- </div>
- <!-- 未激活提示 -->
- <div v-else class="border rounded p-4 text-center text-gray-500">
- <i class="pi pi-info-circle text-2xl mb-2"></i>
- <div>该二维码尚未激活,暂无关联信息</div>
- </div>
- </div>
- <template #footer>
- <Button label="关闭" @click="detailDialog = false" />
- </template>
- </Dialog>
- </div>
- </template>
|