|
|
@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import { useToast } from 'primevue/usetoast'
|
|
|
import Dialog from 'primevue/dialog'
|
|
|
+import TimeZoneSelector from '@/components/TimeZoneSelector.vue'
|
|
|
import {
|
|
|
fetchQrInfoApi,
|
|
|
updateLinkInfoApi,
|
|
|
@@ -10,6 +11,7 @@ import {
|
|
|
fetchRecentScanRecordsApi
|
|
|
} from '@/services/api'
|
|
|
import { saveMaintenanceCode, getMaintenanceCode } from '@/utils/maintenanceCodeCache'
|
|
|
+import { useTimeZone } from '@/composables/useTimeZone'
|
|
|
|
|
|
const route = useRoute()
|
|
|
const router = useRouter()
|
|
|
@@ -43,6 +45,7 @@ const scanRecordsMaintenanceCode = ref('')
|
|
|
const verifiedMaintenanceCode = ref('')
|
|
|
const scanRecords = ref(null)
|
|
|
const loadingScanRecords = ref(false)
|
|
|
+const hasCachedScanRecordsMaintenanceCode = ref(false)
|
|
|
|
|
|
const formData = reactive({
|
|
|
jumpUrl: '',
|
|
|
@@ -280,25 +283,41 @@ const fetchScanRecords = async (maintenanceCode) => {
|
|
|
}
|
|
|
|
|
|
const handleVerifyScanRecordsMaintenance = async () => {
|
|
|
- if (!qrCode.value || !scanRecordsMaintenanceCode.value) {
|
|
|
+ if (!qrCode.value) {
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'QR code is required.', life: 2400 })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ // 如果有缓存的维护码,直接使用;否则使用用户输入的
|
|
|
+ let codeToUse = scanRecordsMaintenanceCode.value.trim()
|
|
|
+ if (!codeToUse && hasCachedScanRecordsMaintenanceCode.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
|
|
|
}
|
|
|
- await fetchScanRecords(scanRecordsMaintenanceCode.value)
|
|
|
+ await fetchScanRecords(codeToUse)
|
|
|
}
|
|
|
|
|
|
-const formatDateTime = (dateString) => {
|
|
|
- if (!dateString) return '-'
|
|
|
- const date = new Date(dateString)
|
|
|
- return date.toLocaleString('en-US', {
|
|
|
- year: 'numeric',
|
|
|
- month: '2-digit',
|
|
|
- day: '2-digit',
|
|
|
- hour: '2-digit',
|
|
|
- minute: '2-digit',
|
|
|
- second: '2-digit'
|
|
|
- })
|
|
|
-}
|
|
|
+// 使用时区 composable
|
|
|
+const { selectedTimeZone, formatDateTime } = useTimeZone()
|
|
|
+
|
|
|
+// 格式化后的扫描记录(响应式,当时区变化时自动更新)
|
|
|
+const formattedScanRecords = computed(() => {
|
|
|
+ if (!scanRecords.value || !scanRecords.value.records) {
|
|
|
+ return null
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ ...scanRecords.value,
|
|
|
+ records: scanRecords.value.records.map(record => ({
|
|
|
+ ...record,
|
|
|
+ formattedScanTime: formatDateTime(record.scanTime)
|
|
|
+ }))
|
|
|
+ }
|
|
|
+})
|
|
|
|
|
|
const openRecordLocation = (record) => {
|
|
|
if (record.latitude && record.longitude) {
|
|
|
@@ -397,10 +416,26 @@ watch(qrCode, (newQrCode) => {
|
|
|
if (newQrCode) {
|
|
|
const cachedCode = getMaintenanceCode(newQrCode)
|
|
|
hasCachedMaintenanceCode.value = !!cachedCode
|
|
|
+ hasCachedScanRecordsMaintenanceCode.value = !!cachedCode
|
|
|
if (cachedCode) {
|
|
|
maintenanceCode.value = cachedCode
|
|
|
+ scanRecordsMaintenanceCode.value = cachedCode
|
|
|
} else {
|
|
|
maintenanceCode.value = ''
|
|
|
+ scanRecordsMaintenanceCode.value = ''
|
|
|
+ }
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+// 当打开扫描记录维护码对话框时,自动填充缓存的维护码
|
|
|
+watch(showScanRecordsMaintenanceDialog, (isOpen) => {
|
|
|
+ if (isOpen && qrCode.value) {
|
|
|
+ const cachedCode = getMaintenanceCode(qrCode.value)
|
|
|
+ hasCachedScanRecordsMaintenanceCode.value = !!cachedCode
|
|
|
+ if (cachedCode) {
|
|
|
+ scanRecordsMaintenanceCode.value = cachedCode
|
|
|
+ } else {
|
|
|
+ scanRecordsMaintenanceCode.value = ''
|
|
|
}
|
|
|
}
|
|
|
})
|
|
|
@@ -708,12 +743,20 @@ onMounted(() => {
|
|
|
:style="{ width: '90vw', maxWidth: '400px' }" class="p-fluid">
|
|
|
<div class="space-y-4">
|
|
|
<p class="text-sm text-slate-600">Enter the maintenance code to view scan records.</p>
|
|
|
- <div class="space-y-2">
|
|
|
+ <div v-if="!hasCachedScanRecordsMaintenanceCode" class="space-y-2">
|
|
|
<label class="block text-sm font-medium text-slate-700">Maintenance code</label>
|
|
|
<input v-model="scanRecordsMaintenanceCode" type="text" maxlength="8"
|
|
|
class="w-full rounded-xl border border-slate-300 bg-white px-4 py-2.5 text-sm text-slate-900 focus:border-cyan-400 focus:outline-none focus:ring-2 focus:ring-cyan-400/30"
|
|
|
placeholder="Enter the maintenance code" @keyup.enter="handleVerifyScanRecordsMaintenance" />
|
|
|
</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 justify-end gap-3 pt-2">
|
|
|
<button type="button"
|
|
|
class="rounded-xl border border-slate-300 bg-white px-4 py-2 text-sm font-medium text-slate-700 hover:bg-slate-50"
|
|
|
@@ -722,7 +765,7 @@ onMounted(() => {
|
|
|
</button>
|
|
|
<button type="button"
|
|
|
class="rounded-xl bg-cyan-500 px-4 py-2 text-sm font-semibold text-white transition hover:bg-cyan-600 disabled:opacity-50"
|
|
|
- :disabled="loadingScanRecords || !scanRecordsMaintenanceCode"
|
|
|
+ :disabled="loadingScanRecords || (!hasCachedScanRecordsMaintenanceCode && !scanRecordsMaintenanceCode)"
|
|
|
@click="handleVerifyScanRecordsMaintenance">
|
|
|
<i v-if="loadingScanRecords" class="pi pi-spin pi-spinner mr-2" />
|
|
|
<i v-else class="pi pi-check mr-2" />
|
|
|
@@ -733,23 +776,43 @@ onMounted(() => {
|
|
|
</Dialog>
|
|
|
|
|
|
<!-- Scan records dialog -->
|
|
|
- <Dialog v-model:visible="showScanRecordsDialog" modal header="Scan Records"
|
|
|
- :style="{ width: '90vw', maxWidth: '600px' }" class="p-fluid">
|
|
|
- <div v-if="loadingScanRecords" class="py-8 text-center text-slate-600">
|
|
|
- <i class="pi pi-spin pi-spinner text-2xl" />
|
|
|
- <p class="mt-2">Loading...</p>
|
|
|
- </div>
|
|
|
- <div v-else-if="scanRecords">
|
|
|
- <div v-if="scanRecords.records && scanRecords.records.length > 0"
|
|
|
+ <Dialog v-model:visible="showScanRecordsDialog" modal :closable="true" :closeOnEscape="true"
|
|
|
+ :dismissableMask="true" :style="{ width: '90vw', maxWidth: '800px' }" :draggable="false">
|
|
|
+ <template #header>
|
|
|
+ <div class="flex items-center justify-between gap-3">
|
|
|
+ <div class="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-history text-lg text-cyan-600" />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <h3 class="text-lg font-semibold text-slate-900">Recent Scan Records</h3>
|
|
|
+ <p class="text-sm text-slate-500">
|
|
|
+ QR Code: <span class="font-mono">{{ qrCode }}</span>
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <!-- 时区选择器 -->
|
|
|
+ <TimeZoneSelector />
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+
|
|
|
+ <div class="space-y-4 py-4">
|
|
|
+ <div v-if="loadingScanRecords" class="flex items-center justify-center py-8">
|
|
|
+ <i class="pi pi-spin pi-spinner text-3xl text-slate-400" />
|
|
|
+ <span class="ml-3 text-slate-600">Loading...</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else-if="formattedScanRecords">
|
|
|
+ <div v-if="formattedScanRecords.records && formattedScanRecords.records.length > 0"
|
|
|
class="space-y-3 max-h-[60vh] overflow-y-auto">
|
|
|
- <div v-for="record in scanRecords.records" :key="record.id"
|
|
|
+ <div v-for="record in formattedScanRecords.records" :key="`${record.id}-${selectedTimeZone}`"
|
|
|
class="rounded-xl border border-slate-200 bg-white p-4 shadow-sm hover:shadow-md transition-shadow">
|
|
|
<div class="flex items-start justify-between gap-4">
|
|
|
<div class="flex-1 space-y-2">
|
|
|
<div class="flex items-center gap-2">
|
|
|
<i class="pi pi-clock text-sm text-slate-500" />
|
|
|
<span class="text-sm font-medium text-slate-700">Scan Time</span>
|
|
|
- <span class="text-sm text-slate-600">{{ formatDateTime(record.scanTime) }}</span>
|
|
|
+ <span class="text-sm text-slate-600">{{ record.formattedScanTime }}</span>
|
|
|
</div>
|
|
|
|
|
|
<div v-if="record.address" class="flex items-start gap-2">
|
|
|
@@ -781,11 +844,35 @@ onMounted(() => {
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
- <div v-else class="py-8 text-center text-slate-500">
|
|
|
- <i class="pi pi-inbox text-3xl mb-2" />
|
|
|
- <p>No scan records</p>
|
|
|
+ <div v-else class="rounded-xl border border-slate-200 bg-slate-50 p-8 text-center">
|
|
|
+ <i class="pi pi-inbox text-4xl text-slate-400 mb-3" />
|
|
|
+ <p class="text-sm text-slate-600">No scan records</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-else class="rounded-xl border border-red-200 bg-red-50 p-4">
|
|
|
+ <div class="flex items-center gap-2">
|
|
|
+ <i class="pi pi-exclamation-triangle text-red-600" />
|
|
|
+ <span class="text-sm text-red-700">Failed to load, please try again later</span>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <template #footer>
|
|
|
+ <div class="flex justify-end gap-3">
|
|
|
+ <button type="button"
|
|
|
+ class="rounded-xl border border-slate-300 px-5 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50"
|
|
|
+ @click="showScanRecordsDialog = false">
|
|
|
+ Close
|
|
|
+ </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="loadingScanRecords || !verifiedMaintenanceCode" @click="fetchScanRecords(verifiedMaintenanceCode)">
|
|
|
+ <i class="pi pi-refresh mr-2" :class="{ 'pi-spin': loadingScanRecords }" />
|
|
|
+ Refresh
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
</Dialog>
|
|
|
</div>
|
|
|
</template>
|
|
|
@@ -798,3 +885,4 @@ onMounted(() => {
|
|
|
linear-gradient(135deg, #020617, #030712);
|
|
|
}
|
|
|
</style>
|
|
|
+
|