mirror of
https://github.com/AstrBotDevs/AstrBot
synced 2026-07-15 17:30:13 +08:00
fix(dashboard): restore beautiful glassmorphism login UI lost during master branch merge
This commit is contained in:
@@ -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>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user