XuqmGroup-Web/tenant-platform/src/views/auth/LoginView.vue

166 行
4.2 KiB
Vue

2026-04-21 22:07:29 +08:00
<template>
<div class="login-page">
<el-card class="login-card">
<h2 class="title">XuqmGroup 开放平台</h2>
<el-form ref="formRef" :model="form" :rules="rules" @submit.prevent="handleLogin">
<el-form-item prop="account">
<el-input v-model="form.account" placeholder="用户名 / 邮箱" prefix-icon="User" />
</el-form-item>
<el-form-item prop="password">
<el-input v-model="form.password" type="password" placeholder="密码"
prefix-icon="Lock" show-password />
</el-form-item>
<el-form-item prop="captchaCode">
<div class="captcha-row">
<el-input v-model="form.captchaCode" placeholder="验证码" style="flex:1" />
<img :src="captchaImage" class="captcha-img" @click="loadCaptcha" alt="captcha" />
</div>
</el-form-item>
<el-form-item>
<el-button type="primary" native-type="submit" :loading="loading" style="width:100%">
</el-button>
</el-form-item>
</el-form>
<div class="links">
<router-link v-if="!isPrivate" to="/register">注册账号</router-link>
2026-04-21 22:07:29 +08:00
<router-link to="/forgot-password">忘记密码</router-link>
</div>
</el-card>
</div>
</template>
<script setup lang="ts">
import { ref, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus'
import type { FormInstance, FormRules } from 'element-plus'
import { authApi } from '@/api/auth'
import { useAuthStore } from '@/stores/auth'
import { getDeploymentStatus } from '@/api/system'
2026-04-21 22:07:29 +08:00
const router = useRouter()
const auth = useAuthStore()
const formRef = ref<FormInstance>()
const loading = ref(false)
const captchaKey = ref('')
const captchaImage = ref('')
const isPrivate = ref(false)
2026-04-21 22:07:29 +08:00
const form = reactive({ account: '', password: '', captchaCode: '' })
const rules: FormRules = {
account: [{ required: true, message: '请输入账号', trigger: 'blur' }],
password: [{ required: true, message: '请输入密码', trigger: 'blur' }],
captchaCode: [{ required: true, message: '请输入验证码', trigger: 'blur' }],
}
async function loadCaptcha() {
try {
const res = await authApi.getCaptcha()
captchaKey.value = res.data.data.key
captchaImage.value = res.data.data.image
} catch {}
}
async function handleLogin() {
await formRef.value?.validate()
loading.value = true
try {
const res = await authApi.login({
account: form.account,
password: form.password,
captchaKey: captchaKey.value,
captchaCode: form.captchaCode,
})
auth.setToken(res.data.data.token)
router.push('/dashboard')
} finally {
loading.value = false
loadCaptcha()
}
}
onMounted(async () => {
loadCaptcha()
try {
const status = await getDeploymentStatus()
isPrivate.value = status.mode === 'PRIVATE'
} catch {}
})
2026-04-21 22:07:29 +08:00
</script>
<style scoped>
.login-page {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 16px;
background:
radial-gradient(circle at top left, rgba(102, 126, 234, 0.35), transparent 35%),
radial-gradient(circle at bottom right, rgba(118, 75, 162, 0.32), transparent 32%),
linear-gradient(135deg, #667eea 0%, #764ba2 100%);
2026-04-21 22:07:29 +08:00
}
.login-card {
width: min(420px, calc(100vw - 32px));
box-sizing: border-box;
border-radius: 18px;
box-shadow: 0 20px 50px rgba(15, 23, 42, 0.18);
backdrop-filter: blur(8px);
2026-04-21 22:07:29 +08:00
}
.title {
text-align: center;
margin-bottom: 24px;
color: #333;
}
.captcha-row {
display: flex;
align-items: center;
gap: 12px;
width: 100%;
}
.captcha-img {
height: 36px;
cursor: pointer;
border: 1px solid #dcdfe6;
border-radius: 4px;
}
.links {
display: flex;
justify-content: space-between;
font-size: 14px;
}
@media (max-width: 767px) {
.login-page {
align-items: flex-start;
padding-top: 12vh;
}
.login-card {
width: 100%;
}
.captcha-row {
flex-direction: column;
align-items: stretch;
gap: 8px;
}
.captcha-img {
width: 100%;
height: 44px;
object-fit: cover;
}
.links {
flex-direction: column;
gap: 8px;
align-items: center;
}
}
2026-04-21 22:07:29 +08:00
</style>