feat: 重构登录页面UI并优化交互体验
This commit is contained in:
27
.gitattributes
vendored
Normal file
27
.gitattributes
vendored
Normal file
@@ -0,0 +1,27 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto eol=lf
|
||||
|
||||
# Source files
|
||||
*.ts text eol=lf
|
||||
*.tsx text eol=lf
|
||||
*.js text eol=lf
|
||||
*.jsx text eol=lf
|
||||
*.vue text eol=lf
|
||||
*.mjs text eol=lf
|
||||
*.cjs text eol=lf
|
||||
|
||||
# Config files
|
||||
*.json text eol=lf
|
||||
*.yml text eol=lf
|
||||
*.yaml text eol=lf
|
||||
|
||||
# Styling
|
||||
*.css text eol=lf
|
||||
*.scss text eol=lf
|
||||
*.less text eol=lf
|
||||
|
||||
# Documentation
|
||||
*.md text eol=lf
|
||||
|
||||
# Shell scripts
|
||||
*.sh text eol=lf
|
||||
2
components.d.ts
vendored
2
components.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable */
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by vite-plugin-uni-components
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
// pages.config.ts
|
||||
// pages.config.ts
|
||||
import { defineUniPages } from "@uni-helper/vite-plugin-uni-pages";
|
||||
|
||||
export default defineUniPages({
|
||||
|
||||
2
shims-uni.d.ts
vendored
2
shims-uni.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/// <reference types='@dcloudio/types' />
|
||||
/// <reference types='@dcloudio/types' />
|
||||
import "vue";
|
||||
|
||||
declare module "@vue/runtime-core" {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request, { publicRequest } from "@/utils/request";
|
||||
import request, { publicRequest } from "@/utils/request";
|
||||
|
||||
const AUTH_BASE_URL = "/api/v1/auth";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getToken } from "@/utils/storage";
|
||||
import { getToken } from "@/utils/storage";
|
||||
import { ApiCode } from "@/enums/api-code-enum";
|
||||
|
||||
// H5 使用 VITE_APP_BASE_API 作为代理路径,其他平台使用 VITE_APP_API_URL 作为请求路径
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import request from "@/utils/request";
|
||||
import request from "@/utils/request";
|
||||
|
||||
const USER_BASE_URL = "/api/v1/users";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ConfigProviderThemeVars } from "wot-design-uni";
|
||||
import type { ConfigProviderThemeVars } from "wot-design-uni";
|
||||
|
||||
/**
|
||||
* 主题色选项接口
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs";
|
||||
import { Client, type IMessage, type StompSubscription } from "@stomp/stompjs";
|
||||
import { getAccessToken } from "@/utils/auth";
|
||||
|
||||
export interface UseStompOptions {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
export interface TabbarItem {
|
||||
export interface TabbarItem {
|
||||
name: string;
|
||||
value: number | null;
|
||||
active: boolean;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ThemeColorOption, ThemeMode } from "@/composables/types/theme";
|
||||
import type { ThemeColorOption, ThemeMode } from "@/composables/types/theme";
|
||||
import { themeColorOptions } from "@/composables/types/theme";
|
||||
import { useThemeStore } from "@/store";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 常量统一导出
|
||||
*/
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* API响应码枚举
|
||||
*/
|
||||
export const enum ApiCode {
|
||||
|
||||
2
src/env.d.ts
vendored
2
src/env.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/// <reference types="vite/client" />
|
||||
/// <reference types="vite/client" />
|
||||
|
||||
declare module "*.vue" {
|
||||
import type { DefineComponent } from "vue";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { createSSRApp } from "vue";
|
||||
import { createSSRApp } from "vue";
|
||||
import App from "./App.vue";
|
||||
|
||||
import "uno.css";
|
||||
|
||||
@@ -177,5 +177,6 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"__esModule": true,
|
||||
"subPackages": []
|
||||
}
|
||||
}
|
||||
@@ -1,111 +1,102 @@
|
||||
<template>
|
||||
<view class="login-page dark:text-[var(--wot-color-text)]">
|
||||
<!-- 背景图 -->
|
||||
<image src="/static/images/login-bg.svg" mode="aspectFill" class="login-page__bg" />
|
||||
<view class="login-page" :style="pageStyle">
|
||||
<!-- 背景 -->
|
||||
<view class="login-page__bg"></view>
|
||||
|
||||
<!-- 登录卡片 -->
|
||||
<view class="login-card">
|
||||
<!-- 账号密码登录表单 -->
|
||||
<wd-form v-if="loginMode === 'ACCOUNT'" ref="loginFormRef" :model="loginFormData">
|
||||
<!-- 用户名输入框 -->
|
||||
<view class="login-form__item">
|
||||
<wd-icon name="user" size="22" class="mr-20rpx" />
|
||||
<input
|
||||
v-model="loginFormData.username"
|
||||
class="login-form__input"
|
||||
placeholder="请输入用户名"
|
||||
placeholder-class="login-form__placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="login-form__divider"></view>
|
||||
|
||||
<!-- 密码输入框 -->
|
||||
<view class="login-form__item">
|
||||
<wd-icon name="lock-on" size="22" class="mr-20rpx" />
|
||||
<input
|
||||
v-model="loginFormData.password"
|
||||
class="login-form__input"
|
||||
:password="!showPassword"
|
||||
placeholder="请输入密码"
|
||||
placeholder-class="login-form__placeholder"
|
||||
/>
|
||||
<wd-icon
|
||||
:name="showPassword ? 'eye-open' : 'eye-close'"
|
||||
size="18"
|
||||
class="p-10rpx"
|
||||
@click="showPassword = !showPassword"
|
||||
/>
|
||||
</view>
|
||||
<view class="login-form__divider"></view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button
|
||||
class="login-btn"
|
||||
:disabled="loading"
|
||||
:class="{ 'login-btn--loading': loading }"
|
||||
@click="handleAccountLogin"
|
||||
>
|
||||
{{ loading ? "登录中..." : "账号登录" }}
|
||||
</button>
|
||||
</wd-form>
|
||||
|
||||
<wd-form v-else ref="smsLoginFormRef" :model="smsLoginFormData">
|
||||
<view class="login-form__item">
|
||||
<wd-icon name="phone" size="22" class="mr-20rpx" />
|
||||
<input
|
||||
v-model="smsLoginFormData.mobile"
|
||||
class="login-form__input"
|
||||
placeholder="请输入手机号"
|
||||
placeholder-class="login-form__placeholder"
|
||||
/>
|
||||
</view>
|
||||
<view class="login-form__divider"></view>
|
||||
|
||||
<view class="login-form__item">
|
||||
<wd-icon name="security" size="22" class="mr-20rpx" />
|
||||
<input
|
||||
v-model="smsLoginFormData.code"
|
||||
class="login-form__input"
|
||||
placeholder="请输入验证码"
|
||||
placeholder-class="login-form__placeholder"
|
||||
/>
|
||||
<wd-button
|
||||
size="small"
|
||||
plain
|
||||
:disabled="smsCountdown > 0"
|
||||
@click="handleSendSmsLoginCode"
|
||||
>
|
||||
{{ smsCountdown > 0 ? `${smsCountdown}s后重试` : "发送验证码" }}
|
||||
</wd-button>
|
||||
</view>
|
||||
<view class="login-form__divider"></view>
|
||||
|
||||
<button
|
||||
class="login-btn"
|
||||
:disabled="loading"
|
||||
:class="{ 'login-btn--loading': loading }"
|
||||
@click="handleSmsLogin"
|
||||
>
|
||||
{{ loading ? "登录中..." : "短信登录" }}
|
||||
</button>
|
||||
</wd-form>
|
||||
|
||||
<view class="login-switch" @click="loginMode = loginMode === 'ACCOUNT' ? 'SMS' : 'ACCOUNT'">
|
||||
{{ loginMode === "ACCOUNT" ? "使用短信验证码登录" : "使用账号密码登录" }}
|
||||
<!-- 主内容 -->
|
||||
<view class="login-content">
|
||||
<!-- Logo -->
|
||||
<view class="login-logo">
|
||||
<image class="login-logo__img" src="/static/logo.png" mode="aspectFit" />
|
||||
<text class="login-logo__name">YouLai Admin</text>
|
||||
</view>
|
||||
|
||||
<!-- 底部协议 -->
|
||||
<view class="login-agreement">
|
||||
<wd-checkbox v-model="isAgreePolicy" shape="square" size="16px">
|
||||
<view class="login-agreement__content">
|
||||
<text class="login-agreement__text">我已阅读并同意</text>
|
||||
<text class="login-agreement__link" @click.stop="navigateToUserAgreement">
|
||||
《用户协议》
|
||||
</text>
|
||||
<text class="login-agreement__text">和</text>
|
||||
<text class="login-agreement__link" @click.stop="navigateToPrivacy">《隐私政策》</text>
|
||||
<!-- 登录卡片 -->
|
||||
<view class="login-card">
|
||||
<!-- 标题 -->
|
||||
<view class="login-card__header">
|
||||
<text class="login-card__title">欢迎登录</text>
|
||||
<text class="login-card__desc">{{ loginMode === 'PASSWORD' ? '使用账号密码登录' : '使用手机验证码登录' }}</text>
|
||||
</view>
|
||||
|
||||
<!-- 表单 -->
|
||||
<view class="login-form">
|
||||
<!-- 用户名/手机号 -->
|
||||
<view class="login-field">
|
||||
<view class="login-field__icon">
|
||||
<wd-icon name="person" size="20" color="#9ca3af" />
|
||||
</view>
|
||||
<input
|
||||
v-model="formData.username"
|
||||
class="login-field__input"
|
||||
:placeholder="loginMode === 'PASSWORD' ? '请输入用户名' : '请输入手机号'"
|
||||
placeholder-class="login-field__placeholder"
|
||||
:maxlength="loginMode === 'PASSWORD' ? 50 : 11"
|
||||
/>
|
||||
</view>
|
||||
</wd-checkbox>
|
||||
|
||||
<!-- 密码 -->
|
||||
<view v-if="loginMode === 'PASSWORD'" class="login-field">
|
||||
<view class="login-field__icon">
|
||||
<wd-icon name="lock" size="20" color="#9ca3af" />
|
||||
</view>
|
||||
<input
|
||||
v-model="formData.password"
|
||||
class="login-field__input"
|
||||
:password="!showPassword"
|
||||
placeholder="请输入密码"
|
||||
placeholder-class="login-field__placeholder"
|
||||
maxlength="50"
|
||||
@confirm="handleLogin"
|
||||
/>
|
||||
<view class="login-field__toggle" @click="showPassword = !showPassword">
|
||||
<wd-icon :name="showPassword ? 'eye-open' : 'eye-close'" size="20" color="#9ca3af" />
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 验证码 -->
|
||||
<view v-else class="login-field login-field--code">
|
||||
<view class="login-field__icon">
|
||||
<wd-icon name="shield" size="20" color="#9ca3af" />
|
||||
</view>
|
||||
<input
|
||||
v-model="formData.code"
|
||||
class="login-field__input"
|
||||
placeholder="请输入验证码"
|
||||
placeholder-class="login-field__placeholder"
|
||||
type="number"
|
||||
maxlength="6"
|
||||
@confirm="handleLogin"
|
||||
/>
|
||||
<view class="login-field__code-btn" :class="{ 'is-disabled': smsCountdown > 0 }" @click="handleSendCode">
|
||||
<text>{{ smsCountdown > 0 ? `${smsCountdown}s` : '获取验证码' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 登录按钮 -->
|
||||
<button class="login-submit" :class="{ 'is-disabled': !canSubmit }" :disabled="loading || !canSubmit" @click="handleLogin">
|
||||
<text v-if="loading">登录中...</text>
|
||||
<text v-else>登 录</text>
|
||||
</button>
|
||||
|
||||
<!-- 切换登录方式 -->
|
||||
<view class="login-switch" @click="toggleLoginMode">
|
||||
<text class="login-switch__prefix">{{ loginMode === 'PASSWORD' ? '忘记密码?' : '记得密码?' }}</text>
|
||||
<text class="login-switch__link">{{ loginMode === 'PASSWORD' ? '使用验证码登录' : '使用密码登录' }}</text>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
<!-- 协议 -->
|
||||
<view class="login-agreement">
|
||||
<wd-checkbox v-model="isAgreePolicy" shape="square" size="16px">
|
||||
<text class="login-agreement__text">
|
||||
登录即代表同意
|
||||
<text class="login-agreement__link" @click.stop="navigateToAgreement('user')">《用户协议》</text>
|
||||
与
|
||||
<text class="login-agreement__link" @click.stop="navigateToAgreement('privacy')">《隐私政策》</text>
|
||||
</text>
|
||||
</wd-checkbox>
|
||||
</view>
|
||||
</view>
|
||||
</view>
|
||||
|
||||
@@ -121,94 +112,108 @@
|
||||
</route>
|
||||
|
||||
<script lang="ts" setup>
|
||||
import { onLoad } from "@dcloudio/uni-app";
|
||||
import { onLoad, onUnload } from "@dcloudio/uni-app";
|
||||
import AuthAPI, { type LoginData } from "@/api/auth";
|
||||
import { useUserStore } from "@/store/modules/user-store";
|
||||
import { useToast } from "wot-design-uni";
|
||||
|
||||
const loginFormRef = ref();
|
||||
const smsLoginFormRef = ref();
|
||||
const toast = useToast();
|
||||
const loading = ref(false);
|
||||
const userStore = useUserStore();
|
||||
const showPassword = ref(false);
|
||||
const isAgreePolicy = ref(false); // 是否同意隐私协议
|
||||
|
||||
const loginMode = ref<"ACCOUNT" | "SMS">("ACCOUNT");
|
||||
const isAgreePolicy = ref(false);
|
||||
const loginMode = ref<"PASSWORD" | "SMS">("PASSWORD");
|
||||
const smsCountdown = ref(0);
|
||||
const smsTimer = ref<ReturnType<typeof setInterval> | null>(null);
|
||||
|
||||
// 登录表单数据
|
||||
const loginFormData = ref<LoginData>({
|
||||
const keyboardHeight = ref(0);
|
||||
const pageStyle = computed(() => {
|
||||
return keyboardHeight.value > 0 ? { paddingBottom: `${keyboardHeight.value}px` } : {};
|
||||
});
|
||||
|
||||
const formData = ref({
|
||||
username: "admin",
|
||||
password: "123456",
|
||||
code: "1234",
|
||||
});
|
||||
|
||||
const smsLoginFormData = ref({
|
||||
mobile: "",
|
||||
code: "",
|
||||
});
|
||||
|
||||
// 仅使用 query 作为重定向来源
|
||||
const redirect = ref("/pages/index/index");
|
||||
|
||||
onLoad((options: any) => {
|
||||
const fromQuery = options && options.redirect ? decodeURIComponent(options.redirect) : "";
|
||||
if (fromQuery && fromQuery !== "/pages/login/index") {
|
||||
redirect.value = fromQuery;
|
||||
}
|
||||
uni.onKeyboardHeightChange((res) => {
|
||||
keyboardHeight.value = res.height || 0;
|
||||
});
|
||||
});
|
||||
|
||||
// 账号密码登录处理
|
||||
const handleAccountLogin = () => {
|
||||
if (loading.value) return;
|
||||
onUnload(() => {
|
||||
if (smsTimer.value) {
|
||||
clearInterval(smsTimer.value);
|
||||
smsTimer.value = null;
|
||||
}
|
||||
});
|
||||
|
||||
// 检查是否同意隐私协议
|
||||
const isValidMobile = (mobile: string) => /^1\d{10}$/.test((mobile || "").trim());
|
||||
|
||||
const canSubmit = computed(() => {
|
||||
if (!isAgreePolicy.value) return false;
|
||||
if (loginMode.value === 'PASSWORD') {
|
||||
return (formData.value.username || "").trim() && (formData.value.password || "").trim();
|
||||
}
|
||||
return isValidMobile(formData.value.username) && (formData.value.code || "").trim();
|
||||
});
|
||||
|
||||
const toggleLoginMode = () => {
|
||||
loginMode.value = loginMode.value === 'PASSWORD' ? 'SMS' : 'PASSWORD';
|
||||
formData.value.code = '';
|
||||
// 切换模式时更新默认用户名
|
||||
formData.value.username = loginMode.value === 'PASSWORD' ? 'admin' : '18812345678';
|
||||
};
|
||||
|
||||
const handleLogin = () => {
|
||||
if (loading.value || !canSubmit.value) return;
|
||||
if (!isAgreePolicy.value) {
|
||||
toast.error("请先阅读并同意用户协议和隐私政策");
|
||||
return;
|
||||
}
|
||||
|
||||
// 表单验证
|
||||
if (!loginFormData.value.username) {
|
||||
toast.error("请输入用户名");
|
||||
return;
|
||||
}
|
||||
if (!loginFormData.value.password) {
|
||||
toast.error("请输入密码");
|
||||
toast.error("请先同意用户协议");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
|
||||
userStore
|
||||
.login(loginFormData.value)
|
||||
.then(() => userStore.getInfo())
|
||||
.then(() => {
|
||||
toast.success("登录成功");
|
||||
|
||||
console.log("跳转链接", redirect.value);
|
||||
|
||||
// 账号密码登录直接跳转到重定向页面
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: redirect.value });
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(error?.message || "登录失败");
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
if (loginMode.value === 'PASSWORD') {
|
||||
userStore
|
||||
.login({ username: formData.value.username, password: formData.value.password })
|
||||
.then(() => userStore.getInfo())
|
||||
.then(() => {
|
||||
toast.success("登录成功");
|
||||
setTimeout(() => { uni.reLaunch({ url: redirect.value }); }, 800);
|
||||
})
|
||||
.catch((error: any) => { toast.error(error?.message || "登录失败"); })
|
||||
.finally(() => { loading.value = false; });
|
||||
} else {
|
||||
const mobile = formData.value.username.trim();
|
||||
userStore
|
||||
.loginBySms({ mobile, code: formData.value.code })
|
||||
.then(() => userStore.getInfo())
|
||||
.then(() => {
|
||||
toast.success("登录成功");
|
||||
setTimeout(() => { uni.reLaunch({ url: redirect.value }); }, 800);
|
||||
})
|
||||
.catch((error: any) => { toast.error(error?.message || "登录失败"); })
|
||||
.finally(() => { loading.value = false; });
|
||||
}
|
||||
};
|
||||
|
||||
const handleSendSmsLoginCode = async () => {
|
||||
const handleSendCode = async () => {
|
||||
if (smsCountdown.value > 0) return;
|
||||
if (!smsLoginFormData.value.mobile) {
|
||||
toast.error("请输入手机号");
|
||||
return;
|
||||
}
|
||||
const mobile = (formData.value.username || "").trim();
|
||||
if (!mobile) { toast.error("请输入手机号"); return; }
|
||||
if (!isValidMobile(mobile)) { toast.error("请输入正确的手机号"); return; }
|
||||
|
||||
try {
|
||||
await AuthAPI.sendSmsLoginCode(smsLoginFormData.value.mobile);
|
||||
await AuthAPI.sendSmsLoginCode(mobile);
|
||||
toast.success("验证码已发送");
|
||||
smsCountdown.value = 60;
|
||||
if (smsTimer.value) clearInterval(smsTimer.value);
|
||||
@@ -216,10 +221,7 @@ const handleSendSmsLoginCode = async () => {
|
||||
smsCountdown.value -= 1;
|
||||
if (smsCountdown.value <= 0) {
|
||||
smsCountdown.value = 0;
|
||||
if (smsTimer.value) {
|
||||
clearInterval(smsTimer.value);
|
||||
smsTimer.value = null;
|
||||
}
|
||||
if (smsTimer.value) { clearInterval(smsTimer.value); smsTimer.value = null; }
|
||||
}
|
||||
}, 1000);
|
||||
} catch (error: any) {
|
||||
@@ -227,217 +229,339 @@ const handleSendSmsLoginCode = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const handleSmsLogin = () => {
|
||||
if (loading.value) return;
|
||||
if (!isAgreePolicy.value) {
|
||||
toast.error("请先阅读并同意用户协议和隐私政策");
|
||||
return;
|
||||
}
|
||||
if (!smsLoginFormData.value.mobile) {
|
||||
toast.error("请输入手机号");
|
||||
return;
|
||||
}
|
||||
if (!smsLoginFormData.value.code) {
|
||||
toast.error("请输入验证码");
|
||||
return;
|
||||
}
|
||||
|
||||
loading.value = true;
|
||||
userStore
|
||||
.loginBySms({ mobile: smsLoginFormData.value.mobile, code: smsLoginFormData.value.code })
|
||||
.then(() => userStore.getInfo())
|
||||
.then(() => {
|
||||
toast.success("登录成功");
|
||||
setTimeout(() => {
|
||||
uni.reLaunch({ url: redirect.value });
|
||||
}, 1000);
|
||||
})
|
||||
.catch((error: any) => {
|
||||
toast.error(error?.message || "登录失败");
|
||||
})
|
||||
.finally(() => {
|
||||
loading.value = false;
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到用户协议页面
|
||||
const navigateToUserAgreement = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/agreement/index",
|
||||
});
|
||||
};
|
||||
|
||||
// 跳转到隐私政策页面
|
||||
const navigateToPrivacy = () => {
|
||||
uni.navigateTo({
|
||||
url: "/pages/mine/settings/privacy/index",
|
||||
});
|
||||
const navigateToAgreement = (type: string) => {
|
||||
const url = type === 'user' ? "/pages/mine/settings/agreement/index" : "/pages/mine/settings/privacy/index";
|
||||
uni.navigateTo({ url });
|
||||
};
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
// 登录页面主容器
|
||||
// Arco Design 色值
|
||||
$primary-color: #165dff;
|
||||
$primary-1: #e8f3ff;
|
||||
$primary-2: #bedaff;
|
||||
$primary-3: #94bfff;
|
||||
$primary-4: #6aa3ff;
|
||||
$primary-5: #4080ff;
|
||||
$disabled-bg: #e5e7eb;
|
||||
|
||||
.login-page {
|
||||
position: relative;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
// ============ 背景 - 参考 PC 端多层渐变设计 ============
|
||||
.login-page__bg {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
z-index: 1;
|
||||
// 多层背景叠加:主渐变 + 左上柔光 + 右下柔光 + 白色覆盖层
|
||||
background:
|
||||
// 白色覆盖层
|
||||
linear-gradient(135deg, rgba(255, 255, 255, 0.6) 0%, rgba(255, 255, 255, 0.2) 35%, transparent 100%),
|
||||
// 右下角次级柔光
|
||||
radial-gradient(ellipse 55% 55% at 80% 70%, rgba(22, 93, 255, 0.3) 0%, rgba(22, 93, 255, 0.12) 50%, transparent 100%),
|
||||
// 左上角主柔光
|
||||
radial-gradient(ellipse 60% 60% at 20% 15%, rgba(64, 128, 255, 0.35) 0%, rgba(64, 128, 255, 0.18) 40%, transparent 100%),
|
||||
// 主渐变背景
|
||||
linear-gradient(135deg, #f3f7ff 0%, #e3edff 60%, #d6e7ff 100%);
|
||||
}
|
||||
|
||||
// ============ 主内容 ============
|
||||
.login-content {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center; // 垂直居中
|
||||
padding: 300rpx 88rpx 60rpx;
|
||||
min-height: 100vh;
|
||||
overflow: hidden;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
// 背景图
|
||||
&__bg {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
z-index: 1;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
// ============ Logo ============
|
||||
.login-logo {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
margin-bottom: 48rpx;
|
||||
|
||||
&__img {
|
||||
width: 96rpx;
|
||||
height: 96rpx;
|
||||
margin-bottom: 16rpx;
|
||||
}
|
||||
|
||||
&__name {
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
letter-spacing: 2rpx;
|
||||
}
|
||||
}
|
||||
|
||||
// 登录卡片
|
||||
// ============ 卡片 ============
|
||||
.login-card {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
width: 85%; // 移动端保持两边边距
|
||||
max-width: 420px; // 桌面端最大宽度
|
||||
padding: 50rpx 40rpx;
|
||||
background-color: var(--wot-color-bg);
|
||||
border: 1rpx solid var(--wot-color-border);
|
||||
width: 100%;
|
||||
padding: 48rpx 48rpx 36rpx;
|
||||
border-radius: 24rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
background: rgba(255, 255, 255, 0.96);
|
||||
backdrop-filter: blur(20px);
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.08);
|
||||
|
||||
&__header {
|
||||
text-align: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
&__title {
|
||||
display: block;
|
||||
font-size: 40rpx;
|
||||
font-weight: 600;
|
||||
color: #1f2937;
|
||||
margin-bottom: 12rpx;
|
||||
}
|
||||
|
||||
&__desc {
|
||||
font-size: 26rpx;
|
||||
color: #86909c;
|
||||
}
|
||||
}
|
||||
|
||||
// 登录表单
|
||||
// ============ 表单 ============
|
||||
.login-form {
|
||||
&__item {
|
||||
margin-bottom: 24rpx;
|
||||
}
|
||||
|
||||
.login-field {
|
||||
position: relative;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
height: 96rpx;
|
||||
padding: 0 28rpx;
|
||||
margin-bottom: 28rpx;
|
||||
background: #f7f8fa;
|
||||
border-radius: 48rpx;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:focus-within {
|
||||
background: #ffffff;
|
||||
box-shadow: 0 0 0 2rpx $primary-color;
|
||||
}
|
||||
|
||||
&__icon {
|
||||
flex-shrink: 0;
|
||||
width: 44rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 24rpx 0;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
&__input {
|
||||
flex: 1;
|
||||
height: 60rpx;
|
||||
font-size: 28rpx;
|
||||
line-height: 60rpx;
|
||||
color: var(--wot-color-text);
|
||||
background-color: transparent !important;
|
||||
border: none;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
&__divider {
|
||||
height: 1px;
|
||||
background-color: var(--wot-color-border-light);
|
||||
height: 100%;
|
||||
font-size: 30rpx;
|
||||
color: #1f2937;
|
||||
}
|
||||
|
||||
&__placeholder {
|
||||
color: var(--wot-color-text-placeholder);
|
||||
}
|
||||
}
|
||||
|
||||
// 登录按钮
|
||||
.login-btn {
|
||||
width: 100%;
|
||||
height: 88rpx;
|
||||
margin-top: 60rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 500;
|
||||
color: #fff;
|
||||
background-color: var(--wot-color-theme);
|
||||
border: none;
|
||||
border-radius: 44rpx;
|
||||
transition: opacity 0.2s;
|
||||
|
||||
&--loading {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
|
||||
// 切换登录方式
|
||||
.login-switch {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 30rpx;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-theme);
|
||||
}
|
||||
|
||||
// 手机号登录
|
||||
.phone-login {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
padding: 40rpx 0;
|
||||
|
||||
&__title {
|
||||
margin-bottom: 16rpx;
|
||||
font-size: 36rpx;
|
||||
font-weight: bold;
|
||||
color: var(--wot-color-text);
|
||||
color: #c9cdd4;
|
||||
}
|
||||
|
||||
&__subtitle {
|
||||
margin-bottom: 60rpx;
|
||||
font-size: 28rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
}
|
||||
}
|
||||
|
||||
// 其他登录方式
|
||||
.other-login {
|
||||
margin-top: 60rpx;
|
||||
|
||||
&__title {
|
||||
&__toggle {
|
||||
flex-shrink: 0;
|
||||
width: 56rpx;
|
||||
height: 56rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-bottom: 40rpx;
|
||||
}
|
||||
|
||||
&__line {
|
||||
width: 80rpx;
|
||||
height: 1rpx;
|
||||
background-color: var(--wot-color-border-light);
|
||||
}
|
||||
|
||||
&__text {
|
||||
margin: 0 20rpx;
|
||||
&__code-btn {
|
||||
flex-shrink: 0;
|
||||
height: 56rpx;
|
||||
padding: 0 24rpx;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
font-size: 26rpx;
|
||||
color: var(--wot-color-text-secondary);
|
||||
font-weight: 500;
|
||||
color: $primary-color;
|
||||
background: rgba(22, 93, 255, 0.08);
|
||||
border-radius: 28rpx;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
background: rgba(22, 93, 255, 0.15);
|
||||
}
|
||||
|
||||
&.is-disabled {
|
||||
color: #c9cdd4;
|
||||
background: #f3f4f6;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 登录协议
|
||||
.login-agreement {
|
||||
// ============ 按钮 ============
|
||||
.login-submit {
|
||||
width: 100%;
|
||||
height: 96rpx;
|
||||
display: flex;
|
||||
align-items: flex-start;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
margin-top: 60rpx;
|
||||
font-size: 24rpx;
|
||||
font-size: 32rpx;
|
||||
font-weight: 600;
|
||||
color: #ffffff;
|
||||
background: linear-gradient(135deg, #165dff, #4080ff);
|
||||
border: none;
|
||||
border-radius: 48rpx;
|
||||
box-shadow: 0 8rpx 24rpx rgba(22, 93, 255, 0.3);
|
||||
transition: all 0.25s;
|
||||
|
||||
&:active:not(:disabled) {
|
||||
transform: scale(0.98);
|
||||
box-shadow: 0 4rpx 16rpx rgba(22, 93, 255, 0.35);
|
||||
}
|
||||
|
||||
&.is-disabled,
|
||||
&:disabled {
|
||||
background: $disabled-bg;
|
||||
color: #ffffff;
|
||||
box-shadow: none;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 切换链接 ============
|
||||
.login-switch {
|
||||
margin-top: 32rpx;
|
||||
text-align: center;
|
||||
padding: 16rpx 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
|
||||
&__prefix {
|
||||
font-size: 26rpx;
|
||||
color: #86909c;
|
||||
}
|
||||
|
||||
&__link {
|
||||
font-size: 26rpx;
|
||||
color: $primary-color;
|
||||
font-weight: 500;
|
||||
padding: 4rpx 8rpx;
|
||||
border-bottom: 2rpx solid $primary-color;
|
||||
transition: all 0.2s;
|
||||
|
||||
&:active {
|
||||
opacity: 0.7;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 协议 ============
|
||||
.login-agreement {
|
||||
padding-top: 28rpx;
|
||||
border-top: 1rpx solid #f2f3f5;
|
||||
|
||||
:deep(.wd-checkbox) {
|
||||
align-items: flex-start;
|
||||
}
|
||||
|
||||
&__content {
|
||||
display: inline-flex;
|
||||
flex-wrap: wrap;
|
||||
align-items: center;
|
||||
line-height: 1.6;
|
||||
}
|
||||
|
||||
&__text {
|
||||
color: var(--wot-color-text-secondary);
|
||||
font-size: 24rpx;
|
||||
color: #86909c;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
&__link {
|
||||
color: var(--wot-color-theme);
|
||||
text-decoration: underline;
|
||||
color: $primary-color;
|
||||
}
|
||||
}
|
||||
|
||||
// ============ 暗黑模式 ============
|
||||
@media (prefers-color-scheme: dark) {
|
||||
.login-page__bg {
|
||||
background:
|
||||
// 深色覆盖层
|
||||
linear-gradient(135deg, rgba(18, 22, 36, 0.3) 0%, transparent 50%, transparent 100%),
|
||||
// 右下角次级柔光
|
||||
radial-gradient(ellipse 55% 55% at 80% 70%, rgba(22, 93, 255, 0.35) 0%, rgba(22, 93, 255, 0.12) 50%, transparent 100%),
|
||||
// 左上角主柔光
|
||||
radial-gradient(ellipse 60% 60% at 20% 15%, rgba(98, 142, 255, 0.4) 0%, rgba(98, 142, 255, 0.18) 50%, transparent 100%),
|
||||
// 主渐变背景
|
||||
linear-gradient(135deg, #0b1324 0%, #162135 60%, #1e2c44 100%);
|
||||
}
|
||||
|
||||
.login-logo__name { color: #e5e6eb; }
|
||||
|
||||
.login-card {
|
||||
background: rgba(29, 33, 41, 0.96);
|
||||
box-shadow: 0 4rpx 24rpx rgba(0, 0, 0, 0.3);
|
||||
|
||||
&__title { color: #e5e6eb; }
|
||||
&__desc { color: #86909c; }
|
||||
}
|
||||
|
||||
.login-field {
|
||||
background: #2a2f3a;
|
||||
|
||||
&:focus-within {
|
||||
background: #323844;
|
||||
box-shadow: 0 0 0 2rpx $primary-color;
|
||||
}
|
||||
|
||||
&__input { color: #e5e6eb; }
|
||||
&__placeholder { color: #6b7280; }
|
||||
&__code-btn {
|
||||
color: #5a8bff;
|
||||
background: rgba(22, 93, 255, 0.2);
|
||||
|
||||
&.is-disabled { color: #4b5563; background: #1f2937; }
|
||||
}
|
||||
}
|
||||
|
||||
.login-submit {
|
||||
background: linear-gradient(135deg, #165dff, #4080ff);
|
||||
box-shadow: 0 8rpx 24rpx rgba(22, 93, 255, 0.35);
|
||||
|
||||
&.is-disabled,
|
||||
&:disabled {
|
||||
background: #3a3a3a;
|
||||
color: #6b7280;
|
||||
}
|
||||
}
|
||||
|
||||
.login-switch {
|
||||
&__prefix { color: #86909c; }
|
||||
&__link { color: #5a8bff; border-bottom-color: #5a8bff; }
|
||||
}
|
||||
|
||||
.login-agreement {
|
||||
border-top-color: #3a3f4a;
|
||||
&__text { color: #86909c; }
|
||||
&__link { color: #5a8bff; }
|
||||
}
|
||||
}
|
||||
|
||||
// 类名兼容
|
||||
:deep(.dark),
|
||||
.dark {
|
||||
.login-page__bg {
|
||||
background:
|
||||
linear-gradient(135deg, rgba(18, 22, 36, 0.3) 0%, transparent 50%, transparent 100%),
|
||||
radial-gradient(ellipse 55% 55% at 80% 70%, rgba(22, 93, 255, 0.35) 0%, rgba(22, 93, 255, 0.12) 50%, transparent 100%),
|
||||
radial-gradient(ellipse 60% 60% at 20% 15%, rgba(98, 142, 255, 0.4) 0%, rgba(98, 142, 255, 0.18) 50%, transparent 100%),
|
||||
linear-gradient(135deg, #0b1324 0%, #162135 60%, #1e2c44 100%);
|
||||
}
|
||||
.login-logo__name { color: #e5e6eb; }
|
||||
.login-card { background: rgba(29, 33, 41, 0.96); &__title { color: #e5e6eb; } &__desc { color: #86909c; } }
|
||||
.login-field { background: #2a2f3a; &__input { color: #e5e6eb; } }
|
||||
.login-submit { background: linear-gradient(135deg, #165dff, #4080ff); &.is-disabled, &:disabled { background: #3a3a3a; color: #6b7280; } }
|
||||
.login-switch__prefix { color: #86909c; }
|
||||
.login-switch__link { color: #5a8bff; border-bottom-color: #5a8bff; }
|
||||
.login-agreement { border-top-color: #3a3f4a; &__text { color: #86909c; } &__link { color: #5a8bff; } }
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { pages, subPackages } from "virtual:uni-pages";
|
||||
import { pages, subPackages } from "virtual:uni-pages";
|
||||
import { isLoggedIn } from "@/utils/auth";
|
||||
import { createRouter } from "uni-mini-router";
|
||||
|
||||
|
||||
2
src/shime-uni.d.ts
vendored
2
src/shime-uni.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
export {};
|
||||
export {};
|
||||
|
||||
declare module "vue" {
|
||||
type Hooks = App.AppInstance & Page.PageInstance;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { App } from "vue";
|
||||
import type { App } from "vue";
|
||||
import { createPinia } from "pinia";
|
||||
|
||||
const store = createPinia();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { defineStore } from "pinia";
|
||||
import { Storage } from "@/utils/storage";
|
||||
import { THEME_MODE_KEY, THEME_COLOR_KEY } from "@/constants";
|
||||
import type { ThemeColorOption, ThemeMode } from "@/composables/types/theme";
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineStore } from "pinia";
|
||||
import { defineStore } from "pinia";
|
||||
import AuthAPI, { type LoginData, type SmsLoginData } from "@/api/auth";
|
||||
import UserAPI, { type UserInfo } from "@/api/user";
|
||||
import { setAccessToken, clearTokens } from "@/utils/auth";
|
||||
|
||||
4
src/types/auto-imports.d.ts
vendored
4
src/types/auto-imports.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable */
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// noinspection JSUnusedGlobalSymbols
|
||||
@@ -325,4 +325,4 @@ declare module 'vue' {
|
||||
readonly watchPostEffect: UnwrapRef<typeof import('vue')['watchPostEffect']>
|
||||
readonly watchSyncEffect: UnwrapRef<typeof import('vue')['watchSyncEffect']>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
2
src/types/env.d.ts
vendored
2
src/types/env.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
// src/types/env.d.ts
|
||||
// src/types/env.d.ts
|
||||
/**
|
||||
* 环境变量类型声明
|
||||
*/
|
||||
|
||||
2
src/types/global.d.ts
vendored
2
src/types/global.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
// 全局类型声明文件
|
||||
// 全局类型声明文件
|
||||
// 自动引入 virtual 模块类型
|
||||
|
||||
/// <reference types="@uni-helper/vite-plugin-uni-pages/client" />
|
||||
|
||||
2
src/types/uni-mini-router.d.ts
vendored
2
src/types/uni-mini-router.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
// 扩展 uni-mini-router 路由类型,支持从 pages.json 注入的 meta 字段
|
||||
// 扩展 uni-mini-router 路由类型,支持从 pages.json 注入的 meta 字段
|
||||
// 该声明文件会被 ts 自动合并到库类型中
|
||||
|
||||
import "uni-mini-router";
|
||||
|
||||
2
src/types/uni-pages.d.ts
vendored
2
src/types/uni-pages.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable */
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by vite-plugin-uni-pages
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useUserStore } from "@/store/modules/user-store";
|
||||
import { useUserStore } from "@/store/modules/user-store";
|
||||
import { Storage } from "./storage";
|
||||
import { ACCESS_TOKEN_KEY, REFRESH_TOKEN_KEY } from "@/constants";
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 防抖函数
|
||||
* @param fn 函数
|
||||
* @param delay 延迟时间
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { getAccessToken } from "./auth";
|
||||
import { getAccessToken } from "./auth";
|
||||
|
||||
// 请求配置
|
||||
interface RequestOptions<T = any> {
|
||||
@@ -52,6 +52,7 @@ function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
timeout: options.timeout || 30000,
|
||||
responseType: options.responseType,
|
||||
success: (res: any) => {
|
||||
const serverMsg = res?.data?.msg || res?.data?.message;
|
||||
// 请求成功
|
||||
if (res.statusCode >= 200 && res.statusCode < 300) {
|
||||
resolve(res.data.data);
|
||||
@@ -64,11 +65,11 @@ function request<T = any>(options: RequestOptions): Promise<T> {
|
||||
url: "/pages/login/index",
|
||||
});
|
||||
}
|
||||
reject(new Error(res.data.message || "未授权,请重新登录"));
|
||||
reject(new Error(serverMsg || "未授权,请重新登录"));
|
||||
}
|
||||
// 其他错误
|
||||
else {
|
||||
const errorMsg = res.data.message || `请求失败: ${res.statusCode}`;
|
||||
const errorMsg = serverMsg || `请求失败: ${res.statusCode}`;
|
||||
reject(new Error(errorMsg));
|
||||
}
|
||||
},
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
/**
|
||||
/**
|
||||
* 存储工具类
|
||||
* 提供localStorage和sessionStorage操作方法
|
||||
*/
|
||||
|
||||
2
uni-pages.d.ts
vendored
2
uni-pages.d.ts
vendored
@@ -1,4 +1,4 @@
|
||||
/* eslint-disable */
|
||||
/* eslint-disable */
|
||||
/* prettier-ignore */
|
||||
// @ts-nocheck
|
||||
// Generated by vite-plugin-uni-pages
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import presetWeapp from "unocss-preset-weapp";
|
||||
import presetWeapp from "unocss-preset-weapp";
|
||||
import { extractorAttributify, transformerClass } from "unocss-preset-weapp/transformer";
|
||||
|
||||
const { presetWeappAttributify, transformerAttributify } = extractorAttributify();
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { defineConfig, type UserConfig, type ConfigEnv, loadEnv } from "vite";
|
||||
import { defineConfig, type UserConfig, type ConfigEnv, loadEnv } from "vite";
|
||||
import uni from "@dcloudio/vite-plugin-uni";
|
||||
import AutoImport from "unplugin-auto-import/vite";
|
||||
import UniLayouts from "@uni-helper/vite-plugin-uni-layouts";
|
||||
|
||||
Reference in New Issue
Block a user