|
|
@@ -1,7 +1,7 @@
|
|
|
<script setup>
|
|
|
import { ref, onMounted, computed, inject } from 'vue'
|
|
|
import { useRouter } from 'vue-router'
|
|
|
-import { fetchMyQrCodesApi, fetchQrInfoApi, resetPasswordApi } from '@/services/api'
|
|
|
+import { fetchMyQrCodesApi, fetchQrInfoApi, resetPasswordApi, bindQrCodeApi } from '@/services/api'
|
|
|
import { useToast } from 'primevue/usetoast'
|
|
|
|
|
|
const router = useRouter()
|
|
|
@@ -10,9 +10,18 @@ const toast = useToast()
|
|
|
// 注入父组件提供的注册方法
|
|
|
const registerOpenPasswordDialog = inject('registerOpenPasswordDialog', null)
|
|
|
|
|
|
+// 注入时区相关的功能
|
|
|
+const selectedTimeZone = inject('selectedTimeZone', ref('America/New_York'))
|
|
|
+const formatDateTime = inject('formatDateTime', (dateString) => dateString || '-')
|
|
|
+
|
|
|
// 二维码列表
|
|
|
const qrCodes = ref([])
|
|
|
const loading = ref(false)
|
|
|
+const pagination = ref({
|
|
|
+ page: 0,
|
|
|
+ pageSize: 20,
|
|
|
+ total: 0
|
|
|
+})
|
|
|
|
|
|
// 修改密码弹窗
|
|
|
const showPasswordDialog = ref(false)
|
|
|
@@ -26,33 +35,91 @@ const qrCode = ref('')
|
|
|
const qrLookupLoading = ref(false)
|
|
|
const qrLookupData = ref(null)
|
|
|
|
|
|
+// 绑定二维码弹窗
|
|
|
+const showBindDialog = ref(false)
|
|
|
+const bindQrCode = ref('')
|
|
|
+const bindMaintenanceCode = ref('')
|
|
|
+const bindLoading = ref(false)
|
|
|
+
|
|
|
const qrTypeLabel = computed(() => {
|
|
|
if (!qrLookupData.value?.qrType) return '-'
|
|
|
- return qrLookupData.value.qrType === 'person' ? '人员' : qrLookupData.value.qrType === 'pet' ? '宠物/物品' : qrLookupData.value.qrType
|
|
|
+ return qrLookupData.value.qrType === 'person' ? 'Person' : qrLookupData.value.qrType === 'pet' ? 'Pet/Item' : qrLookupData.value.qrType
|
|
|
})
|
|
|
|
|
|
+// 根据 qrType 获取类型名称
|
|
|
+const getQrTypeName = (qrType) => {
|
|
|
+ const typeMap = {
|
|
|
+ person: 'Person',
|
|
|
+ pet: 'Pet',
|
|
|
+ goods: 'Item',
|
|
|
+ link: 'Link'
|
|
|
+ }
|
|
|
+ return typeMap[qrType] || qrType || 'Unknown'
|
|
|
+}
|
|
|
+
|
|
|
+// 根据 qrType 获取类型颜色
|
|
|
+const getQrTypeColor = (qrType) => {
|
|
|
+ const colorMap = {
|
|
|
+ person: 'bg-blue-100 text-blue-700',
|
|
|
+ pet: 'bg-green-100 text-green-700',
|
|
|
+ goods: 'bg-purple-100 text-purple-700',
|
|
|
+ link: 'bg-cyan-100 text-cyan-700'
|
|
|
+ }
|
|
|
+ return colorMap[qrType] || 'bg-slate-100 text-slate-700'
|
|
|
+}
|
|
|
+
|
|
|
+// 根据类型获取显示名称或链接
|
|
|
+const getDisplayName = (qr) => {
|
|
|
+ if (qr.qrType === 'link') {
|
|
|
+ return qr.jumpUrl || '-'
|
|
|
+ }
|
|
|
+ return qr.name || '-'
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
// 获取我的二维码列表
|
|
|
-const fetchMyQrCodes = async () => {
|
|
|
+const fetchMyQrCodes = async (page = 0) => {
|
|
|
loading.value = true
|
|
|
try {
|
|
|
- const data = await fetchMyQrCodesApi()
|
|
|
- qrCodes.value = Array.isArray(data) ? data : (data?.list || data?.qrCodes || [])
|
|
|
+ const data = await fetchMyQrCodesApi(page, pagination.value.pageSize)
|
|
|
+ // 根据文档,响应格式为 { content: [], metadata: { total, page, size } }
|
|
|
+ if (data?.content) {
|
|
|
+ qrCodes.value = data.content
|
|
|
+ pagination.value = {
|
|
|
+ page: data.metadata?.page ?? page,
|
|
|
+ pageSize: data.metadata?.size ?? pagination.value.pageSize,
|
|
|
+ total: data.metadata?.total ?? 0
|
|
|
+ }
|
|
|
+ } else {
|
|
|
+ // 兼容旧格式
|
|
|
+ qrCodes.value = Array.isArray(data) ? data : (data?.list || data?.qrCodes || [])
|
|
|
+ pagination.value.total = qrCodes.value.length
|
|
|
+ }
|
|
|
} catch (e) {
|
|
|
toast.add({
|
|
|
severity: 'error',
|
|
|
- summary: '加载失败',
|
|
|
- detail: e?.message || e?.error || '获取二维码列表失败',
|
|
|
+ summary: 'Load Failed',
|
|
|
+ detail: e?.message || e?.error || 'Failed to fetch QR code list',
|
|
|
life: 3000
|
|
|
})
|
|
|
qrCodes.value = []
|
|
|
+ pagination.value.total = 0
|
|
|
} finally {
|
|
|
loading.value = false
|
|
|
}
|
|
|
}
|
|
|
|
|
|
// 跳转到二维码详情
|
|
|
-const goToQrDetail = (qrCode) => {
|
|
|
- router.push({ name: 'scan', params: { qrCode } })
|
|
|
+const goToQrDetail = (qr) => {
|
|
|
+ const qrCode = typeof qr === 'string' ? qr : (qr?.qrCode || qr?.code || qr?.id)
|
|
|
+ const qrType = typeof qr === 'object' ? qr?.qrType : null
|
|
|
+
|
|
|
+ // 如果是 link 类型,跳转到 jump 路由,否则跳转到 scan 路由
|
|
|
+ if (qrType === 'link') {
|
|
|
+ router.push({ name: 'jumpWithCode', params: { qrCode } })
|
|
|
+ } else {
|
|
|
+ router.push({ name: 'scan', params: { qrCode } })
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
// 修改密码相关
|
|
|
@@ -70,18 +137,18 @@ const closePasswordDialog = () => {
|
|
|
|
|
|
const validatePassword = () => {
|
|
|
const p = password.value
|
|
|
- if (p.length < 8) return '密码长度必须至少8位'
|
|
|
- if (!/[a-z]/.test(p)) return '密码必须包含小写字母'
|
|
|
- if (!/[A-Z]/.test(p)) return '密码必须包含大写字母'
|
|
|
- if (!/[0-9]/.test(p)) return '密码必须包含数字'
|
|
|
- if (p !== confirmPassword.value) return '两次输入的密码不一致'
|
|
|
+ if (p.length < 8) return 'Password must be at least 8 characters'
|
|
|
+ if (!/[a-z]/.test(p)) return 'Password must contain lowercase letters'
|
|
|
+ if (!/[A-Z]/.test(p)) return 'Password must contain uppercase letters'
|
|
|
+ if (!/[0-9]/.test(p)) return 'Password must contain numbers'
|
|
|
+ if (p !== confirmPassword.value) return 'Passwords do not match'
|
|
|
return ''
|
|
|
}
|
|
|
|
|
|
const submitPassword = async () => {
|
|
|
const err = validatePassword()
|
|
|
if (err) {
|
|
|
- toast.add({ severity: 'warn', summary: '校验失败', detail: err, life: 3000 })
|
|
|
+ toast.add({ severity: 'warn', summary: 'Validation Failed', detail: err, life: 3000 })
|
|
|
return
|
|
|
}
|
|
|
|
|
|
@@ -90,16 +157,16 @@ const submitPassword = async () => {
|
|
|
const data = await resetPasswordApi(password.value)
|
|
|
toast.add({
|
|
|
severity: 'success',
|
|
|
- summary: '成功',
|
|
|
- detail: data?.message || '密码重置成功',
|
|
|
+ summary: 'Success',
|
|
|
+ detail: data?.message || 'Password reset successfully',
|
|
|
life: 2500
|
|
|
})
|
|
|
closePasswordDialog()
|
|
|
} catch (e) {
|
|
|
toast.add({
|
|
|
severity: 'error',
|
|
|
- summary: '失败',
|
|
|
- detail: e?.message || e?.error || '重置失败,请确认已登录且密码符合规则',
|
|
|
+ summary: 'Failed',
|
|
|
+ detail: e?.message || e?.error || 'Reset failed, please confirm you are logged in and the password meets the requirements',
|
|
|
life: 3500
|
|
|
})
|
|
|
} finally {
|
|
|
@@ -123,7 +190,7 @@ const closeQrLookupDialog = () => {
|
|
|
const queryQrCode = async () => {
|
|
|
const code = qrCode.value.trim()
|
|
|
if (!code) {
|
|
|
- toast.add({ severity: 'warn', summary: '提示', detail: '请输入 qrCode', life: 2000 })
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter qrCode', life: 2000 })
|
|
|
return
|
|
|
}
|
|
|
qrLookupLoading.value = true
|
|
|
@@ -133,8 +200,8 @@ const queryQrCode = async () => {
|
|
|
} catch (e) {
|
|
|
toast.add({
|
|
|
severity: 'error',
|
|
|
- summary: '查询失败',
|
|
|
- detail: e?.message || e?.error || '请确认 qrCode 是否正确',
|
|
|
+ summary: 'Query Failed',
|
|
|
+ detail: e?.message || e?.error || 'Please confirm the qrCode is correct',
|
|
|
life: 3000
|
|
|
})
|
|
|
} finally {
|
|
|
@@ -148,6 +215,53 @@ const openScanPage = async () => {
|
|
|
await router.push({ name: 'scan', params: { qrCode: code } })
|
|
|
}
|
|
|
|
|
|
+// 绑定二维码相关
|
|
|
+const openBindDialog = () => {
|
|
|
+ showBindDialog.value = true
|
|
|
+ bindQrCode.value = ''
|
|
|
+ bindMaintenanceCode.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+const closeBindDialog = () => {
|
|
|
+ showBindDialog.value = false
|
|
|
+ bindQrCode.value = ''
|
|
|
+ bindMaintenanceCode.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+const submitBind = async () => {
|
|
|
+ const code = bindQrCode.value.trim()
|
|
|
+ if (!code) {
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter QR code number', life: 2000 })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ const maintenanceCode = bindMaintenanceCode.value.trim()
|
|
|
+ if (!maintenanceCode) {
|
|
|
+ toast.add({ severity: 'warn', summary: 'Notice', detail: 'Please enter maintenance code', life: 2000 })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ bindLoading.value = true
|
|
|
+ try {
|
|
|
+ await bindQrCodeApi({ qrCode: code, maintenanceCode })
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: 'Success',
|
|
|
+ detail: 'Binding successful',
|
|
|
+ life: 2500
|
|
|
+ })
|
|
|
+ closeBindDialog()
|
|
|
+ await fetchMyQrCodes(pagination.value.page)
|
|
|
+ } catch (e) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: 'Binding Failed',
|
|
|
+ detail: e?.message || e?.error || 'Binding failed, please check if the QR code number is correct',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ bindLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
onMounted(() => {
|
|
|
fetchMyQrCodes()
|
|
|
|
|
|
@@ -161,78 +275,138 @@ onMounted(() => {
|
|
|
<template>
|
|
|
<div>
|
|
|
<!-- 顶部操作栏 -->
|
|
|
- <div class="mb-6 flex items-center justify-between">
|
|
|
+ <div class="mb-6 flex items-center justify-between flex-wrap gap-4">
|
|
|
<div>
|
|
|
- <div class="text-xl font-semibold text-slate-900">我的二维码</div>
|
|
|
- <div class="mt-1 text-sm text-slate-500">管理您创建的二维码</div>
|
|
|
+ <div class="text-xl font-semibold text-slate-900">My QR Codes</div>
|
|
|
+ <div class="mt-1 text-sm text-slate-500">Manage your created QR codes</div>
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <button
|
|
|
+ class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 transition-colors"
|
|
|
+ @click="openBindDialog"
|
|
|
+ >
|
|
|
+ Bind QR Code
|
|
|
+ </button>
|
|
|
</div>
|
|
|
- <button
|
|
|
- class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-colors"
|
|
|
- @click="openQrLookupDialog"
|
|
|
- >
|
|
|
- 查询二维码
|
|
|
- </button>
|
|
|
</div>
|
|
|
|
|
|
<!-- 二维码列表 -->
|
|
|
<div v-if="loading" class="flex items-center justify-center py-12">
|
|
|
- <div class="text-sm text-slate-500">加载中...</div>
|
|
|
+ <div class="text-sm text-slate-500">Loading...</div>
|
|
|
</div>
|
|
|
|
|
|
<div v-else-if="qrCodes.length === 0" class="rounded-2xl border border-slate-200 bg-slate-50 p-12 text-center">
|
|
|
- <div class="text-sm text-slate-500">暂无二维码</div>
|
|
|
+ <div class="text-sm text-slate-500">No QR codes</div>
|
|
|
</div>
|
|
|
|
|
|
- <div v-else class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
|
+ <div v-else class="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
|
<div
|
|
|
v-for="qr in qrCodes"
|
|
|
:key="qr.qrCode || qr.id"
|
|
|
- class="group cursor-pointer rounded-2xl border border-slate-200 bg-white p-4 transition-all hover:border-slate-300 hover:shadow-md"
|
|
|
- @click="goToQrDetail(qr.qrCode || qr.code || qr.id)"
|
|
|
+ class="group cursor-pointer rounded-2xl border border-slate-200 bg-white p-5 transition-all hover:border-slate-300 hover:shadow-lg"
|
|
|
+ @click="goToQrDetail(qr)"
|
|
|
>
|
|
|
- <div class="flex items-start justify-between">
|
|
|
- <div class="flex-1">
|
|
|
- <div class="text-sm font-semibold text-slate-900">
|
|
|
- {{ qr.name || qr.title || `二维码 ${qr.qrCode || qr.code || qr.id}` }}
|
|
|
- </div>
|
|
|
- <div class="mt-1 text-xs text-slate-500">
|
|
|
+ <!-- 头部:二维码编号和操作按钮 -->
|
|
|
+ <div class="flex items-start justify-between mb-3">
|
|
|
+ <div class="flex-1 min-w-0">
|
|
|
+ <div class="text-sm font-mono text-slate-500 mb-1">QR Code</div>
|
|
|
+ <div class="text-base font-semibold text-slate-900 truncate">
|
|
|
{{ qr.qrCode || qr.code || qr.id }}
|
|
|
</div>
|
|
|
- <div v-if="qr.qrType" class="mt-2">
|
|
|
- <span
|
|
|
- class="inline-block rounded-lg px-2 py-0.5 text-xs font-medium"
|
|
|
- :class="
|
|
|
- qr.qrType === 'person'
|
|
|
- ? 'bg-blue-100 text-blue-700'
|
|
|
- : qr.qrType === 'pet'
|
|
|
- ? 'bg-green-100 text-green-700'
|
|
|
- : 'bg-slate-100 text-slate-700'
|
|
|
- "
|
|
|
- >
|
|
|
- {{ qr.qrType === 'person' ? '人员' : qr.qrType === 'pet' ? '宠物' : qr.qrType }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
- <div v-if="qr.isActivated !== undefined" class="mt-2">
|
|
|
- <span
|
|
|
- class="inline-block rounded-lg px-2 py-0.5 text-xs font-medium"
|
|
|
- :class="qr.isActivated ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'"
|
|
|
- >
|
|
|
- {{ qr.isActivated ? '已激活' : '未激活' }}
|
|
|
- </span>
|
|
|
- </div>
|
|
|
</div>
|
|
|
- <svg
|
|
|
- xmlns="http://www.w3.org/2000/svg"
|
|
|
- fill="none"
|
|
|
- viewBox="0 0 24 24"
|
|
|
- stroke-width="1.5"
|
|
|
- stroke="currentColor"
|
|
|
- class="h-5 w-5 text-slate-400 group-hover:text-slate-600 transition-colors"
|
|
|
+ <button
|
|
|
+ class="ml-2 rounded-lg border border-slate-200 bg-white px-3 py-1.5 text-xs font-semibold text-slate-700 hover:bg-slate-50 hover:border-slate-300 transition-colors flex items-center gap-1 flex-shrink-0"
|
|
|
+ @click.stop="goToQrDetail(qr)"
|
|
|
>
|
|
|
- <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
|
|
- </svg>
|
|
|
+ <span>View</span>
|
|
|
+ <svg
|
|
|
+ xmlns="http://www.w3.org/2000/svg"
|
|
|
+ fill="none"
|
|
|
+ viewBox="0 0 24 24"
|
|
|
+ stroke-width="1.5"
|
|
|
+ stroke="currentColor"
|
|
|
+ class="h-3.5 w-3.5"
|
|
|
+ >
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
|
|
+ </svg>
|
|
|
+ </button>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 名称或链接 -->
|
|
|
+ <div class="mb-3">
|
|
|
+ <div class="text-xs text-slate-500 mb-1">
|
|
|
+ {{ qr.qrType === 'link' ? 'Jump URL' : 'Name' }}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-if="qr.qrType === 'link'"
|
|
|
+ class="text-sm font-medium text-cyan-600 truncate"
|
|
|
+ :title="getDisplayName(qr)"
|
|
|
+ >
|
|
|
+ {{ getDisplayName(qr) }}
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="text-sm font-medium text-slate-900 truncate"
|
|
|
+ :title="getDisplayName(qr)"
|
|
|
+ >
|
|
|
+ {{ getDisplayName(qr) }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 类型和状态标签 -->
|
|
|
+ <div class="flex items-center gap-2 flex-wrap mb-3">
|
|
|
+ <span
|
|
|
+ v-if="qr.qrType"
|
|
|
+ class="inline-block rounded-lg px-2.5 py-1 text-xs font-semibold"
|
|
|
+ :class="getQrTypeColor(qr.qrType)"
|
|
|
+ >
|
|
|
+ {{ getQrTypeName(qr.qrType) }}
|
|
|
+ </span>
|
|
|
+ <span
|
|
|
+ v-if="qr.isActivated !== undefined"
|
|
|
+ class="inline-block rounded-lg px-2.5 py-1 text-xs font-medium"
|
|
|
+ :class="qr.isActivated ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'"
|
|
|
+ >
|
|
|
+ {{ qr.isActivated ? 'Activated' : 'Not Activated' }}
|
|
|
+ </span>
|
|
|
+ <span
|
|
|
+ v-if="qr.isVisible !== undefined"
|
|
|
+ class="inline-block rounded-lg px-2.5 py-1 text-xs font-medium"
|
|
|
+ :class="qr.isVisible ? 'bg-blue-100 text-blue-700' : 'bg-slate-100 text-slate-600'"
|
|
|
+ >
|
|
|
+ {{ qr.isVisible ? 'Visible' : 'Hidden' }}
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 最后扫描时间 -->
|
|
|
+ <div class="pt-3 border-t border-slate-100">
|
|
|
+ <div class="text-xs text-slate-500">
|
|
|
+ <span class="font-medium">Last Scan:</span>
|
|
|
+ <span class="ml-1">{{ formatDateTime(qr.lastScanAt) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分页 -->
|
|
|
+ <div v-if="pagination.total > pagination.pageSize" class="mt-6 flex items-center justify-center gap-2">
|
|
|
+ <button
|
|
|
+ class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ :disabled="pagination.page === 0"
|
|
|
+ @click="fetchMyQrCodes(pagination.page - 1)"
|
|
|
+ >
|
|
|
+ Previous
|
|
|
+ </button>
|
|
|
+ <div class="text-sm text-slate-600">
|
|
|
+ Page {{ pagination.page + 1 }} of {{ Math.ceil(pagination.total / pagination.pageSize) }}
|
|
|
</div>
|
|
|
+ <button
|
|
|
+ class="rounded-xl border border-slate-200 bg-white px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 transition-colors disabled:opacity-50 disabled:cursor-not-allowed"
|
|
|
+ :disabled="(pagination.page + 1) * pagination.pageSize >= pagination.total"
|
|
|
+ @click="fetchMyQrCodes(pagination.page + 1)"
|
|
|
+ >
|
|
|
+ Next
|
|
|
+ </button>
|
|
|
</div>
|
|
|
|
|
|
<!-- 修改密码对话框 -->
|
|
|
@@ -243,28 +417,28 @@ onMounted(() => {
|
|
|
>
|
|
|
<div class="w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-xl">
|
|
|
<div class="mb-4">
|
|
|
- <div class="text-xl font-semibold text-slate-900">修改密码</div>
|
|
|
- <div class="mt-1 text-sm text-slate-500">密码规则:至少8位,包含大小写字母和数字</div>
|
|
|
+ <div class="text-xl font-semibold text-slate-900">Change Password</div>
|
|
|
+ <div class="mt-1 text-sm text-slate-500">Password rules: at least 8 characters, including uppercase, lowercase letters and numbers</div>
|
|
|
</div>
|
|
|
|
|
|
<form class="space-y-4" @submit.prevent="submitPassword">
|
|
|
<div>
|
|
|
- <label class="mb-1 block text-sm font-medium text-slate-700">新密码</label>
|
|
|
+ <label class="mb-1 block text-sm font-medium text-slate-700">New Password</label>
|
|
|
<input
|
|
|
v-model="password"
|
|
|
type="password"
|
|
|
class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
|
|
|
- placeholder="例如:Abcdefg1"
|
|
|
+ placeholder="e.g., Abcdefg1"
|
|
|
autocomplete="new-password"
|
|
|
/>
|
|
|
</div>
|
|
|
<div>
|
|
|
- <label class="mb-1 block text-sm font-medium text-slate-700">确认新密码</label>
|
|
|
+ <label class="mb-1 block text-sm font-medium text-slate-700">Confirm New Password</label>
|
|
|
<input
|
|
|
v-model="confirmPassword"
|
|
|
type="password"
|
|
|
class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
|
|
|
- placeholder="再次输入"
|
|
|
+ placeholder="Re-enter password"
|
|
|
autocomplete="new-password"
|
|
|
/>
|
|
|
</div>
|
|
|
@@ -276,14 +450,14 @@ onMounted(() => {
|
|
|
@click="closePasswordDialog"
|
|
|
:disabled="passwordLoading"
|
|
|
>
|
|
|
- 取消
|
|
|
+ Cancel
|
|
|
</button>
|
|
|
<button
|
|
|
type="submit"
|
|
|
class="flex-1 rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 disabled:opacity-60"
|
|
|
:disabled="passwordLoading"
|
|
|
>
|
|
|
- {{ passwordLoading ? '提交中...' : '确认修改' }}
|
|
|
+ {{ passwordLoading ? 'Submitting...' : 'Confirm Change' }}
|
|
|
</button>
|
|
|
</div>
|
|
|
</form>
|
|
|
@@ -298,8 +472,8 @@ onMounted(() => {
|
|
|
>
|
|
|
<div class="w-full max-w-2xl max-h-[90vh] overflow-y-auto rounded-2xl border border-slate-200 bg-white p-6 shadow-xl">
|
|
|
<div class="mb-4">
|
|
|
- <div class="text-xl font-semibold text-slate-900">二维码查询</div>
|
|
|
- <div class="mt-1 text-sm text-slate-500">调用扫码页接口 /qr/info(不需要 token)</div>
|
|
|
+ <div class="text-xl font-semibold text-slate-900">QR Code Lookup</div>
|
|
|
+ <div class="mt-1 text-sm text-slate-500">Call scan page API /qr/info (no token required)</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
|
|
@@ -308,7 +482,7 @@ onMounted(() => {
|
|
|
<input
|
|
|
v-model="qrCode"
|
|
|
class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
|
|
|
- placeholder="例如:QR2025ABC..."
|
|
|
+ placeholder="e.g., QR2025ABC..."
|
|
|
/>
|
|
|
</div>
|
|
|
<div class="flex gap-2">
|
|
|
@@ -317,20 +491,20 @@ onMounted(() => {
|
|
|
:disabled="qrLookupLoading"
|
|
|
@click="queryQrCode"
|
|
|
>
|
|
|
- {{ qrLookupLoading ? '查询中...' : '查询' }}
|
|
|
+ {{ qrLookupLoading ? 'Querying...' : 'Query' }}
|
|
|
</button>
|
|
|
<button
|
|
|
class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50 disabled:opacity-60"
|
|
|
:disabled="!qrLookupData"
|
|
|
@click="openScanPage"
|
|
|
>
|
|
|
- 打开扫码页
|
|
|
+ Open Scan Page
|
|
|
</button>
|
|
|
<button
|
|
|
class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
|
|
|
@click="closeQrLookupDialog"
|
|
|
>
|
|
|
- 关闭
|
|
|
+ Close
|
|
|
</button>
|
|
|
</div>
|
|
|
</div>
|
|
|
@@ -338,26 +512,81 @@ onMounted(() => {
|
|
|
<div v-if="qrLookupData" class="mt-6 space-y-3">
|
|
|
<div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
|
|
|
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
|
|
- <div class="text-xs font-medium text-slate-500">二维码类型</div>
|
|
|
+ <div class="text-xs font-medium text-slate-500">QR Code Type</div>
|
|
|
<div class="mt-1 text-base font-semibold text-slate-900">{{ qrTypeLabel }}</div>
|
|
|
</div>
|
|
|
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
|
|
- <div class="text-xs font-medium text-slate-500">是否激活</div>
|
|
|
- <div class="mt-1 text-base font-semibold text-slate-900">{{ qrLookupData?.isActivated ? '是' : '否' }}</div>
|
|
|
+ <div class="text-xs font-medium text-slate-500">Activated</div>
|
|
|
+ <div class="mt-1 text-base font-semibold text-slate-900">{{ qrLookupData?.isActivated ? 'Yes' : 'No' }}</div>
|
|
|
</div>
|
|
|
<div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
|
|
|
- <div class="text-xs font-medium text-slate-500">扫描次数</div>
|
|
|
+ <div class="text-xs font-medium text-slate-500">Scan Count</div>
|
|
|
<div class="mt-1 text-base font-semibold text-slate-900">{{ qrLookupData?.scanCount ?? '-' }}</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
|
<div class="rounded-2xl border border-slate-200 p-4">
|
|
|
- <div class="mb-2 text-sm font-semibold text-slate-900">绑定信息(info)</div>
|
|
|
+ <div class="mb-2 text-sm font-semibold text-slate-900">Binding Info (info)</div>
|
|
|
<pre class="overflow-auto rounded-xl bg-slate-950 p-3 text-xs text-slate-100">{{ JSON.stringify(qrLookupData?.info, null, 2) }}</pre>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
</div>
|
|
|
+
|
|
|
+ <!-- 绑定二维码对话框 -->
|
|
|
+ <div
|
|
|
+ v-if="showBindDialog"
|
|
|
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
|
|
+ @click.self="closeBindDialog"
|
|
|
+ >
|
|
|
+ <div class="w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-xl">
|
|
|
+ <div class="mb-4">
|
|
|
+ <div class="text-xl font-semibold text-slate-900">Bind QR Code</div>
|
|
|
+ <div class="mt-1 text-sm text-slate-500">Bind by QR code number</div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <form class="space-y-4" @submit.prevent="submitBind">
|
|
|
+ <div>
|
|
|
+ <label class="mb-1 block text-sm font-medium text-slate-700">QR Code Number <span class="text-red-500">*</span></label>
|
|
|
+ <input
|
|
|
+ v-model="bindQrCode"
|
|
|
+ class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
|
|
|
+ placeholder="e.g., ABC123XYZ456"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="mb-1 block text-sm font-medium text-slate-700">Maintenance Code <span class="text-red-500">*</span></label>
|
|
|
+ <input
|
|
|
+ v-model="bindMaintenanceCode"
|
|
|
+ type="text"
|
|
|
+ maxlength="8"
|
|
|
+ class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
|
|
|
+ placeholder="e.g., AB12CD34"
|
|
|
+ required
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div class="flex gap-3 pt-2">
|
|
|
+ <button
|
|
|
+ type="button"
|
|
|
+ class="flex-1 rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
|
|
|
+ @click="closeBindDialog"
|
|
|
+ :disabled="bindLoading"
|
|
|
+ >
|
|
|
+ Cancel
|
|
|
+ </button>
|
|
|
+ <button
|
|
|
+ type="submit"
|
|
|
+ class="flex-1 rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 disabled:opacity-60"
|
|
|
+ :disabled="bindLoading"
|
|
|
+ >
|
|
|
+ {{ bindLoading ? 'Binding...' : 'Confirm Bind' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|