1
This commit is contained in:
361
app/views/LoginPage.vue
Normal file
361
app/views/LoginPage.vue
Normal file
@@ -0,0 +1,361 @@
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user