Files
smart_grid_iot/client/src/views/login/index.vue
T
andyesfly 22d27a940d feat: 重构勘察业务为明细模式并新增定时任务人性化配置
- 重构勘察业务:移除SOP相关表,新增勘察明细表支持建筑/楼层/房间/照片管理
- 新增定时任务人性化频率配置,支持分钟/小时/天/周/月/自定义六种模式
- 新增频率预览接口,支持配置转换为cron表达式与执行时间预览
- 更新系统品牌名称为鑫潼智电,修改全局标题与登录页文案
2026-10-17 10:08:42 +08:00

342 lines
7.2 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">
<div class="bg-decoration">
<div class="circle circle-1"></div>
<div class="circle circle-2"></div>
<div class="circle circle-3"></div>
</div>
<div class="login-card" :class="{ 'fade-in': true }">
<div class="brand-section">
<div class="logo-icon">
<ThunderboltOutlined />
</div>
<h1 class="title">鑫潼智电管理系统</h1>
<p class="subtitle">XINTONG SMART POWER MANAGEMENT SYSTEM</p>
</div>
<a-divider />
<a-form ref="formRef" :model="formState" :rules="rules" size="large" @finish="handleLogin">
<a-form-item name="username">
<a-input ref="usernameInputRef" v-model:value="formState.username" placeholder="请输入用户名" allow-clear
:disabled="loading" @pressEnter="handlePasswordEnter">
<template #prefix>
<UserOutlined class="input-icon" />
</template>
</a-input>
</a-form-item>
<a-form-item name="password">
<a-input-password ref="passwordInputRef" v-model:value="formState.password" placeholder="请输入密码" allow-clear
:disabled="loading" @pressEnter="handleLogin">
<template #prefix>
<LockOutlined class="input-icon" />
</template>
</a-input-password>
</a-form-item>
<a-form-item class="options-row">
<a-checkbox v-model:checked="formState.rememberMe" :disabled="loading">
记住我
</a-checkbox>
<a class="forgot-link" href="javascript:void(0)">忘记密码?</a>
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit" :loading="loading" block class="login-btn">
<template #icon v-if="!loading">
<span></span>
</template>
{{ loading ? '登录中...' : '登 录' }}
</a-button>
</a-form-item>
</a-form>
<div class="footer-info">
<p>默认账户:admin / admin123456</p>
</div>
</div>
<div class="copyright">
<p>&copy; 2026 鑫潼智电管理系统 All Rights Reserved</p>
</div>
</div>
</template>
<script setup lang="ts">
import { reactive, ref, onMounted, nextTick } from 'vue'
import { useRouter, useRoute } from 'vue-router'
import { message } from 'ant-design-vue'
import {
UserOutlined,
LockOutlined,
ThunderboltOutlined,
} from '@ant-design/icons-vue'
import { usePermissionStore } from '@/stores/permission'
const router = useRouter()
const route = useRoute()
const permissionStore = usePermissionStore()
const formRef = ref()
const usernameInputRef = ref()
const passwordInputRef = ref()
const loading = ref(false)
const formState = reactive({
username: '',
password: '',
rememberMe: false,
})
const rules = {
username: [
{ required: true, message: '请输入用户名', trigger: 'blur' },
{ min: 3, max: 20, message: '用户名长度为3-20个字符', trigger: 'blur' },
],
password: [
{ required: true, message: '请输入密码', trigger: 'blur' },
{ min: 6, max: 20, message: '密码长度为6-20个字符', trigger: 'blur' },
],
}
onMounted(() => {
nextTick(() => {
const savedUsername = localStorage.getItem('remembered_username')
if (savedUsername) {
formState.username = savedUsername
formState.rememberMe = true
}
usernameInputRef.value?.focus()
})
})
function handlePasswordEnter() {
passwordInputRef.value?.focus()
}
async function handleLogin() {
try {
loading.value = true
await permissionStore.login(formState.username, formState.password)
if (formState.rememberMe) {
localStorage.setItem('remembered_username', formState.username)
} else {
localStorage.removeItem('remembered_username')
}
message.success('登录成功!')
const redirect = (route.query.redirect as string) || '/'
router.push(redirect)
} catch (error: any) {
console.error('登录失败:', error)
message.error(error.message || '登录失败,请检查用户名和密码')
formState.password = ''
nextTick(() => {
passwordInputRef.value?.focus()
})
} finally {
loading.value = false
}
}
</script>
<style scoped lang="less">
.login-container {
min-height: 100vh;
display: flex;
align-items: center;
justify-content: center;
background: linear-gradient(135deg, #1e3c72 0%, #2a5298 50%, #7e22ce 100%);
position: relative;
overflow: hidden;
padding: 20px;
}
.bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
overflow: hidden;
.circle {
position: absolute;
border-radius: 50%;
opacity: 0.08;
}
.circle-1 {
width: 600px;
height: 600px;
top: -200px;
right: -100px;
background: #fff;
animation: float 20s ease-in-out infinite;
}
.circle-2 {
width: 400px;
height: 400px;
bottom: -150px;
left: -100px;
background: #fff;
animation: float 15s ease-in-out infinite reverse;
}
.circle-3 {
width: 200px;
height: 200px;
top: 50%;
left: 10%;
background: #fff;
animation: float 12s ease-in-out infinite;
}
}
@keyframes float {
0%,
100% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-30px) rotate(5deg);
}
}
.login-card {
width: 420px;
max-width: 100%;
background: #fff;
border-radius: 12px;
padding: 40px;
box-shadow: 0 20px 60px rgba(0, 0, 0, 0.15);
position: relative;
z-index: 1;
animation: slideUp 0.6s ease-out;
&.fade-in {
animation: slideUp 0.6s ease-out;
}
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.brand-section {
text-align: center;
margin-bottom: 10px;
.logo-icon {
margin-bottom: 16px;
:deep(.anticon) {
font-size: 48px;
color: #1890ff;
filter: drop-shadow(0 2px 4px rgba(24, 144, 255, 0.3));
}
}
.title {
font-size: 24px;
font-weight: 600;
color: #1a1a2e;
margin: 0 0 8px 0;
letter-spacing: 1px;
}
.subtitle {
font-size: 14px;
color: #999;
margin: 0;
font-weight: 400;
letter-spacing: 0.5px;
}
}
.options-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 4px !important;
}
.forgot-link {
color: #1890ff;
font-size: 14px;
transition: color 0.3s;
&:hover {
color: #40a9ff;
}
}
.login-btn {
height: 44px;
font-size: 16px;
font-weight: 500;
border-radius: 6px;
letter-spacing: 4px;
}
.footer-info {
text-align: center;
margin-top: 20px;
color: #bbb;
font-size: 12px;
}
.input-icon {
color: rgba(0, 0, 0, 0.25);
}
.copyright {
position: absolute;
bottom: 20px;
left: 0;
right: 0;
text-align: center;
color: rgba(255, 255, 255, 0.6);
font-size: 12px;
z-index: 1;
p {
margin: 0;
}
}
@media (max-width: 480px) {
.login-card {
width: 100%;
padding: 30px 20px;
}
.brand-section .title {
font-size: 20px;
}
.copyright {
position: relative;
bottom: auto;
margin-top: 20px;
color: rgba(255, 255, 255, 0.5);
}
}
</style>