feat: 重构登录页面UI并优化交互体验

This commit is contained in:
Ray.Hao
2026-02-28 18:14:25 +08:00
parent 7cbf29ff90
commit a8b7177a0a
34 changed files with 510 additions and 357 deletions

27
.gitattributes vendored Normal file
View 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
View File

@@ -1,4 +1,4 @@
/* eslint-disable */
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by vite-plugin-uni-components

View File

@@ -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
View File

@@ -1,4 +1,4 @@
/// <reference types='@dcloudio/types' />
/// <reference types='@dcloudio/types' />
import "vue";
declare module "@vue/runtime-core" {

View File

@@ -1,4 +1,4 @@
import request, { publicRequest } from "@/utils/request";
import request, { publicRequest } from "@/utils/request";
const AUTH_BASE_URL = "/api/v1/auth";

View File

@@ -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 作为请求路径

View File

@@ -1,4 +1,4 @@
import request from "@/utils/request";
import request from "@/utils/request";
const USER_BASE_URL = "/api/v1/users";

View File

@@ -1,4 +1,4 @@
import type { ConfigProviderThemeVars } from "wot-design-uni";
import type { ConfigProviderThemeVars } from "wot-design-uni";
/**
* 主题色选项接口

View File

@@ -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 {

View File

@@ -1,4 +1,4 @@
export interface TabbarItem {
export interface TabbarItem {
name: string;
value: number | null;
active: boolean;

View File

@@ -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";

View File

@@ -1,4 +1,4 @@
/**
/**
* 常量统一导出
*/

View File

@@ -1,4 +1,4 @@
/**
/**
* API响应码枚举
*/
export const enum ApiCode {

2
src/env.d.ts vendored
View File

@@ -1,4 +1,4 @@
/// <reference types="vite/client" />
/// <reference types="vite/client" />
declare module "*.vue" {
import type { DefineComponent } from "vue";

View File

@@ -1,4 +1,4 @@
import { createSSRApp } from "vue";
import { createSSRApp } from "vue";
import App from "./App.vue";
import "uno.css";

View File

@@ -177,5 +177,6 @@
}
]
},
"__esModule": true,
"subPackages": []
}
}

View File

@@ -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>

View File

@@ -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
View File

@@ -1,4 +1,4 @@
export {};
export {};
declare module "vue" {
type Hooks = App.AppInstance & Page.PageInstance;

View File

@@ -1,4 +1,4 @@
import type { App } from "vue";
import type { App } from "vue";
import { createPinia } from "pinia";
const store = createPinia();

View File

@@ -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";

View File

@@ -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";

View File

@@ -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
View File

@@ -1,4 +1,4 @@
// src/types/env.d.ts
// src/types/env.d.ts
/**
* 环境变量类型声明
*/

View File

@@ -1,4 +1,4 @@
// 全局类型声明文件
// 全局类型声明文件
// 自动引入 virtual 模块类型
/// <reference types="@uni-helper/vite-plugin-uni-pages/client" />

View File

@@ -1,4 +1,4 @@
// 扩展 uni-mini-router 路由类型,支持从 pages.json 注入的 meta 字段
// 扩展 uni-mini-router 路由类型,支持从 pages.json 注入的 meta 字段
// 该声明文件会被 ts 自动合并到库类型中
import "uni-mini-router";

View File

@@ -1,4 +1,4 @@
/* eslint-disable */
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by vite-plugin-uni-pages

View File

@@ -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";

View File

@@ -1,4 +1,4 @@
/**
/**
* 防抖函数
* @param fn 函数
* @param delay 延迟时间

View File

@@ -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));
}
},

View File

@@ -1,4 +1,4 @@
/**
/**
* 存储工具类
* 提供localStorage和sessionStorage操作方法
*/

2
uni-pages.d.ts vendored
View File

@@ -1,4 +1,4 @@
/* eslint-disable */
/* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// Generated by vite-plugin-uni-pages

View File

@@ -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();

View File

@@ -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";