feat(auth): 添加二维码登录功能- 实现了二维码登录的前端组件和API接口
- 添加了二维码登录的后端逻辑和数据库设计 - 编写了详细的使用说明和接口文档 - 提供了演示页面和测试工具
This commit is contained in:
@@ -182,7 +182,10 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="loginType == 'scan'">
|
||||
二维码
|
||||
<QrLogin
|
||||
@loginSuccess="onQrLoginSuccess"
|
||||
@loginError="onQrLoginError"
|
||||
/>
|
||||
</template>
|
||||
</a-form>
|
||||
<div class="login-copyright">
|
||||
@@ -267,6 +270,7 @@ import {
|
||||
} from '@ant-design/icons-vue';
|
||||
import {goHomeRoute, cleanPageTabs} from '@/utils/page-tab-util';
|
||||
import {login, loginBySms, getCaptcha} from '@/api/passport/login';
|
||||
import QrLogin from '@/components/QrLogin/index.vue';
|
||||
|
||||
import {User} from '@/api/system/user/model';
|
||||
import {TEMPLATE_ID, THEME_STORE_NAME} from '@/config/setting';
|
||||
@@ -521,6 +525,20 @@ const onScan = () => {
|
||||
}
|
||||
}
|
||||
|
||||
/* 二维码登录成功处理 */
|
||||
const onQrLoginSuccess = (token: string) => {
|
||||
// 设置token到localStorage或其他存储
|
||||
localStorage.setItem('access_token', token);
|
||||
message.success('扫码登录成功');
|
||||
cleanPageTabs();
|
||||
goHome();
|
||||
};
|
||||
|
||||
/* 二维码登录错误处理 */
|
||||
const onQrLoginError = (error: string) => {
|
||||
message.error(error || '扫码登录失败');
|
||||
};
|
||||
|
||||
// const goBack = () => {
|
||||
// openUrl(getDomain());
|
||||
// return;
|
||||
|
||||
371
src/views/passport/qrConfirm/index.vue
Normal file
371
src/views/passport/qrConfirm/index.vue
Normal file
@@ -0,0 +1,371 @@
|
||||
<template>
|
||||
<div class="qr-confirm-container">
|
||||
<a-card :bordered="false" class="confirm-card">
|
||||
<!-- 头部信息 -->
|
||||
<div class="header-info">
|
||||
<div class="app-info">
|
||||
<img :src="appLogo" alt="应用图标" class="app-logo" />
|
||||
<h3 class="app-name">{{ appName }}</h3>
|
||||
</div>
|
||||
<p class="confirm-tip">确认登录到Web端管理后台?</p>
|
||||
</div>
|
||||
|
||||
<!-- 用户信息 -->
|
||||
<div class="user-info" v-if="userInfo">
|
||||
<a-avatar :size="64" :src="userInfo.avatar">
|
||||
<template v-if="!userInfo.avatar" #icon>
|
||||
<UserOutlined />
|
||||
</template>
|
||||
</a-avatar>
|
||||
<div class="user-details">
|
||||
<h4 class="username">{{ userInfo.nickname || userInfo.username }}</h4>
|
||||
<p class="user-phone">{{ userInfo.phone || userInfo.mobile }}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 设备信息 -->
|
||||
<div class="device-info">
|
||||
<div class="info-item">
|
||||
<span class="label">登录设备:</span>
|
||||
<span class="value">{{ deviceInfo.browser }} {{ deviceInfo.version }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">操作系统:</span>
|
||||
<span class="value">{{ deviceInfo.os }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">IP地址:</span>
|
||||
<span class="value">{{ deviceInfo.ip }}</span>
|
||||
</div>
|
||||
<div class="info-item">
|
||||
<span class="label">登录时间:</span>
|
||||
<span class="value">{{ formatTime(new Date()) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 操作按钮 -->
|
||||
<div class="action-buttons">
|
||||
<a-button
|
||||
size="large"
|
||||
@click="handleCancel"
|
||||
:loading="cancelLoading"
|
||||
class="cancel-btn"
|
||||
>
|
||||
取消登录
|
||||
</a-button>
|
||||
<a-button
|
||||
type="primary"
|
||||
size="large"
|
||||
@click="handleConfirm"
|
||||
:loading="confirmLoading"
|
||||
class="confirm-btn"
|
||||
>
|
||||
确认登录
|
||||
</a-button>
|
||||
</div>
|
||||
|
||||
<!-- 安全提示 -->
|
||||
<div class="security-tip">
|
||||
<ExclamationCircleOutlined style="color: #faad14; margin-right: 8px;" />
|
||||
<span>请确认是您本人操作,如非本人操作请点击"取消登录"</span>
|
||||
</div>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, onMounted } from 'vue';
|
||||
import { useRoute, useRouter } from 'vue-router';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { UserOutlined, ExclamationCircleOutlined } from '@ant-design/icons-vue';
|
||||
import { confirmQrLogin, scanQrCode, type QrLoginConfirmRequest } from '@/api/passport/qrLogin';
|
||||
import { getUserInfo } from '@/api/system/user';
|
||||
import { getToken } from '@/utils/token-util';
|
||||
|
||||
const route = useRoute();
|
||||
const router = useRouter();
|
||||
|
||||
// 响应式数据
|
||||
const qrCodeKey = ref<string>('');
|
||||
const userInfo = ref<any>(null);
|
||||
const confirmLoading = ref<boolean>(false);
|
||||
const cancelLoading = ref<boolean>(false);
|
||||
|
||||
// 应用信息
|
||||
const appName = ref<string>('唐九运售电云');
|
||||
const appLogo = ref<string>('/logo.png');
|
||||
|
||||
// 设备信息(这里可以从后端获取或前端检测)
|
||||
const deviceInfo = ref({
|
||||
browser: 'Chrome',
|
||||
version: '120.0',
|
||||
os: 'Windows 10',
|
||||
ip: '192.168.1.100'
|
||||
});
|
||||
|
||||
/**
|
||||
* 格式化时间
|
||||
*/
|
||||
const formatTime = (date: Date): string => {
|
||||
return date.toLocaleString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
second: '2-digit'
|
||||
});
|
||||
};
|
||||
|
||||
/**
|
||||
* 获取用户信息
|
||||
*/
|
||||
const fetchUserInfo = async () => {
|
||||
try {
|
||||
const token = getToken();
|
||||
if (!token) {
|
||||
message.error('请先登录');
|
||||
router.push('/login');
|
||||
return;
|
||||
}
|
||||
|
||||
const info = await getUserInfo();
|
||||
userInfo.value = info;
|
||||
} catch (error: any) {
|
||||
console.error('获取用户信息失败:', error);
|
||||
message.error('获取用户信息失败');
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 标记二维码已扫描
|
||||
*/
|
||||
const markScanned = async () => {
|
||||
try {
|
||||
if (!qrCodeKey.value) return;
|
||||
|
||||
await scanQrCode(qrCodeKey.value);
|
||||
} catch (error: any) {
|
||||
console.error('标记扫描失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 确认登录
|
||||
*/
|
||||
const handleConfirm = async () => {
|
||||
if (!qrCodeKey.value) {
|
||||
message.error('二维码参数错误');
|
||||
return;
|
||||
}
|
||||
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
if (!userInfo.value || !userInfo.value.userId) {
|
||||
message.error('用户信息获取失败');
|
||||
return;
|
||||
}
|
||||
|
||||
const requestData: QrLoginConfirmRequest = {
|
||||
token: qrCodeKey.value,
|
||||
userId: userInfo.value.userId,
|
||||
platform: 'web'
|
||||
};
|
||||
|
||||
await confirmQrLogin(requestData);
|
||||
message.success('登录确认成功');
|
||||
|
||||
// 可以跳转到成功页面或关闭页面
|
||||
setTimeout(() => {
|
||||
// 如果是在APP内的webview中,可以调用原生方法关闭页面
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
}, 1500);
|
||||
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '确认登录失败');
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 取消登录
|
||||
*/
|
||||
const handleCancel = async () => {
|
||||
cancelLoading.value = true;
|
||||
try {
|
||||
// 直接返回,不调用后端接口
|
||||
message.info('已取消登录');
|
||||
|
||||
setTimeout(() => {
|
||||
if (window.history.length > 1) {
|
||||
router.go(-1);
|
||||
} else {
|
||||
router.push('/');
|
||||
}
|
||||
}, 1000);
|
||||
|
||||
} catch (error: any) {
|
||||
message.error(error.message || '取消登录失败');
|
||||
} finally {
|
||||
cancelLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 组件挂载时初始化
|
||||
onMounted(async () => {
|
||||
// 从URL参数获取二维码key
|
||||
qrCodeKey.value = route.query.qrCodeKey as string;
|
||||
|
||||
if (!qrCodeKey.value) {
|
||||
message.error('二维码参数错误');
|
||||
router.push('/');
|
||||
return;
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
await fetchUserInfo();
|
||||
|
||||
// 标记二维码已扫描
|
||||
await markScanned();
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.qr-confirm-container {
|
||||
min-height: 100vh;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.confirm-card {
|
||||
width: 100%;
|
||||
max-width: 400px;
|
||||
border-radius: 12px;
|
||||
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
||||
}
|
||||
|
||||
.header-info {
|
||||
text-align: center;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.app-info {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.app-logo {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 8px;
|
||||
}
|
||||
|
||||
.app-name {
|
||||
margin: 0;
|
||||
color: #333;
|
||||
font-size: 18px;
|
||||
font-weight: 600;
|
||||
}
|
||||
}
|
||||
|
||||
.confirm-tip {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
margin: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.user-info {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 16px;
|
||||
background: #f8f9fa;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 24px;
|
||||
|
||||
.user-details {
|
||||
margin-left: 16px;
|
||||
flex: 1;
|
||||
|
||||
.username {
|
||||
margin: 0 0 4px 0;
|
||||
color: #333;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.user-phone {
|
||||
margin: 0;
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.device-info {
|
||||
margin-bottom: 24px;
|
||||
|
||||
.info-item {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
padding: 8px 0;
|
||||
border-bottom: 1px solid #f0f0f0;
|
||||
|
||||
&:last-child {
|
||||
border-bottom: none;
|
||||
}
|
||||
|
||||
.label {
|
||||
color: #666;
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.value {
|
||||
color: #333;
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.action-buttons {
|
||||
display: flex;
|
||||
gap: 12px;
|
||||
margin-bottom: 16px;
|
||||
|
||||
.cancel-btn,
|
||||
.confirm-btn {
|
||||
flex: 1;
|
||||
height: 44px;
|
||||
border-radius: 6px;
|
||||
font-size: 16px;
|
||||
font-weight: 500;
|
||||
}
|
||||
|
||||
.cancel-btn {
|
||||
border-color: #d9d9d9;
|
||||
color: #666;
|
||||
}
|
||||
}
|
||||
|
||||
.security-tip {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 12px;
|
||||
background: #fffbe6;
|
||||
border: 1px solid #ffe58f;
|
||||
border-radius: 6px;
|
||||
font-size: 12px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
323
src/views/test/qrLoginTest.vue
Normal file
323
src/views/test/qrLoginTest.vue
Normal file
@@ -0,0 +1,323 @@
|
||||
<template>
|
||||
<div class="qr-login-test">
|
||||
<a-card title="二维码登录接口测试" :bordered="false">
|
||||
<a-space direction="vertical" style="width: 100%;" size="large">
|
||||
|
||||
<!-- 生成二维码测试 -->
|
||||
<a-card size="small" title="1. 生成二维码">
|
||||
<a-button type="primary" @click="testGenerateQrCode" :loading="generateLoading">
|
||||
生成二维码
|
||||
</a-button>
|
||||
<div v-if="qrCodeData" style="margin-top: 16px;">
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item label="Token">{{ qrCodeData.token }}</a-descriptions-item>
|
||||
<a-descriptions-item label="二维码内容">{{ qrCodeData.qrCode }}</a-descriptions-item>
|
||||
<a-descriptions-item label="过期时间">{{ qrCodeData.expiresIn }}秒</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
<div style="margin-top: 16px;">
|
||||
<ele-qr-code-svg :value="qrCodeUrl" :size="200" />
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 检查状态测试 -->
|
||||
<a-card size="small" title="2. 检查二维码状态">
|
||||
<a-space>
|
||||
<a-button @click="testCheckStatus" :loading="checkLoading" :disabled="!qrCodeData">
|
||||
检查状态
|
||||
</a-button>
|
||||
<a-switch
|
||||
v-model:checked="autoCheck"
|
||||
checked-children="自动检查"
|
||||
un-checked-children="手动检查"
|
||||
@change="onAutoCheckChange"
|
||||
/>
|
||||
</a-space>
|
||||
<div v-if="statusData" style="margin-top: 16px;">
|
||||
<a-descriptions :column="1" size="small">
|
||||
<a-descriptions-item label="状态">
|
||||
<a-tag :color="getStatusColor(statusData.status)">
|
||||
{{ getStatusText(statusData.status) }}
|
||||
</a-tag>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="剩余时间" v-if="statusData.expiresIn">
|
||||
{{ statusData.expiresIn }}秒
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="访问令牌" v-if="statusData.accessToken">
|
||||
{{ statusData.accessToken.substring(0, 50) }}...
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
<!-- 扫码标记测试 -->
|
||||
<a-card size="small" title="3. 扫码标记">
|
||||
<a-button @click="testScanQrCode" :loading="scanLoading" :disabled="!qrCodeData">
|
||||
模拟扫码
|
||||
</a-button>
|
||||
</a-card>
|
||||
|
||||
<!-- 确认登录测试 -->
|
||||
<a-card size="small" title="4. 确认登录">
|
||||
<a-space>
|
||||
<a-input-number
|
||||
v-model:value="testUserId"
|
||||
placeholder="用户ID"
|
||||
:min="1"
|
||||
style="width: 120px;"
|
||||
/>
|
||||
<a-button @click="testConfirmLogin" :loading="confirmLoading" :disabled="!qrCodeData">
|
||||
确认登录
|
||||
</a-button>
|
||||
</a-space>
|
||||
</a-card>
|
||||
|
||||
<!-- 操作日志 -->
|
||||
<a-card size="small" title="操作日志">
|
||||
<div class="log-container">
|
||||
<div
|
||||
v-for="(log, index) in logs"
|
||||
:key="index"
|
||||
class="log-item"
|
||||
:class="log.type"
|
||||
>
|
||||
<span class="log-time">{{ log.time }}</span>
|
||||
<span class="log-message">{{ log.message }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</a-card>
|
||||
|
||||
</a-space>
|
||||
</a-card>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { ref, computed, onUnmounted } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import {
|
||||
generateQrCode,
|
||||
checkQrCodeStatus,
|
||||
scanQrCode,
|
||||
confirmQrLogin,
|
||||
type QrCodeResponse,
|
||||
type QrCodeStatusResponse,
|
||||
type QrLoginConfirmRequest
|
||||
} from '@/api/passport/qrLogin';
|
||||
|
||||
// 响应式数据
|
||||
const generateLoading = ref(false);
|
||||
const checkLoading = ref(false);
|
||||
const scanLoading = ref(false);
|
||||
const confirmLoading = ref(false);
|
||||
const autoCheck = ref(false);
|
||||
const testUserId = ref<number>(1);
|
||||
|
||||
const qrCodeData = ref<QrCodeResponse | null>(null);
|
||||
const statusData = ref<QrCodeStatusResponse | null>(null);
|
||||
const logs = ref<Array<{time: string, message: string, type: string}>>([]);
|
||||
|
||||
// 自动检查定时器
|
||||
let autoCheckTimer: number | null = null;
|
||||
|
||||
// 计算二维码URL
|
||||
const qrCodeUrl = computed(() => {
|
||||
if (!qrCodeData.value) return '';
|
||||
const baseUrl = window.location.origin;
|
||||
return `${baseUrl}/qr-confirm?qrCodeKey=${qrCodeData.value.token}`;
|
||||
});
|
||||
|
||||
// 添加日志
|
||||
const addLog = (message: string, type: 'info' | 'success' | 'error' | 'warning' = 'info') => {
|
||||
const now = new Date();
|
||||
const time = now.toLocaleTimeString();
|
||||
logs.value.unshift({ time, message, type });
|
||||
|
||||
if (logs.value.length > 100) {
|
||||
logs.value = logs.value.slice(0, 100);
|
||||
}
|
||||
};
|
||||
|
||||
// 获取状态颜色
|
||||
const getStatusColor = (status: string) => {
|
||||
const colors = {
|
||||
pending: 'blue',
|
||||
scanned: 'orange',
|
||||
confirmed: 'green',
|
||||
expired: 'red'
|
||||
};
|
||||
return colors[status] || 'default';
|
||||
};
|
||||
|
||||
// 获取状态文本
|
||||
const getStatusText = (status: string) => {
|
||||
const texts = {
|
||||
pending: '等待扫码',
|
||||
scanned: '已扫码',
|
||||
confirmed: '已确认',
|
||||
expired: '已过期'
|
||||
};
|
||||
return texts[status] || status;
|
||||
};
|
||||
|
||||
// 测试生成二维码
|
||||
const testGenerateQrCode = async () => {
|
||||
generateLoading.value = true;
|
||||
try {
|
||||
const response = await generateQrCode();
|
||||
qrCodeData.value = response;
|
||||
statusData.value = null;
|
||||
addLog(`生成二维码成功: ${response.token}`, 'success');
|
||||
message.success('生成二维码成功');
|
||||
} catch (error: any) {
|
||||
addLog(`生成二维码失败: ${error.message}`, 'error');
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
generateLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 测试检查状态
|
||||
const testCheckStatus = async () => {
|
||||
if (!qrCodeData.value) return;
|
||||
|
||||
checkLoading.value = true;
|
||||
try {
|
||||
const response = await checkQrCodeStatus(qrCodeData.value.token);
|
||||
statusData.value = response;
|
||||
addLog(`检查状态成功: ${response.status}`, 'info');
|
||||
} catch (error: any) {
|
||||
addLog(`检查状态失败: ${error.message}`, 'error');
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
checkLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 测试扫码标记
|
||||
const testScanQrCode = async () => {
|
||||
if (!qrCodeData.value) return;
|
||||
|
||||
scanLoading.value = true;
|
||||
try {
|
||||
await scanQrCode(qrCodeData.value.token);
|
||||
addLog('扫码标记成功', 'success');
|
||||
message.success('扫码标记成功');
|
||||
|
||||
// 自动检查状态
|
||||
setTimeout(() => {
|
||||
testCheckStatus();
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
addLog(`扫码标记失败: ${error.message}`, 'error');
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
scanLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 测试确认登录
|
||||
const testConfirmLogin = async () => {
|
||||
if (!qrCodeData.value || !testUserId.value) return;
|
||||
|
||||
confirmLoading.value = true;
|
||||
try {
|
||||
const requestData: QrLoginConfirmRequest = {
|
||||
token: qrCodeData.value.token,
|
||||
userId: testUserId.value,
|
||||
platform: 'web'
|
||||
};
|
||||
|
||||
const response = await confirmQrLogin(requestData);
|
||||
statusData.value = response;
|
||||
addLog(`确认登录成功: ${response.status}`, 'success');
|
||||
message.success('确认登录成功');
|
||||
} catch (error: any) {
|
||||
addLog(`确认登录失败: ${error.message}`, 'error');
|
||||
message.error(error.message);
|
||||
} finally {
|
||||
confirmLoading.value = false;
|
||||
}
|
||||
};
|
||||
|
||||
// 自动检查状态变化
|
||||
const onAutoCheckChange = (checked: boolean) => {
|
||||
if (checked) {
|
||||
autoCheckTimer = window.setInterval(() => {
|
||||
if (qrCodeData.value) {
|
||||
testCheckStatus();
|
||||
}
|
||||
}, 3000);
|
||||
addLog('开启自动检查状态', 'info');
|
||||
} else {
|
||||
if (autoCheckTimer) {
|
||||
clearInterval(autoCheckTimer);
|
||||
autoCheckTimer = null;
|
||||
}
|
||||
addLog('关闭自动检查状态', 'info');
|
||||
}
|
||||
};
|
||||
|
||||
// 组件卸载时清理定时器
|
||||
onUnmounted(() => {
|
||||
if (autoCheckTimer) {
|
||||
clearInterval(autoCheckTimer);
|
||||
}
|
||||
});
|
||||
|
||||
// 初始化日志
|
||||
addLog('二维码登录接口测试页面已加载', 'info');
|
||||
</script>
|
||||
|
||||
<style lang="less" scoped>
|
||||
.qr-login-test {
|
||||
max-width: 800px;
|
||||
margin: 0 auto;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.log-container {
|
||||
max-height: 300px;
|
||||
overflow-y: auto;
|
||||
background: #f8f9fa;
|
||||
border-radius: 4px;
|
||||
padding: 12px;
|
||||
}
|
||||
|
||||
.log-item {
|
||||
display: flex;
|
||||
margin-bottom: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
|
||||
&:last-child {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.log-time {
|
||||
color: #999;
|
||||
margin-right: 8px;
|
||||
min-width: 80px;
|
||||
}
|
||||
|
||||
.log-message {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
&.info .log-message {
|
||||
color: #333;
|
||||
}
|
||||
|
||||
&.success .log-message {
|
||||
color: #52c41a;
|
||||
}
|
||||
|
||||
&.error .log-message {
|
||||
color: #ff4d4f;
|
||||
}
|
||||
|
||||
&.warning .log-message {
|
||||
color: #faad14;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user