diff --git a/frontend/src/components/ModelEditorDialog.vue b/frontend/src/components/ModelEditorDialog.vue index 3d8ef378..5df2abca 100644 --- a/frontend/src/components/ModelEditorDialog.vue +++ b/frontend/src/components/ModelEditorDialog.vue @@ -1,24 +1,15 @@ diff --git a/frontend/src/components/settings/SettingDrawer.vue b/frontend/src/components/settings/SettingDrawer.vue new file mode 100644 index 00000000..c9f0d9e9 --- /dev/null +++ b/frontend/src/components/settings/SettingDrawer.vue @@ -0,0 +1,118 @@ + + + + + diff --git a/frontend/src/components/settings/useConfirmDelete.ts b/frontend/src/components/settings/useConfirmDelete.ts new file mode 100644 index 00000000..f52463ba --- /dev/null +++ b/frontend/src/components/settings/useConfirmDelete.ts @@ -0,0 +1,36 @@ +import { DialogPlugin } from 'tdesign-vue-next' +import { useI18n } from 'vue-i18n' + +interface ConfirmDeleteOptions { + title?: string + body: string + confirmText?: string + cancelText?: string + onConfirm: () => Promise | void +} + +/** + * 统一的删除确认交互,基于 TDesign DialogPlugin.confirm。 + * 取代散落在各处的 window.confirm / t-popconfirm / 自定义 Dialog 写法。 + */ +export function useConfirmDelete() { + const { t } = useI18n() + + return (opts: ConfirmDeleteOptions) => { + const dialog = DialogPlugin.confirm({ + header: opts.title || (t('common.confirmDelete') as string), + body: opts.body, + confirmBtn: opts.confirmText || (t('common.delete') as string), + cancelBtn: opts.cancelText || (t('common.cancel') as string), + theme: 'warning', + onConfirm: async () => { + try { + await opts.onConfirm() + } finally { + dialog.hide() + } + } + }) + return dialog + } +} diff --git a/frontend/src/i18n/locales/en-US.ts b/frontend/src/i18n/locales/en-US.ts index 0074845e..faa77f80 100755 --- a/frontend/src/i18n/locales/en-US.ts +++ b/frontend/src/i18n/locales/en-US.ts @@ -2855,6 +2855,13 @@ export default { modelSettings: { title: 'Model Settings', description: 'Manage different types of AI models, including local Ollama and remote APIs', + typeShort: { + chat: 'Chat', + embedding: 'Embedding', + rerank: 'ReRank', + vllm: 'Vision', + asr: 'Speech', + }, actions: { addModel: 'Add Model', setDefault: 'Set as Default' diff --git a/frontend/src/i18n/locales/ko-KR.ts b/frontend/src/i18n/locales/ko-KR.ts index cb5c9116..1200a1ac 100755 --- a/frontend/src/i18n/locales/ko-KR.ts +++ b/frontend/src/i18n/locales/ko-KR.ts @@ -2902,6 +2902,13 @@ export default { modelSettings: { title: "모델 설정", description: "다양한 유형의 AI 모델을 관리합니다. Ollama 로컬 모델과 원격 API를 지원합니다", + typeShort: { + chat: "대화", + embedding: "Embedding", + rerank: "ReRank", + vllm: "비전", + asr: "음성", + }, actions: { addModel: "모델 추가", setDefault: "기본값으로 설정", diff --git a/frontend/src/i18n/locales/ru-RU.ts b/frontend/src/i18n/locales/ru-RU.ts index 57dd89d0..80ac774d 100755 --- a/frontend/src/i18n/locales/ru-RU.ts +++ b/frontend/src/i18n/locales/ru-RU.ts @@ -2660,6 +2660,13 @@ export default { modelSettings: { title: 'Настройки моделей', description: 'Управление типами AI‑моделей: локальные (Ollama) и удалённые API', + typeShort: { + chat: 'Чат', + embedding: 'Embedding', + rerank: 'ReRank', + vllm: 'Зрение', + asr: 'Речь', + }, actions: { addModel: 'Добавить модель', setDefault: 'Сделать по умолчанию' diff --git a/frontend/src/i18n/locales/zh-CN.ts b/frontend/src/i18n/locales/zh-CN.ts index e42782d2..7fecf219 100755 --- a/frontend/src/i18n/locales/zh-CN.ts +++ b/frontend/src/i18n/locales/zh-CN.ts @@ -2852,6 +2852,13 @@ export default { modelSettings: { title: "模型配置", description: "管理不同类型的 AI 模型,支持 Ollama 本地模型和远程 API", + typeShort: { + chat: "对话", + embedding: "Embedding", + rerank: "ReRank", + vllm: "视觉", + asr: "语音", + }, actions: { addModel: "添加模型", setDefault: "设为默认", diff --git a/frontend/src/views/settings/McpSettings.vue b/frontend/src/views/settings/McpSettings.vue index f0b57d17..b9ffb52f 100644 --- a/frontend/src/views/settings/McpSettings.vue +++ b/frontend/src/views/settings/McpSettings.vue @@ -11,9 +11,9 @@ -
-
-
+ - + import { ref, onMounted } from 'vue' -import { MessagePlugin, DialogPlugin } from 'tdesign-vue-next' +import { MessagePlugin } from 'tdesign-vue-next' import { useI18n } from 'vue-i18n' import { listMCPServices, @@ -121,8 +115,11 @@ import { } from '@/api/mcp-service' import McpServiceDialog from './components/McpServiceDialog.vue' import McpTestResult from './components/McpTestResult.vue' +import SettingCard from '@/components/settings/SettingCard.vue' +import { useConfirmDelete } from '@/components/settings/useConfirmDelete' const { t } = useI18n() +const confirmDelete = useConfirmDelete() const services = ref([]) const loading = ref(false) @@ -170,13 +167,12 @@ const handleDialogSuccess = () => { // Handle toggle enabled/disabled const handleToggleEnabled = async (service: MCPService) => { if (!service || !service.id) return - + const originalState = service.enabled try { await updateMCPService(service.id, { enabled: service.enabled }) MessagePlugin.success(service.enabled ? t('mcpSettings.toasts.enabled') : t('mcpSettings.toasts.disabled')) } catch (error) { - // Revert on error service.enabled = originalState MessagePlugin.error(t('mcpSettings.toasts.updateStateFailed')) console.error('Failed to update MCP service:', error) @@ -186,28 +182,22 @@ const handleToggleEnabled = async (service: MCPService) => { // Handle test button click const handleTest = async (service: MCPService) => { if (!service || !service.id) return - + testingServiceName.value = service.name testing.value = true - - // 显示测试开始提示 + MessagePlugin.info({ content: t('mcpSettings.toasts.testing', { name: service.name }), - duration: 0, // 不自动关闭 + duration: 0, closeBtn: false }) - + try { const result = await testMCPService(service.id) - - console.log('Test result received:', result) - - // 关闭所有消息提示 + MessagePlugin.closeAll() - - // 检查结果是否存在 + if (!result) { - // 即使没有结果,也显示错误对话框 testResult.value = { success: false, message: t('mcpSettings.toasts.noResponse') @@ -215,48 +205,35 @@ const handleTest = async (service: MCPService) => { testDialogVisible.value = true return } - - // 设置测试结果 + testResult.value = result - - // 显示详细结果对话框 - console.log('Opening test dialog, result:', testResult.value) testDialogVisible.value = true } catch (error: any) { - // 关闭所有消息提示 MessagePlugin.closeAll() - - // 显示错误信息 + const errorMessage = error?.response?.data?.error?.message || error?.message || t('mcpSettings.toasts.testFailed') console.error('Failed to test MCP service:', error) - - // 即使出错也显示结果对话框,显示错误信息 + testResult.value = { success: false, message: errorMessage } testDialogVisible.value = true } finally { - // 确保关闭 loading testing.value = false } } // Handle delete button click -const handleDelete = async (service: MCPService) => { +const handleDelete = (service: MCPService) => { if (!service || !service.id) return - - const confirmDialog = DialogPlugin.confirm({ - header: t('common.confirmDelete'), + + confirmDelete({ body: t('mcpSettings.deleteConfirmBody', { name: service.name || t('mcpSettings.unnamed') }), - confirmBtn: t('common.delete'), - cancelBtn: t('common.cancel'), - theme: 'warning', onConfirm: async () => { try { await deleteMCPService(service.id) MessagePlugin.success(t('mcpSettings.toasts.deleted')) - confirmDialog.hide() loadServices() } catch (error) { MessagePlugin.error(t('mcpSettings.toasts.deleteFailed')) @@ -267,44 +244,34 @@ const handleDelete = async (service: MCPService) => { } // Get service options for dropdown menu -const getServiceOptions = (service: MCPService) => { +const getServiceOptions = () => { return [ - { - content: t('mcpSettings.actions.test'), - value: `test-${service.id}` - }, - { - content: t('common.edit'), - value: `edit-${service.id}` - }, - { - content: t('common.delete'), - value: `delete-${service.id}`, - theme: 'error' - } + { content: t('mcpSettings.actions.test'), value: 'test' }, + { content: t('common.edit'), value: 'edit' }, + { content: t('common.delete'), value: 'delete', theme: 'error' as const } ] } -// Get service options for builtin services (test only) -const getBuiltinServiceOptions = (service: MCPService) => { +// Builtin: 仅测试 +const getBuiltinServiceOptions = () => { return [ - { - content: t('mcpSettings.actions.test'), - value: `test-${service.id}` - } + { content: t('mcpSettings.actions.test'), value: 'test' } ] } // Handle menu action const handleMenuAction = (data: { value: string }, service: MCPService) => { - const value = data.value - - if (value.startsWith('test-')) { - handleTest(service) - } else if (value.startsWith('edit-')) { - handleEdit(service) - } else if (value.startsWith('delete-')) { - handleDelete(service) + if (testing.value) return + switch (data.value) { + case 'test': + handleTest(service) + break + case 'edit': + handleEdit(service) + break + case 'delete': + handleDelete(service) + break } } @@ -347,7 +314,7 @@ onMounted(() => { } .section-header { - margin-bottom: 32px; + margin-bottom: 28px; h2 { font-size: 20px; @@ -360,7 +327,7 @@ onMounted(() => { font-size: 14px; color: var(--td-text-color-secondary); margin: 0; - line-height: 1.5; + line-height: 1.6; } } @@ -369,20 +336,16 @@ onMounted(() => { text-align: center; } -.services-container { - margin-top: 16px; -} - -.services-header { +.settings-toolbar { display: flex; align-items: flex-start; justify-content: space-between; + gap: 12px; margin-bottom: 16px; - padding-bottom: 16px; - border-bottom: 1px solid var(--td-component-stroke); - .header-info { + .toolbar-info { flex: 1; + min-width: 0; h3 { font-size: 15px; @@ -411,91 +374,23 @@ onMounted(() => { } } -.services-list { - display: flex; - flex-direction: column; - gap: 0; - border: 1px solid var(--td-component-stroke); - border-radius: 6px; - padding: 16px; - background: var(--td-bg-color-secondarycontainer); +.services-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; } -.service-card { - padding: 12px 0; - border-bottom: 1px solid var(--td-component-stroke); - transition: all 0.2s; +.service-meta-item { + display: inline-flex; + align-items: center; + gap: 4px; + max-width: 100%; + overflow: hidden; - &:last-child { - border-bottom: none; - padding-bottom: 0; - } - - &:first-child { - padding-top: 0; - } -} - -.service-info { - .service-header { - display: flex; - align-items: center; - justify-content: space-between; - margin-bottom: 8px; - - .service-name { - font-size: 15px; - font-weight: 500; - color: var(--td-text-color-primary); - display: flex; - align-items: center; - gap: 8px; - flex: 1; - } - - .service-controls { - display: flex; - align-items: center; - gap: 8px; - flex-shrink: 0; - - .more-btn { - color: var(--td-text-color-placeholder); - padding: 4px; - transition: all 0.2s; - - &:hover { - background: var(--td-bg-color-secondarycontainer); - color: var(--td-text-color-primary); - } - } - } - } - - .service-description { - font-size: 13px; - color: var(--td-text-color-secondary); - margin-bottom: 8px; - line-height: 1.5; - } - - .service-meta { - display: flex; - align-items: center; - gap: 12px; - font-size: 12px; - color: var(--td-text-color-placeholder); - - .meta-item { - display: flex; - align-items: center; - gap: 4px; - - .meta-icon { - font-size: 12px; - } - } + .service-meta-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } - diff --git a/frontend/src/views/settings/ModelSettings.vue b/frontend/src/views/settings/ModelSettings.vue index 05a439db..af4f465a 100644 --- a/frontend/src/views/settings/ModelSettings.vue +++ b/frontend/src/views/settings/ModelSettings.vue @@ -1,8 +1,22 @@ @@ -158,8 +155,12 @@ import { type WebSearchProviderEntity, type WebSearchProviderTypeInfo, } from '@/api/web-search-provider' +import SettingCard from '@/components/settings/SettingCard.vue' +import SettingDrawer from '@/components/settings/SettingDrawer.vue' +import { useConfirmDelete } from '@/components/settings/useConfirmDelete' const { t } = useI18n() +const confirmDelete = useConfirmDelete() // ===== State ===== const providerEntities = ref([]) @@ -169,6 +170,7 @@ const editingProvider = ref(null) const testing = ref(false) const testingId = ref(null) const saving = ref(false) +const formRef = ref() const providerForm = ref<{ name: string @@ -193,6 +195,11 @@ const isProviderFree = (providerType: WebSearchProviderTypeInfo) => { return !providerType.requires_api_key && !providerType.requires_engine_id } +const isEntityFree = (entity: WebSearchProviderEntity) => { + const pt = providerTypes.value.find(p => p.id === entity.provider) + return pt ? isProviderFree(pt) : false +} + // ===== Methods ===== const onProviderTypeChange = () => { providerForm.value.parameters = {} @@ -219,12 +226,12 @@ const loadProviderTypes = async () => { const openAddDialog = () => { editingProvider.value = null - providerForm.value = { - name: '', - provider: providerTypes.value[0]?.id || 'duckduckgo', - description: '', - parameters: {}, - is_default: providerEntities.value.length === 0 + providerForm.value = { + name: '', + provider: providerTypes.value[0]?.id || 'duckduckgo', + description: '', + parameters: {}, + is_default: providerEntities.value.length === 0 } showAddProviderDialog.value = true } @@ -245,12 +252,14 @@ const editProvider = (entity: WebSearchProviderEntity) => { showAddProviderDialog.value = true } -const saveProvider = async ({ validateResult, firstError }: any) => { +const saveProvider = async () => { + const validateResult = await formRef.value?.validate() if (validateResult !== true && validateResult !== undefined) { - MessagePlugin.warning(firstError || 'Please check the form fields') + const firstError = typeof validateResult === 'object' ? Object.values(validateResult)[0] : '' + MessagePlugin.warning(typeof firstError === 'string' ? firstError : 'Please check the form fields') return } - + saving.value = true try { const data: Partial = { @@ -260,7 +269,7 @@ const saveProvider = async ({ validateResult, firstError }: any) => { parameters: { ...providerForm.value.parameters }, is_default: providerForm.value.is_default, } - + if (editingProvider.value && !data.parameters!.api_key) { delete data.parameters!.api_key } @@ -281,14 +290,19 @@ const saveProvider = async ({ validateResult, firstError }: any) => { } } -const deleteProvider = async (id: string) => { - try { - await deleteWebSearchProviderAPI(id) - MessagePlugin.success(t('webSearchSettings.toasts.providerDeleted')) - await loadProviderEntities() - } catch (error: any) { - MessagePlugin.error(error?.message || 'Failed to delete provider') - } +const deleteProvider = (entity: WebSearchProviderEntity) => { + confirmDelete({ + body: t('webSearchSettings.deleteConfirm'), + onConfirm: async () => { + try { + await deleteWebSearchProviderAPI(entity.id!) + MessagePlugin.success(t('webSearchSettings.toasts.providerDeleted')) + await loadProviderEntities() + } catch (error: any) { + MessagePlugin.error(error?.message || 'Failed to delete provider') + } + } + }) } const testConnection = async () => { @@ -298,7 +312,7 @@ const testConnection = async () => { provider: providerForm.value.provider, parameters: { ...providerForm.value.parameters }, } - + if (editingProvider.value && !data.parameters.api_key) { const res = await testWebSearchProvider(editingProvider.value.id!) if (res.success) { @@ -337,6 +351,28 @@ const testExistingConnection = async (entity: WebSearchProviderEntity) => { } } +const getProviderOptions = (_entity: WebSearchProviderEntity) => { + return [ + { content: t('webSearchSettings.testConnection'), value: 'test' }, + { content: t('common.edit'), value: 'edit' }, + { content: t('common.delete'), value: 'delete', theme: 'error' as const } + ] +} + +const handleMenuAction = (data: { value: string }, entity: WebSearchProviderEntity) => { + switch (data.value) { + case 'test': + testExistingConnection(entity) + break + case 'edit': + editProvider(entity) + break + case 'delete': + deleteProvider(entity) + break + } +} + // ===== Init ===== onMounted(async () => { await Promise.all([loadProviderTypes(), loadProviderEntities()]) @@ -349,7 +385,7 @@ onMounted(async () => { } .section-header { - margin-bottom: 32px; + margin-bottom: 28px; h2 { font-size: 20px; @@ -362,16 +398,11 @@ onMounted(async () => { font-size: 14px; color: var(--td-text-color-secondary); margin: 0; - line-height: 1.5; + line-height: 1.6; } } -.settings-group { - display: flex; - flex-direction: column; -} - -.section-subheader { +.settings-toolbar { display: flex; align-items: center; justify-content: space-between; @@ -385,67 +416,35 @@ onMounted(async () => { } } -.provider-list { - display: flex; - flex-direction: column; - gap: 8px; +.provider-grid { + display: grid; + grid-template-columns: repeat(2, minmax(0, 1fr)); + gap: 12px; } -.provider-item { - display: flex; +.provider-meta-item { + display: inline-flex; align-items: center; - justify-content: space-between; - padding: 14px 16px; - background: var(--td-bg-color-container); - border: 1px solid var(--td-component-stroke); - border-radius: 8px; - transition: all 0.2s ease; + gap: 4px; + max-width: 100%; + overflow: hidden; - &:hover { - border-color: var(--td-brand-color); + .provider-meta-text { + overflow: hidden; + text-overflow: ellipsis; + white-space: nowrap; } } -.item-info { - display: flex; - flex-direction: column; - gap: 4px; -} - -.item-header { - display: flex; - align-items: center; - gap: 8px; -} - -.item-name { - font-size: 14px; - font-weight: 500; - color: var(--td-text-color-primary); -} - -.item-desc { - font-size: 13px; - color: var(--td-text-color-secondary); -} - -.item-actions { - display: flex; - gap: 4px; - align-items: center; -} - -.empty-providers { - padding: 32px; +.empty-state { + padding: 64px 0; text-align: center; - color: var(--td-text-color-placeholder); - border: 1px dashed var(--td-component-stroke); - border-radius: 8px; - font-size: 14px; -} -.dialog-form-container { - margin-top: 12px; + :deep(.t-empty__description) { + font-size: 14px; + color: var(--td-text-color-placeholder); + margin-bottom: 16px; + } } .provider-option { @@ -464,11 +463,11 @@ onMounted(async () => { .credentials-hint { margin-bottom: 12px; font-size: 13px; - + a { color: var(--td-brand-color); text-decoration: none; - + &:hover { text-decoration: underline; } @@ -481,18 +480,4 @@ onMounted(async () => { margin-top: 4px; line-height: 1.4; } - -.dialog-footer { - display: flex; - justify-content: space-between; - align-items: center; - margin-top: 32px; - padding-top: 20px; - border-top: 1px solid var(--td-component-border); - - .footer-right { - display: flex; - gap: 12px; - } -} diff --git a/frontend/src/views/settings/components/McpServiceDialog.vue b/frontend/src/views/settings/components/McpServiceDialog.vue index 106a92cd..aea5f667 100644 --- a/frontend/src/views/settings/components/McpServiceDialog.vue +++ b/frontend/src/views/settings/components/McpServiceDialog.vue @@ -1,17 +1,17 @@