362 lines
8.4 KiB
Vue
362 lines
8.4 KiB
Vue
<template>
|
||
<div class="page-login">
|
||
<div class="login-container">
|
||
<div class="login-header">
|
||
<h1>用户登录</h1>
|
||
<p class="login-subtitle">请输入您的邮箱和密码进行登录</p>
|
||
</div>
|
||
|
||
<div class="login-form">
|
||
<Message
|
||
v-if="errorMessage"
|
||
severity="error"
|
||
:closable="false"
|
||
class="error-message"
|
||
>
|
||
{{ errorMessage }}
|
||
</Message>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">邮箱</label>
|
||
<InputText
|
||
v-model="email"
|
||
type="email"
|
||
placeholder="请输入邮箱地址"
|
||
autocomplete="email"
|
||
class="form-input"
|
||
@keyup.enter="handleLogin"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-group">
|
||
<label class="form-label">密码</label>
|
||
<InputText
|
||
v-model="password"
|
||
type="password"
|
||
placeholder="请输入密码"
|
||
autocomplete="current-password"
|
||
class="form-input"
|
||
@keyup.enter="handleLogin"
|
||
/>
|
||
</div>
|
||
|
||
<div class="form-options">
|
||
<div class="remember-me">
|
||
<Checkbox v-model="rememberMe" inputId="remember" binary />
|
||
<label for="remember" class="remember-label">记住登录信息</label>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="form-actions">
|
||
<Button
|
||
label="登录"
|
||
@click="handleLogin"
|
||
:disabled="isLoading || !email || !password"
|
||
:loading="isLoading"
|
||
class="btn-block"
|
||
/>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</template>
|
||
|
||
<script>
|
||
import { InputText, Password, Button, Checkbox, Message } from '../components/PrimeVue';
|
||
|
||
export default {
|
||
name: 'LoginPage',
|
||
components: {
|
||
InputText,
|
||
Password,
|
||
Button,
|
||
Checkbox,
|
||
Message
|
||
},
|
||
data() {
|
||
return {
|
||
email: '',
|
||
password: '',
|
||
rememberMe: true,
|
||
isLoading: false,
|
||
errorMessage: ''
|
||
};
|
||
},
|
||
mounted() {
|
||
// 从 store 加载保存的邮箱
|
||
if (this.$store) {
|
||
const savedEmail = this.$store.state.config.email || this.$store.state.auth.email;
|
||
if (savedEmail) {
|
||
this.email = savedEmail;
|
||
}
|
||
const rememberMe = this.$store.state.config.rememberMe;
|
||
if (rememberMe !== undefined) {
|
||
this.rememberMe = rememberMe;
|
||
}
|
||
}
|
||
},
|
||
methods: {
|
||
/**
|
||
* 生成设备ID(基于浏览器特征)
|
||
* @returns {string} 设备ID
|
||
*/
|
||
generateDeviceId() {
|
||
// 尝试从 store 获取已保存的设备ID
|
||
if (this.$store) {
|
||
let deviceId = this.$store.state.config.deviceId || this.$store.state.auth.deviceId;
|
||
if (deviceId) {
|
||
return deviceId;
|
||
}
|
||
}
|
||
|
||
// 基于浏览器特征生成设备ID
|
||
try {
|
||
const navigatorInfo = [
|
||
navigator.userAgent,
|
||
navigator.language,
|
||
navigator.platform,
|
||
screen.width,
|
||
screen.height,
|
||
screen.colorDepth,
|
||
new Date().getTimezoneOffset()
|
||
].join('|');
|
||
|
||
// 使用简单的哈希算法生成ID
|
||
let hash = 0;
|
||
for (let i = 0; i < navigatorInfo.length; i++) {
|
||
const char = navigatorInfo.charCodeAt(i);
|
||
hash = ((hash << 5) - hash) + char;
|
||
hash = hash & hash; // Convert to 32bit integer
|
||
}
|
||
|
||
const deviceId = 'web_' + Math.abs(hash).toString(16).substring(0, 32);
|
||
|
||
// 保存生成的 device_id 到 store
|
||
if (this.$store) {
|
||
this.$store.dispatch('config/setDeviceId', deviceId);
|
||
this.$store.commit('auth/SET_DEVICE_ID', deviceId);
|
||
}
|
||
|
||
return deviceId;
|
||
} catch (error) {
|
||
console.error('生成设备ID失败:', error);
|
||
// 如果生成失败,使用随机字符串作为后备方案
|
||
const fallbackId = 'web_' + Date.now().toString(36) + Math.random().toString(36).substring(2, 15);
|
||
if (this.$store) {
|
||
this.$store.dispatch('config/setDeviceId', fallbackId);
|
||
this.$store.commit('auth/SET_DEVICE_ID', fallbackId);
|
||
}
|
||
return fallbackId;
|
||
}
|
||
},
|
||
|
||
async handleLogin() {
|
||
if (!this.email) {
|
||
this.errorMessage = '请输入邮箱地址';
|
||
return;
|
||
}
|
||
if (!this.password) {
|
||
this.errorMessage = '请输入密码';
|
||
return;
|
||
}
|
||
|
||
// 验证邮箱格式
|
||
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
||
if (!emailRegex.test(this.email)) {
|
||
this.errorMessage = '邮箱格式不正确,请输入有效的邮箱地址';
|
||
return;
|
||
}
|
||
|
||
this.isLoading = true;
|
||
this.errorMessage = '';
|
||
|
||
try {
|
||
// 生成或获取设备ID
|
||
const deviceId = this.generateDeviceId();
|
||
|
||
// 调用 Vuex store 的 login action(会自动调用 HTTP 接口)
|
||
const result = await this.$store.dispatch('auth/login', {
|
||
email: this.email,
|
||
password: this.password,
|
||
deviceId: deviceId
|
||
});
|
||
|
||
if (result.success) {
|
||
// 保存记住登录选项到 store
|
||
if (this.$store) {
|
||
this.$store.dispatch('config/setRememberMe', this.rememberMe);
|
||
if (this.rememberMe) {
|
||
this.$store.dispatch('config/setEmail', this.email);
|
||
}
|
||
}
|
||
|
||
// 等待 store 状态更新完成后再跳转
|
||
await this.$nextTick();
|
||
|
||
// 跳转到控制台(App.vue 的 watch 会自动连接 MQTT)
|
||
this.$router.push('/console');
|
||
} else {
|
||
this.errorMessage = result.error || '登录失败,请检查邮箱和密码';
|
||
this.isLoading = false;
|
||
}
|
||
} catch (error) {
|
||
this.errorMessage = `登录失败: ${error.message}`;
|
||
this.isLoading = false;
|
||
}
|
||
},
|
||
|
||
setError(message) {
|
||
this.errorMessage = message;
|
||
this.isLoading = false;
|
||
},
|
||
|
||
reset() {
|
||
this.password = '';
|
||
this.errorMessage = '';
|
||
this.isLoading = false;
|
||
}
|
||
}
|
||
};
|
||
</script>
|
||
|
||
<style lang="less" scoped>
|
||
.page-login {
|
||
position: fixed;
|
||
top: 0;
|
||
left: 0;
|
||
right: 0;
|
||
bottom: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
padding: 20px;
|
||
overflow: auto;
|
||
}
|
||
|
||
.login-container {
|
||
background: #fff;
|
||
border-radius: 12px;
|
||
box-shadow: 0 10px 40px rgba(0, 0, 0, 0.15);
|
||
width: 100%;
|
||
max-width: 420px;
|
||
padding: 40px;
|
||
box-sizing: border-box;
|
||
}
|
||
|
||
.login-header {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
|
||
h1 {
|
||
font-size: 28px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin: 0 0 10px 0;
|
||
}
|
||
|
||
.login-subtitle {
|
||
font-size: 14px;
|
||
color: #666;
|
||
margin: 0;
|
||
}
|
||
}
|
||
|
||
.login-form {
|
||
.form-group {
|
||
margin-bottom: 20px;
|
||
|
||
.form-label {
|
||
display: block;
|
||
margin-bottom: 8px;
|
||
font-size: 14px;
|
||
color: #333;
|
||
font-weight: 500;
|
||
}
|
||
|
||
// PrimeVue InputText 样式覆盖
|
||
:deep(.p-inputtext) {
|
||
width: 100%;
|
||
padding: 12px 16px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 6px;
|
||
font-size: 14px;
|
||
transition: border-color 0.3s, box-shadow 0.3s;
|
||
|
||
&:enabled:hover {
|
||
border-color: #667eea;
|
||
}
|
||
|
||
&:enabled:focus {
|
||
outline: none;
|
||
border-color: #667eea;
|
||
box-shadow: 0 0 0 0.2rem rgba(102, 126, 234, 0.25);
|
||
}
|
||
|
||
&::placeholder {
|
||
color: #999;
|
||
}
|
||
}
|
||
}
|
||
|
||
.form-options {
|
||
margin-bottom: 24px;
|
||
|
||
.remember-me {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
|
||
.remember-label {
|
||
cursor: pointer;
|
||
user-select: none;
|
||
}
|
||
}
|
||
}
|
||
|
||
.form-actions {
|
||
:deep(.p-button) {
|
||
width: 100%;
|
||
padding: 12px;
|
||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||
border: none;
|
||
border-radius: 6px;
|
||
font-size: 15px;
|
||
font-weight: 500;
|
||
transition: transform 0.2s, box-shadow 0.3s;
|
||
|
||
&:enabled:hover {
|
||
background: linear-gradient(135deg, #5568d3 0%, #653a8b 100%);
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4);
|
||
}
|
||
|
||
&:enabled:active {
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
}
|
||
|
||
.error-message {
|
||
margin-bottom: 20px;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.page-login {
|
||
padding: 15px;
|
||
}
|
||
|
||
.login-container {
|
||
padding: 30px 20px;
|
||
}
|
||
|
||
.login-header h1 {
|
||
font-size: 24px;
|
||
}
|
||
}
|
||
</style>
|
||
|