fix(dashboard): restore beautiful glassmorphism login UI lost during master branch merge

This commit is contained in:
LIghtJUNction
2026-04-04 18:29:57 +08:00
parent 3b8a77b053
commit b4f3718413
2 changed files with 312 additions and 129 deletions

View File

@@ -1,30 +1,80 @@
<script setup lang="ts">
import AuthLogin from "../authForms/AuthLogin.vue";
import DailyQuote from "@/components/shared/DailyQuote.vue";
import DiamondBg from "@/components/auth/DiamondBg.vue";
import LanguageSwitcher from "@/components/shared/LanguageSwitcher.vue";
import { onMounted, ref } from "vue";
import { onMounted, ref, computed } from "vue";
import { useAuthStore } from "@/stores/auth";
import { useApiStore } from "@/stores/api";
import { useRouter } from "vue-router";
import { useCustomizerStore } from "@/stores/customizer";
import { useModuleI18n } from "@/i18n/composables";
import { useTheme } from "vuetify";
const vuetifyTheme = useTheme();
const isDark = computed(() => vuetifyTheme.global.name.value === 'BlueBusinessDarkTheme');
import { useI18n, useModuleI18n } from "@/i18n/composables";
import { useToast } from "@/utils/toast";
import { getApiBaseUrlValidationError } from "@/utils/request";
const cardVisible = ref(false);
const router = useRouter();
const authStore = useAuthStore();
const apiStore = useApiStore();
const customizer = useCustomizerStore();
const { locale } = useI18n();
const { tm: t } = useModuleI18n("features/auth");
const theme = useTheme();
const toast = useToast();
const serverConfigDialog = ref(false);
const apiUrl = ref(apiStore.apiBaseUrl);
// URL parameter handling for shareable config
function applyUrlParams() {
const params = new URLSearchParams(window.location.search);
const apiUrlParam = params.get("api_url");
const usernameParam = params.get("username");
if (apiUrlParam) {
apiUrl.value = apiUrlParam;
const validationError = getApiBaseUrlValidationError(apiUrlParam);
if (!validationError) {
apiStore.setApiBaseUrl(apiUrlParam);
}
}
if (usernameParam) {
window.dispatchEvent(
new CustomEvent("astrbot-url-param-username", {
detail: { username: usernameParam },
}),
);
}
}
function getShareableUrl() {
const url = new URL(window.location.href);
url.searchParams.set("api_url", apiUrl.value);
return url.toString();
}
async function copyShareableUrl() {
try {
await navigator.clipboard.writeText(getShareableUrl());
toast.success(t("linkCopied"));
} catch {
toast.error(t("linkCopyFailed"));
}
}
const showAddPreset = ref(false);
const newPresetName = ref("");
const newPresetUrl = ref("");
function saveApiUrl() {
const validationError = getApiBaseUrlValidationError(apiUrl.value);
if (validationError) {
toast.error(validationError);
return;
}
apiStore.setApiBaseUrl(apiUrl.value);
serverConfigDialog.value = false;
window.location.reload();
@@ -47,15 +97,13 @@ function isCustomPreset(name: string) {
// 主题切换函数
function toggleTheme() {
const newTheme =
customizer.uiTheme === "PurpleThemeDark"
? "PurpleTheme"
: "PurpleThemeDark";
customizer.SET_UI_THEME(newTheme);
theme.global.name.value = newTheme;
customizer.TOGGLE_DARK_MODE();
}
onMounted(() => {
// 应用URL参数用于分享预设配置
applyUrlParams();
// 检查用户是否已登录,如果已登录则重定向
if (authStore.has_token()) {
router.push(authStore.returnUrl || "/");
@@ -71,8 +119,9 @@ onMounted(() => {
<template>
<div class="login-page-container">
<DiamondBg v-if="isDark" />
<v-card class="login-card" elevation="1">
<v-card-title>
<v-card-title :key="locale">
<div class="d-flex justify-space-between align-center w-100">
<img
width="80"
@@ -88,24 +137,17 @@ onMounted(() => {
height: 24px !important;
opacity: 0.9 !important;
align-self: center !important;
border-color: rgba(180, 148, 246, 0.8) !important;
border-color: rgba(var(--v-theme-primary), 0.45) !important;
"
></v-divider>
/>
<v-btn
@click="serverConfigDialog = true"
icon
variant="text"
size="small"
@click="serverConfigDialog = true"
>
<v-icon
size="18"
:color="
useCustomizerStore().uiTheme === 'PurpleTheme'
? '#5e35b1'
: '#d7c5fa'
"
>
<v-icon size="18" :color="'rgb(var(--v-theme-primary))'">
mdi-server
</v-icon>
<v-tooltip activator="parent" location="top">
@@ -114,31 +156,34 @@ onMounted(() => {
</v-btn>
<v-btn
@click="toggleTheme"
class="theme-toggle-btn"
icon
variant="text"
size="small"
@click="toggleTheme"
>
<v-icon
size="18"
:color="
useCustomizerStore().uiTheme === 'PurpleTheme'
? '#5e35b1'
: '#d7c5fa'
"
>
mdi-white-balance-sunny
<v-icon size="18" :color="'rgb(var(--v-theme-primary))'">
{{
customizer.isDarkTheme
? "mdi-weather-night"
: "mdi-white-balance-sunny"
}}
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t("theme.switchToLight") }}
{{
customizer.isDarkTheme
? t("theme.switchToLight")
: t("theme.switchToDark")
}}
</v-tooltip>
</v-btn>
</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 class="ml-2" style="font-size: 26px">
{{ t("logo.title") }}
</div>
<div class="mt-2 ml-2" style="font-size: 14px; color: var(--v-theme-on-surface-variant)">
<DailyQuote />
</div>
</v-card-title>
<v-card-text>
@@ -149,82 +194,89 @@ onMounted(() => {
<v-dialog v-model="serverConfigDialog" max-width="450">
<v-card>
<v-card-title>{{ t("serverConfig.title") }}</v-card-title>
<v-card-text>
<v-card-text class="pt-0">
<div class="text-body-2 text-medium-emphasis mb-4">
{{ t("serverConfig.description") }}
</div>
<div
v-if="
(apiStore.presets && apiStore.presets.length > 0) ||
apiStore.customPresets
"
class="mb-4"
>
<!-- Presets section -->
<div class="mb-4">
<div class="d-flex justify-space-between align-center mb-2">
<div class="text-caption text-medium-emphasis">
{{ t("serverConfig.presetLabel") }}
</div>
<v-btn
size="x-small"
variant="text"
icon
@click="showAddPreset = !showAddPreset"
>
<v-icon>mdi-plus</v-icon>
</v-btn>
</div>
<v-expand-transition>
<div
v-if="showAddPreset"
class="mb-2 pa-2 bg-grey-lighten-4 rounded border"
>
<v-text-field
v-model="newPresetName"
label="Name"
density="compact"
hide-details
class="mb-2"
variant="outlined"
bg-color="white"
></v-text-field>
<v-text-field
v-model="newPresetUrl"
label="URL"
density="compact"
hide-details
class="mb-2"
variant="outlined"
bg-color="white"
></v-text-field>
<v-btn
size="small"
block
color="primary"
variant="flat"
@click="savePreset"
>Add Preset</v-btn
>
</div>
</v-expand-transition>
<v-chip-group column>
<v-chip-group
v-if="apiStore.presets.length > 0 || apiStore.customPresets.length > 0"
column
class="mb-2"
>
<v-chip
v-for="preset in apiStore.presets"
:key="preset.name"
size="small"
@click="apiUrl = preset.url"
:variant="apiUrl === preset.url ? 'flat' : 'tonal'"
:color="apiUrl === preset.url ? 'primary' : undefined"
:closable="isCustomPreset(preset.name)"
@click="apiUrl = preset.url"
@click:close="apiStore.removePreset(preset.name)"
>
{{ preset.name }}
</v-chip>
</v-chip-group>
<!-- Add preset inline form -->
<div v-if="showAddPreset" class="preset-add-form rounded pa-3 mb-2">
<div class="d-flex align-center gap-2 mb-2">
<v-text-field
v-model="newPresetName"
:label="t('presetName')"
density="compact"
hide-details
variant="outlined"
class="flex-1"
/>
<v-text-field
v-model="newPresetUrl"
:label="t('presetUrl')"
density="compact"
hide-details
variant="outlined"
class="flex-1"
/>
<v-btn
size="small"
color="primary"
variant="flat"
icon
@click="savePreset"
>
<v-icon size="18">mdi-check</v-icon>
</v-btn>
<v-btn
size="small"
variant="text"
icon
@click="showAddPreset = false"
>
<v-icon size="18">mdi-close</v-icon>
</v-btn>
</div>
</div>
<v-btn
v-if="!showAddPreset"
size="x-small"
variant="tonal"
prepend-icon="mdi-plus"
@click="showAddPreset = true"
>
{{ t("addPreset") }}
</v-btn>
</div>
<!-- API URL field -->
<v-text-field
v-model="apiUrl"
:label="t('serverConfig.label')"
@@ -233,16 +285,42 @@ onMounted(() => {
persistent-hint
variant="outlined"
density="compact"
></v-text-field>
class="mb-3"
/>
<!-- Share link button -->
<v-btn
variant="tonal"
size="small"
block
class="mb-3"
prepend-icon="mdi-share-variant"
@click="copyShareableUrl"
>
{{ t("shareLink") }}
</v-btn>
<!-- Auto theme switch -->
<div class="d-flex align-center justify-space-between preset-auto-switch">
<div class="text-caption">{{ t('autoTheme') }}</div>
<v-switch
v-model="customizer.autoSwitchTheme"
color="primary"
density="compact"
hide-details
inset
@update:model-value="customizer.SET_AUTO_SYNC($event)"
/>
</div>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn variant="text" @click="serverConfigDialog = false">{{
t("serverConfig.cancel")
}}</v-btn>
<v-btn color="primary" variant="flat" @click="saveApiUrl">{{
t("serverConfig.save")
}}</v-btn>
<v-spacer />
<v-btn variant="text" @click="serverConfigDialog = false">
{{ t("serverConfig.cancel") }}
</v-btn>
<v-btn color="primary" variant="flat" @click="saveApiUrl">
{{ t("serverConfig.save") }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
@@ -261,8 +339,57 @@ onMounted(() => {
align-items: center;
}
// Dark mode: radial mask + DiamondBg
.v-theme--bluebusinessdarktheme .login-page-container {
background-color: rgb(var(--v-theme-containerBg));
mask-image: radial-gradient(ellipse 60% 70% at 50% 50%, black 30%, transparent 70%);
-webkit-mask-image: radial-gradient(ellipse 60% 70% at 50% 50%, black 30%, transparent 70%);
}
// Light mode: pure white
.v-theme--bluebusinesstheme .login-page-container {
background-color: #FFFFFF;
mask-image: none;
-webkit-mask-image: none;
}
.login-card {
width: 400px;
padding: 8px;
background: var(--v-theme-surface) !important;
backdrop-filter: blur(28px) saturate(1.1);
border: 1px solid rgba(var(--v-theme-primary), 0.2);
box-shadow:
0 0 80px rgba(var(--v-theme-on-surface), 0.3),
0 0 120px rgba(var(--v-theme-on-surface), 0.15),
inset 0 0 20px rgba(0, 0, 0, 0.1);
}
// Light mode: clean white card
.v-theme--bluebusinesstheme .login-card {
background: #FFFFFF !important;
border: 1px solid rgba(0, 49, 83, 0.12) !important;
box-shadow: 0 4px 24px rgba(0, 49, 83, 0.08) !important;
backdrop-filter: none;
}
// Dark mode: preset add form
.v-theme--bluebusinessdarktheme .preset-add-form {
background: rgba(255, 255, 255, 0.05);
border: 1px solid rgba(0, 242, 255, 0.15);
}
// Light mode: preset add form
.v-theme--bluebusinesstheme .preset-add-form {
background: rgba(0, 49, 83, 0.04);
border: 1px solid rgba(0, 49, 83, 0.12);
}
// Auto switch row
.preset-auto-switch {
padding: 8px 12px;
border: 1px solid rgba(var(--v-theme-on-surface), 0.1);
border-radius: 8px;
background: rgba(var(--v-theme-on-surface), 0.02);
}
</style>

View File

@@ -1,56 +1,112 @@
<script setup lang="ts">
import { ref, useCssModule } from 'vue';
import { useAuthStore } from '@/stores/auth';
import { Form } from 'vee-validate';
import { useModuleI18n } from '@/i18n/composables';
import { ref, onMounted, onUnmounted } from "vue";
import { useAuthStore } from "@/stores/auth";
import { Form } from "vee-validate";
import { useModuleI18n } from "@/i18n/composables";
const { tm: t } = useModuleI18n('features/auth');
const { tm: t } = useModuleI18n("features/auth");
const valid = ref(false);
const show1 = ref(false);
const password = ref('');
const username = ref('');
const password = ref("");
const username = ref("");
const loading = ref(false);
/* eslint-disable @typescript-eslint/no-explicit-any */
async function validate(values: any, { setErrors }: any) {
// 从URL参数读取用户名
const params = new URLSearchParams(window.location.search);
const usernameParam = params.get("username");
if (usernameParam) {
username.value = usernameParam;
}
// 监听从LoginPage传来的用户名参数
function handleUsernameParam(event: Event) {
const customEvent = event as CustomEvent<{ username: string }>;
username.value = customEvent.detail.username;
}
onMounted(() => {
window.addEventListener("astrbot-url-param-username", handleUsernameParam);
});
onUnmounted(() => {
window.removeEventListener("astrbot-url-param-username", handleUsernameParam);
});
async function validate(_values: any, { setErrors }: any) {
loading.value = true;
const authStore = useAuthStore();
// @ts-ignore
authStore.returnUrl = new URLSearchParams(window.location.search).get('redirect');
return authStore.login(username.value, password.value).then((res) => {
console.log(res);
loading.value = false;
}).catch((err) => {
setErrors({ apiError: err });
loading.value = false;
});
const redirectParam = new URLSearchParams(window.location.search).get(
"redirect",
);
// 将 string | null 显式断言为与 store 兼容的类型,避免因 store 初始状态推断不完整而导致的编译错误
authStore.returnUrl = redirectParam as unknown as string | null;
return authStore
.login(username.value, password.value)
.then(() => {
loading.value = false;
})
.catch((err) => {
setErrors({ apiError: err });
loading.value = false;
});
}
</script>
<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 hide-details="auto"
variant="outlined" prepend-inner-icon="mdi-account" :disabled="loading"></v-text-field>
<Form
v-slot="{ errors, isSubmitting }"
class="mt-4 login-form"
@submit="validate"
>
<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-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>
<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'"
class="pwd-input"
prepend-inner-icon="mdi-lock"
:disabled="loading"
@click:append="show1 = !show1"
/>
<div class="mt-2">
<small style="color: grey;">{{ t('defaultHint') }}</small>
<small class="hint-label">{{ 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
color="secondary"
:loading="isSubmitting || loading"
block
class="login-btn mt-8"
variant="flat"
size="large"
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" icon="mdi-alert-circle" border="start">
<v-alert
color="error"
variant="tonal"
icon="mdi-alert-circle"
border="start"
>
{{ errors.apiError }}
</v-alert>
</div>
@@ -122,8 +178,8 @@ async function validate(values: any, { setErrors }: any) {
}
}
.hint-text {
color: var(--v-theme-secondaryText);
.hint-label {
color: var(--v-theme-on-surface-variant);
padding-left: 5px;
}