Files
CheckInApp/frontend/src/views/LoginView.vue
T

551 lines
13 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="login-container">
<a-row justify="center" align="middle" style="height: 100%">
<a-col :xs="22" :sm="18" :md="12" :lg="10" :xl="8">
<a-card class="login-card">
<template #title>
<div class="card-header">
<h2>接龙自动打卡系统</h2>
<p class="subtitle">
{{ loginMode === 'qrcode' ? 'QQ 扫码登录/注册' : '用户名密码登录' }}
</p>
</div>
</template>
<!-- 登录模式切换 -->
<div class="mode-switch">
<a-segmented v-model:value="loginMode" :options="loginModeOptions" block />
</div>
<!-- QR码登录表单 -->
<a-form
v-if="loginMode === 'qrcode'"
ref="qrcodeFormRef"
:model="qrcodeForm"
:rules="qrcodeRules"
layout="vertical"
@submit.prevent="handleQRCodeLogin"
>
<a-form-item name="alias">
<a-input
v-model:value="qrcodeForm.alias"
placeholder="请输入您的用户名"
size="large"
allow-clear
@keyup.enter="handleQRCodeLogin"
>
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item>
<a-button
type="primary"
size="large"
block
:loading="loading"
@click="handleQRCodeLogin"
>
{{ loading ? '正在登录...' : '扫码登录/注册' }}
</a-button>
</a-form-item>
</a-form>
<!-- 别名+密码登录表单 -->
<a-form
v-else
ref="passwordFormRef"
:model="passwordForm"
:rules="passwordRules"
layout="vertical"
>
<a-form-item name="alias">
<a-input
v-model:value="passwordForm.alias"
placeholder="请输入您的用户名"
size="large"
allow-clear
>
<template #prefix>
<UserOutlined />
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password
v-model:value="passwordForm.password"
placeholder="请输入密码"
size="large"
@keyup.enter="handlePasswordLogin"
>
<template #prefix>
<KeyOutlined />
</template>
</a-input-password>
</a-form-item>
<a-form-item>
<a-button
type="primary"
size="large"
block
:loading="loading"
@click="handlePasswordLogin"
>
{{ loading ? '登录中...' : '登录' }}
</a-button>
</a-form-item>
<div class="tips-link">
<a class="link-text" @click="loginMode = 'qrcode'"> 没有密码使用扫码登录 </a>
</div>
</a-form>
<div class="tips">
<a-alert
:message="loginMode === 'qrcode' ? '扫码登录提示' : '密码登录提示'"
type="info"
:closable="false"
show-icon
>
<template #description>
<template v-if="loginMode === 'qrcode'">
<p>1. 输入您的用户名(用于标识身份)</p>
<p>2. 点击"扫码登录/注册"按钮</p>
<p>3. 使用手机 QQ 扫描弹出的二维码</p>
<p>4. 扫码成功后即可登录系统</p>
<p class="tip-note">💡 新用户首次扫码将自动注册账户</p>
</template>
<template v-else>
<p>1. 输入您的用户名和密码</p>
<p>2. 点击"登录"按钮直接登录</p>
<p>3. 首次使用请先扫码登录/注册然后在设置中设置密码</p>
</template>
</template>
</a-alert>
</div>
</a-card>
</a-col>
</a-row>
<!-- QR 码弹窗 -->
<QRCodeModal
v-model:visible="qrcodeVisible"
:alias="qrcodeForm.alias"
@success="handleLoginSuccess"
@error="handleLoginError"
/>
</div>
</template>
<script setup>
import { ref, watch } from 'vue';
import { useRouter, useRoute } from 'vue-router';
import { message } from 'ant-design-vue';
import { UserOutlined, KeyOutlined } from '@ant-design/icons-vue';
import { authAPI } from '@/api';
import { useAuthStore } from '@/stores/auth';
import QRCodeModal from '@/components/QRCodeModal.vue';
const router = useRouter();
const route = useRoute();
const authStore = useAuthStore();
const qrcodeFormRef = ref(null);
const passwordFormRef = ref(null);
const loading = ref(false);
const qrcodeVisible = ref(false);
// 登录模式
const loginMode = ref('qrcode');
const loginModeOptions = [
{ label: '扫码登录', value: 'qrcode' },
{ label: '密码登录', value: 'password' },
];
// 监听登录模式切换,同步用户名
watch(loginMode, () => {
// 从密码登录切换到扫码登录
if (loginMode.value === 'qrcode' && passwordForm.value.alias) {
qrcodeForm.value.alias = passwordForm.value.alias;
}
// 从扫码登录切换到密码登录
else if (loginMode.value === 'password' && qrcodeForm.value.alias) {
passwordForm.value.alias = qrcodeForm.value.alias;
}
});
// QR码登录表单
const qrcodeForm = ref({
alias: '',
});
const qrcodeRules = {
alias: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
],
};
// 密码登录表单
const passwordForm = ref({
alias: '',
password: '',
});
const passwordRules = {
alias: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 2, max: 50, message: '长度在 2 到 50 个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, message: '密码至少6个字符', trigger: 'blur' },
],
};
// QR码登录
const handleQRCodeLogin = async () => {
if (!qrcodeFormRef.value) return;
try {
await qrcodeFormRef.value.validate();
// 显示 QR 码弹窗
qrcodeVisible.value = true;
} catch {
// 表单验证失败,不需要打印错误(由 Ant Design 自动显示错误提示)
}
};
// 密码登录
const handlePasswordLogin = async () => {
if (!passwordFormRef.value) return;
try {
await passwordFormRef.value.validate();
loading.value = true;
const response = await authAPI.aliasLogin(
passwordForm.value.alias,
passwordForm.value.password
);
if (response.success) {
// 使用 authStore 保存认证信息
const user = {
id: response.user_id,
alias: response.alias,
role: response.role || 'user',
is_approved: response.is_approved !== false,
};
// 如果没有 authorization(测试账号),使用 user_id 作为认证凭据
const authToken = response.authorization || `user_id:${response.user_id}`;
authStore.setAuth(authToken, user);
// 只有当有真实 authorization 时才获取完整用户信息
if (response.authorization) {
try {
await authStore.fetchCurrentUser();
} catch (err) {
console.warn('获取完整用户信息失败,使用基本信息:', err);
// 即使失败也继续登录流程
}
} else {
// 没有 authorization 的测试账号,提示用户需要扫码绑定
message.info({
content: '您正在使用密码登录模式。如需使用打卡功能,请先扫码绑定 QQ。',
duration: 5,
});
}
// 如果有 Token 警告,显示提示
if (response.token_warning && response.warning_message) {
message.warning({
content: response.warning_message,
duration: 5,
});
} else if (response.authorization) {
// 只有有 token 的用户才显示"欢迎回来"
message.success(`欢迎回来,${response.alias}`);
} else {
// 测试账号登录成功提示
message.success(`登录成功,${response.alias}`);
}
// 跳转到重定向页面或仪表盘
const redirect = route.query.redirect || '/dashboard';
router.push(redirect);
} else {
// 根据不同错误类型提供友好提示
handlePasswordLoginError(response.message);
}
} catch (error) {
console.error('密码登录失败:', error);
const errorMsg = error.response?.data?.detail || error.message || '登录失败,请稍后重试';
handlePasswordLoginError(errorMsg);
} finally {
loading.value = false;
}
};
// 处理密码登录错误
const handlePasswordLoginError = msg => {
if (!msg) {
message.error('登录失败,请稍后重试');
return;
}
// 用户不存在或密码错误
if (msg.includes('用户名或密码错误')) {
message.error('用户名或密码错误');
return;
}
// 未设置密码
if (msg.includes('未设置密码')) {
message.warning('该账户未设置密码,请使用扫码登录');
return;
}
// 用户不存在
if (msg.includes('用户不存在')) {
message.error('用户不存在,请检查用户名或使用扫码登录注册');
return;
}
// 其他错误
message.error(msg || '登录失败,请稍后重试');
};
const handleLoginSuccess = user => {
message.success(`欢迎回来,${user.alias}`);
// 跳转到重定向页面或仪表盘
const redirect = route.query.redirect || '/dashboard';
router.push(redirect);
};
const handleLoginError = error => {
message.error(error.message || '登录失败');
};
</script>
<style scoped>
.login-container {
width: 100vw;
height: 100vh;
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
overflow-y: auto;
padding: 16px;
transition: background 0.3s ease;
}
/* 暗色模式背景 */
.dark .login-container {
background: linear-gradient(135deg, #1a237e 0%, #4a148c 100%);
}
.login-card {
border-radius: 16px;
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
width: 100%;
margin: 20px 0;
}
/* 暗色模式卡片阴影 */
.dark .login-card {
box-shadow: 0 8px 32px rgba(0, 0, 0, 0.4);
}
.card-header {
text-align: center;
}
.card-header h2 {
margin: 0;
font-size: 24px;
color: #303133;
transition: color 0.3s ease;
}
/* 暗色模式标题 */
.dark .card-header h2 {
color: #e6e1e5;
}
.subtitle {
margin: 10px 0 0 0;
font-size: 14px;
color: #909399;
transition: color 0.3s ease;
}
/* 暗色模式副标题 */
.dark .subtitle {
color: #cac4d0;
}
.mode-switch {
margin-bottom: 20px;
}
.tips-link {
text-align: center;
margin-top: 10px;
}
.link-text {
color: #2196f3;
cursor: pointer;
text-decoration: none;
transition: color 0.3s ease;
}
.link-text:hover {
text-decoration: underline;
}
/* 暗色模式链接 */
.dark .link-text {
color: #64b5f6;
}
.dark .link-text:hover {
color: #90caf9;
}
.tips {
margin-top: 20px;
}
.tips :deep(p) {
margin: 5px 0;
font-size: 14px;
line-height: 1.5;
}
.tip-note {
margin-top: 12px !important;
padding-top: 8px;
border-top: 1px dashed #e0e0e0;
color: #606266;
font-weight: 500;
transition: all 0.3s ease;
}
/* 暗色模式提示注释 */
.dark .tip-note {
border-top-color: #49454f;
color: #cac4d0;
}
/* 确保 Ant Design Row 占满高度 */
.login-container :deep(.ant-row) {
width: 100%;
min-height: 100%;
}
/* 移动端优化 */
@media (max-width: 768px) {
.login-container {
padding: 12px;
}
.login-card {
border-radius: 12px;
}
.card-header h2 {
font-size: 20px;
}
.subtitle {
font-size: 13px;
}
.tips :deep(p) {
font-size: 13px;
}
.tips :deep(.ant-alert) {
font-size: 13px;
}
}
/* 小屏手机优化 */
@media (max-width: 576px) {
.login-container {
padding: 8px;
}
.login-card {
border-radius: 8px;
margin: 10px 0;
}
.card-header h2 {
font-size: 18px;
}
.subtitle {
font-size: 12px;
}
.mode-switch {
margin-bottom: 16px;
}
.tips {
margin-top: 16px;
}
.tips :deep(p) {
font-size: 12px;
margin: 4px 0;
}
}
/* 横屏优化 */
@media (max-height: 600px) and (orientation: landscape) {
.login-container {
padding: 8px;
align-items: flex-start;
}
.login-card {
margin: 8px 0;
}
.card-header h2 {
font-size: 18px;
}
.tips :deep(p) {
margin: 3px 0;
font-size: 12px;
}
.mode-switch {
margin-bottom: 12px;
}
.tips {
margin-top: 12px;
}
}
</style>