HomeView.vue 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363
  1. <script setup>
  2. import { ref, onMounted, computed, inject } from 'vue'
  3. import { useRouter } from 'vue-router'
  4. import { fetchMyQrCodesApi, fetchQrInfoApi, resetPasswordApi } from '@/services/api'
  5. import { useToast } from 'primevue/usetoast'
  6. const router = useRouter()
  7. const toast = useToast()
  8. // 注入父组件提供的注册方法
  9. const registerOpenPasswordDialog = inject('registerOpenPasswordDialog', null)
  10. // 二维码列表
  11. const qrCodes = ref([])
  12. const loading = ref(false)
  13. // 修改密码弹窗
  14. const showPasswordDialog = ref(false)
  15. const password = ref('')
  16. const confirmPassword = ref('')
  17. const passwordLoading = ref(false)
  18. // 二维码查询弹窗
  19. const showQrLookupDialog = ref(false)
  20. const qrCode = ref('')
  21. const qrLookupLoading = ref(false)
  22. const qrLookupData = ref(null)
  23. const qrTypeLabel = computed(() => {
  24. if (!qrLookupData.value?.qrType) return '-'
  25. return qrLookupData.value.qrType === 'person' ? '人员' : qrLookupData.value.qrType === 'pet' ? '宠物/物品' : qrLookupData.value.qrType
  26. })
  27. // 获取我的二维码列表
  28. const fetchMyQrCodes = async () => {
  29. loading.value = true
  30. try {
  31. const data = await fetchMyQrCodesApi()
  32. qrCodes.value = Array.isArray(data) ? data : (data?.list || data?.qrCodes || [])
  33. } catch (e) {
  34. toast.add({
  35. severity: 'error',
  36. summary: '加载失败',
  37. detail: e?.message || e?.error || '获取二维码列表失败',
  38. life: 3000
  39. })
  40. qrCodes.value = []
  41. } finally {
  42. loading.value = false
  43. }
  44. }
  45. // 跳转到二维码详情
  46. const goToQrDetail = (qrCode) => {
  47. router.push({ name: 'scan', params: { qrCode } })
  48. }
  49. // 修改密码相关
  50. const openPasswordDialog = () => {
  51. showPasswordDialog.value = true
  52. password.value = ''
  53. confirmPassword.value = ''
  54. }
  55. const closePasswordDialog = () => {
  56. showPasswordDialog.value = false
  57. password.value = ''
  58. confirmPassword.value = ''
  59. }
  60. const validatePassword = () => {
  61. const p = password.value
  62. if (p.length < 8) return '密码长度必须至少8位'
  63. if (!/[a-z]/.test(p)) return '密码必须包含小写字母'
  64. if (!/[A-Z]/.test(p)) return '密码必须包含大写字母'
  65. if (!/[0-9]/.test(p)) return '密码必须包含数字'
  66. if (p !== confirmPassword.value) return '两次输入的密码不一致'
  67. return ''
  68. }
  69. const submitPassword = async () => {
  70. const err = validatePassword()
  71. if (err) {
  72. toast.add({ severity: 'warn', summary: '校验失败', detail: err, life: 3000 })
  73. return
  74. }
  75. passwordLoading.value = true
  76. try {
  77. const data = await resetPasswordApi(password.value)
  78. toast.add({
  79. severity: 'success',
  80. summary: '成功',
  81. detail: data?.message || '密码重置成功',
  82. life: 2500
  83. })
  84. closePasswordDialog()
  85. } catch (e) {
  86. toast.add({
  87. severity: 'error',
  88. summary: '失败',
  89. detail: e?.message || e?.error || '重置失败,请确认已登录且密码符合规则',
  90. life: 3500
  91. })
  92. } finally {
  93. passwordLoading.value = false
  94. }
  95. }
  96. // 二维码查询相关
  97. const openQrLookupDialog = () => {
  98. showQrLookupDialog.value = true
  99. qrCode.value = ''
  100. qrLookupData.value = null
  101. }
  102. const closeQrLookupDialog = () => {
  103. showQrLookupDialog.value = false
  104. qrCode.value = ''
  105. qrLookupData.value = null
  106. }
  107. const queryQrCode = async () => {
  108. const code = qrCode.value.trim()
  109. if (!code) {
  110. toast.add({ severity: 'warn', summary: '提示', detail: '请输入 qrCode', life: 2000 })
  111. return
  112. }
  113. qrLookupLoading.value = true
  114. qrLookupData.value = null
  115. try {
  116. qrLookupData.value = await fetchQrInfoApi(code)
  117. } catch (e) {
  118. toast.add({
  119. severity: 'error',
  120. summary: '查询失败',
  121. detail: e?.message || e?.error || '请确认 qrCode 是否正确',
  122. life: 3000
  123. })
  124. } finally {
  125. qrLookupLoading.value = false
  126. }
  127. }
  128. const openScanPage = async () => {
  129. const code = (qrLookupData.value?.qrCode || qrCode.value).trim()
  130. if (!code) return
  131. await router.push({ name: 'scan', params: { qrCode: code } })
  132. }
  133. onMounted(() => {
  134. fetchMyQrCodes()
  135. // 注册打开密码对话框的方法到父组件
  136. if (registerOpenPasswordDialog) {
  137. registerOpenPasswordDialog(openPasswordDialog)
  138. }
  139. })
  140. </script>
  141. <template>
  142. <div>
  143. <!-- 顶部操作栏 -->
  144. <div class="mb-6 flex items-center justify-between">
  145. <div>
  146. <div class="text-xl font-semibold text-slate-900">我的二维码</div>
  147. <div class="mt-1 text-sm text-slate-500">管理您创建的二维码</div>
  148. </div>
  149. <button
  150. 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"
  151. @click="openQrLookupDialog"
  152. >
  153. 查询二维码
  154. </button>
  155. </div>
  156. <!-- 二维码列表 -->
  157. <div v-if="loading" class="flex items-center justify-center py-12">
  158. <div class="text-sm text-slate-500">加载中...</div>
  159. </div>
  160. <div v-else-if="qrCodes.length === 0" class="rounded-2xl border border-slate-200 bg-slate-50 p-12 text-center">
  161. <div class="text-sm text-slate-500">暂无二维码</div>
  162. </div>
  163. <div v-else class="grid grid-cols-1 gap-4 sm:grid-cols-2">
  164. <div
  165. v-for="qr in qrCodes"
  166. :key="qr.qrCode || qr.id"
  167. class="group cursor-pointer rounded-2xl border border-slate-200 bg-white p-4 transition-all hover:border-slate-300 hover:shadow-md"
  168. @click="goToQrDetail(qr.qrCode || qr.code || qr.id)"
  169. >
  170. <div class="flex items-start justify-between">
  171. <div class="flex-1">
  172. <div class="text-sm font-semibold text-slate-900">
  173. {{ qr.name || qr.title || `二维码 ${qr.qrCode || qr.code || qr.id}` }}
  174. </div>
  175. <div class="mt-1 text-xs text-slate-500">
  176. {{ qr.qrCode || qr.code || qr.id }}
  177. </div>
  178. <div v-if="qr.qrType" class="mt-2">
  179. <span
  180. class="inline-block rounded-lg px-2 py-0.5 text-xs font-medium"
  181. :class="
  182. qr.qrType === 'person'
  183. ? 'bg-blue-100 text-blue-700'
  184. : qr.qrType === 'pet'
  185. ? 'bg-green-100 text-green-700'
  186. : 'bg-slate-100 text-slate-700'
  187. "
  188. >
  189. {{ qr.qrType === 'person' ? '人员' : qr.qrType === 'pet' ? '宠物' : qr.qrType }}
  190. </span>
  191. </div>
  192. <div v-if="qr.isActivated !== undefined" class="mt-2">
  193. <span
  194. class="inline-block rounded-lg px-2 py-0.5 text-xs font-medium"
  195. :class="qr.isActivated ? 'bg-green-100 text-green-700' : 'bg-amber-100 text-amber-700'"
  196. >
  197. {{ qr.isActivated ? '已激活' : '未激活' }}
  198. </span>
  199. </div>
  200. </div>
  201. <svg
  202. xmlns="http://www.w3.org/2000/svg"
  203. fill="none"
  204. viewBox="0 0 24 24"
  205. stroke-width="1.5"
  206. stroke="currentColor"
  207. class="h-5 w-5 text-slate-400 group-hover:text-slate-600 transition-colors"
  208. >
  209. <path stroke-linecap="round" stroke-linejoin="round" d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3" />
  210. </svg>
  211. </div>
  212. </div>
  213. </div>
  214. <!-- 修改密码对话框 -->
  215. <div
  216. v-if="showPasswordDialog"
  217. class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm"
  218. @click.self="closePasswordDialog"
  219. >
  220. <div class="w-full max-w-md rounded-2xl border border-slate-200 bg-white p-6 shadow-xl">
  221. <div class="mb-4">
  222. <div class="text-xl font-semibold text-slate-900">修改密码</div>
  223. <div class="mt-1 text-sm text-slate-500">密码规则:至少8位,包含大小写字母和数字</div>
  224. </div>
  225. <form class="space-y-4" @submit.prevent="submitPassword">
  226. <div>
  227. <label class="mb-1 block text-sm font-medium text-slate-700">新密码</label>
  228. <input
  229. v-model="password"
  230. type="password"
  231. class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
  232. placeholder="例如:Abcdefg1"
  233. autocomplete="new-password"
  234. />
  235. </div>
  236. <div>
  237. <label class="mb-1 block text-sm font-medium text-slate-700">确认新密码</label>
  238. <input
  239. v-model="confirmPassword"
  240. type="password"
  241. class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
  242. placeholder="再次输入"
  243. autocomplete="new-password"
  244. />
  245. </div>
  246. <div class="flex gap-3 pt-2">
  247. <button
  248. type="button"
  249. class="flex-1 rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
  250. @click="closePasswordDialog"
  251. :disabled="passwordLoading"
  252. >
  253. 取消
  254. </button>
  255. <button
  256. type="submit"
  257. 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"
  258. :disabled="passwordLoading"
  259. >
  260. {{ passwordLoading ? '提交中...' : '确认修改' }}
  261. </button>
  262. </div>
  263. </form>
  264. </div>
  265. </div>
  266. <!-- 二维码查询对话框 -->
  267. <div
  268. v-if="showQrLookupDialog"
  269. class="fixed inset-0 z-50 flex items-center justify-center bg-black/50 backdrop-blur-sm p-4"
  270. @click.self="closeQrLookupDialog"
  271. >
  272. <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">
  273. <div class="mb-4">
  274. <div class="text-xl font-semibold text-slate-900">二维码查询</div>
  275. <div class="mt-1 text-sm text-slate-500">调用扫码页接口 /qr/info(不需要 token)</div>
  276. </div>
  277. <div class="flex flex-col gap-3 sm:flex-row sm:items-end">
  278. <div class="w-full sm:max-w-md">
  279. <label class="mb-1 block text-sm font-medium text-slate-700">qrCode</label>
  280. <input
  281. v-model="qrCode"
  282. class="w-full rounded-xl border border-slate-200 px-3 py-2 outline-none focus:border-slate-900"
  283. placeholder="例如:QR2025ABC..."
  284. />
  285. </div>
  286. <div class="flex gap-2">
  287. <button
  288. class="rounded-xl bg-slate-900 px-4 py-2 text-sm font-semibold text-white hover:bg-slate-800 disabled:opacity-60"
  289. :disabled="qrLookupLoading"
  290. @click="queryQrCode"
  291. >
  292. {{ qrLookupLoading ? '查询中...' : '查询' }}
  293. </button>
  294. <button
  295. 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"
  296. :disabled="!qrLookupData"
  297. @click="openScanPage"
  298. >
  299. 打开扫码页
  300. </button>
  301. <button
  302. class="rounded-xl border border-slate-200 px-4 py-2 text-sm font-semibold text-slate-700 hover:bg-slate-50"
  303. @click="closeQrLookupDialog"
  304. >
  305. 关闭
  306. </button>
  307. </div>
  308. </div>
  309. <div v-if="qrLookupData" class="mt-6 space-y-3">
  310. <div class="grid grid-cols-1 gap-3 sm:grid-cols-3">
  311. <div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
  312. <div class="text-xs font-medium text-slate-500">二维码类型</div>
  313. <div class="mt-1 text-base font-semibold text-slate-900">{{ qrTypeLabel }}</div>
  314. </div>
  315. <div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
  316. <div class="text-xs font-medium text-slate-500">是否激活</div>
  317. <div class="mt-1 text-base font-semibold text-slate-900">{{ qrLookupData?.isActivated ? '是' : '否' }}</div>
  318. </div>
  319. <div class="rounded-2xl border border-slate-200 bg-slate-50 p-4">
  320. <div class="text-xs font-medium text-slate-500">扫描次数</div>
  321. <div class="mt-1 text-base font-semibold text-slate-900">{{ qrLookupData?.scanCount ?? '-' }}</div>
  322. </div>
  323. </div>
  324. <div class="rounded-2xl border border-slate-200 p-4">
  325. <div class="mb-2 text-sm font-semibold text-slate-900">绑定信息(info)</div>
  326. <pre class="overflow-auto rounded-xl bg-slate-950 p-3 text-xs text-slate-100">{{ JSON.stringify(qrLookupData?.info, null, 2) }}</pre>
  327. </div>
  328. </div>
  329. </div>
  330. </div>
  331. </div>
  332. </template>