refactor(frontend): move views into app and pages structure

Reorganize the frontend around app-level routing and page modules so the runtime and feature screens share a clearer navigation and composition layout for future work.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-03-21 22:13:12 +08:00
parent a27736a832
commit b024a2bcb5
25 changed files with 2628 additions and 1656 deletions

View File

@@ -0,0 +1,314 @@
import { computed, onMounted, ref } from 'vue'
import { settingsApi, type LLMConfig, type LLMModelConfig, type LLMType, type SchedulerConfig } from '@/api/settings'
type ToastState = {
show: boolean
message: string
type: 'success' | 'error'
}
type EditingSnapshot = {
type: string
index: number
data: LLMModelConfig
}
type ProfileState = {
email: string
full_name: string
created_at: string
}
function cloneLLMConfig(config: LLMConfig): LLMConfig {
return JSON.parse(JSON.stringify(config)) as LLMConfig
}
function cloneSchedulerConfig(config: SchedulerConfig): SchedulerConfig {
return JSON.parse(JSON.stringify(config)) as SchedulerConfig
}
function getErrorMessage(error: unknown, fallback: string) {
return (error as { response?: { data?: { detail?: string } } })?.response?.data?.detail || fallback
}
export function useSettingsView() {
const loading = ref(false)
const saving = ref(false)
const savingModel = ref<string | null>(null)
const toast = ref<ToastState>({
show: false,
message: '',
type: 'success',
})
const expandedRow = ref<string | null>(null)
const editingSnapshot = ref<EditingSnapshot | null>(null)
const profile = ref<ProfileState>({
email: '',
full_name: '',
created_at: '',
})
const originalProfile = ref({ email: '', full_name: '' })
const newPassword = ref('')
const llmConfig = ref<LLMConfig>({
chat: [],
vlm: [],
embedding: [],
rerank: [],
})
const originalLlmConfig = ref<LLMConfig>({
chat: [],
vlm: [],
embedding: [],
rerank: [],
})
const schedulerConfig = ref<SchedulerConfig>({
daily_plan_time: '08:00',
forum_scan_interval_minutes: 30,
todo_ai_generate_time: '08:00',
enabled: true,
})
const originalSchedulerConfig = ref<SchedulerConfig>({})
const showRequiredWarning = computed(() => {
return (llmConfig.value.chat?.length || 0) === 0 ||
(llmConfig.value.embedding?.length || 0) === 0 ||
(llmConfig.value.rerank?.length || 0) === 0
})
const isProfileDirty = computed(() => {
return profile.value.full_name !== originalProfile.value.full_name || newPassword.value !== ''
})
const isSchedulerDirty = computed(() => {
return JSON.stringify(schedulerConfig.value) !== JSON.stringify(originalSchedulerConfig.value)
})
function showToast(message: string, type: 'success' | 'error' = 'success') {
toast.value = { show: true, message, type }
window.setTimeout(() => {
toast.value.show = false
}, 3000)
}
function createEmptyModel(type: string): LLMModelConfig {
return {
name: `${type.toUpperCase()}-${Date.now()}`,
provider: 'openai',
model: type === 'chat'
? 'gpt-4o'
: type === 'vlm'
? 'gpt-4o'
: type === 'embedding'
? 'text-embedding-3-small'
: 'bge-reranker-v2',
base_url: '',
api_key: '',
enabled: true,
}
}
function getRowKey(type: string, index: number): string {
return `${type}-${index}`
}
function addModel(type: string) {
if (!llmConfig.value[type as keyof LLMConfig]) {
llmConfig.value[type as keyof LLMConfig] = []
}
if ((type === 'embedding' || type === 'rerank') &&
llmConfig.value[type as keyof LLMConfig]!.length >= 1) {
showToast(`${type === 'embedding' ? 'Embedding' : 'Rerank'} 最多配置 1 个`, 'error')
return
}
const newModel = createEmptyModel(type)
llmConfig.value[type as keyof LLMConfig]!.push(newModel)
const newIndex = llmConfig.value[type as keyof LLMConfig]!.length - 1
expandedRow.value = getRowKey(type, newIndex)
editingSnapshot.value = { type, index: newIndex, data: JSON.parse(JSON.stringify(newModel)) as LLMModelConfig }
}
async function removeModel(type: string, index: number) {
if ((type === 'embedding' || type === 'rerank') &&
llmConfig.value[type as keyof LLMConfig]!.length <= 1) {
showToast(`${type === 'embedding' ? 'Embedding' : 'Rerank'} 为知识库必填,至少保留 1 个`, 'error')
return
}
llmConfig.value[type as keyof LLMConfig]!.splice(index, 1)
expandedRow.value = null
editingSnapshot.value = null
try {
await settingsApi.updateLLM(llmConfig.value)
originalLlmConfig.value = cloneLLMConfig(llmConfig.value)
showToast('删除成功')
} catch (error: unknown) {
showToast(getErrorMessage(error, '删除失败'), 'error')
}
}
function toggleRow(type: string, index: number, model: LLMModelConfig) {
const key = getRowKey(type, index)
if (expandedRow.value === key) {
expandedRow.value = null
editingSnapshot.value = null
} else {
editingSnapshot.value = { type, index, data: JSON.parse(JSON.stringify(model)) as LLMModelConfig }
expandedRow.value = key
}
}
function updateModel(type: string, index: number, model: LLMModelConfig) {
llmConfig.value[type as keyof LLMConfig]![index] = model
}
async function loadSettings() {
loading.value = true
try {
const response = await settingsApi.get()
profile.value = {
email: response.data.profile.email,
full_name: response.data.profile.full_name || '',
created_at: response.data.profile.created_at,
}
originalProfile.value = { email: profile.value.email, full_name: profile.value.full_name }
if (response.data.llm_config) {
llmConfig.value = {
chat: response.data.llm_config.chat || [],
vlm: response.data.llm_config.vlm || [],
embedding: response.data.llm_config.embedding || [],
rerank: response.data.llm_config.rerank || [],
}
} else {
llmConfig.value = { chat: [], vlm: [], embedding: [], rerank: [] }
}
originalLlmConfig.value = cloneLLMConfig(llmConfig.value)
if (response.data.scheduler_config && Object.keys(response.data.scheduler_config).length > 0) {
schedulerConfig.value = response.data.scheduler_config as SchedulerConfig
}
originalSchedulerConfig.value = cloneSchedulerConfig(schedulerConfig.value)
} catch (error) {
console.error('加载设置失败', error)
showToast('加载设置失败', 'error')
} finally {
loading.value = false
}
}
async function saveProfile() {
saving.value = true
try {
await settingsApi.updateProfile({
full_name: profile.value.full_name,
password: newPassword.value || undefined,
})
originalProfile.value = { email: profile.value.email, full_name: profile.value.full_name }
newPassword.value = ''
showToast('资料保存成功')
} catch (error: unknown) {
showToast(getErrorMessage(error, '保存失败'), 'error')
} finally {
saving.value = false
}
}
async function saveModel(type: string, index: number, model: LLMModelConfig) {
const key = getRowKey(type, index)
llmConfig.value[type as keyof LLMConfig]![index] = JSON.parse(JSON.stringify(model)) as LLMModelConfig
savingModel.value = key
try {
await settingsApi.updateLLM(llmConfig.value)
originalLlmConfig.value = cloneLLMConfig(llmConfig.value)
expandedRow.value = null
editingSnapshot.value = null
showToast('保存成功')
} catch (error: unknown) {
showToast(getErrorMessage(error, '保存失败'), 'error')
} finally {
savingModel.value = null
}
}
async function testModel(type: string, index: number, model: LLMModelConfig) {
try {
const response = await settingsApi.testLLM({
type: type as LLMType,
provider: model.provider,
model: model.model,
base_url: model.base_url,
api_key: model.api_key,
})
if (response.data.success) {
llmConfig.value[type as keyof LLMConfig]![index].enabled = true
showToast('连接成功')
} else {
llmConfig.value[type as keyof LLMConfig]![index].enabled = false
showToast(`连接失败: ${response.data.error}`, 'error')
}
} catch (error) {
llmConfig.value[type as keyof LLMConfig]![index].enabled = false
showToast('测试连接失败', 'error')
}
}
async function saveScheduler() {
saving.value = true
try {
await settingsApi.updateScheduler(schedulerConfig.value)
originalSchedulerConfig.value = cloneSchedulerConfig(schedulerConfig.value)
showToast('定时任务配置保存成功')
} catch (error: unknown) {
showToast(getErrorMessage(error, '保存失败'), 'error')
} finally {
saving.value = false
}
}
function resetProfile() {
profile.value.full_name = originalProfile.value.full_name
newPassword.value = ''
}
function resetScheduler() {
schedulerConfig.value = cloneSchedulerConfig(originalSchedulerConfig.value)
}
onMounted(loadSettings)
return {
loading,
saving,
savingModel,
toast,
expandedRow,
editingSnapshot,
showRequiredWarning,
profile,
newPassword,
llmConfig,
schedulerConfig,
isProfileDirty,
isSchedulerDirty,
addModel,
removeModel,
getRowKey,
toggleRow,
updateModel,
saveProfile,
saveModel,
testModel,
saveScheduler,
resetProfile,
resetScheduler,
}
}