feat(app): 初始化项目配置和页面结构
- 添加 .dockerignore 和 .env.example 配置文件 - 添加 .gitignore 忽略规则配置 - 创建服务端代理API路由(_file、_modules、_server) - 集成 Ant Design Vue 组件库并配置SSR样式提取 - 定义API响应类型封装 - 创建基础布局组件(blank、console) - 实现应用中心页面和组件(AppsCenter) - 添加文章列表测试页面 - 配置控制台导航菜单结构 - 实现控制台头部组件 - 创建联系页面表单
This commit is contained in:
252
app/pages/qr-confirm.vue
Normal file
252
app/pages/qr-confirm.vue
Normal file
@@ -0,0 +1,252 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user