|
@@ -1,27 +1,26 @@
|
|
|
<template>
|
|
|
<div class="model-config">
|
|
|
- <div class="config-trigger" @click="showDrawer">
|
|
|
- <SettingOutlined class="trigger-icon" />
|
|
|
+ <div class="config-header" @click="togglePanel">
|
|
|
+ <img src="../../assets/svg/setting.svg" alt="setting" class="config-icon" />
|
|
|
<span>模型配置</span>
|
|
|
+ <img
|
|
|
+ :src="isPanelOpen ? '../../assets/svg/up.svg' : '../../assets/svg/down.svg'"
|
|
|
+ alt="toggle"
|
|
|
+ class="toggle-icon"
|
|
|
+ />
|
|
|
</div>
|
|
|
|
|
|
- <a-drawer
|
|
|
- title="AI模型配置"
|
|
|
- placement="left"
|
|
|
- :width="400"
|
|
|
- :visible="visible"
|
|
|
- @close="closeDrawer"
|
|
|
- class="model-drawer"
|
|
|
- >
|
|
|
+ <div v-show="isPanelOpen" class="config-panel">
|
|
|
<div class="model-list">
|
|
|
<div v-for="(model, index) in modelList" :key="index" class="model-item">
|
|
|
<div class="model-header">
|
|
|
<span class="model-title">模型 {{index + 1}}</span>
|
|
|
- <a-button type="link" danger @click="removeModel(index)">删除</a-button>
|
|
|
+ <a-button type="link" danger size="small" @click="removeModel(index)">删除</a-button>
|
|
|
</div>
|
|
|
|
|
|
- <a-form layout="vertical">
|
|
|
- <a-form-item label="选择模型">
|
|
|
+ <div class="model-form">
|
|
|
+ <div class="form-item">
|
|
|
+ <label>选择模型</label>
|
|
|
<a-select
|
|
|
v-model:value="model.type"
|
|
|
placeholder="请选择模型"
|
|
@@ -37,41 +36,45 @@
|
|
|
</a-select-option>
|
|
|
</a-select-opt-group>
|
|
|
</a-select>
|
|
|
- </a-form-item>
|
|
|
+ </div>
|
|
|
|
|
|
<template v-if="model.type">
|
|
|
- <a-form-item label="API Key">
|
|
|
+ <div class="form-item">
|
|
|
+ <label>API Key</label>
|
|
|
<a-input-password
|
|
|
v-model:value="model.key"
|
|
|
:placeholder="getKeyPlaceholder(model.type)"
|
|
|
/>
|
|
|
- </a-form-item>
|
|
|
+ </div>
|
|
|
|
|
|
<template v-if="model.type === 'ollama'">
|
|
|
- <a-form-item label="服务器地址">
|
|
|
+ <div class="form-item">
|
|
|
+ <label>服务器地址</label>
|
|
|
<a-input
|
|
|
v-model:value="model.endpoint"
|
|
|
placeholder="例如: http://localhost:11434"
|
|
|
/>
|
|
|
- </a-form-item>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
<template v-if="model.type === 'custom'">
|
|
|
- <a-form-item label="模型名称">
|
|
|
+ <div class="form-item">
|
|
|
+ <label>模型名称</label>
|
|
|
<a-input
|
|
|
v-model:value="model.name"
|
|
|
placeholder="请输入自定义模型名称"
|
|
|
/>
|
|
|
- </a-form-item>
|
|
|
- <a-form-item label="服务器地址">
|
|
|
+ </div>
|
|
|
+ <div class="form-item">
|
|
|
+ <label>服务器地址</label>
|
|
|
<a-input
|
|
|
v-model:value="model.endpoint"
|
|
|
placeholder="请输入API地址"
|
|
|
/>
|
|
|
- </a-form-item>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
</template>
|
|
|
- </a-form>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</div>
|
|
|
|
|
@@ -84,19 +87,18 @@
|
|
|
<PlusOutlined /> 添加模型
|
|
|
</a-button>
|
|
|
|
|
|
- <div class="drawer-footer">
|
|
|
- <a-button style="margin-right: 8px" @click="closeDrawer">取消</a-button>
|
|
|
+ <div class="action-buttons">
|
|
|
<a-button type="primary" :loading="saving" @click="saveConfig">
|
|
|
保存配置
|
|
|
</a-button>
|
|
|
</div>
|
|
|
- </a-drawer>
|
|
|
+ </div>
|
|
|
</div>
|
|
|
</template>
|
|
|
|
|
|
-<script>
|
|
|
-import { defineComponent, ref, reactive } from 'vue';
|
|
|
-import { SettingOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
|
|
+<script setup>
|
|
|
+import { ref, reactive } from 'vue';
|
|
|
+import { PlusOutlined } from '@ant-design/icons-vue';
|
|
|
import { message } from 'ant-design-vue';
|
|
|
|
|
|
const MODEL_GROUPS = [
|
|
@@ -132,200 +134,191 @@ const MODEL_GROUPS = [
|
|
|
}
|
|
|
];
|
|
|
|
|
|
-export default defineComponent({
|
|
|
- name: 'ModelConfig',
|
|
|
- components: {
|
|
|
- SettingOutlined,
|
|
|
- PlusOutlined
|
|
|
- },
|
|
|
- setup() {
|
|
|
- const visible = ref(false);
|
|
|
- const saving = ref(false);
|
|
|
- const modelList = ref([
|
|
|
- { type: '', key: '', endpoint: '', name: '' }
|
|
|
- ]);
|
|
|
-
|
|
|
- const modelGroups = ref(MODEL_GROUPS);
|
|
|
-
|
|
|
- const getKeyPlaceholder = (type) => {
|
|
|
- const placeholders = {
|
|
|
- 'chatgpt': '请输入 OpenAI API Key',
|
|
|
- 'gpt4': '请输入 OpenAI API Key',
|
|
|
- 'claude': '请输入 Anthropic API Key',
|
|
|
- 'deepseek': '请输入 DeepSeek API Key',
|
|
|
- 'qianwen': '请输入阿里云 API Key',
|
|
|
- 'wenxin': '请输入百度 API Key',
|
|
|
- 'spark': '请输入讯飞 API Key',
|
|
|
- 'chatglm': '请输入智谱 API Key',
|
|
|
- 'ollama': '可选:请输入访问密钥',
|
|
|
- 'custom': '请输入 API Key'
|
|
|
- };
|
|
|
- return placeholders[type] || '请输入 API Key';
|
|
|
- };
|
|
|
-
|
|
|
- const handleModelChange = (value, index) => {
|
|
|
- const model = modelList.value[index];
|
|
|
- const selectedOption = MODEL_GROUPS.flatMap(group => group.options)
|
|
|
- .find(option => option.value === value);
|
|
|
-
|
|
|
- if (selectedOption) {
|
|
|
- model.endpoint = selectedOption.endpoint;
|
|
|
- model.name = selectedOption.label;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- const showDrawer = () => {
|
|
|
- visible.value = true;
|
|
|
- };
|
|
|
-
|
|
|
- const closeDrawer = () => {
|
|
|
- visible.value = false;
|
|
|
- };
|
|
|
-
|
|
|
- const addModel = () => {
|
|
|
- modelList.value.push({ type: '', key: '', endpoint: '', name: '' });
|
|
|
- };
|
|
|
-
|
|
|
- const removeModel = (index) => {
|
|
|
- if (modelList.value.length === 1) {
|
|
|
- message.warning('至少保留一个模型配置');
|
|
|
- return;
|
|
|
- }
|
|
|
- modelList.value.splice(index, 1);
|
|
|
- };
|
|
|
-
|
|
|
- const validateModel = (model) => {
|
|
|
- if (!model.type) return false;
|
|
|
- if (!model.key) return false;
|
|
|
- if (model.type === 'ollama' && !model.endpoint) return false;
|
|
|
- if (model.type === 'custom' && (!model.name || !model.endpoint)) return false;
|
|
|
- return true;
|
|
|
- };
|
|
|
-
|
|
|
- const saveConfig = async () => {
|
|
|
- // 验证所有字段都已填写
|
|
|
- const hasInvalidModel = modelList.value.some(model => !validateModel(model));
|
|
|
- if (hasInvalidModel) {
|
|
|
- message.error('请填写完整的模型信息');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- saving.value = true;
|
|
|
- try {
|
|
|
- // 这里添加保存配置的逻辑
|
|
|
- localStorage.setItem('modelConfig', JSON.stringify(modelList.value));
|
|
|
- message.success('配置保存成功');
|
|
|
- closeDrawer();
|
|
|
- } catch (error) {
|
|
|
- message.error('配置保存失败');
|
|
|
- } finally {
|
|
|
- saving.value = false;
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 初始化时从localStorage加载配置
|
|
|
- const initConfig = () => {
|
|
|
- const savedConfig = localStorage.getItem('modelConfig');
|
|
|
- if (savedConfig) {
|
|
|
- modelList.value = JSON.parse(savedConfig);
|
|
|
- }
|
|
|
- };
|
|
|
-
|
|
|
- // 组件挂载时加载配置
|
|
|
- initConfig();
|
|
|
-
|
|
|
- return {
|
|
|
- visible,
|
|
|
- modelList,
|
|
|
- modelGroups,
|
|
|
- saving,
|
|
|
- showDrawer,
|
|
|
- closeDrawer,
|
|
|
- addModel,
|
|
|
- removeModel,
|
|
|
- saveConfig,
|
|
|
- handleModelChange,
|
|
|
- getKeyPlaceholder
|
|
|
- };
|
|
|
+const isPanelOpen = ref(false);
|
|
|
+const saving = ref(false);
|
|
|
+const modelList = ref([
|
|
|
+ { type: '', key: '', endpoint: '', name: '' }
|
|
|
+]);
|
|
|
+
|
|
|
+const modelGroups = ref(MODEL_GROUPS);
|
|
|
+
|
|
|
+const getKeyPlaceholder = (type) => {
|
|
|
+ const placeholders = {
|
|
|
+ 'chatgpt': '请输入 OpenAI API Key',
|
|
|
+ 'gpt4': '请输入 OpenAI API Key',
|
|
|
+ 'claude': '请输入 Anthropic API Key',
|
|
|
+ 'deepseek': '请输入 DeepSeek API Key',
|
|
|
+ 'qianwen': '请输入阿里云 API Key',
|
|
|
+ 'wenxin': '请输入百度 API Key',
|
|
|
+ 'spark': '请输入讯飞 API Key',
|
|
|
+ 'chatglm': '请输入智谱 API Key',
|
|
|
+ 'ollama': '可选:请输入访问密钥',
|
|
|
+ 'custom': '请输入 API Key'
|
|
|
+ };
|
|
|
+ return placeholders[type] || '请输入 API Key';
|
|
|
+};
|
|
|
+
|
|
|
+const handleModelChange = (value, index) => {
|
|
|
+ const model = modelList.value[index];
|
|
|
+ const selectedOption = MODEL_GROUPS.flatMap(group => group.options)
|
|
|
+ .find(option => option.value === value);
|
|
|
+
|
|
|
+ if (selectedOption) {
|
|
|
+ model.endpoint = selectedOption.endpoint;
|
|
|
+ model.name = selectedOption.label;
|
|
|
}
|
|
|
-});
|
|
|
-</script>
|
|
|
+};
|
|
|
|
|
|
-<style lang="less" scoped>
|
|
|
-.model-config {
|
|
|
- position: fixed;
|
|
|
- left: 20px;
|
|
|
- bottom: 20px;
|
|
|
- z-index: 1000;
|
|
|
-
|
|
|
- .config-trigger {
|
|
|
- display: flex;
|
|
|
- align-items: center;
|
|
|
- gap: 8px;
|
|
|
- padding: 8px 16px;
|
|
|
- background: #fff;
|
|
|
- border-radius: 20px;
|
|
|
- cursor: pointer;
|
|
|
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
|
|
|
- transition: all 0.3s;
|
|
|
-
|
|
|
- &:hover {
|
|
|
- background: #f5f5f5;
|
|
|
- transform: translateY(-2px);
|
|
|
- }
|
|
|
-
|
|
|
- .trigger-icon {
|
|
|
- font-size: 16px;
|
|
|
- color: #1677ff;
|
|
|
- }
|
|
|
-
|
|
|
- span {
|
|
|
- font-size: 14px;
|
|
|
- color: #1677ff;
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
+const togglePanel = () => {
|
|
|
+ isPanelOpen.value = !isPanelOpen.value;
|
|
|
+};
|
|
|
+
|
|
|
+const addModel = () => {
|
|
|
+ modelList.value.push({ type: '', key: '', endpoint: '', name: '' });
|
|
|
+};
|
|
|
|
|
|
-.model-drawer {
|
|
|
- .model-list {
|
|
|
- .model-item {
|
|
|
- margin-bottom: 24px;
|
|
|
- padding: 16px;
|
|
|
- background: #fafafa;
|
|
|
- border-radius: 8px;
|
|
|
-
|
|
|
- .model-header {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- margin-bottom: 16px;
|
|
|
-
|
|
|
- .model-title {
|
|
|
- font-weight: 500;
|
|
|
- color: #1677ff;
|
|
|
- }
|
|
|
- }
|
|
|
- }
|
|
|
+const removeModel = (index) => {
|
|
|
+ if (modelList.value.length === 1) {
|
|
|
+ message.warning('至少保留一个模型配置');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ modelList.value.splice(index, 1);
|
|
|
+};
|
|
|
+
|
|
|
+const validateModel = (model) => {
|
|
|
+ if (!model.type) return false;
|
|
|
+ if (!model.key) return false;
|
|
|
+ if (model.type === 'ollama' && !model.endpoint) return false;
|
|
|
+ if (model.type === 'custom' && (!model.name || !model.endpoint)) return false;
|
|
|
+ return true;
|
|
|
+};
|
|
|
+
|
|
|
+const saveConfig = async () => {
|
|
|
+ const hasInvalidModel = modelList.value.some(model => !validateModel(model));
|
|
|
+ if (hasInvalidModel) {
|
|
|
+ message.error('请填写完整的模型信息');
|
|
|
+ return;
|
|
|
}
|
|
|
|
|
|
- .add-model-btn {
|
|
|
- margin-bottom: 24px;
|
|
|
+ saving.value = true;
|
|
|
+ try {
|
|
|
+ localStorage.setItem('modelConfig', JSON.stringify(modelList.value));
|
|
|
+ message.success('配置保存成功');
|
|
|
+ } catch (error) {
|
|
|
+ message.error('配置保存失败');
|
|
|
+ } finally {
|
|
|
+ saving.value = false;
|
|
|
}
|
|
|
+};
|
|
|
|
|
|
- .drawer-footer {
|
|
|
- position: absolute;
|
|
|
- bottom: 0;
|
|
|
- width: 100%;
|
|
|
- border-top: 1px solid #f0f0f0;
|
|
|
- padding: 16px 24px;
|
|
|
- text-align: right;
|
|
|
- left: 0;
|
|
|
- background: #fff;
|
|
|
+// 初始化时从localStorage加载配置
|
|
|
+const initConfig = () => {
|
|
|
+ const savedConfig = localStorage.getItem('modelConfig');
|
|
|
+ if (savedConfig) {
|
|
|
+ modelList.value = JSON.parse(savedConfig);
|
|
|
}
|
|
|
+};
|
|
|
+
|
|
|
+// 组件挂载时加载配置
|
|
|
+initConfig();
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.model-config {
|
|
|
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
|
+ background: #fff;
|
|
|
+}
|
|
|
+
|
|
|
+.config-header {
|
|
|
+ padding: 12px 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: background-color 0.2s;
|
|
|
+}
|
|
|
+
|
|
|
+.config-header:hover {
|
|
|
+ background: rgba(0, 0, 0, 0.02);
|
|
|
+}
|
|
|
+
|
|
|
+.config-icon {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.toggle-icon {
|
|
|
+ width: 12px;
|
|
|
+ height: 12px;
|
|
|
+ margin-left: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.config-panel {
|
|
|
+ padding: 12px 20px;
|
|
|
+ border-top: 1px solid rgba(0, 0, 0, 0.06);
|
|
|
+}
|
|
|
+
|
|
|
+.model-list {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.model-item {
|
|
|
+ margin-bottom: 16px;
|
|
|
+ padding: 12px;
|
|
|
+ background: #fafafa;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.model-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.model-title {
|
|
|
+ font-size: 13px;
|
|
|
+ color: rgba(0, 0, 0, 0.85);
|
|
|
+}
|
|
|
+
|
|
|
+.model-form {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-item {
|
|
|
+ margin-bottom: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.form-item label {
|
|
|
+ display: block;
|
|
|
+ font-size: 12px;
|
|
|
+ color: rgba(0, 0, 0, 0.45);
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.add-model-btn {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.action-buttons {
|
|
|
+ display: flex;
|
|
|
+ justify-content: flex-end;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.ant-select),
|
|
|
+:deep(.ant-input),
|
|
|
+:deep(.ant-input-password) {
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.ant-select-selector) {
|
|
|
+ border-radius: 4px !important;
|
|
|
}
|
|
|
|
|
|
-:deep(.ant-drawer-body) {
|
|
|
- padding: 24px;
|
|
|
- padding-bottom: 80px;
|
|
|
+:deep(.ant-input),
|
|
|
+:deep(.ant-input-password) {
|
|
|
+ border-radius: 4px;
|
|
|
}
|
|
|
</style>
|