|
|
@@ -10,6 +10,7 @@ import {
|
|
|
updatePetProfileApi,
|
|
|
updateGoodsInfoApi,
|
|
|
verifyMaintenanceCodeApi,
|
|
|
+ verifyMaintenanceCodeInfoApi,
|
|
|
uploadFile,
|
|
|
fetchRecentScanRecordsApi
|
|
|
} from '@/services/api'
|
|
|
@@ -40,6 +41,8 @@ const maintenanceCode = ref('')
|
|
|
const maintenancePassed = ref(false)
|
|
|
const showMaintenancePanel = ref(false)
|
|
|
const showMaintenanceDialog = ref(false)
|
|
|
+const viewMaintenanceCode = ref('')
|
|
|
+const loadingViewVerification = ref(false)
|
|
|
const isEditing = ref(false)
|
|
|
const showLocationDialog = ref(false)
|
|
|
const showLocationViewDialog = ref(false)
|
|
|
@@ -57,10 +60,11 @@ const FORM_KEY_MAP = Object.freeze({
|
|
|
'emergencyContactName',
|
|
|
'emergencyContactPhone',
|
|
|
'emergencyContactEmail',
|
|
|
- 'location'
|
|
|
+ 'location',
|
|
|
+ 'isVisible'
|
|
|
],
|
|
|
- pet: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark'],
|
|
|
- goods: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark']
|
|
|
+ pet: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark', 'isVisible'],
|
|
|
+ goods: ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark', 'isVisible']
|
|
|
})
|
|
|
|
|
|
const DEFAULT_FORM_STATE = Object.freeze({
|
|
|
@@ -76,7 +80,8 @@ const DEFAULT_FORM_STATE = Object.freeze({
|
|
|
contactName: '',
|
|
|
contactPhone: '',
|
|
|
contactEmail: '',
|
|
|
- location: ''
|
|
|
+ location: '',
|
|
|
+ isVisible: true
|
|
|
})
|
|
|
|
|
|
const createDefaultFormState = () => ({
|
|
|
@@ -103,7 +108,13 @@ const isPerson = computed(() => qrType.value === 'person')
|
|
|
const isPet = computed(() => qrType.value === 'pet')
|
|
|
const isGoods = computed(() => qrType.value === 'goods')
|
|
|
const hasProfile = computed(() => Boolean(profile.value))
|
|
|
-const isFirstFill = computed(() => Boolean(qrDetail.value) && !hasProfile.value)
|
|
|
+const isFirstFill = computed(() => {
|
|
|
+ // 优先判断 isVisible,如果不可见则不认为是第一次使用
|
|
|
+ if (qrDetail.value?.isVisible === false) {
|
|
|
+ return false
|
|
|
+ }
|
|
|
+ return Boolean(qrDetail.value) && !hasProfile.value
|
|
|
+})
|
|
|
const heroTitle = computed(() => {
|
|
|
if (!qrDetail.value) return 'Contact Card'
|
|
|
if (isPerson.value) return 'Person Card'
|
|
|
@@ -131,10 +142,21 @@ const resetForm = (source = null) => {
|
|
|
if (source) {
|
|
|
getActiveFormKeys().forEach((key) => {
|
|
|
if (Object.prototype.hasOwnProperty.call(source, key)) {
|
|
|
- nextState[key] = source[key] ?? DEFAULT_FORM_STATE[key] ?? ''
|
|
|
+ // 对于布尔值,直接使用值(包括 false)
|
|
|
+ if (key === 'isVisible') {
|
|
|
+ nextState[key] = source[key] !== undefined ? source[key] : true
|
|
|
+ } else {
|
|
|
+ nextState[key] = source[key] ?? DEFAULT_FORM_STATE[key] ?? ''
|
|
|
+ }
|
|
|
}
|
|
|
})
|
|
|
}
|
|
|
+ // 如果 source 中没有 isVisible,则从 qrDetail 中获取
|
|
|
+ if (!source || !Object.prototype.hasOwnProperty.call(source, 'isVisible')) {
|
|
|
+ if (qrDetail.value && Object.prototype.hasOwnProperty.call(qrDetail.value, 'isVisible')) {
|
|
|
+ nextState.isVisible = qrDetail.value.isVisible !== false
|
|
|
+ }
|
|
|
+ }
|
|
|
formData.value = nextState
|
|
|
}
|
|
|
|
|
|
@@ -158,6 +180,14 @@ const fetchQrDetails = async () => {
|
|
|
try {
|
|
|
const data = await fetchQrInfoApi(qrCode.value)
|
|
|
qrDetail.value = data
|
|
|
+ if (data.isVisible === false) {
|
|
|
+ infoStatus.state = 'notVisible'
|
|
|
+ infoStatus.message = 'QR code information is not visible'
|
|
|
+ profile.value = null
|
|
|
+ resetForm()
|
|
|
+ setDocumentTitle()
|
|
|
+ return
|
|
|
+ }
|
|
|
profile.value = data.info || null
|
|
|
resetForm(profile.value)
|
|
|
infoStatus.state = 'ready'
|
|
|
@@ -206,12 +236,51 @@ const handleVerifyMaintenance = async () => {
|
|
|
}
|
|
|
}
|
|
|
|
|
|
+const handleVerifyMaintenanceForView = async () => {
|
|
|
+ if (!qrCode.value || !viewMaintenanceCode.value) {
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ loadingViewVerification.value = true
|
|
|
+ try {
|
|
|
+ const data = await verifyMaintenanceCodeInfoApi({
|
|
|
+ qrCode: qrCode.value,
|
|
|
+ maintenanceCode: viewMaintenanceCode.value
|
|
|
+ })
|
|
|
+ if (data.valid) {
|
|
|
+ // 更新二维码详情和用户信息
|
|
|
+ qrDetail.value = {
|
|
|
+ ...qrDetail.value,
|
|
|
+ ...data,
|
|
|
+ isVisible: true // 验证成功后视为可见
|
|
|
+ }
|
|
|
+ profile.value = data.info || null
|
|
|
+ resetForm(profile.value)
|
|
|
+ infoStatus.state = 'ready'
|
|
|
+ infoStatus.message = ''
|
|
|
+ viewMaintenanceCode.value = ''
|
|
|
+ setDocumentTitle()
|
|
|
+ toast.add({ severity: 'success', summary: 'Verified', detail: data.message || 'Verification successful.', life: 2600 })
|
|
|
+ } else {
|
|
|
+ throw new Error(data.message || 'Verification failed')
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ toast.add({ severity: 'error', summary: 'Verification failed', detail: parseError(error), life: 3200 })
|
|
|
+ } finally {
|
|
|
+ loadingViewVerification.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
const buildProfilePayload = () => {
|
|
|
const payload = {
|
|
|
qrCode: qrCode.value
|
|
|
}
|
|
|
getActiveFormKeys().forEach((key) => {
|
|
|
- payload[key] = formData.value[key] ?? ''
|
|
|
+ if (key === 'isVisible') {
|
|
|
+ payload[key] = formData.value[key] !== undefined ? formData.value[key] : true
|
|
|
+ } else {
|
|
|
+ payload[key] = formData.value[key] ?? ''
|
|
|
+ }
|
|
|
})
|
|
|
if (maintenanceCode.value) {
|
|
|
payload.maintenanceCode = maintenanceCode.value
|
|
|
@@ -228,7 +297,6 @@ const handleSaveProfile = async () => {
|
|
|
await updater(payload)
|
|
|
toast.add({ severity: 'success', summary: 'Saved', detail: 'Profile has been updated.', life: 3000 })
|
|
|
await fetchQrDetails()
|
|
|
- // Exit edit mode after a successful save
|
|
|
isEditing.value = false
|
|
|
maintenancePassed.value = false
|
|
|
maintenanceCode.value = ''
|
|
|
@@ -339,6 +407,7 @@ const resetPageState = () => {
|
|
|
maintenancePassed.value = false
|
|
|
showMaintenancePanel.value = false
|
|
|
showMaintenanceDialog.value = false
|
|
|
+ viewMaintenanceCode.value = ''
|
|
|
isEditing.value = false
|
|
|
infoStatus.state = qrCode.value ? 'loading' : 'idle'
|
|
|
infoStatus.message = ''
|
|
|
@@ -509,6 +578,45 @@ onMounted(() => {
|
|
|
<p class="mt-2 text-xs text-red-200">Please double-check the QR code or reach out for assistance.</p>
|
|
|
</div>
|
|
|
|
|
|
+ <div v-if="infoStatus.state === 'notVisible'" class="rounded-3xl border border-amber-500/40 bg-amber-500/10 p-6">
|
|
|
+ <div class="flex items-start gap-4">
|
|
|
+ <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-amber-500/20 flex-shrink-0">
|
|
|
+ <i class="pi pi-eye-slash text-2xl text-amber-300" />
|
|
|
+ </div>
|
|
|
+ <div class="flex-1">
|
|
|
+ <p class="text-lg font-semibold text-amber-100">{{ infoStatus.message }}</p>
|
|
|
+ <p class="mt-2 text-sm text-amber-200">This QR code information has been set to invisible and cannot be viewed.</p>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div v-if="infoStatus.state === 'notVisible'" class="rounded-3xl border border-cyan-500/40 bg-cyan-500/10 p-6">
|
|
|
+ <div class="flex items-start gap-4">
|
|
|
+ <div class="flex h-12 w-12 items-center justify-center rounded-2xl bg-cyan-500/20 flex-shrink-0">
|
|
|
+ <i class="pi pi-key text-2xl text-cyan-300" />
|
|
|
+ </div>
|
|
|
+ <div class="flex-1">
|
|
|
+ <p class="text-lg font-semibold text-cyan-100">View with maintenance code</p>
|
|
|
+ <p class="mt-2 text-sm text-cyan-200">Enter the maintenance code to view complete information.</p>
|
|
|
+ <div class="mt-4 flex flex-col gap-3 sm:flex-row sm:items-end">
|
|
|
+ <div class="flex-1">
|
|
|
+ <label class="mb-1.5 block text-xs font-medium text-cyan-100">Maintenance code</label>
|
|
|
+ <input v-model="viewMaintenanceCode" type="text" maxlength="8"
|
|
|
+ class="w-full rounded-xl border border-cyan-400/40 bg-white/10 px-4 py-2.5 text-sm text-white placeholder:text-cyan-300/60 focus:border-cyan-300 focus:bg-white/15 focus:outline-none focus:ring-2 focus:ring-cyan-300/30"
|
|
|
+ placeholder="Enter maintenance code" @keyup.enter="handleVerifyMaintenanceForView" />
|
|
|
+ </div>
|
|
|
+ <button type="button"
|
|
|
+ class="rounded-xl bg-cyan-500 px-6 py-2.5 text-sm font-semibold text-white transition hover:bg-cyan-600 disabled:opacity-50 sm:whitespace-nowrap"
|
|
|
+ :disabled="loadingViewVerification || !viewMaintenanceCode" @click="handleVerifyMaintenanceForView">
|
|
|
+ <i v-if="loadingViewVerification" class="pi pi-spin pi-spinner mr-2" />
|
|
|
+ <i v-else class="pi pi-check mr-2" />
|
|
|
+ {{ loadingViewVerification ? 'Verifying...' : 'Verify & View' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div v-if="loading.info" class="rounded-3xl border border-white/10 bg-white/5 p-6 text-base text-white">
|
|
|
Loading QR information...
|
|
|
</div>
|
|
|
@@ -837,6 +945,19 @@ onMounted(() => {
|
|
|
:placeholder="isGoods ? 'e.g. item features, usage notes, handling tips' : 'e.g. pet habits, health info, behavior notes'" />
|
|
|
</label>
|
|
|
|
|
|
+ <div class="rounded-2xl border border-slate-200 bg-slate-50 p-5">
|
|
|
+ <div class="flex items-center justify-between">
|
|
|
+ <div class="flex-1">
|
|
|
+ <p class="text-sm font-medium text-slate-700">Visibility</p>
|
|
|
+ <p class="mt-1 text-xs text-slate-500">When disabled, others will not be able to view information when scanning this QR code</p>
|
|
|
+ </div>
|
|
|
+ <label class="relative inline-flex items-center cursor-pointer">
|
|
|
+ <input v-model="formData.isVisible" type="checkbox" class="sr-only peer" />
|
|
|
+ <div class="w-11 h-6 bg-slate-300 peer-focus:outline-none peer-focus:ring-4 peer-focus:ring-cyan-300 rounded-full peer peer-checked:after:translate-x-full peer-checked:after:border-white after:content-[''] after:absolute after:top-[2px] after:left-[2px] after:bg-white after:border-slate-300 after:border after:rounded-full after:h-5 after:w-5 after:transition-all peer-checked:bg-cyan-600"></div>
|
|
|
+ </label>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
|
<p class="text-sm text-slate-500">
|
|
|
Submitted data will sync to the public scan page.
|