| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363 |
- <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>
|