fix(frontend): 接通设置状态与搜索降级
This commit is contained in:
@@ -1,9 +1,10 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { computed, onMounted, watch } from 'vue'
|
||||||
import { useRoute, useRouter } from 'vue-router'
|
import { useRoute, useRouter } from 'vue-router'
|
||||||
import { useWorkspaceStore } from '@/stores/workspace'
|
import { useWorkspaceStore } from '@/stores/workspace'
|
||||||
import { useThemeStore } from '@/stores/theme'
|
import { useThemeStore } from '@/stores/theme'
|
||||||
import { useEditorStore } from '@/stores/editor'
|
import { useEditorStore } from '@/stores/editor'
|
||||||
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
import PrimarySidebar from './PrimarySidebar.vue'
|
import PrimarySidebar from './PrimarySidebar.vue'
|
||||||
import SecondarySidebar from './SecondarySidebar.vue'
|
import SecondarySidebar from './SecondarySidebar.vue'
|
||||||
import StatusBar from './StatusBar.vue'
|
import StatusBar from './StatusBar.vue'
|
||||||
@@ -17,9 +18,16 @@ defineProps<{
|
|||||||
const workspaceStore = useWorkspaceStore()
|
const workspaceStore = useWorkspaceStore()
|
||||||
const themeStore = useThemeStore()
|
const themeStore = useThemeStore()
|
||||||
const editorStore = useEditorStore()
|
const editorStore = useEditorStore()
|
||||||
|
const settingsStore = useSettingsStore()
|
||||||
const route = useRoute()
|
const route = useRoute()
|
||||||
const router = useRouter()
|
const router = useRouter()
|
||||||
|
|
||||||
|
onMounted(() => { void settingsStore.loadDiagnostics() })
|
||||||
|
watch(() => settingsStore.defaultEditorMode, (mode) => editorStore.setMode(mode), { immediate: true })
|
||||||
|
watch(() => settingsStore.editorLineWidth, (width) => {
|
||||||
|
document.documentElement.style.setProperty('--editor-line-width', `${width}ch`)
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
const routeName = computed(() => route.name as string)
|
const routeName = computed(() => route.name as string)
|
||||||
|
|
||||||
const secondaryComponent = computed(() => {
|
const secondaryComponent = computed(() => {
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ const navItems = [
|
|||||||
]
|
]
|
||||||
|
|
||||||
const currentName = computed(() => {
|
const currentName = computed(() => {
|
||||||
const name = route.name as string
|
return route.name as string
|
||||||
if (name === 'skills' || name === 'plugins') return 'skills'
|
|
||||||
return name
|
|
||||||
})
|
})
|
||||||
|
|
||||||
function navigate(name: string) {
|
function navigate(name: string) {
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import { onMounted, reactive, ref } from 'vue'
|
|||||||
import type { ProviderConfig, ProviderType } from '@/contracts'
|
import type { ProviderConfig, ProviderType } from '@/contracts'
|
||||||
import { useProviderStore } from '@/stores/provider'
|
import { useProviderStore } from '@/stores/provider'
|
||||||
import { useSettingsStore } from '@/stores/settings'
|
import { useSettingsStore } from '@/stores/settings'
|
||||||
|
import { useThemeStore } from '@/stores/theme'
|
||||||
|
|
||||||
type Section = 'general' | 'editor' | 'providers' | 'index' | 'permissions' | 'ai-core'
|
type Section = 'general' | 'editor' | 'providers' | 'index' | 'permissions' | 'ai-core'
|
||||||
const sections: Array<{ id: Section; label: string }> = [
|
const sections: Array<{ id: Section; label: string }> = [
|
||||||
@@ -12,6 +13,7 @@ const sections: Array<{ id: Section; label: string }> = [
|
|||||||
const activeSection = ref<Section>('general')
|
const activeSection = ref<Section>('general')
|
||||||
const settingsStore = useSettingsStore()
|
const settingsStore = useSettingsStore()
|
||||||
const providerStore = useProviderStore()
|
const providerStore = useProviderStore()
|
||||||
|
const themeStore = useThemeStore()
|
||||||
const showProviderForm = ref(false)
|
const showProviderForm = ref(false)
|
||||||
const editingProviderId = ref<string | null>(null)
|
const editingProviderId = ref<string | null>(null)
|
||||||
const providerAction = ref('')
|
const providerAction = ref('')
|
||||||
@@ -43,7 +45,7 @@ async function testProvider(provider: ProviderConfig) { testResults.value[provid
|
|||||||
|
|
||||||
<div v-if="activeSection === 'general'" class="panel settings-section"><h2>通用</h2><label class="setting-row"><span><strong>恢复上次 Vault</strong><small>启动后自动打开最近使用的知识库</small></span><input v-model="settingsStore.restoreLastVault" type="checkbox" /></label><div class="setting-row"><span><strong>自动保存间隔</strong><small>编辑停止后等待多久写入文件</small></span><select v-model.number="settingsStore.autoSaveInterval" class="select short"><option :value="500">0.5 秒</option><option :value="1500">1.5 秒</option><option :value="3000">3 秒</option></select></div><div class="setting-row"><span><strong>界面语言</strong><small>当前阶段支持中文和英文入口</small></span><select v-model="settingsStore.language" class="select short"><option value="zh-CN">简体中文</option><option value="en">English</option></select></div><div class="setting-row"><span><strong>版本</strong><small>Desktop / AI Core</small></span><span>{{ settingsStore.appVersion }} / {{ settingsStore.aiCoreVersion }}</span></div></div>
|
<div v-if="activeSection === 'general'" class="panel settings-section"><h2>通用</h2><label class="setting-row"><span><strong>恢复上次 Vault</strong><small>启动后自动打开最近使用的知识库</small></span><input v-model="settingsStore.restoreLastVault" type="checkbox" /></label><div class="setting-row"><span><strong>自动保存间隔</strong><small>编辑停止后等待多久写入文件</small></span><select v-model.number="settingsStore.autoSaveInterval" class="select short"><option :value="500">0.5 秒</option><option :value="1500">1.5 秒</option><option :value="3000">3 秒</option></select></div><div class="setting-row"><span><strong>界面语言</strong><small>当前阶段支持中文和英文入口</small></span><select v-model="settingsStore.language" class="select short"><option value="zh-CN">简体中文</option><option value="en">English</option></select></div><div class="setting-row"><span><strong>版本</strong><small>Desktop / AI Core</small></span><span>{{ settingsStore.appVersion }} / {{ settingsStore.aiCoreVersion }}</span></div></div>
|
||||||
|
|
||||||
<div v-else-if="activeSection === 'editor'" class="panel settings-section"><h2>编辑器</h2><div class="setting-row"><span><strong>默认模式</strong><small>新打开文件使用的编辑器模式</small></span><select v-model="settingsStore.defaultEditorMode" class="select short"><option value="wysiwyg">所见即所得</option><option value="source">Markdown 源码</option></select></div><div class="setting-row"><span><strong>字号</strong></span><input v-model.number="settingsStore.editorFontSize" class="input short" type="number" min="12" max="32" /></div><div class="setting-row"><span><strong>行高</strong></span><input v-model.number="settingsStore.editorLineHeight" class="input short" type="number" min="1.2" max="2.4" step="0.1" /></div><div class="setting-row"><span><strong>行宽</strong><small>正文最大字符宽度</small></span><input v-model.number="settingsStore.editorLineWidth" class="input short" type="number" min="40" max="140" /></div><label class="setting-row"><span><strong>拼写检查</strong></span><input v-model="settingsStore.spellCheck" type="checkbox" /></label></div>
|
<div v-else-if="activeSection === 'editor'" class="panel settings-section"><h2>编辑器</h2><div class="setting-row"><span><strong>默认模式</strong><small>新打开文件使用的编辑器模式</small></span><select v-model="settingsStore.defaultEditorMode" class="select short"><option value="wysiwyg">写作与预览</option><option value="source">Markdown 源码</option></select></div><div class="setting-row"><span><strong>字号</strong></span><input v-model.number="themeStore.fontEditorSize" class="input short" type="number" min="12" max="32" /></div><div class="setting-row"><span><strong>行高</strong></span><input v-model.number="themeStore.lineHeight" class="input short" type="number" min="1.2" max="2.4" step="0.1" /></div><div class="setting-row"><span><strong>行宽</strong><small>Markdown 预览最大字符宽度</small></span><input v-model.number="settingsStore.editorLineWidth" class="input short" type="number" min="40" max="140" /></div><label class="setting-row"><span><strong>拼写检查</strong></span><input v-model="settingsStore.spellCheck" type="checkbox" /></label></div>
|
||||||
|
|
||||||
<div v-else-if="activeSection === 'providers'" class="settings-section"><div class="section-head"><h2>模型提供商</h2><button class="button-primary" @click="openProvider()">新增 Provider</button></div><div v-if="providerStore.error || providerAction" class="error-banner">{{ providerStore.error || providerAction }}</div><div class="provider-list"><article v-for="provider in providerStore.providers" :key="provider.provider_id" class="item-card provider-card"><div><div class="inline-actions"><strong>{{ provider.name }}</strong><span class="badge" :class="{ success: provider.enabled }">{{ provider.provider_type }}</span></div><p class="subtle">{{ provider.base_url || '本地内置' }} · 默认模型 {{ provider.default_model || '未设置' }}</p><div class="tag-list"><span v-for="(_, capability) in provider.capabilities" :key="capability" class="badge">{{ capability }}</span></div><p v-if="testResults[provider.provider_id]" class="test-result">{{ testResults[provider.provider_id] }}</p></div><div class="inline-actions"><button class="button-secondary" @click="testProvider(provider)">测试</button><button class="button-secondary" @click="openProvider(provider)">编辑</button><button class="button-danger" :disabled="provider.provider_id === 'mock'" @click="removeProvider(provider)">删除</button></div></article></div></div>
|
<div v-else-if="activeSection === 'providers'" class="settings-section"><div class="section-head"><h2>模型提供商</h2><button class="button-primary" @click="openProvider()">新增 Provider</button></div><div v-if="providerStore.error || providerAction" class="error-banner">{{ providerStore.error || providerAction }}</div><div class="provider-list"><article v-for="provider in providerStore.providers" :key="provider.provider_id" class="item-card provider-card"><div><div class="inline-actions"><strong>{{ provider.name }}</strong><span class="badge" :class="{ success: provider.enabled }">{{ provider.provider_type }}</span></div><p class="subtle">{{ provider.base_url || '本地内置' }} · 默认模型 {{ provider.default_model || '未设置' }}</p><div class="tag-list"><span v-for="(_, capability) in provider.capabilities" :key="capability" class="badge">{{ capability }}</span></div><p v-if="testResults[provider.provider_id]" class="test-result">{{ testResults[provider.provider_id] }}</p></div><div class="inline-actions"><button class="button-secondary" @click="testProvider(provider)">测试</button><button class="button-secondary" @click="openProvider(provider)">编辑</button><button class="button-danger" :disabled="provider.provider_id === 'mock'" @click="removeProvider(provider)">删除</button></div></article></div></div>
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,12 @@ const newVaultPath = ref('')
|
|||||||
const aiCoreStatus = ref<'checking' | 'running' | 'stopped'>('checking')
|
const aiCoreStatus = ref<'checking' | 'running' | 'stopped'>('checking')
|
||||||
|
|
||||||
onMounted(async () => {
|
onMounted(async () => {
|
||||||
await workspaceStore.loadRecentVaults()
|
await Promise.all([workspaceStore.loadRecentVaults(), settingsStore.loadDiagnostics()])
|
||||||
|
const lastVaultPath = localStorage.getItem('last-vault-path')
|
||||||
|
if (settingsStore.restoreLastVault && lastVaultPath) {
|
||||||
|
await openVault(lastVaultPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
aiCoreStatus.value = settingsStore.aiCoreStatus === 'running' ? 'running' : 'stopped'
|
aiCoreStatus.value = settingsStore.aiCoreStatus === 'running' ? 'running' : 'stopped'
|
||||||
}, 800)
|
}, 800)
|
||||||
|
|||||||
@@ -2,6 +2,12 @@ import { defineStore } from 'pinia'
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import type { SearchResult, SearchRequest } from '@/contracts'
|
import type { SearchResult, SearchRequest } from '@/contracts'
|
||||||
import * as searchService from '@/services/searchService'
|
import * as searchService from '@/services/searchService'
|
||||||
|
import { ApiErrorClass } from '@/services/apiClient'
|
||||||
|
|
||||||
|
const VECTOR_ERROR_CODES = new Set([
|
||||||
|
'VECTOR_UNAVAILABLE', 'EMBEDDING_UNAVAILABLE', 'INDEX_UNAVAILABLE',
|
||||||
|
'MODEL_NOT_FOUND', 'MODEL_CAPABILITY_MISMATCH', 'PROVIDER_UNAVAILABLE',
|
||||||
|
])
|
||||||
|
|
||||||
export const useSearchStore = defineStore('search', () => {
|
export const useSearchStore = defineStore('search', () => {
|
||||||
const query = ref('')
|
const query = ref('')
|
||||||
@@ -19,16 +25,33 @@ export const useSearchStore = defineStore('search', () => {
|
|||||||
mode.value = request.mode || 'hybrid'
|
mode.value = request.mode || 'hybrid'
|
||||||
isSearching.value = true
|
isSearching.value = true
|
||||||
error.value = null
|
error.value = null
|
||||||
|
vectorUnavailable.value = false
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const resp = await searchService.search(request)
|
const resp = await searchService.search(request)
|
||||||
results.value = resp.results
|
results.value = resp.results
|
||||||
total.value = resp.total
|
total.value = resp.total
|
||||||
selectedIndex.value = 0
|
selectedIndex.value = 0
|
||||||
} catch (e: any) {
|
} catch (reason) {
|
||||||
error.value = e.message || '搜索失败'
|
const canFallback = mode.value !== 'fts' && reason instanceof ApiErrorClass && VECTOR_ERROR_CODES.has(reason.code)
|
||||||
|
if (canFallback) {
|
||||||
|
try {
|
||||||
|
const fallback = await searchService.search({ ...request, mode: 'fts' })
|
||||||
|
results.value = fallback.results
|
||||||
|
total.value = fallback.total
|
||||||
|
mode.value = 'fts'
|
||||||
|
vectorUnavailable.value = true
|
||||||
|
selectedIndex.value = 0
|
||||||
|
} catch (fallbackError) {
|
||||||
|
error.value = fallbackError instanceof Error ? fallbackError.message : '全文检索降级失败'
|
||||||
results.value = []
|
results.value = []
|
||||||
total.value = 0
|
total.value = 0
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
error.value = reason instanceof Error ? reason.message : '搜索失败'
|
||||||
|
results.value = []
|
||||||
|
total.value = 0
|
||||||
|
}
|
||||||
} finally {
|
} finally {
|
||||||
isSearching.value = false
|
isSearching.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,24 +1,26 @@
|
|||||||
import { defineStore } from 'pinia'
|
import { defineStore } from 'pinia'
|
||||||
import { ref } from 'vue'
|
import { ref, watch } from 'vue'
|
||||||
import type { AiCoreStatus, IndexStatus } from '@/contracts'
|
import type { AiCoreStatus, IndexStatus } from '@/contracts'
|
||||||
import { mockIndexStatus } from '@/services/indexService'
|
import { mockIndexStatus } from '@/services/indexService'
|
||||||
import * as indexService from '@/services/indexService'
|
import * as indexService from '@/services/indexService'
|
||||||
import * as systemService from '@/services/systemService'
|
import * as systemService from '@/services/systemService'
|
||||||
|
|
||||||
export const useSettingsStore = defineStore('settings', () => {
|
export const useSettingsStore = defineStore('settings', () => {
|
||||||
|
const saved = (() => {
|
||||||
|
try { return JSON.parse(localStorage.getItem('app-settings') ?? '{}') as Record<string, unknown> }
|
||||||
|
catch { localStorage.removeItem('app-settings'); return {} }
|
||||||
|
})()
|
||||||
// General
|
// General
|
||||||
const restoreLastVault = ref(true)
|
const restoreLastVault = ref(saved.restoreLastVault !== false)
|
||||||
const autoSaveInterval = ref(1500)
|
const autoSaveInterval = ref(typeof saved.autoSaveInterval === 'number' ? saved.autoSaveInterval : 1500)
|
||||||
const language = ref<'zh-CN' | 'en'>('zh-CN')
|
const language = ref<'zh-CN' | 'en'>(saved.language === 'en' ? 'en' : 'zh-CN')
|
||||||
const appVersion = ref('0.1.0')
|
const appVersion = ref('0.1.0')
|
||||||
const aiCoreVersion = ref('0.1.0')
|
const aiCoreVersion = ref('0.1.0')
|
||||||
|
|
||||||
// Editor
|
// Editor
|
||||||
const defaultEditorMode = ref<'wysiwyg' | 'source'>('wysiwyg')
|
const defaultEditorMode = ref<'wysiwyg' | 'source'>(saved.defaultEditorMode === 'source' ? 'source' : 'wysiwyg')
|
||||||
const editorFontSize = ref(15)
|
const editorLineWidth = ref(typeof saved.editorLineWidth === 'number' ? saved.editorLineWidth : 80)
|
||||||
const editorLineHeight = ref(1.7)
|
const spellCheck = ref(saved.spellCheck === true)
|
||||||
const editorLineWidth = ref(80)
|
|
||||||
const spellCheck = ref(false)
|
|
||||||
|
|
||||||
// AI Core
|
// AI Core
|
||||||
const aiCoreStatus = ref<AiCoreStatus>('running')
|
const aiCoreStatus = ref<AiCoreStatus>('running')
|
||||||
@@ -41,6 +43,12 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
})
|
})
|
||||||
const diagnosticsError = ref<string | null>(null)
|
const diagnosticsError = ref<string | null>(null)
|
||||||
|
|
||||||
|
watch(() => ({
|
||||||
|
restoreLastVault: restoreLastVault.value, autoSaveInterval: autoSaveInterval.value,
|
||||||
|
language: language.value, defaultEditorMode: defaultEditorMode.value,
|
||||||
|
editorLineWidth: editorLineWidth.value, spellCheck: spellCheck.value,
|
||||||
|
}), (value) => localStorage.setItem('app-settings', JSON.stringify(value)), { deep: true })
|
||||||
|
|
||||||
async function loadDiagnostics() {
|
async function loadDiagnostics() {
|
||||||
try {
|
try {
|
||||||
const [health, status, index] = await Promise.all([
|
const [health, status, index] = await Promise.all([
|
||||||
@@ -97,8 +105,6 @@ export const useSettingsStore = defineStore('settings', () => {
|
|||||||
appVersion,
|
appVersion,
|
||||||
aiCoreVersion,
|
aiCoreVersion,
|
||||||
defaultEditorMode,
|
defaultEditorMode,
|
||||||
editorFontSize,
|
|
||||||
editorLineHeight,
|
|
||||||
editorLineWidth,
|
editorLineWidth,
|
||||||
spellCheck,
|
spellCheck,
|
||||||
aiCoreStatus,
|
aiCoreStatus,
|
||||||
|
|||||||
@@ -37,6 +37,15 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initTheme() {
|
function initTheme() {
|
||||||
|
const savedAppearance = localStorage.getItem('editor-appearance')
|
||||||
|
if (savedAppearance) {
|
||||||
|
try {
|
||||||
|
const value = JSON.parse(savedAppearance) as { size?: number; family?: string; lineHeight?: number }
|
||||||
|
if (value.size) fontEditorSize.value = value.size
|
||||||
|
if (value.family) fontEditorFamily.value = value.family
|
||||||
|
if (value.lineHeight) lineHeight.value = value.lineHeight
|
||||||
|
} catch { localStorage.removeItem('editor-appearance') }
|
||||||
|
}
|
||||||
const saved = localStorage.getItem('theme')
|
const saved = localStorage.getItem('theme')
|
||||||
if (saved && themes.value.find((t) => t.theme_id === saved)) {
|
if (saved && themes.value.find((t) => t.theme_id === saved)) {
|
||||||
applyTheme(saved)
|
applyTheme(saved)
|
||||||
@@ -57,13 +66,24 @@ export const useThemeStore = defineStore('theme', () => {
|
|||||||
lineHeight.value = 1.7
|
lineHeight.value = 1.7
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const persistAppearance = () => localStorage.setItem('editor-appearance', JSON.stringify({
|
||||||
|
size: fontEditorSize.value, family: fontEditorFamily.value, lineHeight: lineHeight.value,
|
||||||
|
}))
|
||||||
|
|
||||||
watch(fontEditorSize, (v) => {
|
watch(fontEditorSize, (v) => {
|
||||||
document.documentElement.style.setProperty('--font-editor-size', `${v}px`)
|
document.documentElement.style.setProperty('--font-editor-size', `${v}px`)
|
||||||
})
|
persistAppearance()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
watch(lineHeight, (v) => {
|
watch(lineHeight, (v) => {
|
||||||
document.documentElement.style.setProperty('--font-editor-line-height', String(v))
|
document.documentElement.style.setProperty('--font-editor-line-height', String(v))
|
||||||
})
|
persistAppearance()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
watch(fontEditorFamily, (v) => {
|
||||||
|
document.documentElement.style.setProperty('--font-editor-sans', v)
|
||||||
|
persistAppearance()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
return {
|
return {
|
||||||
themes,
|
themes,
|
||||||
|
|||||||
@@ -158,6 +158,26 @@
|
|||||||
--shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.6);
|
--shadow-xl: 0 16px 48px rgba(0, 0, 0, 0.6);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[data-theme='sepia'] {
|
||||||
|
--color-background-primary: #fbf3df;
|
||||||
|
--color-background-secondary: #f4e8ca;
|
||||||
|
--color-background-tertiary: #eadbb8;
|
||||||
|
--color-background-hover: #eee0bf;
|
||||||
|
--color-background-active: #e3d1aa;
|
||||||
|
--color-surface-primary: #fff8e8;
|
||||||
|
--color-surface-secondary: #f8edd3;
|
||||||
|
--color-surface-elevated: #fffaf0;
|
||||||
|
--color-text-primary: #40372b;
|
||||||
|
--color-text-secondary: #746653;
|
||||||
|
--color-text-tertiary: #9a8a72;
|
||||||
|
--color-border-default: #ddcfad;
|
||||||
|
--color-border-subtle: #eadfc4;
|
||||||
|
--color-accent-primary: #8a5b32;
|
||||||
|
--color-accent-primary-hover: #704724;
|
||||||
|
--color-accent-soft: #edddbd;
|
||||||
|
--color-text-link: #7b512e;
|
||||||
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
|||||||
Reference in New Issue
Block a user