Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
35 changes: 21 additions & 14 deletions dashboard/src/components/extension/PinnedPluginItem.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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>
Expand Down Expand Up @@ -182,8 +190,7 @@ const authorDisplay = computed(() => {
.pinned-card-wrapper {
position: relative;
display: inline-block;
width: 72px;
height: 72px;
width: 80px;
}

.pinned-item {
Expand Down
44 changes: 43 additions & 1 deletion dashboard/src/components/shared/ConsoleDisplayer.vue
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,22 @@ import { EventSourcePolyfill } from 'event-source-polyfill';
</script>

<template>
<div>
<div class="console-displayer-wrapper" id="console-wrapper">
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

建议同时添加 ref 属性,以便在 Vue 组件内部更安全地引用该 DOM 元素,减少对全局 document.getElementById 的依赖。

  <div class="console-displayer-wrapper" id="console-wrapper" ref="consoleWrapper">

<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"
:text-color="level === 'DEBUG' || level === 'INFO' ? 'black' : 'white'" class="font-weight-medium">
{{ 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%">
Expand All @@ -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;',
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -253,6 +264,21 @@ export default {
this.autoScroll = !this.autoScroll;
},

toggleFullscreen() {
const container = document.getElementById('console-wrapper');
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

建议使用 this.$refs 获取元素引用,这比 document.getElementById 更符合 Vue 的开发范式,且能避免潜在的 ID 冲突。

      const container = this.$refs.consoleWrapper;

if (!document.fullscreenElement) {
Comment on lines +267 to +269
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (bug_risk): Guard against a missing console-wrapper element before calling requestFullscreen.

If document.getElementById('console-wrapper') returns null (e.g., after refactors or conditional rendering), container.requestFullscreen() will throw. Add a null check before calling requestFullscreen, for example:

const container = document.getElementById('console-wrapper');
if (!container) {
  console.warn('Console wrapper element not found for fullscreen');
  return;
}

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) {
Expand Down Expand Up @@ -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;
Expand Down
37 changes: 36 additions & 1 deletion dashboard/src/stores/auth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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> {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

issue (complexity): Consider extracting the onboarding check logic into a dedicated service that also centralizes provider type resolution to keep the auth store focused and easier to maintain.

You can keep the new behavior but reduce complexity and coupling by:

  1. Extracting onboarding checks out of the store into a small service/composable.
  2. Simplifying provider detection with a helper that normalizes provider type resolution.

For example, create a dedicated onboarding service:

// services/onboardingService.ts
import axios from 'axios';

async function fetchPlatformHasConfig(): Promise<boolean> {
  const res = await axios.get('/api/config/get');
  return (res.data.data.config.platform || []).length > 0;
}

function resolveProviderType(
  provider: any,
  sourceTypeById: Record<string, string>,
): string | null {
  if (provider.provider_type) return provider.provider_type;
  if (provider.provider_source_id) {
    return sourceTypeById[provider.provider_source_id] || null;
  }
  const legacyType = String(provider.type || '');
  return legacyType || null;
}

async function fetchHasChatCompletionProvider(): Promise<boolean> {
  const res = await axios.get('/api/config/provider/template');
  const providers = res.data.data?.providers || [];
  const sources = res.data.data?.provider_sources || [];

  const sourceTypeById: Record<string, string> = {};
  sources.forEach((s: any) => {
    sourceTypeById[s.id] = s.provider_type;
  });

  return providers.some((p: any) => {
    const type = resolveProviderType(p, sourceTypeById);
    return type === 'chat_completion' || String(type).includes('chat_completion');
  });
}

export async function isOnboardingCompleted(): Promise<boolean> {
  try {
    const hasPlatform = await fetchPlatformHasConfig();
    if (!hasPlatform) return false;

    return await fetchHasChatCompletionProvider();
  } catch (e) {
    console.error('Failed to check onboarding status:', e);
    return false; // preserve existing behavior
  }
}

Then the store stays focused and orchestration is clearer:

// in auth store
import { isOnboardingCompleted } from '@/services/onboardingService';

async login(username: string, password: string) {
  try {
    const res = await axios.post('/api/login', { username, password });
    // ... existing login logic ...

    const onboardingCompleted = await isOnboardingCompleted();
    this.returnUrl = null;

    router.push(onboardingCompleted ? '/dashboard/default' : '/welcome');
  } catch (error) {
    return Promise.reject(error);
  }
}

This keeps functionality identical while:

  • Removing cross-cutting onboarding logic from the auth store.
  • Encapsulating network + domain logic into a service.
  • Making provider type resolution self-contained and easier to reason about.

try {
// 1. 检查平台配置
const platformRes = await axios.get('/api/config/get');
const hasPlatform = (platformRes.data.data.config.platform || []).length > 0;
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

建议使用可选链(Optional Chaining)来安全地访问属性。如果 API 返回的数据结构中 dataconfig 缺失,直接访问 config.platform 会导致运行时错误。

Suggested change
const hasPlatform = (platformRes.data.data.config.platform || []).length > 0;
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');
Expand Down
9 changes: 7 additions & 2 deletions dashboard/src/views/authentication/auth/LoginPage.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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;
}

Expand Down
Loading