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:
314
frontend/src/pages/settings/composables/useSettingsView.ts
Normal file
314
frontend/src/pages/settings/composables/useSettingsView.ts
Normal 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,
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user