AI-accounting-soft-uniApp/pages/login/login.vue

685 lines
15 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-container">
<!-- 背景装饰圆球 -->
<view class="bg-ball ball-1"></view>
<view class="bg-ball ball-2"></view>
<view class="bg-ball ball-3"></view>
<view class="bg-ball ball-4"></view>
<!-- Logo区域 -->
<view class="login-header fade-in">
<image class="app-logo" src="/static/images/logo.png" mode="aspectFit"></image>
<view class="title-decoration">
<view class="deco-line left"></view>
<text class="app-title">强宝爱记账</text>
<view class="deco-line right"></view>
</view>
<text class="app-subtitle">智能记账轻松管理</text>
</view>
<!-- 登录表单卡片 -->
<view class="login-form glass-card">
<!-- 用户名输入框 -->
<view class="form-item">
<view :class="['input-wrapper', focusField === 'username' ? 'focused' : '']">
<text class="input-icon">👤</text>
<input
v-model="form.username"
placeholder="请输入用户名"
class="input"
maxlength="20"
@focus="focusField = 'username'"
@blur="focusField = ''"
/>
</view>
</view>
<!-- 密码输入框 -->
<view class="form-item">
<view :class="['input-wrapper', focusField === 'password' ? 'focused' : '']">
<text class="input-icon">🔒</text>
<input
v-model="form.password"
placeholder="请输入密码"
:type="showPassword ? 'text' : 'password'"
class="input"
maxlength="20"
@focus="focusField = 'password'"
@blur="focusField = ''"
/>
<text class="eye-icon" @click="togglePassword">
{{ showPassword ? '👁️' : '👁️‍🗨️' }}
</text>
</view>
</view>
<!-- 昵称输入框(注册时) -->
<view v-if="isRegister" class="form-item">
<view :class="['input-wrapper', focusField === 'nickname' ? 'focused' : '']">
<text class="input-icon">✨</text>
<input
v-model="form.nickname"
placeholder="请输入昵称(可选)"
class="input"
maxlength="20"
@focus="focusField = 'nickname'"
@blur="focusField = ''"
/>
</view>
</view>
<!-- 记住密码和忘记密码 -->
<view v-if="!isRegister" class="options-row">
<view class="checkbox-wrapper" @click="rememberMe = !rememberMe">
<view :class="['checkbox', rememberMe ? 'checked' : '']">
<text v-if="rememberMe" class="check-icon">✓</text>
</view>
<text class="checkbox-label">记住密码</text>
</view>
</view>
<!-- 协议勾选(注册时) -->
<view v-if="isRegister" class="options-row">
<view class="checkbox-wrapper" @click="agreeProtocol = !agreeProtocol">
<view :class="['checkbox', agreeProtocol ? 'checked' : '']">
<text v-if="agreeProtocol" class="check-icon">✓</text>
</view>
<text class="checkbox-label">我已阅读并同意用户协议和隐私政策</text>
</view>
</view>
<!-- 登录/注册按钮 -->
<button class="login-btn gradient-btn" @click="handleSubmit" :loading="loading" :disabled="loading">
<view v-if="!loading" class="btn-content">
{{ isRegister ? '🎉 立即注册' : '🚀 立即登录' }}
</view>
<view v-else class="btn-loading">
<view class="loading-spinner"></view>
<text>{{ isRegister ? '注册中...' : '登录中...' }}</text>
</view>
</button>
<!-- 切换模式 -->
<view class="switch-mode" @click="switchMode">
<text>{{ isRegister ? '已有账号?去登录 →' : '没有账号?去注册 →' }}</text>
</view>
</view>
</view>
</template>
<script setup>
import { ref, onMounted } from 'vue'
import { register, login } from '../../api/auth'
import { useUserStore } from '../../store/user'
import { encryptPassword, decryptPassword } from '../../utils/crypto'
const userStore = useUserStore()
const isRegister = ref(false)
const loading = ref(false)
const showPassword = ref(false)
const focusField = ref('')
const rememberMe = ref(false)
const agreeProtocol = ref(false)
const form = ref({
username: '',
password: '',
nickname: ''
})
// 从本地存储加载记住的密码
onMounted(() => {
try {
const savedUsername = uni.getStorageSync('saved_username')
const encryptedPassword = uni.getStorageSync('saved_password')
if (savedUsername && encryptedPassword) {
form.value.username = savedUsername
// 解密密码
form.value.password = decryptPassword(encryptedPassword)
rememberMe.value = true
}
} catch (error) {
console.error('加载保存的密码失败:', error)
// 清除可能损坏的数据
uni.removeStorageSync('saved_username')
uni.removeStorageSync('saved_password')
}
})
// 切换密码显示/隐藏
const togglePassword = () => {
showPassword.value = !showPassword.value
}
// 切换登录/注册模式
const switchMode = () => {
isRegister.value = !isRegister.value
form.value = {
username: '',
password: '',
nickname: ''
}
agreeProtocol.value = false
}
// 提交表单
const handleSubmit = async () => {
if (!form.value.username || !form.value.password) {
uni.showToast({
title: '请输入用户名和密码',
icon: 'none'
})
return
}
if (isRegister.value && form.value.password.length < 6) {
uni.showToast({
title: '密码长度至少6位',
icon: 'none'
})
return
}
// 注册时检查协议
if (isRegister.value && !agreeProtocol.value) {
uni.showToast({
title: '请先阅读并同意用户协议',
icon: 'none'
})
return
}
loading.value = true
try {
let response
if (isRegister.value) {
response = await register({
username: form.value.username,
password: form.value.password,
nickname: form.value.nickname || form.value.username
})
} else {
response = await login({
username: form.value.username,
password: form.value.password
})
}
// 保存token和用户信息
userStore.login(response.token, {
username: response.username,
nickname: response.nickname
})
// 记住密码
if (!isRegister.value && rememberMe.value) {
uni.setStorageSync('saved_username', form.value.username)
// 加密密码后保存
uni.setStorageSync('saved_password', encryptPassword(form.value.password))
} else if (!rememberMe.value) {
uni.removeStorageSync('saved_username')
uni.removeStorageSync('saved_password')
}
uni.showToast({
title: isRegister.value ? '🎉 注册成功' : '🚀 登录成功',
icon: 'success'
})
// 跳转到首页
setTimeout(() => {
uni.switchTab({
url: '/pages/index/index'
})
}, 1500)
} catch (error) {
console.error('登录失败:', error)
uni.showToast({
title: error.message || '操作失败',
icon: 'none'
})
} finally {
loading.value = false
}
}
</script>
<style scoped>
/* 主容器 - 卡通背景 */
.login-container {
min-height: 100vh;
background: linear-gradient(135deg, #FFE5CC 0%, #FFD4A8 50%, #FFC08A 100%);
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 40rpx;
position: relative;
overflow: hidden;
}
/* 背景装饰圆球 */
.bg-ball {
position: absolute;
border-radius: 50%;
opacity: 0.3;
filter: blur(80rpx);
animation: float 6s ease-in-out infinite;
}
.ball-1 {
width: 400rpx;
height: 400rpx;
background: linear-gradient(135deg, #C8956E, #FFB6C1);
top: -100rpx;
left: -100rpx;
animation-delay: 0s;
}
.ball-2 {
width: 300rpx;
height: 300rpx;
background: linear-gradient(135deg, #F5D59E, #FFD700);
top: 200rpx;
right: -50rpx;
animation-delay: 1s;
}
.ball-3 {
width: 350rpx;
height: 350rpx;
background: linear-gradient(135deg, #FFB6C1, #C8956E);
bottom: 100rpx;
left: 50rpx;
animation-delay: 2s;
}
.ball-4 {
width: 250rpx;
height: 250rpx;
background: linear-gradient(135deg, #FFD700, #F5D59E);
bottom: -50rpx;
right: 100rpx;
animation-delay: 3s;
}
@keyframes float {
0%, 100% {
transform: translate(0, 0) scale(1);
}
25% {
transform: translate(20rpx, -30rpx) scale(1.1);
}
50% {
transform: translate(-20rpx, 20rpx) scale(0.9);
}
75% {
transform: translate(30rpx, 10rpx) scale(1.05);
}
}
/* Logo区域 */
.login-header {
text-align: center;
margin-bottom: 60rpx;
position: relative;
z-index: 10;
}
/* 入场动画 */
.fade-in {
animation: fadeInDown 0.8s ease-out;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-50rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* Logo图片 */
.app-logo {
width: 280rpx;
height: 280rpx;
margin-bottom: 30rpx;
filter: drop-shadow(0 8rpx 20rpx rgba(93, 64, 55, 0.3));
animation: bounce 2s ease-in-out infinite;
}
@keyframes bounce {
0%, 100% {
transform: translateY(0);
}
50% {
transform: translateY(-20rpx);
}
}
/* 标题装饰容器 */
.title-decoration {
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
margin-bottom: 20rpx;
}
/* 装饰线条 */
.deco-line {
width: 60rpx;
height: 6rpx;
background: linear-gradient(90deg, #C8956E, #FFD700);
border-radius: 10rpx;
position: relative;
}
.deco-line::after {
content: '';
position: absolute;
width: 12rpx;
height: 12rpx;
background: #FFD700;
border-radius: 50%;
top: 50%;
transform: translateY(-50%);
box-shadow: 0 0 10rpx #FFD700;
}
.deco-line.left::after {
left: -6rpx;
}
.deco-line.right::after {
right: -6rpx;
}
/* 标题文字 */
.app-title {
display: block;
font-size: 56rpx;
font-weight: bold;
color: #5D4037;
text-shadow:
3rpx 3rpx 0 #FFD700,
-1rpx -1rpx 0 #FFD700,
1rpx -1rpx 0 #FFD700,
-1rpx 1rpx 0 #FFD700,
0 8rpx 20rpx rgba(255, 215, 0, 0.4);
letter-spacing: 4rpx;
}
/* 副标题 */
.app-subtitle {
display: block;
font-size: 28rpx;
color: #8D6E63;
font-weight: 500;
}
/* 表单卡片 - 毛玻璃效果 */
.login-form {
width: 100%;
max-width: 600rpx;
position: relative;
z-index: 10;
}
.glass-card {
background: rgba(255, 255, 255, 0.75);
backdrop-filter: blur(20rpx);
border-radius: 40rpx;
padding: 60rpx 50rpx;
box-shadow:
0 20rpx 60rpx rgba(93, 64, 55, 0.15),
0 0 0 2rpx rgba(255, 255, 255, 0.5),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.8);
border: 3rpx solid #5D4037;
animation: slideUp 0.8s ease-out 0.3s both;
}
@keyframes slideUp {
from {
opacity: 0;
transform: translateY(100rpx);
}
to {
opacity: 1;
transform: translateY(0);
}
}
/* 表单项 */
.form-item {
margin-bottom: 30rpx;
}
/* 输入框容器 */
.input-wrapper {
position: relative;
display: flex;
align-items: center;
background: rgba(255, 255, 255, 0.9);
border-radius: 20rpx;
padding: 0 30rpx;
height: 96rpx;
border: 3rpx solid #C8956E;
box-shadow: 0 4rpx 15rpx rgba(200, 149, 110, 0.2);
transition: all 0.3s ease;
}
.input-wrapper.focused {
border-color: #FFD700;
box-shadow:
0 0 0 4rpx rgba(255, 215, 0, 0.3),
0 8rpx 25rpx rgba(255, 215, 0, 0.4);
transform: translateY(-2rpx);
}
/* 输入框图标 */
.input-icon {
font-size: 40rpx;
margin-right: 20rpx;
filter: drop-shadow(0 2rpx 4rpx rgba(0, 0, 0, 0.1));
}
/* 输入框 */
.input {
flex: 1;
height: 100%;
font-size: 28rpx;
color: #5D4037;
background: transparent;
border: none;
}
.input::placeholder {
color: #BCAAA4;
}
/* 眼睛图标 */
.eye-icon {
font-size: 36rpx;
padding: 10rpx;
cursor: pointer;
user-select: none;
transition: transform 0.2s;
}
.eye-icon:active {
transform: scale(0.9);
}
/* 选项行 */
.options-row {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 30rpx;
}
/* 复选框容器 */
.checkbox-wrapper {
display: flex;
align-items: center;
cursor: pointer;
user-select: none;
}
/* 复选框 */
.checkbox {
width: 40rpx;
height: 40rpx;
border: 3rpx solid #C8956E;
border-radius: 10rpx;
background: #fff;
display: flex;
align-items: center;
justify-content: center;
margin-right: 15rpx;
transition: all 0.3s ease;
}
.checkbox.checked {
background: linear-gradient(135deg, #FFD700, #F5D59E);
border-color: #FFD700;
box-shadow: 0 0 10rpx rgba(255, 215, 0, 0.5);
}
.check-icon {
color: #5D4037;
font-size: 28rpx;
font-weight: bold;
}
/* 复选框标签 */
.checkbox-label {
font-size: 26rpx;
color: #6D4C41;
}
/* 渐变按钮 */
.login-btn {
width: 100%;
height: 100rpx;
background: linear-gradient(135deg, #C8956E 0%, #F5D59E 50%, #FFD700 100%);
background-size: 200% 200%;
color: #5D4037;
border-radius: 50rpx;
font-size: 32rpx;
font-weight: bold;
margin-top: 40rpx;
border: 4rpx solid #5D4037;
box-shadow:
0 10rpx 30rpx rgba(200, 149, 110, 0.4),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.6);
position: relative;
overflow: hidden;
transition: all 0.3s ease;
}
.login-btn::before {
content: '';
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(90deg,
transparent,
rgba(255, 255, 255, 0.4),
transparent
);
transition: left 0.5s;
}
.login-btn:active::before {
left: 100%;
}
.login-btn:active {
transform: scale(0.98);
box-shadow:
0 5rpx 15rpx rgba(200, 149, 110, 0.3),
inset 0 2rpx 4rpx rgba(255, 255, 255, 0.6);
}
.login-btn[disabled] {
opacity: 0.7;
}
/* 按钮内容 */
.btn-content {
display: flex;
align-items: center;
justify-content: center;
gap: 10rpx;
}
/* 加载动画 */
.btn-loading {
display: flex;
align-items: center;
justify-content: center;
gap: 20rpx;
}
.loading-spinner {
width: 40rpx;
height: 40rpx;
border: 4rpx solid rgba(93, 64, 55, 0.2);
border-top-color: #5D4037;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 切换模式 */
.switch-mode {
text-align: center;
margin-top: 40rpx;
padding: 20rpx;
}
.switch-mode text {
color: #6D4C41;
font-size: 28rpx;
font-weight: 500;
position: relative;
cursor: pointer;
transition: color 0.3s;
}
.switch-mode text::after {
content: '';
position: absolute;
bottom: -5rpx;
left: 0;
width: 0;
height: 3rpx;
background: linear-gradient(90deg, #C8956E, #FFD700);
transition: width 0.3s;
}
.switch-mode:active text {
color: #FFD700;
}
.switch-mode:active text::after {
width: 100%;
}
</style>