|
@@ -3,6 +3,7 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
|
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
import { useRoute, useRouter } from 'vue-router'
|
|
|
import { useToast } from 'primevue/usetoast'
|
|
import { useToast } from 'primevue/usetoast'
|
|
|
import Dialog from 'primevue/dialog'
|
|
import Dialog from 'primevue/dialog'
|
|
|
|
|
+import LocationPicker from '@/components/LocationPicker.vue'
|
|
|
import {
|
|
import {
|
|
|
fetchQrInfoApi,
|
|
fetchQrInfoApi,
|
|
|
updatePersonProfileApi,
|
|
updatePersonProfileApi,
|
|
@@ -16,7 +17,7 @@ const route = useRoute()
|
|
|
const router = useRouter()
|
|
const router = useRouter()
|
|
|
const toast = useToast()
|
|
const toast = useToast()
|
|
|
|
|
|
|
|
-// 从路径参数获取二维码编号
|
|
|
|
|
|
|
+// Pull QR code from route params
|
|
|
const qrCode = ref(route.params.qrCode?.toString().trim() || '')
|
|
const qrCode = ref(route.params.qrCode?.toString().trim() || '')
|
|
|
const queryInput = ref(qrCode.value)
|
|
const queryInput = ref(qrCode.value)
|
|
|
|
|
|
|
@@ -39,6 +40,8 @@ const maintenancePassed = ref(false)
|
|
|
const showMaintenancePanel = ref(false)
|
|
const showMaintenancePanel = ref(false)
|
|
|
const showMaintenanceDialog = ref(false)
|
|
const showMaintenanceDialog = ref(false)
|
|
|
const isEditing = ref(false)
|
|
const isEditing = ref(false)
|
|
|
|
|
+const showLocationDialog = ref(false)
|
|
|
|
|
+const showLocationViewDialog = ref(false)
|
|
|
|
|
|
|
|
const personKeys = [
|
|
const personKeys = [
|
|
|
'photoUrl',
|
|
'photoUrl',
|
|
@@ -50,8 +53,8 @@ const personKeys = [
|
|
|
'emergencyContactPhone',
|
|
'emergencyContactPhone',
|
|
|
'emergencyContactEmail'
|
|
'emergencyContactEmail'
|
|
|
]
|
|
]
|
|
|
-const petKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'remark']
|
|
|
|
|
-const goodsKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'remark']
|
|
|
|
|
|
|
+const petKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark']
|
|
|
|
|
+const goodsKeys = ['photoUrl', 'name', 'contactName', 'contactPhone', 'contactEmail', 'location', 'remark']
|
|
|
|
|
|
|
|
const formData = reactive({
|
|
const formData = reactive({
|
|
|
photoUrl: '',
|
|
photoUrl: '',
|
|
@@ -65,7 +68,8 @@ const formData = reactive({
|
|
|
remark: '',
|
|
remark: '',
|
|
|
contactName: '',
|
|
contactName: '',
|
|
|
contactPhone: '',
|
|
contactPhone: '',
|
|
|
- contactEmail: ''
|
|
|
|
|
|
|
+ contactEmail: '',
|
|
|
|
|
+ location: ''
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const qrType = computed(() => qrDetail.value?.qrType || 'person')
|
|
const qrType = computed(() => qrDetail.value?.qrType || 'person')
|
|
@@ -75,22 +79,22 @@ const isGoods = computed(() => qrType.value === 'goods')
|
|
|
const hasProfile = computed(() => Boolean(profile.value))
|
|
const hasProfile = computed(() => Boolean(profile.value))
|
|
|
const isFirstFill = computed(() => Boolean(qrDetail.value) && !hasProfile.value)
|
|
const isFirstFill = computed(() => Boolean(qrDetail.value) && !hasProfile.value)
|
|
|
const heroTitle = computed(() => {
|
|
const heroTitle = computed(() => {
|
|
|
- if (!qrDetail.value) return '联系卡'
|
|
|
|
|
- if (isPerson.value) return '人员卡'
|
|
|
|
|
- if (isPet.value) return '宠物卡'
|
|
|
|
|
- if (isGoods.value) return '物品卡'
|
|
|
|
|
- return '联系卡'
|
|
|
|
|
|
|
+ if (!qrDetail.value) return 'Contact Card'
|
|
|
|
|
+ if (isPerson.value) return 'Person Card'
|
|
|
|
|
+ if (isPet.value) return 'Pet Card'
|
|
|
|
|
+ if (isGoods.value) return 'Item Card'
|
|
|
|
|
+ return 'Contact Card'
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
const parseError = (error) => {
|
|
const parseError = (error) => {
|
|
|
- if (!error) return '请求失败'
|
|
|
|
|
|
|
+ if (!error) return 'Request failed'
|
|
|
if (typeof error === 'string') return error
|
|
if (typeof error === 'string') return error
|
|
|
if (error.message) return error.message
|
|
if (error.message) return error.message
|
|
|
if (error.detail) return error.detail
|
|
if (error.detail) return error.detail
|
|
|
if (error.response?.data?.message) return error.response.data.message
|
|
if (error.response?.data?.message) return error.response.data.message
|
|
|
if (error.data?.message) return error.data.message
|
|
if (error.data?.message) return error.data.message
|
|
|
if (error.message === undefined && error?.error) return error.error
|
|
if (error.message === undefined && error?.error) return error.error
|
|
|
- return '请求失败'
|
|
|
|
|
|
|
+ return 'Request failed'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const resetForm = (source) => {
|
|
const resetForm = (source) => {
|
|
@@ -114,11 +118,11 @@ const resetForm = (source) => {
|
|
|
|
|
|
|
|
const setDocumentTitle = () => {
|
|
const setDocumentTitle = () => {
|
|
|
if (typeof document === 'undefined') return
|
|
if (typeof document === 'undefined') return
|
|
|
- let defaultName = '人员'
|
|
|
|
|
- if (isPet.value) defaultName = '宠物'
|
|
|
|
|
- if (isGoods.value) defaultName = '物品'
|
|
|
|
|
|
|
+ let defaultName = 'Person'
|
|
|
|
|
+ if (isPet.value) defaultName = 'Pet'
|
|
|
|
|
+ if (isGoods.value) defaultName = 'Item'
|
|
|
const name = profile.value?.name || defaultName
|
|
const name = profile.value?.name || defaultName
|
|
|
- document.title = `${name} | 应急二维码`
|
|
|
|
|
|
|
+ document.title = `${name} | Emergency QR`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const fetchQrDetails = async () => {
|
|
const fetchQrDetails = async () => {
|
|
@@ -143,7 +147,7 @@ const fetchQrDetails = async () => {
|
|
|
|
|
|
|
|
const handleQrSubmit = () => {
|
|
const handleQrSubmit = () => {
|
|
|
if (!queryInput.value.trim()) {
|
|
if (!queryInput.value.trim()) {
|
|
|
- toast.add({ severity: 'warn', summary: '提示', detail: '请先输入二维码编号', life: 2600 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter a QR code first.', life: 2600 })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
router.push({ name: 'scan', params: { qrCode: queryInput.value.trim() } })
|
|
router.push({ name: 'scan', params: { qrCode: queryInput.value.trim() } })
|
|
@@ -151,7 +155,7 @@ const handleQrSubmit = () => {
|
|
|
|
|
|
|
|
const handleVerifyMaintenance = async () => {
|
|
const handleVerifyMaintenance = async () => {
|
|
|
if (!qrCode.value || !maintenanceCode.value) {
|
|
if (!qrCode.value || !maintenanceCode.value) {
|
|
|
- toast.add({ severity: 'warn', summary: '提示', detail: '请输入维护码', life: 2400 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter the maintenance code.', life: 2400 })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
loading.verifying = true
|
|
loading.verifying = true
|
|
@@ -164,14 +168,14 @@ const handleVerifyMaintenance = async () => {
|
|
|
showMaintenancePanel.value = false
|
|
showMaintenancePanel.value = false
|
|
|
showMaintenanceDialog.value = false
|
|
showMaintenanceDialog.value = false
|
|
|
isEditing.value = true
|
|
isEditing.value = true
|
|
|
- toast.add({ severity: 'success', summary: '验证成功', detail: '已解锁信息维护', life: 2600 })
|
|
|
|
|
- // 首次填写时自动滚动到表单
|
|
|
|
|
|
|
+ toast.add({ severity: 'success', summary: 'Verified', detail: 'Maintenance editing unlocked.', life: 2600 })
|
|
|
|
|
+ // Scroll to the form the first time we collect data
|
|
|
if (isFirstFill.value) {
|
|
if (isFirstFill.value) {
|
|
|
setTimeout(scrollToForm, 300)
|
|
setTimeout(scrollToForm, 300)
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
maintenancePassed.value = false
|
|
maintenancePassed.value = false
|
|
|
- toast.add({ severity: 'error', summary: '验证失败', detail: parseError(error), life: 3200 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'error', summary: 'Verification failed', detail: parseError(error), life: 3200 })
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.verifying = false
|
|
loading.verifying = false
|
|
|
}
|
|
}
|
|
@@ -202,14 +206,14 @@ const handleSaveProfile = async () => {
|
|
|
if (isPet.value) updater = updatePetProfileApi
|
|
if (isPet.value) updater = updatePetProfileApi
|
|
|
if (isGoods.value) updater = updateGoodsInfoApi
|
|
if (isGoods.value) updater = updateGoodsInfoApi
|
|
|
await updater(payload)
|
|
await updater(payload)
|
|
|
- toast.add({ severity: 'success', summary: '保存成功', detail: '资料已更新', life: 3000 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'success', summary: 'Saved', detail: 'Profile has been updated.', life: 3000 })
|
|
|
await fetchQrDetails()
|
|
await fetchQrDetails()
|
|
|
- // 保存成功后退出编辑模式
|
|
|
|
|
|
|
+ // Exit edit mode after a successful save
|
|
|
isEditing.value = false
|
|
isEditing.value = false
|
|
|
maintenancePassed.value = false
|
|
maintenancePassed.value = false
|
|
|
maintenanceCode.value = ''
|
|
maintenanceCode.value = ''
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- toast.add({ severity: 'error', summary: '保存失败', detail: parseError(error), life: 3200 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'error', summary: 'Save failed', detail: parseError(error), life: 3200 })
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.saving = false
|
|
loading.saving = false
|
|
|
}
|
|
}
|
|
@@ -221,8 +225,8 @@ const triggerPhotoPicker = () => {
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const handleImageError = (event) => {
|
|
const handleImageError = (event) => {
|
|
|
- // 图片加载失败时的处理
|
|
|
|
|
- console.warn('图片加载失败:', formData.photoUrl)
|
|
|
|
|
|
|
+ // Handle broken image preview
|
|
|
|
|
+ console.warn('Image load failed:', formData.photoUrl)
|
|
|
event.target.style.display = 'none'
|
|
event.target.style.display = 'none'
|
|
|
}
|
|
}
|
|
|
|
|
|
|
@@ -230,35 +234,35 @@ const handlePhotoChange = async (event) => {
|
|
|
const file = event.target.files?.[0]
|
|
const file = event.target.files?.[0]
|
|
|
if (!file) return
|
|
if (!file) return
|
|
|
|
|
|
|
|
- // 验证文件类型
|
|
|
|
|
|
|
+ // Validate mime type
|
|
|
if (!file.type.startsWith('image/')) {
|
|
if (!file.type.startsWith('image/')) {
|
|
|
- toast.add({ severity: 'warn', summary: '提示', detail: '请选择图片文件', life: 2400 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please choose an image file.', life: 2400 })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // 验证文件大小(限制为 15MB)
|
|
|
|
|
|
|
+ // Validate file size (limit 15MB)
|
|
|
const maxSize = 15 * 1024 * 1024
|
|
const maxSize = 15 * 1024 * 1024
|
|
|
if (file.size > maxSize) {
|
|
if (file.size > maxSize) {
|
|
|
- toast.add({ severity: 'warn', summary: '提示', detail: '图片大小不能超过 15MB', life: 2400 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Image size cannot exceed 15MB.', life: 2400 })
|
|
|
return
|
|
return
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
loading.photo = true
|
|
loading.photo = true
|
|
|
try {
|
|
try {
|
|
|
const response = await uploadFile(file)
|
|
const response = await uploadFile(file)
|
|
|
- // 根据返回的数据结构获取 URL
|
|
|
|
|
|
|
+ // Normalize upload response shape
|
|
|
const url = response?.data?.url || response?.url || ''
|
|
const url = response?.data?.url || response?.url || ''
|
|
|
if (url) {
|
|
if (url) {
|
|
|
formData.photoUrl = url
|
|
formData.photoUrl = url
|
|
|
- toast.add({ severity: 'success', summary: '上传成功', detail: '图片已更新', life: 2400 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'success', summary: 'Uploaded', detail: 'Photo has been updated.', life: 2400 })
|
|
|
} else {
|
|
} else {
|
|
|
- throw new Error('未获取到图片地址')
|
|
|
|
|
|
|
+ throw new Error('Image URL not found')
|
|
|
}
|
|
}
|
|
|
} catch (error) {
|
|
} catch (error) {
|
|
|
- toast.add({ severity: 'error', summary: '上传失败', detail: parseError(error), life: 3200 })
|
|
|
|
|
|
|
+ toast.add({ severity: 'error', summary: 'Upload failed', detail: parseError(error), life: 3200 })
|
|
|
} finally {
|
|
} finally {
|
|
|
loading.photo = false
|
|
loading.photo = false
|
|
|
- // 清空 input,允许重复上传同一文件
|
|
|
|
|
|
|
+ // Reset input so the same file can trigger change
|
|
|
if (event.target) {
|
|
if (event.target) {
|
|
|
event.target.value = ''
|
|
event.target.value = ''
|
|
|
}
|
|
}
|
|
@@ -282,6 +286,33 @@ const sendEmail = (email) => {
|
|
|
window.location.href = `mailto:${email}`
|
|
window.location.href = `mailto:${email}`
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+const copyInfo = async (value, label = 'info') => {
|
|
|
|
|
+ const content = value?.toString().trim()
|
|
|
|
|
+ if (!content) {
|
|
|
|
|
+ toast.add({ severity: 'info', summary: 'Notice', detail: `Nothing to copy for ${label}.`, life: 2200 })
|
|
|
|
|
+ return
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ if (navigator?.clipboard?.writeText) {
|
|
|
|
|
+ await navigator.clipboard.writeText(content)
|
|
|
|
|
+ } else {
|
|
|
|
|
+ const textarea = document.createElement('textarea')
|
|
|
|
|
+ textarea.value = content
|
|
|
|
|
+ textarea.style.position = 'fixed'
|
|
|
|
|
+ textarea.style.opacity = '0'
|
|
|
|
|
+ document.body.appendChild(textarea)
|
|
|
|
|
+ textarea.focus()
|
|
|
|
|
+ textarea.select()
|
|
|
|
|
+ document.execCommand('copy')
|
|
|
|
|
+ document.body.removeChild(textarea)
|
|
|
|
|
+ }
|
|
|
|
|
+ toast.add({ severity: 'success', summary: 'Copied', detail: `${label} copied to clipboard.`, life: 2000 })
|
|
|
|
|
+ } catch (error) {
|
|
|
|
|
+ console.error('Copy failed', error)
|
|
|
|
|
+ toast.add({ severity: 'error', summary: 'Copy failed', detail: 'Please copy manually.', life: 2200 })
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
const resetPageState = () => {
|
|
const resetPageState = () => {
|
|
|
qrDetail.value = null
|
|
qrDetail.value = null
|
|
|
profile.value = null
|
|
profile.value = null
|
|
@@ -317,13 +348,43 @@ watch([qrType, () => profile.value?.name], () => {
|
|
|
setDocumentTitle()
|
|
setDocumentTitle()
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
-// 首次填写时自动显示验证弹窗
|
|
|
|
|
|
|
+// Auto open verification dialog on first fill
|
|
|
watch(isFirstFill, (value) => {
|
|
watch(isFirstFill, (value) => {
|
|
|
if (value && !maintenancePassed.value) {
|
|
if (value && !maintenancePassed.value) {
|
|
|
showMaintenanceDialog.value = true
|
|
showMaintenanceDialog.value = true
|
|
|
}
|
|
}
|
|
|
})
|
|
})
|
|
|
|
|
|
|
|
|
|
+const handleOpenLocationDialog = () => {
|
|
|
|
|
+ showLocationDialog.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleSaveLocation = (location) => {
|
|
|
|
|
+ formData.location = location
|
|
|
|
|
+ toast.add({ severity: 'success', summary: 'Saved', detail: 'Address selected.', life: 2400 })
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const handleOpenLocationView = () => {
|
|
|
|
|
+ showLocationViewDialog.value = true
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
|
|
+const openGoogleMaps = () => {
|
|
|
|
|
+ const location = isPerson.value ? null : profile.value?.location
|
|
|
|
|
+ if (!location) return
|
|
|
|
|
+
|
|
|
|
|
+ // Try to parse coordinate format (lat, lng)
|
|
|
|
|
+ const coordsMatch = location.match(/(-?\d+\.?\d*),\s*(-?\d+\.?\d*)/)
|
|
|
|
|
+ if (coordsMatch) {
|
|
|
|
|
+ const lat = coordsMatch[1]
|
|
|
|
|
+ const lng = coordsMatch[2]
|
|
|
|
|
+ // Open Google Maps with coordinates
|
|
|
|
|
+ window.open(`https://www.google.com/maps?q=${lat},${lng}`, '_blank')
|
|
|
|
|
+ } else {
|
|
|
|
|
+ // Otherwise search by address string
|
|
|
|
|
+ window.open(`https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(location)}`, '_blank')
|
|
|
|
|
+ }
|
|
|
|
|
+}
|
|
|
|
|
+
|
|
|
onMounted(() => {
|
|
onMounted(() => {
|
|
|
if (!qrCode.value) {
|
|
if (!qrCode.value) {
|
|
|
setDocumentTitle()
|
|
setDocumentTitle()
|
|
@@ -353,15 +414,15 @@ onMounted(() => {
|
|
|
<i class="pi pi-qrcode text-2xl text-cyan-300" />
|
|
<i class="pi pi-qrcode text-2xl text-cyan-300" />
|
|
|
</div>
|
|
</div>
|
|
|
<div class="flex-1">
|
|
<div class="flex-1">
|
|
|
- <p class="text-lg font-semibold text-white">欢迎使用应急二维码系统</p>
|
|
|
|
|
|
|
+ <p class="text-lg font-semibold text-white">Welcome to the Emergency QR system</p>
|
|
|
<p class="mt-2 text-sm text-slate-300">
|
|
<p class="mt-2 text-sm text-slate-300">
|
|
|
- 请扫描二维码或在下方输入二维码编号进行查询
|
|
|
|
|
|
|
+ Scan the QR code or enter its value below to continue.
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-start">
|
|
<div class="mt-6 flex flex-col gap-4 sm:flex-row sm:items-start">
|
|
|
<div class="flex-1 space-y-2">
|
|
<div class="flex-1 space-y-2">
|
|
|
- <input v-model="queryInput" type="text" placeholder="输入二维码编号"
|
|
|
|
|
|
|
+ <input v-model="queryInput" type="text" placeholder="Enter QR code"
|
|
|
class="w-full rounded-2xl border border-white/20 bg-white/5 px-5 py-3.5 text-base text-white placeholder:text-slate-400 backdrop-blur-sm transition-all duration-200 focus:border-cyan-400 focus:bg-white/10 focus:outline-none focus:ring-2 focus:ring-cyan-400/30"
|
|
class="w-full rounded-2xl border border-white/20 bg-white/5 px-5 py-3.5 text-base text-white placeholder:text-slate-400 backdrop-blur-sm transition-all duration-200 focus:border-cyan-400 focus:bg-white/10 focus:outline-none focus:ring-2 focus:ring-cyan-400/30"
|
|
|
@keyup.enter="handleQrSubmit" />
|
|
@keyup.enter="handleQrSubmit" />
|
|
|
<div class="flex items-center gap-2 px-1">
|
|
<div class="flex items-center gap-2 px-1">
|
|
@@ -370,14 +431,14 @@ onMounted(() => {
|
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2"
|
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
d="M13 16h-1v-4h-1m1-4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z" />
|
|
|
</svg>
|
|
</svg>
|
|
|
- <span class="text-sm text-slate-400/90">例如:<span
|
|
|
|
|
|
|
+ <span class="text-sm text-slate-400/90">Example: <span
|
|
|
class="font-mono text-slate-300/80">QRMIH76J350CE2CF6150A51F4E</span></span>
|
|
class="font-mono text-slate-300/80">QRMIH76J350CE2CF6150A51F4E</span></span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<button type="button"
|
|
<button type="button"
|
|
|
class="rounded-2xl bg-gradient-to-r from-cyan-400 to-blue-400 px-8 py-3.5 text-base font-semibold text-slate-900 shadow-lg shadow-cyan-500/30 transition-all duration-200 hover:scale-105 hover:shadow-xl hover:shadow-cyan-500/40 sm:whitespace-nowrap"
|
|
class="rounded-2xl bg-gradient-to-r from-cyan-400 to-blue-400 px-8 py-3.5 text-base font-semibold text-slate-900 shadow-lg shadow-cyan-500/30 transition-all duration-200 hover:scale-105 hover:shadow-xl hover:shadow-cyan-500/40 sm:whitespace-nowrap"
|
|
|
@click="handleQrSubmit">
|
|
@click="handleQrSubmit">
|
|
|
- 查看信息
|
|
|
|
|
|
|
+ View info
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
@@ -385,14 +446,14 @@ onMounted(() => {
|
|
|
<section v-else class="space-y-6">
|
|
<section v-else class="space-y-6">
|
|
|
<div v-if="infoStatus.state === 'error'" class="rounded-3xl border border-red-500/40 bg-red-500/10 p-5">
|
|
<div v-if="infoStatus.state === 'error'" class="rounded-3xl border border-red-500/40 bg-red-500/10 p-5">
|
|
|
<p class="text-sm text-red-100">{{ infoStatus.message }}</p>
|
|
<p class="text-sm text-red-100">{{ infoStatus.message }}</p>
|
|
|
- <p class="mt-2 text-xs text-red-200">请检查二维码编号是否正确,或联系管理员获取帮助。</p>
|
|
|
|
|
|
|
+ <p class="mt-2 text-xs text-red-200">Please double-check the QR code or reach out for assistance.</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div v-if="loading.info" class="rounded-3xl border border-white/10 bg-white/5 p-6 text-base text-white">
|
|
<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>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 只有验证通过后才显示内容 -->
|
|
|
|
|
|
|
+ <!-- Only render details after maintenance verification -->
|
|
|
<div v-if="infoStatus.state === 'ready' && qrDetail && (maintenancePassed || hasProfile)" class="space-y-6">
|
|
<div v-if="infoStatus.state === 'ready' && qrDetail && (maintenancePassed || hasProfile)" class="space-y-6">
|
|
|
<div v-if="hasProfile && !isEditing"
|
|
<div v-if="hasProfile && !isEditing"
|
|
|
class="rounded-3xl border border-white/10 bg-white text-slate-900 shadow-2xl shadow-cyan-500/10 relative">
|
|
class="rounded-3xl border border-white/10 bg-white text-slate-900 shadow-2xl shadow-cyan-500/10 relative">
|
|
@@ -406,12 +467,12 @@ onMounted(() => {
|
|
|
@error="(e) => e.target.style.display = 'none'" />
|
|
@error="(e) => e.target.style.display = 'none'" />
|
|
|
<div v-else class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
|
|
<div v-else class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
|
|
|
<i class="pi pi-image text-4xl" />
|
|
<i class="pi pi-image text-4xl" />
|
|
|
- <span class="text-sm">无照片</span>
|
|
|
|
|
|
|
+ <span class="text-sm">No photo</span>
|
|
|
</div>
|
|
</div>
|
|
|
- <!-- 编辑按钮 -->
|
|
|
|
|
|
|
+ <!-- Edit button -->
|
|
|
<button v-if="!isEditing" type="button"
|
|
<button v-if="!isEditing" type="button"
|
|
|
class="absolute top-3 right-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 backdrop-blur-sm text-slate-600 hover:bg-white hover:text-slate-900 transition-all shadow-lg hover:shadow-xl"
|
|
class="absolute top-3 right-3 flex h-9 w-9 items-center justify-center rounded-full bg-white/90 backdrop-blur-sm text-slate-600 hover:bg-white hover:text-slate-900 transition-all shadow-lg hover:shadow-xl"
|
|
|
- @click="showMaintenanceDialog = true" title="编辑资料">
|
|
|
|
|
|
|
+ @click="showMaintenanceDialog = true" title="Edit profile">
|
|
|
<i class="pi pi-pencil text-xs" />
|
|
<i class="pi pi-pencil text-xs" />
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
@@ -422,49 +483,81 @@ onMounted(() => {
|
|
|
<div class="space-y-4">
|
|
<div class="space-y-4">
|
|
|
<div>
|
|
<div>
|
|
|
<p class="mt-1 text-sm text-slate-500">
|
|
<p class="mt-1 text-sm text-slate-500">
|
|
|
- {{ isPerson ? '紧急联系人卡' : isGoods ? '物品信息' : '宠物信息' }}
|
|
|
|
|
|
|
+ {{ isPerson ? 'Emergency contact card' : isGoods ? 'Item information' : 'Pet information' }}
|
|
|
</p>
|
|
</p>
|
|
|
<p class="text-2xl font-semibold text-slate-900">
|
|
<p class="text-2xl font-semibold text-slate-900">
|
|
|
- {{ profile?.name || '未命名' }}
|
|
|
|
|
|
|
+ {{ profile?.name || 'Unnamed' }}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
<div
|
|
<div
|
|
|
class="rounded-2xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-5 transition hover:shadow-md">
|
|
class="rounded-2xl border border-slate-200 bg-gradient-to-br from-slate-50 to-white p-5 transition hover:shadow-md">
|
|
|
- <div class="flex items-center gap-2">
|
|
|
|
|
- <div class="flex h-8 w-8 items-center justify-center rounded-full bg-emerald-100">
|
|
|
|
|
- <i class="pi pi-user text-sm text-emerald-600" />
|
|
|
|
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
|
|
+ <div
|
|
|
|
|
+ class="flex h-12 w-12 items-center justify-center rounded-full bg-emerald-100 shadow-inner">
|
|
|
|
|
+ <i class="pi pi-user text-lg text-emerald-600" />
|
|
|
</div>
|
|
</div>
|
|
|
- <p class="text-xs font-medium uppercase tracking-wider text-slate-500">{{ isPerson ? '紧急联系人' :
|
|
|
|
|
- '联系人' }}</p>
|
|
|
|
|
|
|
+ <p class="text-[11px] font-semibold uppercase tracking-[0.35em] text-slate-500">{{ isPerson ?
|
|
|
|
|
+ 'EMERGENCY CONTACT' : 'CONTACT' }}</p>
|
|
|
</div>
|
|
</div>
|
|
|
- <p class="mt-3 text-xl font-semibold text-slate-900">
|
|
|
|
|
|
|
+ <p class="mt-3 cursor-pointer text-3xl font-semibold leading-tight text-slate-800" title="Click to copy name"
|
|
|
|
|
+ @click="copyInfo(isPerson ? profile?.emergencyContactName : profile?.contactName, 'Name')">
|
|
|
{{ isPerson ? profile?.emergencyContactName || '-' : profile?.contactName || '-' }}
|
|
{{ isPerson ? profile?.emergencyContactName || '-' : profile?.contactName || '-' }}
|
|
|
</p>
|
|
</p>
|
|
|
- <div class="mt-3 space-y-1.5 text-sm text-slate-600">
|
|
|
|
|
- <div class="flex items-center gap-2">
|
|
|
|
|
- <i class="pi pi-phone text-xs text-slate-400" />
|
|
|
|
|
- <span>{{ isPerson ? profile?.emergencyContactPhone || '-' : profile?.contactPhone || '-'
|
|
|
|
|
- }}</span>
|
|
|
|
|
|
|
+ <div class="mt-4 space-y-3 text-sm text-slate-600">
|
|
|
|
|
+ <div class="flex items-center gap-3">
|
|
|
|
|
+ <i class="pi pi-phone text-lg text-slate-500" />
|
|
|
|
|
+ <div class="leading-tight cursor-pointer select-text text-slate-600" title="Click to copy phone"
|
|
|
|
|
+ @click="copyInfo(isPerson ? profile?.emergencyContactPhone : profile?.contactPhone, 'Phone')">
|
|
|
|
|
+ <p class="text-[10px] uppercase tracking-[0.4em] text-slate-400">PHONE</p>
|
|
|
|
|
+ <p class="text-base font-semibold text-slate-700">
|
|
|
|
|
+ {{ isPerson ? profile?.emergencyContactPhone || '-' : profile?.contactPhone || '-' }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)"
|
|
<div v-if="(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)"
|
|
|
- class="flex items-center gap-2">
|
|
|
|
|
- <i class="pi pi-envelope text-xs text-slate-400" />
|
|
|
|
|
- <span class="truncate">{{ isPerson ? profile?.emergencyContactEmail : profile?.contactEmail
|
|
|
|
|
- }}</span>
|
|
|
|
|
|
|
+ class="flex items-center gap-3">
|
|
|
|
|
+ <i class="pi pi-envelope text-lg text-slate-500" />
|
|
|
|
|
+ <div class="leading-tight min-w-0 cursor-pointer select-text text-slate-600" title="Click to copy email"
|
|
|
|
|
+ @click="copyInfo(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail, 'Email')">
|
|
|
|
|
+ <p class="text-[10px] uppercase tracking-[0.4em] text-slate-400">EMAIL</p>
|
|
|
|
|
+ <p class="truncate text-base font-semibold text-slate-700">
|
|
|
|
|
+ {{ isPerson ? profile?.emergencyContactEmail : profile?.contactEmail }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ </div>
|
|
|
|
|
+ <div v-if="!isPerson && profile?.location" class="flex items-start gap-3">
|
|
|
|
|
+ <i class="pi pi-map-marker text-lg text-slate-500 mt-0.5 flex-shrink-0" />
|
|
|
|
|
+ <div class="leading-tight cursor-pointer select-text text-slate-600" title="Click to copy address"
|
|
|
|
|
+ @click="copyInfo(profile?.location, 'Address')">
|
|
|
|
|
+ <p class="text-[10px] uppercase tracking-[0.4em] text-slate-400">LOCATION</p>
|
|
|
|
|
+ <p class="flex-1 break-words whitespace-normal text-base font-medium text-slate-700">
|
|
|
|
|
+ {{ profile.location }}
|
|
|
|
|
+ </p>
|
|
|
|
|
+ </div>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
- <div class="mt-4 flex flex-wrap gap-2">
|
|
|
|
|
|
|
+ <div class="mt-4 grid grid-cols-2 gap-2">
|
|
|
<button v-if="profile?.emergencyContactPhone || profile?.contactPhone" type="button"
|
|
<button v-if="profile?.emergencyContactPhone || profile?.contactPhone" type="button"
|
|
|
- class="inline-flex items-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 transition hover:bg-emerald-100"
|
|
|
|
|
|
|
+ class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-emerald-200 bg-emerald-50 px-3 py-1.5 text-xs font-medium text-emerald-700 transition hover:bg-emerald-100"
|
|
|
@click="callNumber(isPerson ? profile?.emergencyContactPhone : profile?.contactPhone)">
|
|
@click="callNumber(isPerson ? profile?.emergencyContactPhone : profile?.contactPhone)">
|
|
|
- <i class="pi pi-phone" /> 拨打
|
|
|
|
|
|
|
+ <i class="pi pi-phone" /> Call
|
|
|
</button>
|
|
</button>
|
|
|
<button
|
|
<button
|
|
|
v-if="(isPerson && profile?.emergencyContactEmail) || (!isPerson && profile?.contactEmail)"
|
|
v-if="(isPerson && profile?.emergencyContactEmail) || (!isPerson && profile?.contactEmail)"
|
|
|
type="button"
|
|
type="button"
|
|
|
- class="inline-flex items-center gap-1.5 rounded-full border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 transition hover:bg-blue-100"
|
|
|
|
|
|
|
+ class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-blue-200 bg-blue-50 px-3 py-1.5 text-xs font-medium text-blue-700 transition hover:bg-blue-100"
|
|
|
@click="sendEmail(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)">
|
|
@click="sendEmail(isPerson ? profile?.emergencyContactEmail : profile?.contactEmail)">
|
|
|
- <i class="pi pi-envelope" /> 邮件
|
|
|
|
|
|
|
+ <i class="pi pi-envelope" /> Email
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button v-if="!isPerson && profile?.location" type="button"
|
|
|
|
|
+ class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-purple-200 bg-purple-50 px-3 py-1.5 text-xs font-medium text-purple-700 transition hover:bg-purple-100"
|
|
|
|
|
+ @click="handleOpenLocationView">
|
|
|
|
|
+ <i class="pi pi-map" /> Map
|
|
|
|
|
+ </button>
|
|
|
|
|
+ <button v-if="!isPerson && profile?.location" type="button"
|
|
|
|
|
+ class="inline-flex w-full items-center justify-center gap-1.5 rounded-full border border-red-200 bg-red-50 px-3 py-1.5 text-xs font-medium text-red-700 transition hover:bg-red-100"
|
|
|
|
|
+ @click="openGoogleMaps">
|
|
|
|
|
+ <i class="pi pi-external-link" /> Google Maps
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -473,11 +566,11 @@ onMounted(() => {
|
|
|
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-amber-100">
|
|
<div class="flex h-8 w-8 items-center justify-center rounded-full bg-amber-100">
|
|
|
<i class="pi pi-info-circle text-sm text-amber-600" />
|
|
<i class="pi pi-info-circle text-sm text-amber-600" />
|
|
|
</div>
|
|
</div>
|
|
|
- <p class="text-xs font-medium uppercase tracking-wider text-slate-500">{{ isPerson ? '特别说明' :
|
|
|
|
|
- isGoods ? '备注说明' : '补充描述' }}</p>
|
|
|
|
|
|
|
+ <p class="text-xs font-medium uppercase tracking-wider text-slate-500">{{ isPerson ? 'Additional notes' :
|
|
|
|
|
+ isGoods ? 'Item notes' : 'Extra description' }}</p>
|
|
|
</div>
|
|
</div>
|
|
|
<p class="mt-3 whitespace-pre-wrap text-sm leading-relaxed text-slate-700">
|
|
<p class="mt-3 whitespace-pre-wrap text-sm leading-relaxed text-slate-700">
|
|
|
- {{ isPerson ? (profile?.specialNote || '暂无说明') : (profile?.remark || '暂无说明') }}
|
|
|
|
|
|
|
+ {{ isPerson ? (profile?.specialNote || 'No extra details') : (profile?.remark || 'No extra details') }}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -487,18 +580,17 @@ onMounted(() => {
|
|
|
</div>
|
|
</div>
|
|
|
</section>
|
|
</section>
|
|
|
|
|
|
|
|
- <!-- 只有验证通过后才显示表单 -->
|
|
|
|
|
<section v-if="qrCode && maintenancePassed" id="scan-form-section"
|
|
<section v-if="qrCode && maintenancePassed" id="scan-form-section"
|
|
|
class="rounded-3xl border border-white/10 bg-white p-6 text-slate-900 shadow-2xl shadow-cyan-500/10">
|
|
class="rounded-3xl border border-white/10 bg-white p-6 text-slate-900 shadow-2xl shadow-cyan-500/10">
|
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex flex-col gap-2 sm:flex-row sm:items-center sm:justify-between">
|
|
|
<div>
|
|
<div>
|
|
|
<p class="text-xs uppercase tracking-[0.3em] text-slate-400">Profile setup</p>
|
|
<p class="text-xs uppercase tracking-[0.3em] text-slate-400">Profile setup</p>
|
|
|
<h2 class="mt-1 text-2xl font-semibold text-slate-900">
|
|
<h2 class="mt-1 text-2xl font-semibold text-slate-900">
|
|
|
- {{ hasProfile ? '更新资料' : '首次填写资料' }}
|
|
|
|
|
|
|
+ {{ hasProfile ? 'Update profile' : 'First-time setup' }}
|
|
|
</h2>
|
|
</h2>
|
|
|
</div>
|
|
</div>
|
|
|
<p class="text-sm text-slate-500">
|
|
<p class="text-sm text-slate-500">
|
|
|
- 二维码:<span class="font-mono text-slate-700">{{ qrCode }}</span>
|
|
|
|
|
|
|
+ QR Code: <span class="font-mono text-slate-700">{{ qrCode }}</span>
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
@@ -506,9 +598,10 @@ onMounted(() => {
|
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex items-start gap-3">
|
|
|
<i class="pi pi-info-circle text-emerald-600" />
|
|
<i class="pi pi-info-circle text-emerald-600" />
|
|
|
<div class="flex-1 text-sm text-emerald-700">
|
|
<div class="flex-1 text-sm text-emerald-700">
|
|
|
- <p class="font-semibold">首次填写提示</p>
|
|
|
|
|
|
|
+ <p class="font-semibold">First-time tip</p>
|
|
|
<p class="mt-1">
|
|
<p class="mt-1">
|
|
|
- 这是首次为此二维码录入信息,提交后系统会自动激活。请妥善保管维护码,后续修改资料需要验证。
|
|
|
|
|
|
|
+ This is the first record for this QR code. Once submitted, it becomes active. Keep the maintenance code
|
|
|
|
|
+ safe for future edits.
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -517,9 +610,9 @@ onMounted(() => {
|
|
|
<div class="flex items-start gap-3">
|
|
<div class="flex items-start gap-3">
|
|
|
<i class="pi pi-check-circle text-cyan-600" />
|
|
<i class="pi pi-check-circle text-cyan-600" />
|
|
|
<div class="flex-1 text-sm text-cyan-700">
|
|
<div class="flex-1 text-sm text-cyan-700">
|
|
|
- <p class="font-semibold">编辑模式</p>
|
|
|
|
|
|
|
+ <p class="font-semibold">Edit mode</p>
|
|
|
<p class="mt-1">
|
|
<p class="mt-1">
|
|
|
- 您已通过维护码验证,现在可以修改资料了。修改后请记得点击保存按钮。
|
|
|
|
|
|
|
+ Maintenance code verified. You can now edit the profile—remember to save your changes.
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -528,8 +621,8 @@ onMounted(() => {
|
|
|
<form class="mt-6 space-y-6" @submit.prevent="handleSaveProfile">
|
|
<form class="mt-6 space-y-6" @submit.prevent="handleSaveProfile">
|
|
|
<div class="space-y-4">
|
|
<div class="space-y-4">
|
|
|
<div>
|
|
<div>
|
|
|
- <p class="text-sm font-medium text-slate-700">展示头像 / 实物图片</p>
|
|
|
|
|
- <p class="mt-1 text-xs text-slate-500">建议尺寸 640×640 以上,支持 JPG、PNG,最大 15MB</p>
|
|
|
|
|
|
|
+ <p class="text-sm font-medium text-slate-700">Display photo / item image</p>
|
|
|
|
|
+ <p class="mt-1 text-xs text-slate-500">Recommended 640×640+, supports JPG/PNG, up to 15MB.</p>
|
|
|
</div>
|
|
</div>
|
|
|
<div
|
|
<div
|
|
|
class="relative w-full max-w-md mx-auto overflow-hidden rounded-2xl bg-slate-100 ring-2 ring-slate-200"
|
|
class="relative w-full max-w-md mx-auto overflow-hidden rounded-2xl bg-slate-100 ring-2 ring-slate-200"
|
|
@@ -542,7 +635,7 @@ onMounted(() => {
|
|
|
<div v-else-if="!formData.photoUrl"
|
|
<div v-else-if="!formData.photoUrl"
|
|
|
class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
|
|
class="flex h-full w-full flex-col items-center justify-center gap-2 text-slate-400">
|
|
|
<i class="pi pi-image text-5xl" />
|
|
<i class="pi pi-image text-5xl" />
|
|
|
- <span class="text-sm">无图片</span>
|
|
|
|
|
|
|
+ <span class="text-sm">No image</span>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
<input ref="photoInputRef" type="file" accept="image/jpeg,image/jpg,image/png,image/webp" class="hidden"
|
|
<input ref="photoInputRef" type="file" accept="image/jpeg,image/jpg,image/png,image/webp" class="hidden"
|
|
@@ -552,31 +645,31 @@ onMounted(() => {
|
|
|
class="rounded-full border border-slate-300 bg-white px-6 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50 disabled:opacity-50"
|
|
class="rounded-full border border-slate-300 bg-white px-6 py-2.5 text-sm font-medium text-slate-700 transition hover:bg-slate-50 disabled:opacity-50"
|
|
|
:disabled="loading.photo" @click="triggerPhotoPicker">
|
|
:disabled="loading.photo" @click="triggerPhotoPicker">
|
|
|
<i class="pi mr-2" :class="loading.photo ? 'pi-spin pi-spinner' : 'pi-upload'" />
|
|
<i class="pi mr-2" :class="loading.photo ? 'pi-spin pi-spinner' : 'pi-upload'" />
|
|
|
- {{ loading.photo ? '上传中...' : '上传图片' }}
|
|
|
|
|
|
|
+ {{ loading.photo ? 'Uploading...' : 'Upload image' }}
|
|
|
</button>
|
|
</button>
|
|
|
<button v-if="formData.photoUrl" type="button"
|
|
<button v-if="formData.photoUrl" type="button"
|
|
|
class="rounded-full border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 transition hover:bg-red-100"
|
|
class="rounded-full border border-red-200 bg-red-50 px-4 py-2.5 text-sm font-medium text-red-600 transition hover:bg-red-100"
|
|
|
@click="formData.photoUrl = ''">
|
|
@click="formData.photoUrl = ''">
|
|
|
<i class="pi pi-times mr-1" />
|
|
<i class="pi pi-times mr-1" />
|
|
|
- 清除图片
|
|
|
|
|
|
|
+ Clear image
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div class="grid gap-4 md:grid-cols-2">
|
|
<div class="grid gap-4 md:grid-cols-2">
|
|
|
<label class="space-y-2 text-sm">
|
|
<label class="space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">{{ isPerson ? '姓名' : isGoods ? '物品名称' : '宠物名称' }}</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">{{ isPerson ? 'Name' : isGoods ? 'Item name' : 'Pet name' }}</span>
|
|
|
<input v-model="formData.name" type="text" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
|
|
<input v-model="formData.name" type="text" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
|
|
|
- :placeholder="isPerson ? '例如:张三' : isGoods ? '例如:苹果笔记本电脑' : '例如:小白'" required />
|
|
|
|
|
|
|
+ :placeholder="isPerson ? 'e.g. John Doe' : isGoods ? 'e.g. MacBook Pro' : 'e.g. Snowy'" required />
|
|
|
</label>
|
|
</label>
|
|
|
|
|
|
|
|
<label v-if="isPerson" class="space-y-2 text-sm">
|
|
<label v-if="isPerson" class="space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">性别</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Gender</span>
|
|
|
<div class="flex gap-2">
|
|
<div class="flex gap-2">
|
|
|
<button v-for="option in [
|
|
<button v-for="option in [
|
|
|
- { label: '男', value: 'male' },
|
|
|
|
|
- { label: '女', value: 'female' },
|
|
|
|
|
- { label: '保密', value: 'unknown' }
|
|
|
|
|
|
|
+ { label: 'Male', value: 'male' },
|
|
|
|
|
+ { label: 'Female', value: 'female' },
|
|
|
|
|
+ { label: 'Prefer not to say', value: 'unknown' }
|
|
|
]" :key="option.value" type="button" class="flex-1 rounded-2xl border px-4 py-3 text-sm"
|
|
]" :key="option.value" type="button" class="flex-1 rounded-2xl border px-4 py-3 text-sm"
|
|
|
:class="formData.gender === option.value ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 text-slate-500'"
|
|
:class="formData.gender === option.value ? 'border-slate-900 bg-slate-900 text-white' : 'border-slate-200 text-slate-500'"
|
|
|
@click="formData.gender = option.value">
|
|
@click="formData.gender = option.value">
|
|
@@ -588,67 +681,82 @@ onMounted(() => {
|
|
|
|
|
|
|
|
<div v-if="isPerson" class="grid gap-4 md:grid-cols-2">
|
|
<div v-if="isPerson" class="grid gap-4 md:grid-cols-2">
|
|
|
<label class="space-y-2 text-sm">
|
|
<label class="space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">本人电话</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Owner phone</span>
|
|
|
<input v-model="formData.phone" type="tel" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
|
|
<input v-model="formData.phone" type="tel" class="w-full rounded-2xl border border-slate-200 px-4 py-3"
|
|
|
required />
|
|
required />
|
|
|
</label>
|
|
</label>
|
|
|
|
|
|
|
|
<label class="space-y-2 text-sm">
|
|
<label class="space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">紧急联系人姓名</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Emergency contact name</span>
|
|
|
<input v-model="formData.emergencyContactName" type="text"
|
|
<input v-model="formData.emergencyContactName" type="text"
|
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
|
</label>
|
|
</label>
|
|
|
|
|
|
|
|
<label class="space-y-2 text-sm">
|
|
<label class="space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">紧急联系人电话</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Emergency contact phone</span>
|
|
|
<input v-model="formData.emergencyContactPhone" type="tel"
|
|
<input v-model="formData.emergencyContactPhone" type="tel"
|
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
|
</label>
|
|
</label>
|
|
|
|
|
|
|
|
<label class="space-y-2 text-sm md:col-span-2">
|
|
<label class="space-y-2 text-sm md:col-span-2">
|
|
|
- <span class="text-slate-500">紧急联系人邮箱</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Emergency contact email</span>
|
|
|
<input v-model="formData.emergencyContactEmail" type="email"
|
|
<input v-model="formData.emergencyContactEmail" type="email"
|
|
|
- class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="可选" />
|
|
|
|
|
|
|
+ class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="Optional" />
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<div v-else class="grid gap-4 md:grid-cols-2">
|
|
<div v-else class="grid gap-4 md:grid-cols-2">
|
|
|
<label class="space-y-2 text-sm">
|
|
<label class="space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">联系人姓名</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Contact name</span>
|
|
|
<input v-model="formData.contactName" type="text"
|
|
<input v-model="formData.contactName" type="text"
|
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
|
</label>
|
|
</label>
|
|
|
|
|
|
|
|
<label class="space-y-2 text-sm">
|
|
<label class="space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">联系人电话</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Contact phone</span>
|
|
|
<input v-model="formData.contactPhone" type="tel"
|
|
<input v-model="formData.contactPhone" type="tel"
|
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3" required />
|
|
|
</label>
|
|
</label>
|
|
|
|
|
|
|
|
<label class="space-y-2 text-sm md:col-span-2">
|
|
<label class="space-y-2 text-sm md:col-span-2">
|
|
|
- <span class="text-slate-500">联系人邮箱</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">Contact email</span>
|
|
|
<input v-model="formData.contactEmail" type="email"
|
|
<input v-model="formData.contactEmail" type="email"
|
|
|
- class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="可选" />
|
|
|
|
|
|
|
+ class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="Optional" />
|
|
|
|
|
+ </label>
|
|
|
|
|
+
|
|
|
|
|
+ <label class="space-y-2 text-sm md:col-span-2">
|
|
|
|
|
+ <span class="text-slate-500">Contact address</span>
|
|
|
|
|
+ <div class="flex gap-2">
|
|
|
|
|
+ <input v-model="formData.location" type="text" readonly
|
|
|
|
|
+ class="flex-1 rounded-2xl border border-slate-200 px-4 py-3 bg-slate-50 cursor-pointer"
|
|
|
|
|
+ placeholder="Click to select address" @click="handleOpenLocationDialog" />
|
|
|
|
|
+ <button type="button"
|
|
|
|
|
+ class="rounded-2xl border border-slate-300 bg-white px-6 py-3 text-sm font-medium text-slate-700 transition hover:bg-slate-50 whitespace-nowrap"
|
|
|
|
|
+ @click="handleOpenLocationDialog">
|
|
|
|
|
+ <i class="pi pi-map-marker mr-2" />
|
|
|
|
|
+ Select address
|
|
|
|
|
+ </button>
|
|
|
|
|
+ </div>
|
|
|
</label>
|
|
</label>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
<label class="block space-y-2 text-sm">
|
|
<label class="block space-y-2 text-sm">
|
|
|
- <span class="text-slate-500">{{ isPerson ? '特别说明 / 健康提示' : isGoods ? '备注说明' : '补充描述 / 备注' }}</span>
|
|
|
|
|
|
|
+ <span class="text-slate-500">{{ isPerson ? 'Additional notes / health tips' : isGoods ? 'Item notes' : 'Extra remarks' }}</span>
|
|
|
<textarea v-if="isPerson" v-model="formData.specialNote" rows="4"
|
|
<textarea v-if="isPerson" v-model="formData.specialNote" rows="4"
|
|
|
- class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="例如过敏药物、携带物品等" />
|
|
|
|
|
|
|
+ class="w-full rounded-2xl border border-slate-200 px-4 py-3" placeholder="e.g. allergies, medical needs, carried items" />
|
|
|
<textarea v-else v-model="formData.remark" rows="4"
|
|
<textarea v-else v-model="formData.remark" rows="4"
|
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3"
|
|
class="w-full rounded-2xl border border-slate-200 px-4 py-3"
|
|
|
- :placeholder="isGoods ? '例如物品特征、使用说明、联系注意事项等' : '例如宠物习性、健康状况等'" />
|
|
|
|
|
|
|
+ :placeholder="isGoods ? 'e.g. item features, usage notes, handling tips' : 'e.g. pet habits, health info, behavior notes'" />
|
|
|
</label>
|
|
</label>
|
|
|
|
|
|
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
|
|
|
<p class="text-sm text-slate-500">
|
|
<p class="text-sm text-slate-500">
|
|
|
- 提交后将同步到扫码页面。
|
|
|
|
|
|
|
+ Submitted data will sync to the public scan page.
|
|
|
</p>
|
|
</p>
|
|
|
<button type="submit"
|
|
<button type="submit"
|
|
|
class="rounded-full bg-slate-900 px-8 py-3 text-sm font-semibold text-white hover:bg-slate-800"
|
|
class="rounded-full bg-slate-900 px-8 py-3 text-sm font-semibold text-white hover:bg-slate-800"
|
|
|
:disabled="loading.saving">
|
|
:disabled="loading.saving">
|
|
|
- {{ loading.saving ? '保存中...' : hasProfile ? '保存更新' : '提交资料' }}
|
|
|
|
|
|
|
+ {{ loading.saving ? 'Saving...' : hasProfile ? 'Save changes' : 'Submit profile' }}
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
</form>
|
|
</form>
|
|
@@ -656,16 +764,23 @@ onMounted(() => {
|
|
|
|
|
|
|
|
<footer class="mt-8 border-t border-white/10 pt-6 text-center">
|
|
<footer class="mt-8 border-t border-white/10 pt-6 text-center">
|
|
|
<p class="text-xs text-slate-400">
|
|
<p class="text-xs text-slate-400">
|
|
|
- 数据仅用于紧急联系场景,上传即视为同意存储并用于扫码展示
|
|
|
|
|
|
|
+ Data is used solely for emergency contact scenarios. Uploading means you consent to display it when scanned.
|
|
|
</p>
|
|
</p>
|
|
|
<p class="mt-2 text-xs text-slate-500">
|
|
<p class="mt-2 text-xs text-slate-500">
|
|
|
- <i class="pi pi-shield text-cyan-400" /> 您的隐私受到保护
|
|
|
|
|
|
|
+ <i class="pi pi-shield text-cyan-400" /> Your privacy stays protected
|
|
|
</p>
|
|
</p>
|
|
|
</footer>
|
|
</footer>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
|
|
|
|
|
|
- <!-- 维护码验证弹窗 -->
|
|
|
|
|
|
|
+ <!-- Location picker dialog -->
|
|
|
|
|
+ <LocationPicker v-model="showLocationDialog" :initial-location="formData.location" @save="handleSaveLocation" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Location preview dialog (read-only) -->
|
|
|
|
|
+ <LocationPicker v-model="showLocationViewDialog" :initial-location="isPerson ? null : profile?.location"
|
|
|
|
|
+ :closable="true" :readonly="true" />
|
|
|
|
|
+
|
|
|
|
|
+ <!-- Maintenance code dialog -->
|
|
|
<Dialog v-model:visible="showMaintenanceDialog" modal :closable="!isFirstFill" :closeOnEscape="!isFirstFill"
|
|
<Dialog v-model:visible="showMaintenanceDialog" modal :closable="!isFirstFill" :closeOnEscape="!isFirstFill"
|
|
|
:dismissableMask="!isFirstFill" :style="{ width: '90vw', maxWidth: '450px' }" :draggable="false">
|
|
:dismissableMask="!isFirstFill" :style="{ width: '90vw', maxWidth: '450px' }" :draggable="false">
|
|
|
<template #header>
|
|
<template #header>
|
|
@@ -675,10 +790,10 @@ onMounted(() => {
|
|
|
</div>
|
|
</div>
|
|
|
<div>
|
|
<div>
|
|
|
<h3 class="text-lg font-semibold text-slate-900">
|
|
<h3 class="text-lg font-semibold text-slate-900">
|
|
|
- {{ isFirstFill ? '首次使用需要验证' : '编辑资料需要验证' }}
|
|
|
|
|
|
|
+ {{ isFirstFill ? 'Verification required for first use' : 'Verification required to edit' }}
|
|
|
</h3>
|
|
</h3>
|
|
|
<p class="text-sm text-slate-500">
|
|
<p class="text-sm text-slate-500">
|
|
|
- {{ isFirstFill ? '请输入二维码附带的维护码' : '请输入维护码以解锁编辑权限' }}
|
|
|
|
|
|
|
+ {{ isFirstFill ? 'Enter the maintenance code that came with the QR tag.' : 'Enter the maintenance code to unlock editing.' }}
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -686,16 +801,16 @@ onMounted(() => {
|
|
|
|
|
|
|
|
<div class="space-y-4 py-4">
|
|
<div class="space-y-4 py-4">
|
|
|
<div>
|
|
<div>
|
|
|
- <label class="mb-2 block text-sm font-medium text-slate-700">维护码</label>
|
|
|
|
|
|
|
+ <label class="mb-2 block text-sm font-medium text-slate-700">Maintenance code</label>
|
|
|
<input v-model="maintenanceCode" type="text" maxlength="8"
|
|
<input v-model="maintenanceCode" type="text" maxlength="8"
|
|
|
class="w-full rounded-xl border border-slate-300 px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
|
|
class="w-full rounded-xl border border-slate-300 px-4 py-3 text-slate-900 placeholder:text-slate-400 focus:border-cyan-500 focus:outline-none focus:ring-2 focus:ring-cyan-500/20"
|
|
|
- placeholder="输入 8 位维护码" @keyup.enter="handleVerifyMaintenance" autofocus />
|
|
|
|
|
|
|
+ placeholder="Enter the maintenance code" @keyup.enter="handleVerifyMaintenance" autofocus />
|
|
|
</div>
|
|
</div>
|
|
|
<div v-if="isFirstFill" class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
|
|
<div v-if="isFirstFill" class="rounded-xl border border-emerald-200 bg-emerald-50 p-3">
|
|
|
<div class="flex items-start gap-2">
|
|
<div class="flex items-start gap-2">
|
|
|
<i class="pi pi-info-circle text-sm text-emerald-600" />
|
|
<i class="pi pi-info-circle text-sm text-emerald-600" />
|
|
|
<p class="text-xs text-emerald-700">
|
|
<p class="text-xs text-emerald-700">
|
|
|
- 这是首次为此二维码录入信息,验证后即可开始填写资料。
|
|
|
|
|
|
|
+ This is the first record for this QR code. Verify to begin entering details.
|
|
|
</p>
|
|
</p>
|
|
|
</div>
|
|
</div>
|
|
|
</div>
|
|
</div>
|
|
@@ -706,12 +821,12 @@ onMounted(() => {
|
|
|
<button v-if="!isFirstFill" type="button"
|
|
<button v-if="!isFirstFill" 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"
|
|
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="showMaintenanceDialog = false">
|
|
@click="showMaintenanceDialog = false">
|
|
|
- 取消
|
|
|
|
|
|
|
+ Cancel
|
|
|
</button>
|
|
</button>
|
|
|
<button type="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"
|
|
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="loading.verifying || !maintenanceCode" @click="handleVerifyMaintenance">
|
|
:disabled="loading.verifying || !maintenanceCode" @click="handleVerifyMaintenance">
|
|
|
- {{ loading.verifying ? '验证中...' : '验证' }}
|
|
|
|
|
|
|
+ {{ loading.verifying ? 'Verifying...' : 'Verify' }}
|
|
|
</button>
|
|
</button>
|
|
|
</div>
|
|
</div>
|
|
|
</template>
|
|
</template>
|