Files
AstrBot/dashboard/src/views/authentication/auth/LoginPage.vue
LIghtJUNction b9b3d50986 feat(dashboard): rename to Starlight Panel and add daily quote
- Change login page title from "AstrBot WebUI/Dashboard" to "AstrBot Starlight Panel"
- Replace welcome subtitle with DailyQuote component showing programming quotes
- Quotes are localized for en-US, zh-CN, and ru-RU
- Quote of the day stays consistent throughout each day
2026-03-29 16:05:22 +08:00

319 lines
9.0 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import AuthLogin from "../authForms/AuthLogin.vue";
import DailyQuote from "@/components/shared/DailyQuote.vue";
import LanguageSwitcher from "@/components/shared/LanguageSwitcher.vue";
import { onMounted, ref } 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 { 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 { tm: t } = useModuleI18n("features/auth");
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();
}
function savePreset() {
if (!newPresetName.value || !newPresetUrl.value) return;
apiStore.addPreset({
name: newPresetName.value,
url: newPresetUrl.value,
});
showAddPreset.value = false;
newPresetName.value = "";
newPresetUrl.value = "";
}
function isCustomPreset(name: string) {
return apiStore.customPresets.some((p) => p.name === name);
}
// 主题切换函数
function toggleTheme() {
customizer.TOGGLE_DARK_MODE();
}
onMounted(() => {
// 应用URL参数用于分享预设配置
applyUrlParams();
// 检查用户是否已登录,如果已登录则重定向
if (authStore.has_token()) {
router.push(authStore.returnUrl || "/");
return;
}
// 添加一个小延迟以获得更好的动画效果
setTimeout(() => {
cardVisible.value = true;
}, 100);
});
</script>
<template>
<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.9 !important;
align-self: center !important;
border-color: rgba(var(--v-theme-primary), 0.45) !important;
"
/>
<v-btn
icon
variant="text"
size="small"
@click="serverConfigDialog = true"
>
<v-icon size="18" :color="'rgb(var(--v-theme-primary))'">
mdi-server
</v-icon>
<v-tooltip activator="parent" location="top">
{{ t("serverConfig.tooltip") }}
</v-tooltip>
</v-btn>
<v-btn
class="theme-toggle-btn"
icon
variant="text"
size="small"
@click="toggleTheme"
>
<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">
{{
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">
<DailyQuote />
</div>
</v-card-title>
<v-card-text>
<AuthLogin />
</v-card-text>
</v-card>
<v-dialog v-model="serverConfigDialog" max-width="450">
<v-card>
<v-card-title>{{ t("serverConfig.title") }}</v-card-title>
<v-card-text>
<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"
>
<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-model="newPresetUrl"
label="URL"
density="compact"
hide-details
class="mb-2"
variant="outlined"
bg-color="white"
/>
<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
v-for="preset in apiStore.presets"
:key="preset.name"
size="small"
: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>
</div>
<v-text-field
v-model="apiUrl"
:label="t('serverConfig.label')"
:placeholder="t('serverConfig.placeholder')"
:hint="t('serverConfig.hint')"
persistent-hint
variant="outlined"
density="compact"
/>
<v-btn
variant="tonal"
size="small"
block
class="mt-2"
prepend-icon="mdi-share-variant"
@click="copyShareableUrl"
>
{{ t("shareLink") }}
</v-btn>
</v-card-text>
<v-card-actions>
<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>
</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;
}
.login-card {
width: 400px;
padding: 8px;
}
</style>