Files
template-10586/app/pages/qr-confirm.vue
赵忠林 5e26fdc7fb feat(app): 初始化项目配置和页面结构
- 添加 .dockerignore 和 .env.example 配置文件
- 添加 .gitignore 忽略规则配置
- 创建服务端代理API路由(_file、_modules、_server)
- 集成 Ant Design Vue 组件库并配置SSR样式提取
- 定义API响应类型封装
- 创建基础布局组件(blank、console)
- 实现应用中心页面和组件(AppsCenter)
- 添加文章列表测试页面
- 配置控制台导航菜单结构
- 实现控制台头部组件
- 创建联系页面表单
2026-01-17 18:23:37 +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>