| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484 |
- <script setup>
- import {
- generateQrCodes,
- queryQrCodes,
- downloadQrCodesByDate,
- getQrCodeScanRecords
- } from '@/services/api'
- 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 { computed, onMounted, ref } from 'vue'
- 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' }
- ]
- // 激活状态选项
- 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 getQrTypeName = (type) => {
- return type === 'person' ? '人员' : '宠物|物品'
- }
- // 获取激活状态标签
- 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.search = 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
- }
- await downloadByDate(downloadDate.value)
- batchDownloadDialog.value = false
- }
- // 生成二维码
- const handleGenerate = async () => {
- if (generateForm.value.quantity < 1 || generateForm.value.quantity > 1000) {
- toast.add({
- severity: 'warn',
- summary: '警告',
- detail: '生成数量必须在1-1000之间',
- life: 3000
- })
- return
- }
- 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
- })
- fetchData()
- } catch (error) {
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: error.message || '生成二维码失败',
- life: 3000
- })
- } finally {
- generateLoading.value = false
- }
- }
- // 导出生成的二维码
- const exportGeneratedCodes = () => {
- if (generatedCodes.value.length === 0) return
- const csvContent = [
- ['二维码编号', '维护码', '类型', '生成时间'].join(','),
- ...generatedCodes.value.map((code) =>
- [
- code.qrCode,
- code.maintenanceCode,
- getQrTypeName(generateForm.value.qrType),
- 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) => {
- try {
- selectedQrCode.value = qrCode
- const response = await getQrCodeScanRecords(qrCode.qrCode, 20)
- scanRecords.value = response.data
- scanRecordDialog.value = true
- } catch (error) {
- toast.add({
- severity: 'error',
- summary: '错误',
- detail: error.message || '获取扫描记录失败',
- life: 3000
- })
- }
- }
- // 复制到剪贴板
- const copyToClipboard = (text) => {
- navigator.clipboard.writeText(text).then(() => {
- 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="qrType" header="类型" style="min-width: 120px">
- <template #body="slotProps">
- <Tag :value="getQrTypeName(slotProps.data.qrType)"
- :severity="slotProps.data.qrType === 'person' ? 'info' : 'warn'" />
- </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">
- <template #body="slotProps">
- <span class="font-semibold">{{ slotProps.data.scanCount || 0 }}</span>
- </template>
- </Column>
- <Column field="createdAt" header="生成时间" style="min-width: 180px">
- <template #body="slotProps">
- {{ formatDate(slotProps.data.createdAt) }}
- </template>
- </Column>
- <Column header="操作" style="min-width: 150px">
- <template #body="slotProps">
- <div class="flex gap-1">
- <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)" />
- </div>
- </template>
- </Column>
- </DataTable>
- <!-- 生成二维码对话框 -->
- <Dialog v-model:visible="generateDialog" :modal="true" header="生成二维码" :style="{ width: '550px' }" 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' }
- ]" 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="1000" fluid />
- <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>
- <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: '800px' }"
- position="center">
- <div v-if="selectedQrCode" class="mb-4 p-3 bg-gray-50 dark:bg-gray-800 rounded">
- <div class="text-sm text-gray-600 dark:text-gray-400">二维码编号</div>
- <div class="font-mono font-semibold">{{ selectedQrCode.qrCode }}</div>
- <div class="text-sm text-gray-600 dark:text-gray-400 mt-2">总扫描次数</div>
- <div class="font-semibold text-lg">{{ selectedQrCode.scanCount || 0 }} 次</div>
- </div>
- <DataTable :value="scanRecords" scrollable scrollHeight="400px">
- <Column field="scanTime" header="扫描时间" style="min-width: 180px">
- <template #body="slotProps">
- {{ formatDate(slotProps.data.scanTime) }}
- </template>
- </Column>
- <Column field="address" header="地址" style="min-width: 200px">
- <template #body="slotProps">
- {{ slotProps.data.address || '-' }}
- </template>
- </Column>
- <Column field="latitude" 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">
- 📍 查看地图
- </a>
- <span v-else>-</span>
- </template>
- </Column>
- <Column field="ipAddress" header="IP地址" style="min-width: 150px">
- <template #body="slotProps">
- {{ slotProps.data.ipAddress || '-' }}
- </template>
- </Column>
- </DataTable>
- <template #footer>
- <Button label="关闭" @click="scanRecordDialog = false" />
- </template>
- </Dialog>
- <!-- 批量下载二维码对话框 -->
- <Dialog v-model:visible="batchDownloadDialog" :modal="true" header="批量下载二维码" :style="{ width: '450px' }"
- 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>
- </FloatLabel>
- </div>
- </div>
- <template #footer>
- <Button label="取消" severity="secondary" @click="batchDownloadDialog = false" />
- <Button label="下载" @click="handleBatchDownload" />
- </template>
- </Dialog>
- </div>
- </template>
|