685 lines
15 KiB
Vue
685 lines
15 KiB
Vue
<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>
|