Files

1 line
40 KiB
JSON
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
{"repo": "aarnwang/testcase-manager", "files": {"frontend/src/views/AiConfig.vue": {"a3df62198cf151c8a921b3a70994f9a18c164141": {"t": 1785768139.9036076, "n": 35443, "h": "fb44c0aa8599ca06", "c": "<template>\n <div class=\"ai-config-container\">\n <el-tabs tab-position=\"left\" style=\"height: calc(100vh - 80px)\" class=\"demo-tabs\">\n <el-tab-pane label=\"模型配置\">\n <div class=\"tab-content\">\n <div class=\"config-container\">\n <div class=\"config-layout\">\n <div class=\"config-left\">\n <el-form \n :model=\"configForm\" \n :rules=\"configRules\" \n ref=\"configFormRef\" \n label-width=\"120px\"\n class=\"config-form\"\n >\n <div class=\"form-section\">\n <h4>基础信息</h4>\n <el-form-item label=\"配置名称\" prop=\"configName\">\n <el-input \n v-model=\"configForm.configName\" \n placeholder=\"请输入配置名称\"\n class=\"config-input\"\n />\n </el-form-item>\n <el-form-item label=\"模型类型\" prop=\"modelType\">\n <el-select \n v-model=\"configForm.modelType\" \n placeholder=\"请选择模型类型\"\n class=\"config-input\"\n >\n <el-option label=\"通义千问\" value=\"qwen\" />\n <el-option label=\"字节跳动(火山引擎)\" value=\"volcengine\" />\n <el-option label=\"DeepSeek\" value=\"deepseek\" />\n <el-option label=\"智谱AI\" value=\"zhipu\" />\n <el-option label=\"OpenAI\" value=\"openai\" />\n <el-option label=\"百度文心一言\" value=\"wenxin\" />\n <el-option label=\"讯飞星火\" value=\"xunfei\" />\n <el-option label=\"MiniMax\" value=\"minimax\" />\n <el-option label=\"月之暗面\" value=\"moonshot\" />\n <el-option label=\"360智脑\" value=\"360\" />\n <el-option label=\"Claude (Anthropic)\" value=\"claude\" />\n <el-option label=\"Gemini (Google)\" value=\"gemini\" />\n </el-select>\n </el-form-item>\n \n <el-form-item label=\"API密钥\" prop=\"apiKey\">\n <el-input \n v-model=\"configForm.apiKey\" \n placeholder=\"请输入API密钥\"\n type=\"password\"\n show-password\n class=\"config-input\"\n />\n </el-form-item>\n \n <el-form-item label=\"模型URL\" prop=\"baseUrl\">\n <el-input \n v-model=\"configForm.baseUrl\" \n placeholder=\"请输入模型URL\"\n class=\"config-input\"\n />\n </el-form-item>\n \n <el-form-item label=\"模型版本\" prop=\"modelVersion\">\n <el-input \n v-model=\"configForm.modelVersion\" \n placeholder=\"请输入模型版本\"\n class=\"config-input\"\n />\n </el-form-item>\n </div>\n \n <div class=\"form-section\">\n <h4>价格配置</h4>\n <el-form-item label=\"输入价格\" prop=\"promptPrice\">\n <el-input \n v-model=\"configForm.promptPrice\" \n placeholder=\"每1000个输入token的价格\"\n class=\"config-input\"\n >\n <template #append>CNY</template>\n </el-input>\n </el-form-item>\n \n <el-form-item label=\"输出价格\" prop=\"completionPrice\">\n <el-input \n v-model=\"configForm.completionPrice\" \n placeholder=\"每1000个输出token的价格\"\n class=\"config-input\"\n >\n <template #append>CNY</template>\n </el-input>\n </el-form-item>\n </div>\n \n <el-form-item class=\"form-actions\">\n <el-button type=\"primary\" @click=\"saveConfig\" :disabled=\"saving\" class=\"save-btn\">\n <template v-if=\"saving\">\n <div class=\"saving-inline\">\n <div class=\"circular-loading\"></div>\n 正在保存\n </div>\n </template>\n <template v-else>\n <span>保存配置</span>\n </template>\n </el-button>\n <el-button @click=\"resetConfig\" :disabled=\"saving\" class=\"reset-btn\">重置</el-button>\n </el-form-item>\n </el-form>\n </div>\n <div class=\"config-right\">\n <div class=\"form-section\">\n <h4>配置列表</h4>\n <div class=\"config-list\">\n <div v-if=\"configList.length === 0\" class=\"empty-list\">\n <p>暂无配置</p>\n </div>\n <div v-else class=\"config-items\">\n <div class=\"config-header\">\n <div class=\"config-name\">配置名称</div>\n <div class=\"config-info\">\n <div class=\"model-type-container\">\n <span>模型类型</span>\n </div>\n <div class=\"model-version\">模型版本</div>\n <div class=\"config-time\">更新时间</div>\n <div class=\"header-enabled\">启用状态</div>\n <div class=\"config-actions\">操作</div>\n </div>\n </div>\n <div \n v-for=\"config in configList\" \n :key=\"config.id\" \n class=\"config-item\"\n >\n <div class=\"config-name\">{{ config.config_name }}</div>\n <div class=\"config-info\">\n <div class=\"model-type-container\">\n <span class=\"model-type\" :style=\"{ \n backgroundColor: getModelTypeInfo(config.model_type).bgColor, \n color: getModelTypeInfo(config.model_type).color \n }\">\n {{ getModelTypeInfo(config.model_type).name }}\n </span>\n </div>\n <div class=\"model-version\">{{ config.model_version }}</div>\n <div class=\"config-time\">{{ formatTime(config.updated_at) }}</div>\n <div class=\"config-enabled\">\n <el-switch \n :model-value=\"Boolean(config.is_enabled)\" \n @update:model-value=\"(value) => toggleConfigEnabled(config, value)\"\n size=\"small\"\n class=\"animated-switch\"\n />\n </div>\n <div class=\"config-actions\">\n <el-button size=\"small\" type=\"success\" @click=\"viewConfig(config)\" class=\"action-btn view-btn\">查看</el-button>\n <el-button size=\"small\" type=\"primary\" @click=\"editConfig(config)\" class=\"action-btn edit-btn\">编辑</el-button>\n <el-popconfirm\n title=\"确定要删除这个配置吗?\"\n confirm-button-text=\"确定\"\n cancel-button-text=\"取消\"\n placement=\"left-end\"\n @confirm=\"deleteConfig(config)\"\n >\n <template #reference>\n <el-button size=\"small\" type=\"danger\" class=\"action-btn delete-btn\">删除</el-button>\n </template>\n </el-popconfirm>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </div>\n </el-tab-pane>\n \n <el-tab-pane label=\"帮助文档\">\n <div class=\"tab-content\">\n <div class=\"help-container\">\n <el-collapse>\n <el-collapse-item title=\"模型推荐\" name=\"1\" class=\"custom-collapse-item\">\n <p><strong>通义千问系列:</strong></p>\n <ul>\n <li><strong>qwen-turbo</strong>:强烈推荐!快速响应,生成的用例完整覆盖度高</li>\n <li><strong>qwen-plus</strong>:平衡性能,适合复杂对话和中等难度任务</li>\n <li><strong>qwen-max</strong>:最强性能,适合高难度任务和专业应用</li>\n </ul>\n </el-collapse-item>\n <el-collapse-item title=\"如何获取API密钥?\" name=\"2\" class=\"custom-collapse-item\">\n <p>1. 登录您的AI服务提供商账户</p>\n <p>2. 进入API管理页面</p>\n <p>3. 创建新的API密钥</p>\n <p>4. 复制密钥并粘贴到配置中</p>\n </el-collapse-item>\n <el-collapse-item title=\"模型URL接口地址说明\" name=\"3\" class=\"custom-collapse-item\">\n <p>通义千问:https://dashscope.aliyuncs.com/compatible-mode/v1</p>\n <p>火山引擎:https://ark.cn-beijing.volces.com/api/v3</p>\n <p>DeepSeek:https://api.deepseek.com</p>\n <p>智谱AI:https://open.bigmodel.cn/api/paas/v4</p>\n <p>OpenAI:https://api.openai.com/v1</p>\n <p>百度文心一言:https://aip.baidubce.com/rpc/2.0/ai_custom/v1/wenxinworkshop/chat</p>\n <p>讯飞星火:https://spark-api.xf-yun.com/v3.1/chat</p>\n <p>MiniMax:https://api.minimax.chat/v1</p>\n <p>月之暗面:https://api.moonshot.cn/v1</p>\n <p>360智脑:https://api.360.cn/v1</p>\n <p>Claude (Anthropic):https://api.anthropic.com/v1</p>\n <p>Gemini (Google):https://generativelanguage.googleapis.com/v1beta</p>\n </el-collapse-item>\n <el-collapse-item title=\"更新日志\" name=\"4\" class=\"custom-collapse-item update-log-item\">\n <p><strong>V1.0.9 (2025-09-03)</strong></p>\n <ul>\n <li>更新AI模型多配置</li>\n </ul>\n <p><strong>V1.0.8 (2025-09-02)</strong></p>\n <ul>\n <li>优化AI生成用例系统,支持Windows、macOS、Linux跨平台运行</li>\n <li>改进subprocess命令执行机制,提升跨平台兼容性</li>\n <li>增强错误处理和调试信息,便于问题诊断</li>\n </ul>\n <p><strong>V1.0.7 (2025-09-02)</strong></p>\n <ul>\n <li>新增支持Claude (Anthropic)、Gemini (Google)等全球顶级AI模型</li>\n <li>优化AI模型配置界面,支持12种主流AI模型</li>\n <li>完善模型URL接口地址说明文档</li>\n </ul>\n <p><strong>V1.0.6 (2025-09-02)</strong></p>\n <ul>\n <li>新增支持OpenAI、百度文心一言、讯飞星火、MiniMax、月之暗面、360智脑等AI模型</li>\n <li>优化AI模型配置界面,支持10种主流AI模型</li>\n <li>完善模型URL接口地址说明文档</li>\n </ul>\n <p><strong>V1.0.5 (2025-09-02)</strong></p>\n <ul>\n <li>完成AI配置服务层</li>\n <li>增加多模型适配</li>\n <li>修复一些报错</li>\n <li>脱离env文件,改为配置化</li>\n </ul>\n <p><strong>V1.0.4 (2025-08-29)</strong></p>\n <ul>\n <li>增加AI生成用例策略组json识别</li>\n <li>增加用例需求分析功能点规则匹配度</li>\n <li>补充用例上传的部分识别逻辑</li>\n <li>增加系统设置,后续所有的模型配置将不在代码中写死,改为配置</li>\n <li>解决AutoAgent框架识别部分模型时,无法计算价格导致的warning</li>\n </ul>\n <p><strong>V1.0.3 (2025-08-22)</strong></p>\n <ul>\n <li>手动增加测试用例功能</li>\n <li>运行日志</li>\n </ul>\n <p><strong>V1.0.2 (2025-08-12)</strong></p>\n <ul>\n <li>优化AI生成用例的用户体验</li>\n <li>修复文件选择后清除生成总结的问题</li>\n <li>修复用例类型显示问题</li>\n <li>优化下载中心弹窗标题样式</li>\n <li>改进错误处理和用户提示</li>\n </ul>\n <p><strong>V1.0.1 (2025-08-10)</strong></p>\n <ul>\n <li>AI生成能力(已开发完成)接入用例平台</li>\n <li>生成用例总结数据实时显示</li>\n <li>支持多种测试类型(功能测试、接口测试、UI自动化测试)</li>\n <li>智能文档解析和用例生成</li>\n <li>生成历史记录和统计</li>\n </ul>\n <p><strong>v1.0.0 (2025-08-01)</strong></p>\n <ul>\n <li>基础项目管理功能</li>\n <li>Excel文件上传和解析</li>\n <li>测试用例批量导入</li>\n <li>重复数据检测和处理</li>\n <li>响应式Web界面</li>\n </ul>\n </el-collapse-item>\n </el-collapse>\n </div>\n </div>\n </el-tab-pane>\n </el-tabs>\n \n <!-- 查看配置详情对话框 -->\n <el-dialog\n v-model=\"viewDialogVisible\"\n width=\"600px\"\n :close-on-click-modal=\"false\"\n :close-on-press-escape=\"true\"\n >\n <div class=\"config-detail\">\n <div class=\"detail-section\">\n <h4>基础信息</h4>\n <div class=\"detail-item\">\n <label>配置名称:</label>\n <span>{{ viewConfigData.configName }}</span>\n </div>\n <div class=\"detail-item\">\n <label>模型类型:</label>\n <span class=\"model-type-tag\" :style=\"{ \n backgroundColor: getModelTypeInfo(viewConfigData.modelType).bgColor, \n color: getModelTypeInfo(viewConfigData.modelType).color \n }\">\n {{ getModelTypeInfo(viewConfigData.modelType).name }}\n </span>\n </div>\n <div class=\"detail-item\">\n <label>模型版本:</label>\n <span>{{ viewConfigData.modelVersion }}</span>\n </div>\n <div class=\"detail-item\">\n <label>API密钥:</label>\n <span class=\"api-key\">{{ viewConfigData.apiKey }}</span>\n </div>\n <div class=\"detail-item\">\n <label>模型URL:</label>\n <span class=\"url-text\">{{ viewConfigData.baseUrl }}</span>\n </div>\n </div>\n \n <div class=\"detail-section\">\n <h4>价格配置</h4>\n <div class=\"detail-item\">\n <label>输入价格:</label>\n <span>{{ viewConfigData.promptPrice }} CNY / 1000 tokens</span>\n </div>\n <div class=\"detail-item\">\n <label>输出价格:</label>\n <span>{{ viewConfigData.completionPrice }} CNY / 1000 tokens</span>\n </div>\n </div>\n \n <div class=\"detail-section\">\n <h4>状态信息</h4>\n <div class=\"detail-item\">\n <label>启用状态:</label>\n <el-tag :type=\"viewConfigData.isEnabled ? 'success' : 'info'\">\n {{ viewConfigData.isEnabled ? '启用' : '禁用' }}\n </el-tag>\n </div>\n <div class=\"detail-item\">\n <label>创建时间:</label>\n <span>{{ formatTime(viewConfigData.createdAt) }}</span>\n </div>\n <div class=\"detail-item\">\n <label>更新时间:</label>\n <span>{{ formatTime(viewConfigData.updatedAt) }}</span>\n </div>\n </div>\n </div>\n \n <template #footer>\n <div class=\"dialog-footer\">\n <el-button @click=\"viewDialogVisible = false\">关闭</el-button>\n </div>\n </template>\n </el-dialog>\n </div>\n</template>\n\n<script setup>\nimport { ref, reactive, onMounted } from 'vue';\nimport { ElMessage, ElMessageBox } from 'element-plus';\nimport { Setting } from '@element-plus/icons-vue';\n\n// 响应式数据\nconst configFormRef = ref();\nconst saving = ref(false);\nconst configList = ref([]);\nconst viewDialogVisible = ref(false);\nconst viewConfigData = ref({});\n\nconst configForm = reactive({\n configId: null,\n configName: '',\n modelType: 'qwen',\n apiKey: '',\n baseUrl: '',\n modelVersion: '',\n promptPrice: '',\n completionPrice: ''\n});\n\n// 表单验证规则\nconst configRules = {\n configName: [\n { required: true, message: '请输入配置名称', trigger: 'blur' }\n ],\n modelType: [\n { required: true, message: '请选择模型类型', trigger: 'change' }\n ],\n apiKey: [\n { required: true, message: '请输入API密钥', trigger: 'blur' }\n ],\n baseUrl: [\n { required: true, message: '请输入基础URL', trigger: 'blur' }\n ],\n modelVersion: [\n { required: true, message: '请输入模型版本', trigger: 'blur' }\n ],\n promptPrice: [\n { required: true, message: '请输入每1000个输入token的价格', trigger: 'blur' }\n ],\n completionPrice: [\n { required: true, message: '请输入每1000个输出token的价格', trigger: 'blur' }\n ]\n};\n\n// 页面加载时初始化\nonMounted(async () => {\n // 初始化表单为空,只有模型类型默认为通义千问\n resetFormToDefault();\n // 加载配置列表\n await loadConfigList();\n});\n\n// 加载配置列表\nconst loadConfigList = async () => {\n try {\n const response = await fetch('/api/ai_configs?include_sensitive=true');\n const result = await response.json();\n \n if (result.success) {\n configList.value = result.data || [];\n } else {\n console.error('加载配置列表失败:', result.message);\n }\n } catch (error) {\n console.error('加载配置列表失败:', error);\n }\n};\n\n// 获取模型类型中文名称和颜色\nconst getModelTypeInfo = (modelType) => {\n const modelInfo = {\n 'qwen': { name: '通义千问', color: '#ff6b35', bgColor: '#fff2ed' },\n 'volcengine': { name: '字节跳动(火山引擎)', color: '#000000', bgColor: '#f5f5f5' },\n 'deepseek': { name: 'DeepSeek', color: '#6366f1', bgColor: '#eef2ff' },\n 'zhipu': { name: '智谱AI', color: '#059669', bgColor: '#ecfdf5' },\n 'openai': { name: 'OpenAI', color: '#10b981', bgColor: '#ecfdf5' },\n 'wenxin': { name: '百度文心一言', color: '#3b82f6', bgColor: '#eff6ff' },\n 'xunfei': { name: '讯飞星火', color: '#f59e0b', bgColor: '#fffbeb' },\n 'minimax': { name: 'MiniMax', color: '#8b5cf6', bgColor: '#f3f4f6' },\n 'moonshot': { name: '月之暗面', color: '#6366f1', bgColor: '#eef2ff' },\n '360': { name: '360智脑', color: '#059669', bgColor: '#ecfdf5' },\n 'claude': { name: 'Claude (Anthropic)', color: '#dc2626', bgColor: '#fef2f2' },\n 'gemini': { name: 'Gemini (Google)', color: '#ea4335', bgColor: '#fef2f2' }\n };\n return modelInfo[modelType] || { name: modelType, color: '#6b7280', bgColor: '#f9fafb' };\n};\n\n// 格式化时间\nconst formatTime = (timeStr) => {\n if (!timeStr) return '';\n const date = new Date(timeStr);\n return date.toLocaleString('zh-CN', {\n year: 'numeric',\n month: '2-digit',\n day: '2-digit',\n hour: '2-digit',\n minute: '2-digit'\n });\n};\n\n// 查看配置\nconst viewConfig = async (config) => {\n try {\n console.log('查看配置:', config);\n \n const response = await fetch(`/api/ai_config/${config.id}`);\n const result = await response.json();\n \n if (result.success) {\n viewConfigData.value = result.data;\n viewDialogVisible.value = true;\n } else {\n ElMessage.error(result.message || '获取配置详情失败');\n }\n } catch (error) {\n console.error('获取配置详情失败:', error);\n ElMessage.error('获取配置详情失败');\n }\n};\n\n// 编辑配置\nconst editConfig = (config) => {\n console.log('编辑配置:', config);\n // 将选中的配置填充到表单中\n configForm.configId = config.id; // 添加配置ID\n configForm.configName = config.config_name;\n configForm.modelType = config.model_type;\n configForm.apiKey = config.api_key;\n configForm.baseUrl = config.model_url;\n configForm.modelVersion = config.model_version;\n configForm.promptPrice = config.prompt_price_per_1k;\n configForm.completionPrice = config.completion_price_per_1k;\n \n ElMessage.success('配置已加载到表单中,请修改后保存');\n};\n\n// 删除配置\nconst deleteConfig = async (config) => {\n try {\n console.log('删除配置:', config);\n \n const response = await fetch(`/api/ai_config/${config.id}/delete`, {\n method: 'DELETE',\n headers: {\n 'Content-Type': 'application/json',\n }\n });\n \n const result = await response.json();\n \n if (result.success) {\n ElMessage.success(result.message);\n // 刷新配置列表\n await loadConfigList();\n } else {\n ElMessage.error(result.message);\n }\n } catch (error) {\n console.error('删除配置失败:', error);\n ElMessage.error('删除配置失败');\n }\n};\n\n// 切换配置启用状态\nconst toggleConfigEnabled = async (config, newValue) => {\n try {\n console.log('切换配置启用状态:', config, '新值:', newValue);\n \n // 如果要启用,先禁用所有其他配置\n if (newValue) {\n // 找到所有已启用的配置(除了当前配置)\n const otherEnabledConfigs = configList.value.filter(c => c.is_enabled && c.id !== config.id);\n \n if (otherEnabledConfigs.length > 0) {\n // 显示提示信息\n ElMessage.info('正在切换配置,请稍候...');\n \n // 先禁用其他已启用的配置\n for (const otherConfig of otherEnabledConfigs) {\n try {\n const disableResponse = await fetch(`/api/ai_config/${otherConfig.id}/toggle_enabled`, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n enabled: false\n })\n });\n \n const disableResult = await disableResponse.json();\n if (!disableResult.success) {\n console.error('禁用其他配置失败:', disableResult.message);\n }\n } catch (error) {\n console.error('禁用其他配置失败:', error);\n }\n }\n }\n }\n \n // 然后切换当前配置的状态\n const response = await fetch(`/api/ai_config/${config.id}/toggle_enabled`, {\n method: 'PUT',\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n enabled: newValue\n })\n });\n \n const result = await response.json();\n \n if (result.success) {\n ElMessage.success(result.message);\n // 刷新配置列表以获取最新状态\n await loadConfigList();\n } else {\n ElMessage.error(result.message);\n }\n } catch (error) {\n console.error('切换启用状态失败:', error);\n ElMessage.error('切换启用状态失败');\n }\n};\n\n// 重置表单为默认状态\nconst resetFormToDefault = () => {\n configForm.configId = null;\n configForm.configName = '';\n configForm.modelType = 'qwen';\n configForm.apiKey = '';\n configForm.baseUrl = '';\n configForm.modelVersion = '';\n configForm.promptPrice = '';\n configForm.completionPrice = '';\n};\n\n// 加载配置(暂时注释掉,后续从配置列表中选择时调用)\n// const loadConfig = async () => {\n// try {\n// const response = await fetch('/api/ai_config');\n// const result = await response.json();\n// \n// if (result.success && result.data) {\n// // 填充表单数据\n// configForm.modelType = result.data.modelType || 'qwen';\n// configForm.apiKey = result.data.apiKey || '';\n// configForm.baseUrl = result.data.baseUrl || '';\n// configForm.modelVersion = result.data.modelVersion || '';\n// configForm.promptPrice = result.data.promptPrice || '0.001';\n// configForm.completionPrice = result.data.completionPrice || '0.002';\n// }\n// } catch (error) {\n// console.error('加载配置失败:', error);\n// ElMessage.error('加载配置失败');\n// }\n// };\n\n// 保存配置\nconst saveConfig = async () => {\n try {\n await configFormRef.value.validate();\n saving.value = true;\n \n const isEdit = configForm.configId !== null;\n const url = isEdit ? `/api/ai_config/${configForm.configId}` : '/api/ai_config';\n const method = isEdit ? 'PUT' : 'POST';\n \n const response = await fetch(url, {\n method: method,\n headers: {\n 'Content-Type': 'application/json',\n },\n body: JSON.stringify({\n configId: configForm.configId,\n configName: configForm.configName,\n modelType: configForm.modelType,\n apiKey: configForm.apiKey,\n baseUrl: configForm.baseUrl,\n modelVersion: configForm.modelVersion,\n promptPrice: configForm.promptPrice,\n completionPrice: configForm.completionPrice\n })\n });\n \n const result = await response.json();\n \n if (result.success) {\n ElMessage.success(isEdit ? '配置更新成功!' : '配置保存成功!');\n // 保存成功后清空表单\n resetFormToDefault();\n // 刷新配置列表\n await loadConfigList();\n } else {\n // 显示具体的验证错误信息\n if (result.errors) {\n // 显示所有验证错误\n const errorMessages = Object.values(result.errors);\n if (errorMessages.length === 1) {\n ElMessage.error(errorMessages[0]);\n } else {\n // 如果有多个错误,显示所有错误\n const errorText = errorMessages.join(';');\n ElMessage.error(`验证失败:${errorText}`);\n console.error('所有验证错误:', result.errors);\n }\n } else {\n ElMessage.error(result.message || '配置保存失败');\n }\n }\n } catch (error) {\n console.error('配置保存失败:', error);\n ElMessage.error('配置保存失败,请检查输入');\n } finally {\n saving.value = false;\n }\n};\n\n// 重置配置\nconst resetConfig = () => {\n ElMessageBox.confirm(\n '确定要重置所有配置吗?',\n '提示',\n {\n confirmButtonText: '确定',\n cancelButtonText: '取消',\n type: 'warning',\n }\n ).then(() => {\n resetFormToDefault();\n ElMessage.success('配置已重置');\n }).catch(() => {\n // 用户取消\n });\n};\n</script>\n\n<style scoped>\n.ai-config-container {\n padding: 24px;\n width: 100%;\n height: 100%;\n background: #f8fafc;\n border-radius: 20px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);\n overflow: hidden;\n}\n\n.demo-tabs {\n height: 100%;\n border-radius: 16px;\n overflow: hidden;\n}\n\n.demo-tabs > .el-tabs__content {\n padding: 32px;\n color: #6b778c;\n font-size: 16px;\n font-weight: 400;\n}\n\n.el-tabs--right .el-tabs__content,\n.el-tabs--left .el-tabs__content {\n height: 100%;\n}\n\n.el-tabs--left .el-tabs__header {\n border-right: 1px solid #e4e7ed;\n border-bottom: none;\n border-top: none;\n border-left: none;\n}\n\n.el-tabs--left .el-tabs__nav-wrap {\n border-bottom: none;\n}\n\n.el-tabs--left .el-tabs__nav {\n border: none;\n}\n\n.el-tabs--left .el-tabs__item {\n border: none;\n border-right: 2px solid transparent;\n transition: all 0.3s;\n}\n\n.el-tabs--left .el-tabs__item.is-active {\n border-right: 2px solid #409eff;\n background-color: #f0f9ff;\n}\n\n.tab-content {\n height: 100%;\n overflow-y: auto;\n}\n\n.config-container {\n padding: 20px;\n background: #fff;\n border-radius: 12px;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.08);\n height: 100%;\n display: flex;\n flex-direction: column;\n}\n\n.config-layout {\n display: flex;\n gap: 30px;\n height: 100%;\n}\n\n.config-left {\n flex: 1;\n max-width: 620px;\n display: flex;\n flex-direction: column;\n}\n\n.config-right {\n flex: 1;\n max-width: 800px;\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n\n.config-right {\n flex: 1;\n max-width: 800px;\n display: flex;\n flex-direction: column;\n height: 100%;\n}\n\n.config-list {\n margin-top: 15px;\n flex: 1;\n display: flex;\n flex-direction: column;\n}\n\n.config-list {\n margin-top: 15px;\n}\n\n.empty-list {\n text-align: center;\n padding: 40px 20px;\n color: #999;\n font-size: 14px;\n}\n\n.config-items {\n height: calc(100vh - 300px);\n overflow-y: auto;\n /* 隐藏滚动条 */\n scrollbar-width: none; /* Firefox */\n -ms-overflow-style: none; /* IE and Edge */\n}\n\n/* 隐藏 Webkit 浏览器的滚动条 */\n.config-items::-webkit-scrollbar {\n display: none;\n}\n\n.config-header {\n display: flex;\n align-items: center;\n padding: 10px 15px;\n background-color: #f1f5f9;\n border-bottom: 2px solid #e2e8f0;\n font-weight: 600;\n color: #475569;\n font-size: 13px;\n}\n\n.header-name {\n flex: 0 0 150px;\n}\n\n.header-info {\n display: flex;\n align-items: center;\n font-size: 12px;\n flex: 1;\n}\n\n.config-header .config-name {\n font-weight: 600;\n color: #475569;\n font-size: 13px;\n flex: 0 0 120px;\n padding-right: 8px;\n}\n\n.header-model-type {\n width: 140px;\n text-align: left;\n display: flex;\n align-items: center;\n padding-right: 8px;\n}\n\n.header-model-version {\n width: 90px;\n text-align: left;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n padding-right: 8px;\n}\n\n.header-update-time {\n width: 120px;\n text-align: left;\n}\n\n.header-enabled {\n width: 80px;\n text-align: center;\n}\n\n.header-actions {\n width: 140px;\n text-align: left;\n}\n\n.config-item {\n display: flex;\n align-items: center;\n padding: 12px 15px;\n border-bottom: 1px solid #e2e8f0;\n transition: background-color 0.3s ease;\n}\n\n.config-item:hover {\n background-color: #f8fafc;\n}\n\n.config-item:last-child {\n border-bottom: none;\n}\n\n.config-name {\n font-weight: 600;\n color: #1e293b;\n font-size: 14px;\n flex: 0 0 120px;\n overflow: hidden;\n text-overflow: ellipsis;\n white-space: nowrap;\n padding-right: 8px;\n}\n\n.config-info {\n display: flex;\n align-items: center;\n font-size: 12px;\n flex: 1;\n}\n\n.model-type-container {\n width: 140px;\n display: flex;\n align-items: center;\n padding-right: 8px;\n}\n\n.model-type {\n padding: 2px 6px;\n border-radius: 3px;\n white-space: nowrap;\n text-align: left;\n display: inline-block;\n font-size: 12px;\n font-weight: 500;\n}\n\n.model-version {\n color: #64748b;\n white-space: nowrap;\n width: 90px;\n overflow: hidden;\n text-overflow: ellipsis;\n padding-right: 8px;\n}\n\n.config-time {\n color: #94a3b8;\n white-space: nowrap;\n width: 120px;\n}\n\n.config-enabled {\n width: 80px;\n display: flex;\n justify-content: center;\n align-items: center;\n}\n\n.config-actions {\n width: 140px;\n display: flex;\n gap: 6px;\n}\n\n/* 自定义按钮样式 */\n.action-btn {\n font-weight: 500;\n}\n\n.view-btn {\n background-color: #10b981 !important;\n border-color: #10b981 !important;\n color: white !important;\n}\n\n.view-btn:hover {\n background-color: #059669 !important;\n border-color: #059669 !important;\n}\n\n.edit-btn {\n background-color: #409eff !important;\n border-color: #409eff !important;\n color: white !important;\n}\n\n.edit-btn:hover {\n background-color: #337ecc !important;\n border-color: #337ecc !important;\n}\n\n.delete-btn {\n background-color: #dc2626 !important;\n border-color: #dc2626 !important;\n color: white !important;\n}\n\n.delete-btn:hover {\n background-color: #b91c1c !important;\n border-color: #b91c1c !important;\n}\n\n/* 气泡确认框样式优化 */\n:deep(.el-popconfirm) {\n .el-popconfirm__main {\n padding: 12px 16px;\n }\n \n .el-popconfirm__title {\n font-size: 14px;\n color: #374151;\n margin-bottom: 8px;\n }\n \n .el-popconfirm__action {\n margin-top: 12px;\n }\n \n .el-button {\n padding: 6px 12px;\n font-size: 12px;\n }\n}\n\n/* 配置详情对话框样式 */\n.config-detail {\n padding: 0;\n}\n\n.detail-section {\n margin-bottom: 24px;\n}\n\n.detail-section:last-child {\n margin-bottom: 0;\n}\n\n.detail-section h4 {\n margin-bottom: 16px;\n color: #409eff;\n font-size: 16px;\n font-weight: 600;\n display: flex;\n align-items: center;\n border-bottom: 1px solid #e2e8f0;\n padding-bottom: 8px;\n}\n\n.detail-section h4::before {\n content: '';\n width: 3px;\n height: 16px;\n background: #409eff;\n margin-right: 8px;\n border-radius: 2px;\n}\n\n.detail-item {\n display: flex;\n align-items: flex-start;\n margin-bottom: 12px;\n line-height: 1.6;\n}\n\n.detail-item:last-child {\n margin-bottom: 0;\n}\n\n.detail-item label {\n width: 100px;\n font-weight: 600;\n color: #374151;\n flex-shrink: 0;\n}\n\n.detail-item span {\n color: #64748b;\n word-break: break-all;\n}\n\n.detail-item .api-key {\n font-family: 'Courier New', monospace;\n background: #f1f5f9;\n padding: 2px 6px;\n border-radius: 4px;\n font-size: 12px;\n}\n\n.detail-item .url-text {\n font-family: 'Courier New', monospace;\n background: #f1f5f9;\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 12px;\n word-break: break-all;\n}\n\n.detail-item .model-type-tag {\n padding: 4px 8px;\n border-radius: 4px;\n font-size: 12px;\n font-weight: 500;\n display: inline-block;\n}\n\n.dialog-footer {\n text-align: center;\n}\n\n/* 开关动画效果 */\n.animated-switch {\n transition: all 0.3s ease;\n}\n\n:deep(.animated-switch .el-switch__core) {\n transition: all 0.3s ease;\n}\n\n:deep(.animated-switch .el-switch__action) {\n transition: all 0.3s ease;\n}\n\n:deep(.animated-switch.is-checked .el-switch__core) {\n background-color: #10b981;\n border-color: #10b981;\n}\n\n:deep(.animated-switch:not(.is-checked) .el-switch__core) {\n background-color: #d1d5db;\n border-color: #d1d5db;\n}\n\n.form-section {\n margin-bottom: 25px;\n}\n\n.form-section h4 {\n margin-bottom: 15px;\n color: #409eff;\n font-size: 18px;\n font-weight: 600;\n display: flex;\n align-items: center;\n}\n\n.form-section h4::before {\n content: '';\n width: 4px;\n height: 18px;\n background: #409eff;\n margin-right: 8px;\n border-radius: 2px;\n}\n\n.config-form {\n max-width: 600px;\n}\n\n.config-input {\n width: 100%;\n}\n\n.el-form-item {\n margin-bottom: 20px;\n}\n\n.el-form-item:last-child {\n margin-bottom: 0;\n text-align: center;\n}\n\n.form-actions {\n text-align: center;\n margin-top: 30px;\n padding-top: 20px;\n border-top: 1px solid #eee;\n}\n\n.save-btn, .reset-btn {\n margin: 0 10px;\n padding: 12px 24px;\n font-weight: 600;\n border-radius: 8px;\n transition: all 0.3s ease;\n}\n\n.save-btn {\n background: #38bdf8;\n border-color: #38bdf8;\n box-shadow: 0 2px 8px rgba(56,189,248,0.25);\n}\n\n.save-btn:hover {\n background: #0ea5e9;\n border-color: #0ea5e9;\n box-shadow: 0 4px 12px rgba(56,189,248,0.35);\n}\n\n.reset-btn {\n background: #f8fafc;\n border: 1px solid #e2e8f0;\n color: #64748b;\n}\n\n.reset-btn:hover {\n background: #f1f5f9;\n border-color: #cbd5e1;\n color: #475569;\n}\n\n.saving-inline {\n display: inline-flex;\n align-items: center;\n}\n\n.circular-loading {\n width: 16px;\n height: 16px;\n border: 2px solid #e4e7ed;\n border-top: 2px solid #409eff;\n border-radius: 50%;\n margin-right: 8px;\n animation: spin 1s linear infinite;\n}\n\n@keyframes spin {\n 0% { transform: rotate(0deg); }\n 100% { transform: rotate(360deg); }\n}\n\n.stat-card {\n margin-top: 20px;\n max-width: 400px;\n}\n\n.stat-container {\n padding-left: 40px;\n padding-top: 20px;\n}\n\n.help-container {\n /* 隐藏右侧滚动条 */\n -ms-overflow-style: none; /* IE and Edge */\n scrollbar-width: none; /* Firefox */\n padding-left: 40px;\n padding-top: 20px;\n max-height: calc(100vh - 200px);\n overflow-y: auto;\n padding-bottom: 40px;\n}\n\n.custom-collapse-item {\n margin-bottom: 16px;\n border-radius: 8px;\n overflow: hidden;\n box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);\n transition: all 0.3s ease;\n}\n\n.custom-collapse-item:hover {\n box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);\n}\n\n.custom-collapse-item :deep(.el-collapse-item__header) {\n background: #94a3b8;\n color: white;\n font-weight: 600;\n font-size: 16px;\n padding: 16px 20px;\n border: none;\n border-radius: 8px 8px 0 0;\n transition: all 0.3s ease;\n}\n\n.custom-collapse-item :deep(.el-collapse-item__header:hover) {\n background: #64748b;\n}\n\n.custom-collapse-item :deep(.el-collapse-item__content) {\n background: #f8fafc;\n padding: 20px;\n border-radius: 0 0 8px 8px;\n border: 1px solid #e2e8f0;\n border-top: none;\n overflow-y: visible;\n}\n\n/* 更新日志特殊样式 */\n.update-log-item :deep(.el-collapse-item__content) {\n max-height: 220px;\n overflow-y: auto;\n}\n\n/* 隐藏更新日志内容区域的滚动条 */\n.update-log-item :deep(.el-collapse-item__content)::-webkit-scrollbar {\n display: none;\n}\n\n.update-log-item :deep(.el-collapse-item__content) {\n -ms-overflow-style: none; /* IE and Edge */\n scrollbar-width: none; /* Firefox */\n}\n\n.custom-collapse-item :deep(.el-collapse-item__wrap) {\n border: none;\n}\n\n.custom-collapse-item :deep(.el-collapse-item__arrow) {\n color: white;\n font-weight: bold;\n}\n\n/* 自定义滚动条样式 */\n.custom-collapse-item :deep(.el-collapse-item__content)::-webkit-scrollbar {\n width: 6px;\n}\n\n.custom-collapse-item :deep(.el-collapse-item__content)::-webkit-scrollbar-track {\n background: #f1f5f9;\n border-radius: 3px;\n}\n\n.custom-collapse-item :deep(.el-collapse-item__content)::-webkit-scrollbar-thumb {\n background: #cbd5e1;\n border-radius: 3px;\n}\n\n.custom-collapse-item :deep(.el-collapse-item__content)::-webkit-scrollbar-thumb:hover {\n background: #94a3b8;\n}\n\n.stat-item {\n display: flex;\n justify-content: space-between;\n align-items: center;\n padding: 12px 0;\n border-bottom: 1px solid #f0f0f0;\n}\n\n.stat-item:last-child {\n border-bottom: none;\n}\n\n.stat-label {\n font-weight: 500;\n color: #666;\n}\n\n.stat-value {\n font-weight: 600;\n color: #409eff;\n font-size: 18px;\n}\n\nh3 {\n margin-bottom: 16px;\n color: #333;\n font-size: 20px;\n font-weight: 600;\n}\n\np {\n margin-bottom: 16px;\n line-height: 1.6;\n color: #666;\n}\n\nul {\n padding-left: 20px;\n margin-bottom: 16px;\n}\n\nli {\n margin-bottom: 8px;\n color: #666;\n}\n</style> "}}}}