492 lines
10 KiB
Vue
492 lines
10 KiB
Vue
<template>
|
||
<view class="page">
|
||
<!-- 背景装饰 -->
|
||
<view class="bg-decor">
|
||
<view class="shape shape-1" />
|
||
<view class="shape shape-2" />
|
||
<view class="shape shape-3" />
|
||
</view>
|
||
|
||
<!-- 登录卡片 -->
|
||
<view class="login-card">
|
||
<!-- 顶部品牌区 -->
|
||
<view class="brand-header">
|
||
<view class="welcome-info">
|
||
<view class="welcome-title">欢迎回来</view>
|
||
<view class="welcome-desc">请输入您的账号密码进行登录</view>
|
||
</view>
|
||
</view>
|
||
|
||
<!-- 表单区域 -->
|
||
<view class="form-area">
|
||
<!-- 账号输入框 -->
|
||
<view class="input-wrap" :class="{ focused: accountFocused }">
|
||
<input
|
||
v-model="account"
|
||
class="input"
|
||
type="text"
|
||
placeholder="请输入账号"
|
||
@focus="accountFocused = true"
|
||
@blur="accountFocused = false"
|
||
>
|
||
</view>
|
||
|
||
<!-- 密码输入框 -->
|
||
<view class="input-wrap" :class="{ focused: passwordFocused }">
|
||
<input
|
||
v-model="password"
|
||
class="input"
|
||
:type="showPassword ? 'text' : 'password'"
|
||
placeholder="请输入密码"
|
||
@focus="passwordFocused = true"
|
||
@blur="passwordFocused = false"
|
||
>
|
||
<image
|
||
v-if="password"
|
||
class="pwd-toggle"
|
||
:src="showPassword ? '/static/icons/preview-open.png' : '/static/icons/preview-close-one.png'"
|
||
mode="aspectFit"
|
||
@tap="togglePassword"
|
||
/>
|
||
</view>
|
||
|
||
<!-- 登录按钮 -->
|
||
<button
|
||
class="btn"
|
||
:disabled="loading"
|
||
@tap="submit"
|
||
>
|
||
{{ loading ? '登录中...' : '登录' }}
|
||
</button>
|
||
</view>
|
||
</view>
|
||
</view>
|
||
</template>
|
||
|
||
<script setup lang="ts">
|
||
import { HOME_PATH, isTabBarPath, LOGIN_PATH, removeQueryString } from '@/router';
|
||
import { setToken } from '@/utils/auth';
|
||
import { login } from '@/api/auth';
|
||
|
||
const account = ref<string>('');
|
||
const password = ref<string>('');
|
||
const accountFocused = ref<boolean>(false);
|
||
const passwordFocused = ref<boolean>(false);
|
||
const showPassword = ref<boolean>(false);
|
||
const loading = ref<boolean>(false);
|
||
let redirect = HOME_PATH;
|
||
|
||
const isFormValid = computed(() => {
|
||
return account.value.trim().length > 0 && password.value.trim().length > 0;
|
||
});
|
||
|
||
function togglePassword() {
|
||
showPassword.value = !showPassword.value;
|
||
}
|
||
|
||
async function submit() {
|
||
if (!isFormValid.value) {
|
||
if (!account.value.trim()) {
|
||
uni.$u.toast('请输入账号');
|
||
return;
|
||
}
|
||
if (!password.value.trim()) {
|
||
uni.$u.toast('请输入密码');
|
||
return;
|
||
}
|
||
}
|
||
|
||
if (loading.value) return;
|
||
|
||
loading.value = true;
|
||
|
||
try {
|
||
// 调用登录接口(禁用拦截器的自动 toast,由页面层统一处理)
|
||
const res = await login({
|
||
username: account.value.trim(),
|
||
password: password.value.trim(),
|
||
});
|
||
|
||
// 保存 access_token
|
||
setToken(res.access_token);
|
||
|
||
// 保存 refresh_token 到本地存储
|
||
uni.setStorageSync('refresh_token', res.refresh_token);
|
||
|
||
// 保存用户信息到本地存储
|
||
uni.setStorageSync('user_info', res.user);
|
||
|
||
// 调试日志:查看用户信息
|
||
console.log('登录成功,用户信息:', res.user);
|
||
|
||
// 验证用户类型 (只允许 3=代理 和 4=企业 登录)
|
||
if (res.user.user_type !== 3 && res.user.user_type !== 4) {
|
||
const typeNames: Record<number, string> = {
|
||
1: '超级管理员',
|
||
2: '平台用户',
|
||
};
|
||
const typeName = typeNames[res.user.user_type] || '未知';
|
||
|
||
uni.showToast({
|
||
title: `${typeName}账号无法登录此系统`,
|
||
icon: 'none',
|
||
duration: 2500,
|
||
});
|
||
// 清除已保存的 token
|
||
setToken('');
|
||
uni.removeStorageSync('refresh_token');
|
||
uni.removeStorageSync('user_info');
|
||
return;
|
||
}
|
||
|
||
uni.showToast({
|
||
title: '登录成功',
|
||
icon: 'success',
|
||
duration: 1500,
|
||
});
|
||
|
||
setTimeout(() => {
|
||
// 代理商和企业用户都跳转到首页(首页会根据用户类型显示不同功能)
|
||
uni.$u.route({
|
||
type: 'redirectTo',
|
||
url: '/pages/agent-system/home/index',
|
||
});
|
||
}, 1500);
|
||
} catch (error: any) {
|
||
console.error('登录失败:', error);
|
||
// 拦截器已经显示了后端返回的错误信息,这里不需要再次提示
|
||
// 如果需要额外处理,可以访问 error.data.msg 获取错误信息
|
||
} finally {
|
||
loading.value = false;
|
||
}
|
||
}
|
||
|
||
onLoad((options: any) => {
|
||
if (options.redirect && removeQueryString(options.redirect) !== LOGIN_PATH) {
|
||
redirect = decodeURIComponent(options.redirect);
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.page {
|
||
position: fixed;
|
||
inset: 0;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
padding: 32rpx;
|
||
background: linear-gradient(135deg, #fafafa 0%, #f5f5f5 100%);
|
||
overflow: hidden;
|
||
animation: bgFade 0.8s ease-out;
|
||
}
|
||
|
||
@keyframes bgFade {
|
||
0% {
|
||
opacity: 0;
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
}
|
||
}
|
||
|
||
/* 背景装饰 - 小米风格 */
|
||
.bg-decor {
|
||
position: absolute;
|
||
inset: 0;
|
||
pointer-events: none;
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
width: 100%;
|
||
height: 100%;
|
||
background:
|
||
linear-gradient(90deg, transparent 0%, rgba(22, 119, 255, 0.02) 50%, transparent 100%);
|
||
animation: scanline 8s linear infinite;
|
||
}
|
||
|
||
.shape {
|
||
position: absolute;
|
||
}
|
||
|
||
.shape-1 {
|
||
width: 800rpx;
|
||
height: 800rpx;
|
||
top: -300rpx;
|
||
right: -200rpx;
|
||
background: radial-gradient(circle, rgba(22, 119, 255, 0.08), transparent 60%);
|
||
border-radius: 50%;
|
||
animation: pulse1 10s infinite ease-in-out;
|
||
}
|
||
|
||
.shape-2 {
|
||
width: 600rpx;
|
||
height: 600rpx;
|
||
bottom: -200rpx;
|
||
left: -150rpx;
|
||
background: radial-gradient(circle, rgba(22, 119, 255, 0.06), transparent 60%);
|
||
border-radius: 50%;
|
||
animation: pulse2 12s infinite ease-in-out;
|
||
}
|
||
|
||
.shape-3 {
|
||
width: 400rpx;
|
||
height: 400rpx;
|
||
top: 50%;
|
||
right: -100rpx;
|
||
background: radial-gradient(circle, rgba(22, 119, 255, 0.04), transparent 60%);
|
||
border-radius: 50%;
|
||
animation: pulse3 14s infinite ease-in-out;
|
||
}
|
||
}
|
||
|
||
@keyframes scanline {
|
||
0% {
|
||
transform: translateX(-100%);
|
||
}
|
||
100% {
|
||
transform: translateX(100%);
|
||
}
|
||
}
|
||
|
||
@keyframes pulse1 {
|
||
0%, 100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: scale(1.1);
|
||
opacity: 0.8;
|
||
}
|
||
}
|
||
|
||
@keyframes pulse2 {
|
||
0%, 100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: scale(1.15);
|
||
opacity: 0.7;
|
||
}
|
||
}
|
||
|
||
@keyframes pulse3 {
|
||
0%, 100% {
|
||
transform: scale(1);
|
||
opacity: 1;
|
||
}
|
||
50% {
|
||
transform: scale(1.2);
|
||
opacity: 0.6;
|
||
}
|
||
}
|
||
|
||
/* 登录卡片 - 优化风格 */
|
||
.login-card {
|
||
position: relative;
|
||
width: 100%;
|
||
max-width: 680rpx;
|
||
padding: 60rpx 48rpx;
|
||
background: #ffffff;
|
||
border-radius: 20rpx;
|
||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08), 0 2rpx 8rpx rgba(0, 0, 0, 0.04);
|
||
z-index: 1;
|
||
animation: cardFadeIn 0.6s cubic-bezier(0.25, 0.46, 0.45, 0.94);
|
||
}
|
||
|
||
@keyframes cardFadeIn {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translateY(20rpx);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 顶部欢迎区 */
|
||
.brand-header {
|
||
margin-bottom: 64rpx;
|
||
animation: slideInDown 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.1s both;
|
||
|
||
.welcome-info {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 12rpx;
|
||
|
||
.welcome-title {
|
||
font-size: 48rpx;
|
||
font-weight: 600;
|
||
background: linear-gradient(135deg, #333 0%, #666 100%);
|
||
-webkit-background-clip: text;
|
||
-webkit-text-fill-color: transparent;
|
||
background-clip: text;
|
||
letter-spacing: 1rpx;
|
||
line-height: 1.2;
|
||
}
|
||
|
||
.welcome-desc {
|
||
font-size: 26rpx;
|
||
font-weight: 400;
|
||
color: #999;
|
||
letter-spacing: 0.5rpx;
|
||
line-height: 1.5;
|
||
}
|
||
}
|
||
}
|
||
|
||
@keyframes slideInDown {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translateY(-20rpx);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 表单区域 - 小米风格 */
|
||
.form-area {
|
||
animation: fadeInUp 0.5s cubic-bezier(0.25, 0.46, 0.45, 0.94) 0.2s both;
|
||
}
|
||
|
||
@keyframes fadeInUp {
|
||
0% {
|
||
opacity: 0;
|
||
transform: translateY(15rpx);
|
||
}
|
||
100% {
|
||
opacity: 1;
|
||
transform: translateY(0);
|
||
}
|
||
}
|
||
|
||
/* 输入框 - 柔和配色 */
|
||
.input-wrap {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
margin-bottom: 32rpx;
|
||
padding: 0 24rpx;
|
||
height: 100rpx;
|
||
background: #ffffff;
|
||
border: 2rpx solid #e8e8e8;
|
||
border-radius: 25rpx;
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
|
||
&:last-of-type {
|
||
margin-bottom: 48rpx;
|
||
}
|
||
|
||
&.focused {
|
||
background: #ffffff;
|
||
border-color: var(--brand-primary);
|
||
box-shadow: 0 0 0 4rpx rgba(22, 119, 255, 0.08);
|
||
transform: translateY(-2rpx);
|
||
}
|
||
|
||
.input {
|
||
flex: 1;
|
||
width: 100%;
|
||
height: 100%;
|
||
font-size: 28rpx;
|
||
color: #333;
|
||
background: transparent;
|
||
border: none;
|
||
outline: none;
|
||
padding: 0;
|
||
margin: 0;
|
||
line-height: normal;
|
||
|
||
&::placeholder {
|
||
color: #b0b0b0;
|
||
}
|
||
}
|
||
|
||
/* 移除 input 组件的默认边框 */
|
||
:deep(input) {
|
||
border: none !important;
|
||
outline: none !important;
|
||
}
|
||
|
||
.pwd-toggle {
|
||
width: 44rpx;
|
||
height: 44rpx;
|
||
margin-left: 16rpx;
|
||
flex-shrink: 0;
|
||
cursor: pointer;
|
||
opacity: 0.5;
|
||
transition: all 0.2s;
|
||
|
||
&:active {
|
||
opacity: 1;
|
||
transform: scale(1.1);
|
||
}
|
||
}
|
||
}
|
||
|
||
/* 登录按钮 - 统一橙色 */
|
||
.btn {
|
||
position: relative;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
width: 100%;
|
||
height: 100rpx;
|
||
font-size: 32rpx;
|
||
font-weight: 500;
|
||
color: #fff !important;
|
||
background: #ff7c24 !important;
|
||
border: none;
|
||
border-radius: 25rpx;
|
||
letter-spacing: 2rpx;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
overflow: hidden;
|
||
box-shadow: 0 4rpx 16rpx rgba(255, 124, 36, 0.3);
|
||
padding: 0;
|
||
margin: 0;
|
||
line-height: 1;
|
||
|
||
&[disabled] {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
&::before {
|
||
content: '';
|
||
position: absolute;
|
||
inset: 0;
|
||
background: linear-gradient(135deg, transparent, rgba(255, 255, 255, 0.3), transparent);
|
||
transform: translateX(-100%);
|
||
animation: shimmer 2.5s infinite;
|
||
}
|
||
|
||
&:active:not([disabled]) {
|
||
transform: scale(0.98);
|
||
box-shadow: 0 2rpx 10rpx rgba(255, 124, 36, 0.35);
|
||
}
|
||
|
||
&::after {
|
||
content: none;
|
||
border: none;
|
||
}
|
||
}
|
||
|
||
@keyframes shimmer {
|
||
0% {
|
||
transform: translateX(-100%);
|
||
}
|
||
50% {
|
||
transform: translateX(100%);
|
||
}
|
||
100% {
|
||
transform: translateX(100%);
|
||
}
|
||
}
|
||
</style>
|