Feat(webui): dashboard and console qol improvements (#7215)

* feat: support native fullscreen for log console

* feat: skip welcome page for configured users

* feat:  display plugin names under pinned icons

* fix: refine container styles
This commit is contained in:
M1LKT
2026-04-02 13:40:21 +08:00
committed by GitHub
parent 206973e8ad
commit e8d6938d31
4 changed files with 107 additions and 18 deletions

View File

@@ -65,18 +65,26 @@ const authorDisplay = computed(() => {
>
<v-menu offset-y>
<template #activator="{ props: menuProps }">
<v-avatar
v-bind="menuProps"
size="72"
class="pinned-avatar activator-avatar"
:title="plugin.display_name || plugin.name"
>
<img
:src="(typeof plugin.logo === 'string' && plugin.logo.trim()) ? plugin.logo : defaultPluginIcon"
:alt="plugin.name"
@error="handlePinnedImgError"
/>
</v-avatar>
<div class="d-flex flex-column align-center" style="cursor: pointer; width: 80px;">
<v-avatar
v-bind="menuProps"
size="72"
class="pinned-avatar activator-avatar mb-1"
:title="plugin.display_name || plugin.name"
>
<img
:src="(typeof plugin.logo === 'string' && plugin.logo.trim()) ? plugin.logo : defaultPluginIcon"
:alt="plugin.name"
@error="handlePinnedImgError"
/>
</v-avatar>
<span
class="text-caption text-center text-truncate"
style="width: 100%; font-size: 0.75rem; opacity: 0.9; line-height: 1.2;"
>
{{ plugin.display_name || plugin.name }}
</span>
</div>
</template>
<v-card>
@@ -182,8 +190,7 @@ const authorDisplay = computed(() => {
.pinned-card-wrapper {
position: relative;
display: inline-block;
width: 72px;
height: 72px;
width: 80px;
}
.pinned-item {

View File

@@ -5,7 +5,7 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
</script>
<template>
<div>
<div class="console-displayer-wrapper" id="console-wrapper">
<div class="filter-controls mb-2" v-if="showLevelBtns">
<v-chip-group v-model="selectedLevels" column multiple>
<v-chip v-for="level in logLevels" :key="level" :color="getLevelColor(level)" filter variant="flat" size="small"
@@ -13,6 +13,14 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
{{ level }}
</v-chip>
</v-chip-group>
<v-spacer></v-spacer>
<v-btn
:icon="isFullscreen ? 'mdi-fullscreen-exit' : 'mdi-fullscreen'"
variant="text"
density="compact"
class="me-4 fullscreen-btn"
@click="toggleFullscreen"
></v-btn>
</div>
<div id="term" style="background-color: #1e1e1e; padding: 16px; border-radius: 8px; overflow-y:auto; height: 100%">
@@ -26,6 +34,7 @@ export default {
data() {
return {
autoScroll: true,
isFullscreen: false,
logColorAnsiMap: {
'\u001b[1;34m': 'color: #39C5BB; font-weight: bold;',
'\u001b[1;36m': 'color: #00FFFF; font-weight: bold;',
@@ -80,8 +89,10 @@ export default {
async mounted() {
await this.fetchLogHistory();
this.connectSSE();
document.addEventListener('fullscreenchange', this.handleFullscreenChange);
},
beforeUnmount() {
document.removeEventListener('fullscreenchange', this.handleFullscreenChange);
if (this.eventSource) {
this.eventSource.close();
this.eventSource = null;
@@ -253,6 +264,21 @@ export default {
this.autoScroll = !this.autoScroll;
},
toggleFullscreen() {
const container = document.getElementById('console-wrapper');
if (!document.fullscreenElement) {
container.requestFullscreen().catch(err => {
console.error(`Error attempting to enable full-screen mode: ${err.message}`);
});
} else {
document.exitFullscreen();
}
},
handleFullscreenChange() {
this.isFullscreen = !!document.fullscreenElement;
},
printLog(log) {
let ele = document.getElementById('term')
if (!ele) {
@@ -282,14 +308,30 @@ export default {
</script>
<style scoped>
.console-displayer-wrapper {
height: 100%;
display: flex;
flex-direction: column;
}
#console-wrapper:fullscreen {
background-color: #1e1e1e;
padding: 20px;
}
.filter-controls {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
margin-bottom: 8px;
margin-left: 20px;
}
.fullscreen-btn {
color: rgba(255, 255, 255, 0.7) !important; /* 提高在深色背景下的对比度 */
}
:deep(.console-log-line) {
display: block;
margin-bottom: 2px;

View File

@@ -25,12 +25,47 @@ export const useAuthStore = defineStore({
localStorage.setItem('user', this.username);
localStorage.setItem('token', res.data.data.token);
localStorage.setItem('change_pwd_hint', res.data.data?.change_pwd_hint);
const onboardingCompleted = await this.checkOnboardingCompleted();
this.returnUrl = null;
router.push('/welcome');
if (onboardingCompleted) {
router.push('/dashboard/default');
} else {
router.push('/welcome');
}
} catch (error) {
return Promise.reject(error);
}
},
async checkOnboardingCompleted(): Promise<boolean> {
try {
// 1. 检查平台配置
const platformRes = await axios.get('/api/config/get');
const hasPlatform = (platformRes.data.data.config.platform || []).length > 0;
if (!hasPlatform) return false;
// 2. 检查提供者配置
const providerRes = await axios.get('/api/config/provider/template');
const providers = providerRes.data.data?.providers || [];
const sources = providerRes.data.data?.provider_sources || [];
const sourceMap = new Map();
sources.forEach((s: any) => sourceMap.set(s.id, s.provider_type));
const hasProvider = providers.some((provider: any) => {
if (provider.provider_type) return provider.provider_type === 'chat_completion';
if (provider.provider_source_id) {
const type = sourceMap.get(provider.provider_source_id);
if (type === 'chat_completion') return true;
}
return String(provider.type || '').includes('chat_completion');
});
return hasProvider;
} catch (e) {
console.error('Failed to check onboarding status:', e);
return false;
}
},
logout() {
this.username = '';
localStorage.removeItem('user');

View File

@@ -22,10 +22,15 @@ function toggleTheme() {
theme.global.name.value = newTheme;
}
onMounted(() => {
onMounted(async () => {
// 检查用户是否已登录,如果已登录则重定向
if (authStore.has_token()) {
router.push('/welcome');
const onboardingCompleted = await authStore.checkOnboardingCompleted();
if (onboardingCompleted) {
router.push('/dashboard/default');
} else {
router.push('/welcome');
}
return;
}