|
|
@@ -0,0 +1,363 @@
|
|
|
+<script setup>
|
|
|
+import { ref, onMounted, computed, inject } from 'vue'
|
|
|
+import { useRouter } from 'vue-router'
|
|
|
+import { fetchMyQrCodesApi, fetchQrInfoApi, resetPasswordApi } from '@/services/api'
|
|
|
+import { useToast } from 'primevue/usetoast'
|
|
|
+
|
|
|
+const router = useRouter()
|
|
|
+const toast = useToast()
|
|
|
+
|
|
|
+// 注入父组件提供的注册方法
|
|
|
+const registerOpenPasswordDialog = inject('registerOpenPasswordDialog', null)
|
|
|
+
|
|
|
+// 二维码列表
|
|
|
+const qrCodes = ref([])
|
|
|
+const loading = ref(false)
|
|
|
+
|
|
|
+// 修改密码弹窗
|
|
|
+const showPasswordDialog = ref(false)
|
|
|
+const password = ref('')
|
|
|
+const confirmPassword = ref('')
|
|
|
+const passwordLoading = ref(false)
|
|
|
+
|
|
|
+// 二维码查询弹窗
|
|
|
+const showQrLookupDialog = ref(false)
|
|
|
+const qrCode = ref('')
|
|
|
+const qrLookupLoading = ref(false)
|
|
|
+const qrLookupData = ref(null)
|
|
|
+
|
|
|
+const qrTypeLabel = computed(() => {
|
|
|
+ if (!qrLookupData.value?.qrType) return '-'
|
|
|
+ return qrLookupData.value.qrType === 'person' ? '人员' : qrLookupData.value.qrType === 'pet' ? '宠物/物品' : qrLookupData.value.qrType
|
|
|
+})
|
|
|
+
|
|
|
+// 获取我的二维码列表
|
|
|
+const fetchMyQrCodes = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const data = await fetchMyQrCodesApi()
|
|
|
+ qrCodes.value = Array.isArray(data) ? data : (data?.list || data?.qrCodes || [])
|
|
|
+ } catch (e) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '加载失败',
|
|
|
+ detail: e?.message || e?.error || '获取二维码列表失败',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ qrCodes.value = []
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 跳转到二维码详情
|
|
|
+const goToQrDetail = (qrCode) => {
|
|
|
+ router.push({ name: 'scan', params: { qrCode } })
|
|
|
+}
|
|
|
+
|
|
|
+// 修改密码相关
|
|
|
+const openPasswordDialog = () => {
|
|
|
+ showPasswordDialog.value = true
|
|
|
+ password.value = ''
|
|
|
+ confirmPassword.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+const closePasswordDialog = () => {
|
|
|
+ showPasswordDialog.value = false
|
|
|
+ password.value = ''
|
|
|
+ confirmPassword.value = ''
|
|
|
+}
|
|
|
+
|
|
|
+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 '两次输入的密码不一致'
|
|
|
+ return ''
|
|
|
+}
|
|
|
+
|
|
|
+const submitPassword = async () => {
|
|
|
+ const err = validatePassword()
|
|
|
+ if (err) {
|
|
|
+ toast.add({ severity: 'warn', summary: '校验失败', detail: err, life: 3000 })
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ passwordLoading.value = true
|
|
|
+ try {
|
|
|
+ const data = await resetPasswordApi(password.value)
|
|
|
+ toast.add({
|
|
|
+ severity: 'success',
|
|
|
+ summary: '成功',
|
|
|
+ detail: data?.message || '密码重置成功',
|
|
|
+ life: 2500
|
|
|
+ })
|
|
|
+ closePasswordDialog()
|
|
|
+ } catch (e) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '失败',
|
|
|
+ detail: e?.message || e?.error || '重置失败,请确认已登录且密码符合规则',
|
|
|
+ life: 3500
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ passwordLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 二维码查询相关
|
|
|
+const openQrLookupDialog = () => {
|
|
|
+ showQrLookupDialog.value = true
|
|
|
+ qrCode.value = ''
|
|
|
+ qrLookupData.value = null
|
|
|
+}
|
|
|
+
|
|
|
+const closeQrLookupDialog = () => {
|
|
|
+ showQrLookupDialog.value = false
|
|
|
+ qrCode.value = ''
|
|
|
+ qrLookupData.value = null
|
|
|
+}
|
|
|
+
|
|
|
+const queryQrCode = async () => {
|
|
|
+ const code = qrCode.value.trim()
|
|
|
+ if (!code) {
|
|
|
+ toast.add({ severity: 'warn', summary: '提示', detail: '请输入 qrCode', life: 2000 })
|
|
|
+ return
|
|
|
+ }
|
|
|
+ qrLookupLoading.value = true
|
|
|
+ qrLookupData.value = null
|
|
|
+ try {
|
|
|
+ qrLookupData.value = await fetchQrInfoApi(code)
|
|
|
+ } catch (e) {
|
|
|
+ toast.add({
|
|
|
+ severity: 'error',
|
|
|
+ summary: '查询失败',
|
|
|
+ detail: e?.message || e?.error || '请确认 qrCode 是否正确',
|
|
|
+ life: 3000
|
|
|
+ })
|
|
|
+ } finally {
|
|
|
+ qrLookupLoading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const openScanPage = async () => {
|
|
|
+ const code = (qrLookupData.value?.qrCode || qrCode.value).trim()
|
|
|
+ if (!code) return
|
|
|
+ await router.push({ name: 'scan', params: { qrCode: code } })
|
|
|
+}
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ fetchMyQrCodes()
|
|
|
+
|
|
|
+ // 注册打开密码对话框的方法到父组件
|
|
|
+ if (registerOpenPasswordDialog) {
|
|
|
+ registerOpenPasswordDialog(openPasswordDialog)
|
|
|
+ }
|
|
|
+})
|
|
|
+</script>
|
|
|
+
|
|
|
+<template>
|
|
|
+ <div>
|
|
|
+ <!-- 顶部操作栏 -->
|
|
|
+ <div class="mb-6 flex items-center justify-between">
|
|
|
+ <div>
|
|
|
+ <div class="text-xl font-semibold text-slate-900">我的二维码</div>
|
|
|
+ <div class="mt-1 text-sm text-slate-500">管理您创建的二维码</div>
|
|
|
+ </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>
|
|
|
+
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <div v-else class="grid grid-cols-1 gap-4 sm:grid-cols-2">
|
|
|
+ <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)"
|
|
|
+ >
|
|
|
+ <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">
|
|
|
+ {{ 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"
|
|
|
+ >
|
|
|
+ <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
|
|
|
+ </svg>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 修改密码对话框 -->
|
|
|
+ <div
|
|
|
+ v-if="showPasswordDialog"
|
|
|
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
|
|
|
+ @click.self="closePasswordDialog"
|
|
|
+ >
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <form class="space-y-4" @submit.prevent="submitPassword">
|
|
|
+ <div>
|
|
|
+ <label class="mb-1 block text-sm font-medium text-slate-700">新密码</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"
|
|
|
+ autocomplete="new-password"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div>
|
|
|
+ <label class="mb-1 block text-sm font-medium text-slate-700">确认新密码</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="再次输入"
|
|
|
+ autocomplete="new-password"
|
|
|
+ />
|
|
|
+ </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="closePasswordDialog"
|
|
|
+ :disabled="passwordLoading"
|
|
|
+ >
|
|
|
+ 取消
|
|
|
+ </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 ? '提交中...' : '确认修改' }}
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </form>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 二维码查询对话框 -->
|
|
|
+ <div
|
|
|
+ v-if="showQrLookupDialog"
|
|
|
+ class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
|
|
|
+ @click.self="closeQrLookupDialog"
|
|
|
+ >
|
|
|
+ <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>
|
|
|
+
|
|
|
+ <div class="flex flex-col gap-3 sm:flex-row sm:items-end">
|
|
|
+ <div class="w-full sm:max-w-md">
|
|
|
+ <label class="mb-1 block text-sm font-medium text-slate-700">qrCode</label>
|
|
|
+ <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..."
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ <div class="flex gap-2">
|
|
|
+ <button
|
|
|
+ class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 disabled:opacity-60"
|
|
|
+ :disabled="qrLookupLoading"
|
|
|
+ @click="queryQrCode"
|
|
|
+ >
|
|
|
+ {{ qrLookupLoading ? '查询中...' : '查询' }}
|
|
|
+ </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"
|
|
|
+ >
|
|
|
+ 打开扫码页
|
|
|
+ </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"
|
|
|
+ >
|
|
|
+ 关闭
|
|
|
+ </button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <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="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>
|
|
|
+ <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?.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>
|
|
|
+ <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>
|
|
|
+</template>
|
|
|
+
|