first commit

This commit is contained in:
2025-10-27 23:13:08 +08:00
commit 476c5e7658
2968 changed files with 49547 additions and 0 deletions
+927
View File
@@ -0,0 +1,927 @@
<template>
<view class="login-page">
<!-- 背景装饰 -->
<view class="bg-decoration">
<view class="gradient-orb orb-1"></view>
<view class="gradient-orb orb-2"></view>
<view class="gradient-orb orb-3"></view>
<view class="floating-shapes">
<view class="shape shape-1"></view>
<view class="shape shape-2"></view>
<view class="shape shape-3"></view>
</view>
</view>
<!-- 主要内容 -->
<view class="login-container">
<!-- 头部区域 -->
<view class="header-section">
<view class="logo-container">
<view class="logo-wrapper">
<image src="/static/logo.png" class="logo" mode="aspectFit"></image>
<view class="logo-ring"></view>
</view>
</view>
<view class="welcome-content">
<text class="app-title">企业办公系统</text>
<text class="welcome-subtitle">欢迎回来开始您的工作之旅</text>
</view>
</view>
<!-- 登录卡片 -->
<view class="login-card">
<view class="card-header">
<text class="card-title">登录账户</text>
<view class="card-subtitle">请输入您的登录信息</view>
</view>
<view class="form-container">
<!-- 用户名输入 -->
<view class="input-field-group">
<view class="input-container" :class="{ 'focused': usernameFocused, 'error': usernameError }">
<view class="input-icon-wrapper">
<i class="fas fa-user input-icon"></i>
</view>
<input
v-model="form.username"
placeholder="用户名"
class="input"
type="text"
@focus="handleUsernameFocus"
@blur="handleUsernameBlur"
@input="clearUsernameError">
<view class="input-border"></view>
</view>
<text class="error-text" v-if="usernameError">{{ usernameError }}</text>
</view>
<!-- 密码输入 -->
<view class="input-field-group">
<view class="input-container" :class="{ 'focused': passwordFocused, 'error': passwordError }">
<view class="input-icon-wrapper">
<i class="fas fa-lock input-icon"></i>
</view>
<input
v-model="form.password"
placeholder="密码"
class="input"
:type="showPassword ? 'text' : 'password'"
@focus="handlePasswordFocus"
@blur="handlePasswordBlur"
@input="clearPasswordError">
<view class="password-toggle" @click="togglePassword">
<i :class="showPassword ? 'fas fa-eye-slash' : 'fas fa-eye'"></i>
</view>
<view class="input-border"></view>
</view>
<text class="error-text" v-if="passwordError">{{ passwordError }}</text>
</view>
<!-- 选项区域 -->
<view class="options-row">
<view class="remember-section" @click="toggleRemember">
<view class="custom-checkbox" :class="{ 'checked': rememberMe }">
<i class="fas fa-check" v-if="rememberMe"></i>
</view>
<text class="remember-text">记住我</text>
</view>
<text class="forgot-link" @click="handleForgotPassword">忘记密码</text>
</view>
<!-- 登录按钮 -->
<button
:disabled="loading || !isFormValid"
class="login-button"
:class="{ 'loading': loading, 'disabled': !isFormValid }"
@click="handleLogin">
<view class="button-content">
<view class="button-icon" v-if="!loading">
<i class="fas fa-arrow-right"></i>
</view>
<view class="loading-spinner" v-if="loading">
<i class="fas fa-spinner fa-spin"></i>
</view>
<text class="button-text">{{ loading ? '登录中...' : '立即登录' }}</text>
</view>
<view class="button-shine" v-if="!loading"></view>
</button>
</view>
<!-- 测试提示 -->
<view class="test-tips">
<view class="tips-icon">
<i class="fas fa-info-circle"></i>
</view>
<text class="tips-text">测试账号test / 123456</text>
</view>
</view>
</view>
</view>
</template>
<script>
import { userApi } from '../../src/api/index.js'
import { useAuthStore } from '../../src/store/authStore.js'
import { redirectAfterLogin } from '../../src/utils/routeGuard.js'
export default {
data() {
return {
form: {
username: '',
password: ''
},
loading: false,
usernameFocused: false,
passwordFocused: false,
showPassword: false,
rememberMe: false,
usernameError: '',
passwordError: ''
}
},
computed: {
isFormValid() {
return this.form.username.trim() && this.form.password.trim()
}
},
methods: {
async handleLogin() {
this.loading = true;
try {
const res = await userApi.login(this.form);
console.log('登录响应:', res);
// 根据你的后端返回结构调整
const token = res.accessToken || res.token;
if (!token) {
throw new Error('登录失败:未获取到访问令牌');
}
const authStore = useAuthStore();
// 从响应中获取用户信息
const userInfo = res.user || {
username: this.form.username,
id: res.id
};
authStore.login(userInfo, token);
uni.showToast({
title: '登录成功',
icon: 'success'
});
// 延迟跳转,让用户看到成功提示
setTimeout(() => {
redirectAfterLogin();
}, 500);
} catch (e) {
this.loading = false;
console.error('登录错误:', e);
// 显示错误信息
const errorMessage = e.message || e.msg || '登录失败,请检查网络连接';
uni.showToast({
title: errorMessage,
icon: 'none',
duration: 3000
});
}
},
// async handleLogin2() {
// if(!this.form.username || !this.form.password) {
// uni.showToast({
// title: '请输入用户名和密码',
// icon: 'none'
// });
// return;
// }
// this.loading = true;
// try {
// // 模拟登录API请求
// // 实际项目中应该调用真实的后端API
// const mockLogin = () => {
// return new Promise((resolve) => {
// setTimeout(() => {
// // 模拟登录成功
// if (this.form.username === 'admin' && this.form.password === '123456') {
// resolve({
// code: 0,
// message: '登录成功',
// data: {
// token: 'mock_token_' + Date.now(),
// userInfo: {
// id: 1,
// username: this.form.username,
// name: '管理员',
// avatar: '/static/logo.png',
// department: '技术部',
// role: 'admin'
// }
// }
// });
// } else {
// resolve({
// code: 1,
// message: '用户名或密码错误'
// });
// }
// }, 1000);
// });
// };
// const result = await mockLogin();
// if (result.code === 0) {
// // 使用 Pinia store 管理登录状态
// const authStore = useAuthStore();
// authStore.login(result.data.userInfo, result.data.token);
// uni.showToast({
// title: '登录成功',
// icon: 'success'
// });
// // 延迟跳转,让用户看到成功提示
// setTimeout(() => {
// redirectAfterLogin();
// }, 500);
// } else {
// uni.showToast({
// title: result.message || '登录失败',
// icon: 'none'
// });
// }
// } catch (error) {
// console.error('登录错误:', error);
// uni.showToast({
// title: '网络异常,请稍后重试',
// icon: 'none'
// });
// }
// this.loading = false;
// },
// 切换密码显示
togglePassword() {
this.showPassword = !this.showPassword
},
// 切换记住我
toggleRemember() {
this.rememberMe = !this.rememberMe
},
// 忘记密码
handleForgotPassword() {
uni.showToast({
title: '请联系管理员重置密码',
icon: 'none'
})
},
// 处理用户名输入框焦点
handleUsernameFocus() {
this.usernameFocused = true;
this.clearUsernameError();
},
handleUsernameBlur() {
this.usernameFocused = false;
this.validateUsername();
},
// 处理密码输入框焦点
handlePasswordFocus() {
this.passwordFocused = true;
this.clearPasswordError();
},
handlePasswordBlur() {
this.passwordFocused = false;
this.validatePassword();
},
// 清除用户名错误
clearUsernameError() {
this.usernameError = '';
},
// 清除密码错误
clearPasswordError() {
this.passwordError = '';
},
// 验证用户名
validateUsername() {
if (!this.form.username.trim()) {
this.usernameError = '请输入用户名';
return false;
}
return true;
},
// 验证密码
validatePassword() {
if (!this.form.password.trim()) {
this.passwordError = '请输入密码';
return false;
}
if (this.form.password.length < 6) {
this.passwordError = '密码至少6位';
return false;
}
return true;
}
}
}
</script>
<style scoped>
/* 主容器 */
.login-page {
min-height: 100vh;
background: var(--gradient-primary);
position: relative;
overflow: hidden;
display: flex;
align-items: center;
justify-content: center;
padding: 40rpx 20rpx;
}
/* 背景装饰 */
.bg-decoration {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
pointer-events: none;
z-index: 1;
}
.gradient-orb {
position: absolute;
border-radius: 50%;
background: linear-gradient(45deg, rgba(255, 255, 255, 0.1), rgba(255, 255, 255, 0.05));
animation: float 8s ease-in-out infinite;
filter: blur(1rpx);
}
.orb-1 {
width: 300rpx;
height: 300rpx;
top: -150rpx;
left: -150rpx;
animation-delay: 0s;
}
.orb-2 {
width: 200rpx;
height: 200rpx;
top: 20%;
right: -100rpx;
animation-delay: 3s;
}
.orb-3 {
width: 150rpx;
height: 150rpx;
bottom: 10%;
left: 10%;
animation-delay: 6s;
}
.floating-shapes {
position: absolute;
width: 100%;
height: 100%;
}
.shape {
position: absolute;
background: rgba(255, 255, 255, 0.05);
animation: float 6s ease-in-out infinite;
}
.shape-1 {
width: 60rpx;
height: 60rpx;
border-radius: 50%;
top: 30%;
left: 20%;
animation-delay: 1s;
}
.shape-2 {
width: 40rpx;
height: 40rpx;
border-radius: 8rpx;
top: 60%;
right: 30%;
animation-delay: 4s;
}
.shape-3 {
width: 80rpx;
height: 80rpx;
border-radius: 50%;
bottom: 30%;
right: 20%;
animation-delay: 2s;
}
@keyframes float {
0%, 100% {
transform: translateY(0px) rotate(0deg) scale(1);
opacity: 0.7;
}
50% {
transform: translateY(-30px) rotate(180deg) scale(1.1);
opacity: 1;
}
}
/* 登录容器 */
.login-container {
width: 100%;
max-width: 600rpx;
position: relative;
z-index: 2;
}
/* 头部区域 */
.header-section {
text-align: center;
margin-bottom: 60rpx;
}
.logo-container {
margin-bottom: 40rpx;
}
.logo-wrapper {
position: relative;
display: inline-block;
}
.logo {
width: 100rpx;
height: 100rpx;
border-radius: 20rpx;
box-shadow: 0 8rpx 32rpx rgba(0, 0, 0, 0.2);
position: relative;
z-index: 2;
}
.logo-ring {
position: absolute;
top: -8rpx;
left: -8rpx;
right: -8rpx;
bottom: -8rpx;
border: 2rpx solid rgba(255, 255, 255, 0.3);
border-radius: 28rpx;
animation: pulse 2s ease-in-out infinite;
}
@keyframes pulse {
0%, 100% {
transform: scale(1);
opacity: 0.7;
}
50% {
transform: scale(1.05);
opacity: 1;
}
}
.welcome-content {
color: #ffffff;
}
.app-title {
display: block;
font-size: 48rpx;
font-weight: 700;
margin-bottom: 16rpx;
text-shadow: 0 2rpx 8rpx rgba(0, 0, 0, 0.3);
letter-spacing: 1rpx;
}
.welcome-subtitle {
display: block;
font-size: 28rpx;
opacity: 0.9;
font-weight: 400;
}
/* 登录卡片 */
.login-card {
background: rgba(255, 255, 255, 0.95);
border-radius: 24rpx;
padding: 50rpx 40rpx;
box-shadow: var(--shadow-lg);
backdrop-filter: blur(20rpx);
border: 1rpx solid rgba(255, 255, 255, 0.2);
position: relative;
overflow: hidden;
}
.login-card::before {
content: '';
position: absolute;
top: 0;
left: 0;
right: 0;
height: 4rpx;
background: var(--gradient-primary);
}
.card-header {
text-align: center;
margin-bottom: 40rpx;
}
.card-title {
font-size: 36rpx;
font-weight: 700;
color: var(--text-color);
margin-bottom: 8rpx;
display: block;
}
.card-subtitle {
font-size: 26rpx;
color: var(--text-secondary);
font-weight: 400;
}
/* 表单容器 */
.form-container {
margin-bottom: 30rpx;
}
/* 输入框组 */
.input-field-group {
margin-bottom: 32rpx;
}
.input-container {
position: relative;
display: flex;
align-items: center;
background: var(--gray-lighter);
border-radius: 16rpx;
border: 2rpx solid var(--border);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
overflow: hidden;
min-height: 88rpx;
}
.input-container.focused {
border-color: var(--primary-color);
background: var(--white);
box-shadow: 0 0 0 4rpx var(--info-light);
transform: translateY(-2rpx);
}
.input-container.error {
border-color: var(--error);
background: var(--error-light);
}
.input-icon-wrapper {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
margin-left: 20rpx;
}
.input-icon {
color: var(--text-muted);
font-size: 28rpx;
transition: color 0.3s ease;
}
.input-container.focused .input-icon {
color: var(--primary-color);
}
.input-container.error .input-icon {
color: var(--error);
}
.input {
flex: 1;
padding: 24rpx 20rpx;
border: none;
background: transparent;
font-size: 30rpx;
color: var(--text-color);
outline: none;
font-weight: 500;
}
.input::placeholder {
color: var(--text-muted);
font-weight: 400;
}
.password-toggle {
width: 60rpx;
height: 60rpx;
display: flex;
align-items: center;
justify-content: center;
color: var(--text-muted);
font-size: 28rpx;
cursor: pointer;
transition: color 0.3s ease;
margin-right: 20rpx;
}
.password-toggle:hover {
color: var(--primary-color);
}
.input-border {
position: absolute;
bottom: 0;
left: 0;
right: 0;
height: 2rpx;
background: var(--gradient-primary);
transform: scaleX(0);
transition: transform 0.3s ease;
}
.input-container.focused .input-border {
transform: scaleX(1);
}
.error-text {
font-size: 24rpx;
color: var(--error);
margin-top: 8rpx;
margin-left: 20rpx;
display: block;
}
/* 选项区域 */
.options-row {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 40rpx;
}
.remember-section {
display: flex;
align-items: center;
cursor: pointer;
}
.custom-checkbox {
width: 36rpx;
height: 36rpx;
border: 2rpx solid var(--border);
border-radius: 8rpx;
margin-right: 16rpx;
display: flex;
align-items: center;
justify-content: center;
transition: all 0.3s ease;
background: var(--white);
}
.custom-checkbox.checked {
background: var(--primary-color);
border-color: var(--primary-color);
color: var(--white);
}
.custom-checkbox i {
font-size: 20rpx;
}
.remember-text {
font-size: 28rpx;
color: var(--text-color);
font-weight: 500;
}
.forgot-link {
font-size: 28rpx;
color: var(--primary-color);
font-weight: 500;
cursor: pointer;
transition: color 0.3s ease;
}
.forgot-link:hover {
color: var(--primary-dark);
}
/* 登录按钮 */
.login-button {
width: 100%;
height: 88rpx;
background: var(--gradient-primary);
color: var(--white);
border: none;
border-radius: 16rpx;
font-size: 32rpx;
font-weight: 600;
display: flex;
align-items: center;
justify-content: center;
position: relative;
overflow: hidden;
box-shadow: var(--shadow-lg);
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
cursor: pointer;
}
.login-button:hover:not(:disabled) {
transform: translateY(-2rpx);
box-shadow: var(--shadow-lg);
}
.login-button:active:not(:disabled) {
transform: translateY(0);
box-shadow: var(--shadow-md);
}
.login-button.disabled {
opacity: 0.5;
transform: none;
cursor: not-allowed;
background: var(--gray);
color: var(--text-muted);
box-shadow: none;
}
.login-button.loading {
pointer-events: none;
}
.button-content {
display: flex;
align-items: center;
gap: 16rpx;
position: relative;
z-index: 2;
}
.button-icon {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.loading-spinner {
width: 32rpx;
height: 32rpx;
display: flex;
align-items: center;
justify-content: center;
font-size: 20rpx;
}
.button-text {
font-weight: 600;
}
.button-shine {
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.2), transparent);
animation: shine 2s infinite;
}
@keyframes shine {
0% { left: -100%; }
100% { left: 100%; }
}
/* 测试提示 */
.test-tips {
display: flex;
align-items: center;
justify-content: center;
gap: 12rpx;
margin-top: 30rpx;
padding: 20rpx;
background: var(--info-light);
border-radius: 12rpx;
border: 1rpx solid var(--primary-light);
}
.tips-icon {
color: var(--primary-color);
font-size: 24rpx;
}
.tips-text {
font-size: 24rpx;
color: var(--text-color);
font-weight: 500;
}
/* 响应式设计 */
@media screen and (max-width: 750rpx) {
.login-container {
max-width: 95%;
}
.login-card {
padding: 40rpx 30rpx;
}
.app-title {
font-size: 42rpx;
}
.welcome-subtitle {
font-size: 26rpx;
}
.card-title {
font-size: 32rpx;
}
.card-subtitle {
font-size: 24rpx;
}
}
/* 深色模式适配 - 保持亮色主题 */
@media (prefers-color-scheme: dark) {
.login-page {
background: var(--gradient-primary);
}
.login-card {
background: rgba(255, 255, 255, 0.95);
border-color: rgba(255, 255, 255, 0.2);
}
.card-title {
color: var(--text-color);
}
.card-subtitle {
color: var(--text-secondary);
}
.input-container {
background: var(--gray-lighter);
border-color: var(--border);
}
.input-container.focused {
background: var(--white);
}
.input {
color: var(--text-color);
}
.input::placeholder {
color: var(--text-muted);
}
.remember-text {
color: var(--text-color);
}
.forgot-link {
color: var(--primary-color);
}
.test-tips {
background: var(--info-light);
border-color: var(--primary-light);
}
.tips-text {
color: var(--text-color);
}
}
</style>