OfficeSystem/pages/login/index.vue
2025-11-12 15:34:06 +08:00

391 lines
8.3 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>
<view class="login-page">
<view class="login-content">
<view class="login-header">
<view class="logo-wrapper">
<image class="logo-image" :src="logoSrc" mode="aspectFit" />
</view>
<text class="system-title">项目管理系统</text>
</view>
<view class="login-card">
<view class="form-item">
<text class="form-label">账号</text>
<input
class="form-input"
type="text"
placeholder="请输入用户名"
v-model.trim="form.username"
confirm-type="next"
/>
</view>
<view class="form-item">
<text class="form-label">密码</text>
<input
class="form-input"
:password="!showPassword"
placeholder="请输入密码"
v-model="form.password"
confirm-type="done"
/>
</view>
<view class="form-item" v-if="captcha.captchaEnabled">
<text class="form-label">验证码</text>
<view class="captcha-row">
<input
class="form-input code-input"
type="text"
placeholder="请输入验证码"
v-model.trim="form.code"
confirm-type="done"
/>
<image
class="captcha-image"
:src="captchaImageSrc"
mode="aspectFit"
@click="refreshCaptcha"
/>
</view>
</view>
<view class="form-extra">
<label class="remember-label" @click="rememberPassword = !rememberPassword">
<checkbox :checked="rememberPassword" color="#3b82f6" />
<text class="remember-text">记住密码</text>
</label>
<text class="captcha-refresh" @click="refreshCaptcha">看不清换一张</text>
</view>
<button
class="login-button"
:loading="loginLoading"
:disabled="loginLoading"
@click="handleSubmit"
>
登录
</button>
</view>
</view>
</view>
</template>
<script setup>
import { reactive, ref, computed, onMounted } from 'vue';
import { onLoad } from '@dcloudio/uni-app';
import { login, getCaptchaImage, getUserInfo } from '@/api/user';
import { useUserStore } from '@/store/user';
const userStore = useUserStore();
const logoSrc = '/static/logo.png';
const form = reactive({
username: '',
password: '',
code: ''
});
const captcha = reactive({
img: '',
uuid: '',
captchaEnabled: true
});
const rememberPassword = ref(true);
const loginLoading = ref(false);
const showPassword = ref(false);
const captchaImageSrc = computed(() => {
if (!captcha.img) return '';
if (captcha.img.startsWith('data:image')) {
return captcha.img;
}
return `data:image/png;base64,${captcha.img}`;
});
const togglePassword = () => {
showPassword.value = !showPassword.value;
};
const loadRememberedForm = () => {
try {
const cache = uni.getStorageSync('loginFormCache');
if (cache && typeof cache === 'object') {
rememberPassword.value = !!cache.remember;
if (cache.remember) {
form.username = cache.username || '';
form.password = cache.password || '';
}
}
} catch (err) {
console.warn('读取登录缓存失败', err);
}
};
const saveRememberedForm = () => {
if (rememberPassword.value) {
uni.setStorageSync('loginFormCache', {
remember: true,
username: form.username,
password: form.password
});
} else {
uni.removeStorageSync('loginFormCache');
}
};
const refreshCaptcha = async () => {
try {
const res = await getCaptchaImage();
captcha.img = res?.img || '';
captcha.uuid = res?.uuid || '';
captcha.captchaEnabled = res?.captchaEnabled !== false;
} catch (error) {
console.error('获取验证码失败', error);
captcha.img = '';
captcha.uuid = '';
captcha.captchaEnabled = false;
}
};
const validateForm = () => {
if (!form.username) {
uni.showToast({ title: '请输入用户名', icon: 'none' });
return false;
}
if (!form.password) {
uni.showToast({ title: '请输入密码', icon: 'none' });
return false;
}
if (captcha.captchaEnabled && !form.code) {
uni.showToast({ title: '请输入验证码', icon: 'none' });
return false;
}
return true;
};
const handleSubmit = async () => {
if (loginLoading.value) return;
if (!validateForm()) return;
loginLoading.value = true;
try {
const payload = {
username: form.username,
password: form.password,
code: captcha.captchaEnabled ? form.code : undefined,
uuid: captcha.captchaEnabled ? captcha.uuid : undefined
};
const res = await login(payload);
let token = '';
if (typeof res === 'string') {
token = res;
} else if (res?.token) {
token = res.token;
} else if (res?.data?.token) {
token = res.data.token;
} else if (res?.access_token) {
token = res.access_token;
}
if (!token) {
uni.showToast({
title: '登录成功,但未获取到令牌',
icon: 'none'
});
} else {
userStore.login(token);
try {
const userInfo = await getUserInfo();
if (userInfo) {
userStore.setUserInfo(userInfo);
}
} catch (infoErr) {
console.warn('获取用户信息失败', infoErr);
}
saveRememberedForm();
uni.$uv.toast('登录成功');
setTimeout(() => {
uni.reLaunch({ url: '/pages/index/index' });
}, 300);
return;
}
} catch (err) {
console.error('登录失败', err);
uni.showToast({
title: err?.msg || err?.message || '登录失败,请重试',
icon: 'none'
});
await refreshCaptcha();
} finally {
loginLoading.value = false;
}
};
onLoad(() => {
if (userStore.isLogin) {
uni.reLaunch({ url: '/pages/index/index' });
return;
}
loadRememberedForm();
refreshCaptcha();
});
onMounted(() => {
// 如果已经登录,保障直接跳转
if (userStore.isLogin) {
uni.reLaunch({ url: '/pages/index/index' });
}
});
</script>
<style lang="scss" scoped>
.login-page {
min-height: 100vh;
background: linear-gradient(135deg, #1d4ed8, #3b82f6);
display: flex;
justify-content: center;
align-items: center;
padding: 40rpx 32rpx;
box-sizing: border-box;
}
.login-content {
width: 100%;
max-width: 640rpx;
}
.login-header {
text-align: center;
color: #ffffff;
margin-bottom: 48rpx;
display: flex;
flex-direction: column;
align-items: center;
gap: 24rpx;
}
.logo-wrapper {
width: 220rpx;
height: 220rpx;
border-radius: 48rpx;
background-color: #ffffff;
display: flex;
justify-content: center;
align-items: center;
padding: 32rpx;
box-sizing: border-box;
}
.logo-image {
width: 100%;
height: 100%;
}
.system-title {
display: block;
margin-top: 12rpx;
font-size: 32rpx;
letter-spacing: 4rpx;
}
.login-card {
background-color: #ffffff;
border-radius: 24rpx;
padding: 48rpx 40rpx;
box-shadow: 0 24rpx 64rpx rgba(0, 0, 0, 0.12);
box-sizing: border-box;
}
.form-item + .form-item {
margin-top: 32rpx;
}
.form-label {
font-size: 28rpx;
color: #6b7280;
margin-bottom: 16rpx;
display: block;
}
.form-input {
width: 100%;
height: 88rpx;
border-radius: 12rpx;
background-color: #f3f4f6;
padding: 0 28rpx;
box-sizing: border-box;
font-size: 28rpx;
color: #111827;
}
.toggle-password {
position: absolute;
right: 40rpx;
top: 50%;
transform: translateY(-10rpx);
font-size: 26rpx;
color: #2563eb;
}
.form-item {
position: relative;
}
.captcha-row {
display: flex;
align-items: center;
gap: 16rpx;
}
.code-input {
flex: 1;
}
.captcha-image {
width: 220rpx;
height: 88rpx;
border-radius: 12rpx;
border: 1px solid #e5e7eb;
background-color: #f9fafb;
}
.form-extra {
margin-top: 32rpx;
display: flex;
justify-content: space-between;
align-items: center;
font-size: 26rpx;
color: #6b7280;
}
.remember-label {
display: flex;
align-items: center;
gap: 12rpx;
}
.remember-text {
color: #4b5563;
}
.captcha-refresh {
color: #2563eb;
}
.login-button {
margin-top: 48rpx;
height: 96rpx;
border-radius: 16rpx;
background: linear-gradient(135deg, #2563eb, #3b82f6);
color: #ffffff;
font-size: 32rpx;
font-weight: 600;
}
</style>