perf: refine login page

This commit is contained in:
Soulter
2025-11-09 20:57:45 +08:00
parent aca5743ab6
commit bb5f06498e
9 changed files with 89 additions and 479 deletions
File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 58 KiB

+4
View File
@@ -242,6 +242,7 @@ import ProviderModelSelector from '@/components/chat/ProviderModelSelector.vue';
import MessageList from '@/components/chat/MessageList.vue';
import 'highlight.js/styles/github.css';
import { useToast } from '@/utils/toast';
import { useTheme } from 'vuetify';
export default {
name: 'ChatPage',
@@ -258,10 +259,12 @@ export default {
}, setup() {
const { t } = useI18n();
const { tm } = useModuleI18n('features/chat');
const theme = useTheme();
return {
t,
tm,
theme,
router,
ref
};
@@ -427,6 +430,7 @@ export default {
const customizer = useCustomizerStore();
const newTheme = customizer.uiTheme === 'PurpleTheme' ? 'PurpleThemeDark' : 'PurpleTheme';
customizer.SET_UI_THEME(newTheme);
this.theme.global.name.value = newTheme;
},
// 切换侧边栏折叠状态
toggleSidebar() {
-4
View File
@@ -70,10 +70,6 @@ const formatTitle = (title: string) => {
transition: transform 0.3s ease;
}
.logo-image img:hover {
transform: scale(1.05);
}
.logo-text {
display: flex;
flex-direction: column;
@@ -2,6 +2,7 @@
"login": "Login",
"username": "Username",
"password": "Password",
"defaultHint": "Default username and password: astrbot",
"logo": {
"title": "AstrBot Dashboard",
"subtitle": "Welcome"
@@ -2,8 +2,9 @@
"login": "登录",
"username": "用户名",
"password": "密码",
"defaultHint": "默认账户和密码均为:astrbot",
"logo": {
"title": "AstrBot 仪表盘",
"title": "AstrBot WebUI",
"subtitle": "欢迎使用"
},
"theme": {
@@ -10,6 +10,7 @@ import { useCommonStore } from '@/stores/common';
import MarkdownIt from 'markdown-it';
import { useI18n } from '@/i18n/composables';
import { router } from '@/router';
import { useTheme } from 'vuetify';
// 配置markdown-it,默认安全设置
const md = new MarkdownIt({
@@ -20,6 +21,7 @@ const md = new MarkdownIt({
});
const customizer = useCustomizerStore();
const theme = useTheme();
const { t } = useI18n();
let dialog = ref(false);
let accountWarning = ref(false)
@@ -276,7 +278,9 @@ function updateDashboard() {
}
function toggleDarkMode() {
customizer.SET_UI_THEME(customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark');
const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark';
customizer.SET_UI_THEME(newTheme);
theme.global.name.value = newTheme;
}
getVersion();
+16 -2
View File
@@ -18,24 +18,38 @@ setupI18n().then(() => {
const app = createApp(App);
app.use(router);
app.use(createPinia());
const pinia = createPinia();
app.use(pinia);
app.use(print);
app.use(VueApexCharts);
app.use(vuetify);
app.use(confirmPlugin);
app.mount('#app');
// 挂载后同步 Vuetify 主题
import('./stores/customizer').then(({ useCustomizerStore }) => {
const customizer = useCustomizerStore(pinia);
vuetify.theme.global.name.value = customizer.uiTheme;
});
}).catch(error => {
console.error('❌ 新i18n系统初始化失败:', error);
// 即使i18n初始化失败,也要挂载应用(使用回退机制)
const app = createApp(App);
app.use(router);
app.use(createPinia());
const pinia = createPinia();
app.use(pinia);
app.use(print);
app.use(VueApexCharts);
app.use(vuetify);
app.use(confirmPlugin);
app.mount('#app');
// 挂载后同步 Vuetify 主题
import('./stores/customizer').then(({ useCustomizerStore }) => {
const customizer = useCustomizerStore(pinia);
vuetify.theme.global.name.value = customizer.uiTheme;
});
});
@@ -1,24 +1,25 @@
<script setup lang="ts">
import AuthLogin from '../authForms/AuthLogin.vue';
import Logo from '@/components/shared/Logo.vue';
import LanguageSwitcher from '@/components/shared/LanguageSwitcher.vue';
import { onMounted, ref } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { useRouter } from 'vue-router';
import {useCustomizerStore} from "@/stores/customizer";
import { useCustomizerStore } from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
import { useTheme } from 'vuetify';
const cardVisible = ref(false);
const router = useRouter();
const authStore = useAuthStore();
const customizer = useCustomizerStore();
const { tm: t } = useModuleI18n('features/auth');
const theme = useTheme();
// 主题切换函数
function toggleTheme() {
customizer.SET_UI_THEME(
customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark'
);
const newTheme = customizer.uiTheme === 'PurpleThemeDark' ? 'PurpleTheme' : 'PurpleThemeDark';
customizer.SET_UI_THEME(newTheme);
theme.global.name.value = newTheme;
}
onMounted(() => {
@@ -27,7 +28,7 @@ onMounted(() => {
router.push(authStore.returnUrl || '/');
return;
}
// 添加一个小延迟以获得更好的动画效果
setTimeout(() => {
cardVisible.value = true;
@@ -36,139 +37,17 @@ onMounted(() => {
</script>
<template>
<div v-if="useCustomizerStore().uiTheme==='PurpleTheme'" class="login-page-container">
<div class="login-background"></div>
<div class="login-container">
<!-- 桌面端卡片样式 -->
<v-card
v-if="!$vuetify.display.xs"
variant="outlined"
class="login-card"
:class="{ 'card-visible': cardVisible }"
>
<v-card-text class="pa-10">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</v-card-text>
</v-card>
<!-- 移动端全屏样式 -->
<div
v-else
class="mobile-login-container"
:class="{ 'mobile-visible': cardVisible }"
>
<div class="mobile-content">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</div>
</div>
<!-- 悬浮式圆角工具栏 -->
<v-card
class="floating-toolbar"
:class="{ 'toolbar-visible': cardVisible }"
elevation="8"
rounded="xl"
>
<v-card-text class="pa-2">
<div class="login-page-container">
<v-card class="login-card" elevation="1">
<v-card-title>
<div class="d-flex justify-space-between align-center w-100">
<img width="80" src="@/assets/images/icon-no-shadow.svg" alt="AstrBot Logo">
<div class="d-flex align-center gap-1">
<LanguageSwitcher />
<v-divider vertical class="mx-1" style="height: 24px !important; opacity: 0.7 !important; align-self: center !important; border-color: rgba(94, 53, 177, 0.4) !important;"></v-divider>
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="text"
size="small"
>
<v-icon
size="18"
:color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'"
>
mdi-weather-night
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t('theme.switchToDark') }}
</v-tooltip>
</v-btn>
</div>
</v-card-text>
</v-card>
</div>
</div>
<div v-else class="login-page-container-dark">
<div class="login-background-dark"></div>
<div class="login-container">
<!-- 桌面端卡片样式 -->
<v-card
v-if="!$vuetify.display.xs"
variant="outlined"
class="login-card"
:class="{ 'card-visible': cardVisible }"
>
<v-card-text class="pa-10">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</v-card-text>
</v-card>
<!-- 移动端全屏样式 -->
<div
v-else
class="mobile-login-container"
:class="{ 'mobile-visible': cardVisible }"
>
<div class="mobile-content">
<div class="logo-wrapper">
<Logo :title="t('logo.title')" :subtitle="t('logo.subtitle')" />
</div>
<div class="divider-container">
<v-divider class="custom-divider"></v-divider>
</div>
<AuthLogin />
</div>
</div>
<!-- 悬浮式圆角工具栏 -->
<v-card
class="floating-toolbar"
:class="{ 'toolbar-visible': cardVisible }"
elevation="8"
rounded="xl"
>
<v-card-text class="pa-2">
<div class="d-flex align-center gap-1">
<LanguageSwitcher />
<v-divider vertical class="mx-1" style="height: 24px !important; opacity: 0.9 !important; align-self: center !important; border-color: rgba(180, 148, 246, 0.8) !important;"></v-divider>
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="text"
size="small"
>
<v-icon
size="18"
:color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'"
>
<v-divider vertical class="mx-1"
style="height: 24px !important; opacity: 0.9 !important; align-self: center !important; border-color: rgba(180, 148, 246, 0.8) !important;"></v-divider>
<v-btn @click="toggleTheme" class="theme-toggle-btn" icon variant="text" size="small">
<v-icon size="18" :color="useCustomizerStore().uiTheme === 'PurpleTheme' ? '#5e35b1' : '#d7c5fa'">
mdi-white-balance-sunny
</v-icon>
<v-tooltip activator="parent" location="top">
@@ -176,288 +55,31 @@ onMounted(() => {
</v-tooltip>
</v-btn>
</div>
</v-card-text>
</v-card>
</div>
</div>
<div class="ml-2" style="font-size: 26px;">{{ t('logo.title') }}</div>
<div class="mt-2 ml-2" style="font-size: 14px; color: grey;">{{ t('logo.subtitle') }}</div>
</v-card-title>
<v-card-text>
<AuthLogin />
</v-card-text>
</v-card>
</div>
</template>
<style lang="scss">
.login-page-container {
background-color: rgb(var(--v-theme-containerBg));
position: relative;
width: 100vw;
height: 100vh;
overflow: hidden;
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100%;
position: relative;
background:
linear-gradient(-45deg,
#faf9f7 0%,
#f9f2f1 25%,
#f1f9f9 50%,
#f9f3f7 75%,
#faf9f7 100%
);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
overflow: hidden;
}
.login-page-container-dark {
display: flex;
justify-content: center;
align-items: center;
height: 100vh;
width: 100%;
position: relative;
background:
linear-gradient(-45deg,
#1e1f21 0%,
#221e25 25%,
#1e2225 50%,
#221f23 75%,
#1e1f21 100%
);
background-size: 400% 400%;
animation: gradientShift 15s ease infinite;
overflow: hidden;
}
@keyframes gradientShift {
0% {
background-position: 0% 50%;
}
50% {
background-position: 100% 50%;
}
100% {
background-position: 0% 50%;
}
}
.login-background {
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background: radial-gradient(circle, rgba(94, 53, 177, 0.02) 0%, rgba(94, 53, 177, 0.03) 70%);
z-index: 0;
animation: rotate 60s linear infinite;
}
.login-background-dark {
position: absolute;
width: 200%;
height: 200%;
top: -50%;
left: -50%;
background: radial-gradient(circle, rgba(114, 46, 209, 0.03) 0%, rgba(114, 46, 209, 0.04) 70%);
z-index: 0;
animation: rotate 60s linear infinite;
}
@keyframes rotate {
0% {
transform: rotate(0deg);
}
100% {
transform: rotate(360deg);
}
}
.login-container {
display: flex;
flex-direction: column;
align-items: center;
gap: 20px;
}
.floating-toolbar {
background: #f8f6fc !important;
border: 1px solid rgba(94, 53, 177, 0.15) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
backdrop-filter: blur(10px);
transform: translateY(20px);
opacity: 0;
transition: transform 0.6s ease 0.2s, opacity 0.6s ease 0.2s, border-color 0.3s ease, box-shadow 0.3s ease;
min-width: auto !important;
width: fit-content;
&.toolbar-visible {
transform: translateY(0);
opacity: 1;
}
&:hover {
transform: translateY(-2px);
border-color: rgba(158, 126, 222, 0.99) !important;
box-shadow: 0 12px 40px rgba(175, 145, 230, 0.741) !important;
}
}
.login-page-container-dark .floating-toolbar {
background: #2a2733 !important;
border: 1px solid rgba(110, 60, 180, 0.692) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3) !important;
&:hover {
border-color: rgba(160, 118, 219, 0.782) !important;
box-shadow: 0 12px 40px rgba(99, 44, 175, 0.462) !important;
}
}
.theme-toggle-btn {
transition: all 0.3s ease;
border-radius: 50% !important;
min-width: 32px !important;
width: 32px !important;
height: 32px !important;
&:hover {
transform: scale(1.05);
background: rgba(94, 53, 177, 0.08) !important;
}
}
.login-page-container-dark .theme-toggle-btn:hover {
background: rgba(114, 46, 209, 0.12) !important;
}
.login-card {
max-width: 520px;
width: 90%;
position: relative;
color: var(--v-theme-primaryText) !important;
border-radius: 16px !important;
border: 1px solid rgba(94, 53, 177, 0.15) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.08) !important;
background: #f8f6fc !important;
backdrop-filter: blur(10px);
transform: translateY(20px);
opacity: 0;
transition: transform 0.5s ease, opacity 0.5s ease, border-color 0.3s ease, box-shadow 0.3s ease;
z-index: 1;
&::before {
content: '';
position: absolute;
top: 50%;
left: 50%;
width: calc(100% + 4px);
height: calc(100% + 4px);
transform: translate(-50%, -50%);
border-radius: 18px;
border: 2px solid rgba(94, 53, 177, 0);
transition: border-color 0.3s ease;
pointer-events: none;
z-index: -1;
}
&.card-visible {
transform: translateY(0);
opacity: 1;
}
&:hover {
border-color: rgba(158, 126, 222, 0.99) !important;
box-shadow: 0 12px 40px rgba(175, 145, 230, 0.741) !important;
transform: translateY(-2px);
&::before {
border-color: rgba(156, 114, 239, 0.907);
}
}
}
.login-page-container-dark .login-card {
border: 1px solid rgba(110, 60, 180, 0.692) !important;
box-shadow: 0 8px 30px rgba(0, 0, 0, 0.3) !important;
background: #2a2733 !important;
&::before {
border: 2px solid rgba(114, 46, 209, 0);
}
&:hover {
border-color: rgba(160, 118, 219, 0.782) !important;
box-shadow: 0 12px 40px rgba(99, 44, 175, 0.462) !important;
transform: translateY(-2px);
&::before {
border-color: rgba(114, 46, 209, 0.15);
}
}
}
.logo-wrapper {
margin-bottom: 10px;
}
.divider-container {
margin: 20px 0;
}
.custom-divider {
border-color: rgba(94, 53, 177, 0.3) !important;
opacity: 1;
}
.login-page-container-dark .custom-divider {
border-color: rgba(180, 148, 246, 0.4) !important;
}
.loginBox {
max-width: 475px;
margin: 0 auto;
}
/* 移动端全屏登录样式 */
.mobile-login-container {
width: 100%;
height: 100vh;
display: flex;
align-items: center;
justify-content: center;
padding: 20px;
box-sizing: border-box;
transform: translateY(20px);
opacity: 0;
transition: transform 0.5s ease, opacity 0.5s ease;
z-index: 1;
&.mobile-visible {
transform: translateY(0);
opacity: 1;
}
}
.mobile-content {
width: 100%;
max-width: 400px;
padding: 40px 20px;
}
/* 移动端调整工具栏位置 */
@media (max-width: 599px) {
.floating-toolbar {
position: fixed;
bottom: 30px;
left: 50%;
transform: translateX(-50%) translateY(20px);
z-index: 1000;
&.toolbar-visible {
transform: translateX(-50%) translateY(0);
}
&:hover {
transform: translateX(-50%) translateY(-2px);
}
}
.login-container {
gap: 0;
}
width: 400px;
padding: 8px;
}
</style>
@@ -1,9 +1,8 @@
<script setup lang="ts">
import {ref, useCssModule} from 'vue';
import { ref, useCssModule } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { Form } from 'vee-validate';
import md5 from 'js-md5';
import {useCustomizerStore} from "@/stores/customizer";
import { useModuleI18n } from '@/i18n/composables';
const { tm: t } = useModuleI18n('features/auth');
@@ -17,7 +16,7 @@ const loading = ref(false);
/* eslint-disable @typescript-eslint/no-explicit-any */
async function validate(values: any, { setErrors }: any) {
loading.value = true;
// md5加密
let password_ = password.value;
if (password.value != '') {
@@ -41,58 +40,25 @@ async function validate(values: any, { setErrors }: any) {
<template>
<Form @submit="validate" class="mt-4 login-form" v-slot="{ errors, isSubmitting }">
<v-text-field
v-model="username"
:label="t('username')"
class="mb-6 input-field"
required
density="comfortable"
hide-details="auto"
variant="outlined"
:style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'}"
prepend-inner-icon="mdi-account"
:disabled="loading"
></v-text-field>
<v-text-field
v-model="password"
:label="t('password')"
required
density="comfortable"
variant="outlined"
:style="{color: useCustomizerStore().uiTheme === 'PurpleTheme' ? '#000000dd' : '#ffffff'}"
hide-details="auto"
:append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'"
:type="show1 ? 'text' : 'password'"
@click:append="show1 = !show1"
class="pwd-input"
prepend-inner-icon="mdi-lock"
:disabled="loading"
></v-text-field>
<v-btn
color="secondary"
:loading="isSubmitting || loading"
block
class="login-btn mt-8"
variant="flat"
size="large"
:disabled="valid"
type="submit"
elevation="2"
<v-text-field v-model="username" :label="t('username')" class="mb-6 input-field" required hide-details="auto"
variant="outlined" prepend-inner-icon="mdi-account" :disabled="loading"></v-text-field>
>
<v-text-field v-model="password" :label="t('password')" required variant="outlined" hide-details="auto"
:append-icon="show1 ? 'mdi-eye' : 'mdi-eye-off'" :type="show1 ? 'text' : 'password'"
@click:append="show1 = !show1" class="pwd-input" prepend-inner-icon="mdi-lock" :disabled="loading"></v-text-field>
<div class="mt-2">
<small style="color: grey;">{{ t('defaultHint') }}</small>
</div>
<v-btn color="secondary" :loading="isSubmitting || loading" block class="login-btn mt-8" variant="flat" size="large"
:disabled="valid" type="submit">
<span class="login-btn-text">{{ t('login') }}</span>
</v-btn>
<div v-if="errors.apiError" class="mt-4 error-container">
<v-alert
color="error"
variant="tonal"
density="comfortable"
icon="mdi-alert-circle"
border="start"
>
<v-alert color="error" variant="tonal" icon="mdi-alert-circle" border="start">
{{ errors.apiError }}
</v-alert>
</div>
@@ -105,24 +71,25 @@ async function validate(values: any, { setErrors }: any) {
font-weight: 500;
}
.input-field, .pwd-input {
.input-field,
.pwd-input {
.v-field__field {
padding-top: 5px;
padding-bottom: 5px;
}
.v-field__outline {
opacity: 0.7;
}
&:hover .v-field__outline {
opacity: 0.9;
}
.v-field--focused .v-field__outline {
opacity: 1;
}
.v-field__prepend-inner {
padding-right: 8px;
opacity: 0.7;
@@ -138,36 +105,36 @@ async function validate(values: any, { setErrors }: any) {
top: 50%;
transform: translateY(-50%);
opacity: 0.7;
&:hover {
opacity: 1;
}
}
}
.login-btn {
margin-top: 12px;
height: 48px;
transition: all 0.3s ease;
letter-spacing: 0.5px;
border-radius: 8px !important;
&:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(94, 53, 177, 0.2) !important;
}
.login-btn-text {
font-size: 1.05rem;
font-weight: 500;
}
}
.hint-text {
color: var(--v-theme-secondaryText);
padding-left: 5px;
}
.error-container {
.v-alert {
border-left-width: 4px !important;