Files
pc-10584/app/pages/qr-confirm.vue
赵忠林 775841eed3 feat(core): 初始化项目基础架构和CMS功能模块
- 添加Docker相关配置文件(.dockerignore, .env.example, .gitignore)
- 实现服务端API代理功能,支持文件、模块和服务器API转发
- 创建文章详情页、栏目文章列表页和单页内容展示页面
- 集成Ant Design Vue组件库并实现SSR样式提取功能
- 定义API响应数据结构类型和应用布局组件
- 开发开发者应用中心和文章管理页面
- 实现CMS导航菜单获取和多租户切换功能
2026-01-27 00:14:08 +08:00

253 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="page">
<a-card :bordered="false" class="card">
<div class="header">
<div class="app">
<img :src="appLogo" class="logo" alt="logo" />
<h3 class="name">{{ appName }}</h3>
</div>
<p class="tip">确认登录到 Web </p>
</div>
<div v-if="userInfo" class="user">
<a-avatar :size="64" :src="userInfo.avatar">
<template v-if="!userInfo.avatar" #icon><UserOutlined /></template>
</a-avatar>
<div class="user-text">
<h4 class="username">{{ userInfo.nickname || userInfo.username }}</h4>
<p class="phone">{{ userInfo.phone || userInfo.mobile }}</p>
</div>
</div>
<div class="device">
<div class="row"><span class="label">登录设备</span><span class="value">{{ deviceInfo.browser }} {{ deviceInfo.version }}</span></div>
<div class="row"><span class="label">操作系统</span><span class="value">{{ deviceInfo.os }}</span></div>
<div class="row"><span class="label">IP 地址</span><span class="value">{{ deviceInfo.ip }}</span></div>
<div class="row"><span class="label">登录时间</span><span class="value">{{ formatTime(new Date()) }}</span></div>
</div>
<div class="actions">
<a-button size="large" class="cancel" :loading="cancelLoading" @click="handleCancel">取消登录</a-button>
<a-button type="primary" size="large" class="confirm" :loading="confirmLoading" @click="handleConfirm">确认登录</a-button>
</div>
<div class="security">
<ExclamationCircleOutlined class="warn" />
<span>请确认是您本人操作如非本人操作请点击取消登录</span>
</div>
</a-card>
</div>
</template>
<script setup lang="ts">
import { computed, onMounted, ref } from 'vue'
import { message } from 'ant-design-vue'
import { ExclamationCircleOutlined, UserOutlined } from '@ant-design/icons-vue'
import { confirmQrLogin, scanQrCode, type QrLoginConfirmRequest } from '@/api/passport/qrLogin'
import { getUserInfo } from '@/api/layout'
import { getToken } from '@/utils/token-util'
import type { User } from '@/api/system/user/model'
definePageMeta({ layout: 'blank' })
const route = useRoute()
const qrCodeKey = computed(() => String(route.query.qrCodeKey || ''))
const userInfo = ref<User | null>(null)
const confirmLoading = ref(false)
const cancelLoading = ref(false)
const appName = ref('桂乐淘')
const appLogo = ref('/favicon.ico')
const deviceInfo = ref({
browser: 'Mobile',
version: '',
os: 'Unknown',
ip: '-'
})
function formatTime(date: Date) {
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
second: '2-digit'
})
}
async function fetchUser() {
const token = getToken()
if (!token) {
message.error('请先登录')
await navigateTo('/login')
return false
}
try {
userInfo.value = await getUserInfo()
return true
} catch (error: unknown) {
console.error(error)
message.error('获取用户信息失败')
return false
}
}
async function markScanned() {
if (!qrCodeKey.value) return
try {
await scanQrCode(qrCodeKey.value)
} catch {
// ignore
}
}
async function handleConfirm() {
if (!qrCodeKey.value) return message.error('二维码参数错误')
confirmLoading.value = true
try {
if (!userInfo.value?.userId) return message.error('用户信息获取失败')
const requestData: QrLoginConfirmRequest = {
token: qrCodeKey.value,
userId: Number(userInfo.value.userId),
platform: 'web'
}
await confirmQrLogin(requestData)
message.success('登录确认成功')
setTimeout(() => backOrHome(), 1200)
} catch (e: unknown) {
message.error(e instanceof Error ? e.message : '确认登录失败')
} finally {
confirmLoading.value = false
}
}
function backOrHome() {
if (import.meta.client && window.history.length > 1) {
window.history.back()
return
}
navigateTo('/')
}
function handleCancel() {
cancelLoading.value = true
try {
message.info('已取消登录')
setTimeout(() => backOrHome(), 800)
} finally {
cancelLoading.value = false
}
}
onMounted(async () => {
if (!qrCodeKey.value) {
message.error('二维码参数错误')
await navigateTo('/login')
return
}
const ok = await fetchUser()
if (!ok) return
await markScanned()
})
</script>
<style scoped>
.page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 24px 16px;
background: #f5f5f5;
}
.card {
width: 480px;
max-width: 100%;
border-radius: 12px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.08);
}
.header {
text-align: center;
padding: 8px 0 12px;
}
.app {
display: flex;
align-items: center;
justify-content: center;
gap: 10px;
}
.logo {
width: 40px;
height: 40px;
border-radius: 10px;
}
.name {
margin: 0;
font-weight: 700;
color: #111827;
}
.tip {
margin: 10px 0 0;
color: #6b7280;
}
.user {
display: flex;
align-items: center;
gap: 14px;
padding: 14px 4px;
}
.user-text {
flex: 1;
}
.username {
margin: 0;
font-size: 16px;
font-weight: 700;
}
.phone {
margin: 4px 0 0;
color: #6b7280;
}
.device {
padding: 12px 4px 4px;
border-top: 1px solid #f0f0f0;
border-bottom: 1px solid #f0f0f0;
}
.row {
display: flex;
justify-content: space-between;
padding: 6px 0;
gap: 12px;
}
.label {
color: #6b7280;
}
.value {
color: #111827;
font-weight: 500;
}
.actions {
display: flex;
gap: 12px;
padding: 16px 4px 6px;
}
.cancel,
.confirm {
flex: 1;
}
.security {
display: flex;
align-items: center;
gap: 8px;
padding: 10px 4px 2px;
color: #6b7280;
font-size: 12px;
}
.warn {
color: #faad14;
}
</style>