|
@@ -1,16 +1,331 @@
|
|
<template>
|
|
<template>
|
|
<div class="model-config">
|
|
<div class="model-config">
|
|
- <h3>模型配置</h3>
|
|
|
|
- <!-- 在这里添加模型配置的具体内容 -->
|
|
|
|
|
|
+ <div class="config-trigger" @click="showDrawer">
|
|
|
|
+ <SettingOutlined class="trigger-icon" />
|
|
|
|
+ <span>模型配置</span>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <a-drawer
|
|
|
|
+ title="AI模型配置"
|
|
|
|
+ placement="left"
|
|
|
|
+ :width="400"
|
|
|
|
+ :visible="visible"
|
|
|
|
+ @close="closeDrawer"
|
|
|
|
+ class="model-drawer"
|
|
|
|
+ >
|
|
|
|
+ <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>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <a-form layout="vertical">
|
|
|
|
+ <a-form-item label="选择模型">
|
|
|
|
+ <a-select
|
|
|
|
+ v-model:value="model.type"
|
|
|
|
+ placeholder="请选择模型"
|
|
|
|
+ @change="(value) => handleModelChange(value, index)"
|
|
|
|
+ >
|
|
|
|
+ <a-select-opt-group v-for="group in modelGroups" :key="group.label" :label="group.label">
|
|
|
|
+ <a-select-option
|
|
|
|
+ v-for="option in group.options"
|
|
|
|
+ :key="option.value"
|
|
|
|
+ :value="option.value"
|
|
|
|
+ >
|
|
|
|
+ {{ option.label }}
|
|
|
|
+ </a-select-option>
|
|
|
|
+ </a-select-opt-group>
|
|
|
|
+ </a-select>
|
|
|
|
+ </a-form-item>
|
|
|
|
+
|
|
|
|
+ <template v-if="model.type">
|
|
|
|
+ <a-form-item label="API Key">
|
|
|
|
+ <a-input-password
|
|
|
|
+ v-model:value="model.key"
|
|
|
|
+ :placeholder="getKeyPlaceholder(model.type)"
|
|
|
|
+ />
|
|
|
|
+ </a-form-item>
|
|
|
|
+
|
|
|
|
+ <template v-if="model.type === 'ollama'">
|
|
|
|
+ <a-form-item label="服务器地址">
|
|
|
|
+ <a-input
|
|
|
|
+ v-model:value="model.endpoint"
|
|
|
|
+ placeholder="例如: http://localhost:11434"
|
|
|
|
+ />
|
|
|
|
+ </a-form-item>
|
|
|
|
+ </template>
|
|
|
|
+
|
|
|
|
+ <template v-if="model.type === 'custom'">
|
|
|
|
+ <a-form-item label="模型名称">
|
|
|
|
+ <a-input
|
|
|
|
+ v-model:value="model.name"
|
|
|
|
+ placeholder="请输入自定义模型名称"
|
|
|
|
+ />
|
|
|
|
+ </a-form-item>
|
|
|
|
+ <a-form-item label="服务器地址">
|
|
|
|
+ <a-input
|
|
|
|
+ v-model:value="model.endpoint"
|
|
|
|
+ placeholder="请输入API地址"
|
|
|
|
+ />
|
|
|
|
+ </a-form-item>
|
|
|
|
+ </template>
|
|
|
|
+ </template>
|
|
|
|
+ </a-form>
|
|
|
|
+ </div>
|
|
|
|
+ </div>
|
|
|
|
+
|
|
|
|
+ <a-button
|
|
|
|
+ type="dashed"
|
|
|
|
+ block
|
|
|
|
+ class="add-model-btn"
|
|
|
|
+ @click="addModel"
|
|
|
|
+ >
|
|
|
|
+ <PlusOutlined /> 添加模型
|
|
|
|
+ </a-button>
|
|
|
|
+
|
|
|
|
+ <div class="drawer-footer">
|
|
|
|
+ <a-button style="margin-right: 8px" @click="closeDrawer">取消</a-button>
|
|
|
|
+ <a-button type="primary" :loading="saving" @click="saveConfig">
|
|
|
|
+ 保存配置
|
|
|
|
+ </a-button>
|
|
|
|
+ </div>
|
|
|
|
+ </a-drawer>
|
|
</div>
|
|
</div>
|
|
</template>
|
|
</template>
|
|
|
|
|
|
-<script setup>
|
|
|
|
-// 添加必要的逻辑
|
|
|
|
|
|
+<script>
|
|
|
|
+import { defineComponent, ref, reactive } from 'vue';
|
|
|
|
+import { SettingOutlined, PlusOutlined } from '@ant-design/icons-vue';
|
|
|
|
+import { message } from 'ant-design-vue';
|
|
|
|
+
|
|
|
|
+const MODEL_GROUPS = [
|
|
|
|
+ {
|
|
|
|
+ label: '主流模型',
|
|
|
|
+ options: [
|
|
|
|
+ { label: 'ChatGPT', value: 'chatgpt', endpoint: 'https://api.openai.com/v1' },
|
|
|
|
+ { label: 'Claude', value: 'claude', endpoint: 'https://api.anthropic.com' },
|
|
|
|
+ { label: 'GPT-4', value: 'gpt4', endpoint: 'https://api.openai.com/v1' }
|
|
|
|
+ ]
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ label: '开源模型',
|
|
|
|
+ options: [
|
|
|
|
+ { label: 'DeepSeek', value: 'deepseek', endpoint: 'https://api.deepseek.com/v1' },
|
|
|
|
+ { label: 'Ollama', value: 'ollama', endpoint: 'http://localhost:11434' },
|
|
|
|
+ { label: 'ChatGLM', value: 'chatglm', endpoint: 'https://api.zhipuai.cn/v1' }
|
|
|
|
+ ]
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ label: '国内模型',
|
|
|
|
+ options: [
|
|
|
|
+ { label: '阿里通义千问', value: 'qianwen', endpoint: 'https://dashscope.aliyuncs.com/api/v1' },
|
|
|
|
+ { label: '百度文心一言', value: 'wenxin', endpoint: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1' },
|
|
|
|
+ { label: '讯飞星火', value: 'spark', endpoint: 'https://spark-api.xf-yun.com/v1' }
|
|
|
|
+ ]
|
|
|
|
+ },
|
|
|
|
+ {
|
|
|
|
+ label: '其他',
|
|
|
|
+ options: [
|
|
|
|
+ { label: '自定义模型', value: 'custom', endpoint: '' }
|
|
|
|
+ ]
|
|
|
|
+ }
|
|
|
|
+];
|
|
|
|
+
|
|
|
|
+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
|
|
|
|
+ };
|
|
|
|
+ }
|
|
|
|
+});
|
|
</script>
|
|
</script>
|
|
|
|
|
|
-<style scoped>
|
|
|
|
|
|
+<style lang="less" scoped>
|
|
.model-config {
|
|
.model-config {
|
|
- padding: 16px;
|
|
|
|
|
|
+ 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;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+.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;
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .add-model-btn {
|
|
|
|
+ margin-bottom: 24px;
|
|
|
|
+ }
|
|
|
|
+
|
|
|
|
+ .drawer-footer {
|
|
|
|
+ position: absolute;
|
|
|
|
+ bottom: 0;
|
|
|
|
+ width: 100%;
|
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
|
+ padding: 16px 24px;
|
|
|
|
+ text-align: right;
|
|
|
|
+ left: 0;
|
|
|
|
+ background: #fff;
|
|
|
|
+ }
|
|
|
|
+}
|
|
|
|
+
|
|
|
|
+:deep(.ant-drawer-body) {
|
|
|
|
+ padding: 24px;
|
|
|
|
+ padding-bottom: 80px;
|
|
}
|
|
}
|
|
</style>
|
|
</style>
|