index.vue 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368
  1. <template>
  2. <div class="model-config">
  3. <div class="config-header" @click="togglePanel">
  4. <img src="../../assets/svg/setting.png" alt="setting" class="config-icon" />
  5. <span>模型配置</span>
  6. <img
  7. :src="isPanelOpen ? upIcon : downIcon"
  8. alt="toggle"
  9. class="toggle-icon"
  10. />
  11. </div>
  12. <div v-show="isPanelOpen" class="config-panel">
  13. <div class="model-list">
  14. <div v-for="(model, index) in modelList" :key="index" class="model-item">
  15. <div class="model-header">
  16. <span class="model-title">模型 {{index + 1}}</span>
  17. <a-button type="link" danger size="small" @click="removeModel(index)">删除</a-button>
  18. </div>
  19. <div class="model-form">
  20. <div class="form-item">
  21. <label>选择模型</label>
  22. <a-select
  23. style="width: 200px;"
  24. v-model:value="model.type"
  25. placeholder="请选择模型"
  26. @change="(value) => handleModelChange(value, index)"
  27. >
  28. <a-select-opt-group v-for="group in modelGroups" :key="group.label" :label="group.label">
  29. <a-select-option
  30. v-for="option in group.options"
  31. :key="option.value"
  32. :value="option.value"
  33. >
  34. {{ option.label }}
  35. </a-select-option>
  36. </a-select-opt-group>
  37. </a-select>
  38. </div>
  39. <template v-if="model.type">
  40. <div class="form-item">
  41. <label>API Key</label>
  42. <a-input-password
  43. v-model:value="model.key"
  44. :placeholder="getKeyPlaceholder(model.type)"
  45. />
  46. </div>
  47. <template v-if="model.type === 'ollama'">
  48. <div class="form-item">
  49. <label>服务器地址</label>
  50. <a-input
  51. v-model:value="model.endpoint"
  52. placeholder="例如: http://localhost:11434"
  53. />
  54. </div>
  55. </template>
  56. <template v-if="model.type === 'custom'">
  57. <div class="form-item">
  58. <label>模型名称</label>
  59. <a-input
  60. v-model:value="model.name"
  61. placeholder="请输入自定义模型名称"
  62. />
  63. </div>
  64. <div class="form-item">
  65. <label>服务器地址</label>
  66. <a-input
  67. v-model:value="model.endpoint"
  68. placeholder="请输入API地址"
  69. />
  70. </div>
  71. </template>
  72. </template>
  73. </div>
  74. </div>
  75. </div>
  76. <a-button
  77. type="dashed"
  78. block
  79. class="add-model-btn"
  80. @click="addModel"
  81. >
  82. <PlusOutlined /> 添加模型
  83. </a-button>
  84. <div class="action-buttons">
  85. <a-button type="primary" :loading="saving" @click="saveConfig">
  86. 保存配置
  87. </a-button>
  88. </div>
  89. </div>
  90. </div>
  91. </template>
  92. <script setup>
  93. import { ref, reactive, onMounted } from 'vue';
  94. import { PlusOutlined } from '@ant-design/icons-vue';
  95. import { message } from 'ant-design-vue';
  96. import axios from "axios";
  97. import upIcon from '@/assets/svg/ups.svg';
  98. import downIcon from '@/assets/svg/down.svg';
  99. const MODEL_GROUPS = [
  100. {
  101. label: '主流模型',
  102. options: [
  103. { label: 'ChatGPT', value: 'chatgpt', endpoint: 'https://api.openai.com/v1' },
  104. { label: 'Claude', value: 'claude', endpoint: 'https://api.anthropic.com' },
  105. { label: 'GPT-4', value: 'gpt4', endpoint: 'https://api.openai.com/v1' }
  106. ]
  107. },
  108. {
  109. label: '开源模型',
  110. options: [
  111. { label: 'DeepSeek', value: 'deepseek', endpoint: 'https://api.deepseek.com/v1' },
  112. { label: 'Ollama', value: 'ollama', endpoint: 'http://localhost:11434' },
  113. { label: 'ChatGLM', value: 'chatglm', endpoint: 'https://api.zhipuai.cn/v1' }
  114. ]
  115. },
  116. {
  117. label: '国内模型',
  118. options: [
  119. { label: '阿里通义千问', value: 'qianwen', endpoint: 'https://dashscope.aliyuncs.com/api/v1' },
  120. { label: '百度文心一言', value: 'wenxin', endpoint: 'https://aip.baidubce.com/rpc/2.0/ai_custom/v1' },
  121. { label: '讯飞星火', value: 'spark', endpoint: 'https://spark-api.xf-yun.com/v1' }
  122. ]
  123. },
  124. {
  125. label: '其他',
  126. options: [
  127. { label: '自定义模型', value: 'custom', endpoint: '' }
  128. ]
  129. }
  130. ];
  131. const isPanelOpen = ref(false);
  132. const saving = ref(false);
  133. const loading = ref(false);
  134. const modelList = ref([
  135. { type: '', key: '', endpoint: '', name: '' }
  136. ]);
  137. const modelGroups = ref([]);
  138. // 获取模型列表
  139. const fetchModels = async () => {
  140. loading.value = true;
  141. try {
  142. const response = await axios.get(`${import.meta.env.VITE_BASE_AI_API}/api/models`);
  143. const data = response.data;
  144. console.log(data);
  145. if (data.code === 2000) {
  146. // 将API返回的模型数据转换为组件需要的格式
  147. const apiModels = data.data.models.map(model => ({
  148. label: model.name,
  149. value: model.type,
  150. /* endpoint: model.endpoint || '', */
  151. description: model.description || ''
  152. }));
  153. // 更新模型组
  154. modelGroups.value = [
  155. {
  156. label: 'API模型',
  157. options: apiModels
  158. },
  159. ...MODEL_GROUPS // 保留原有的模型组
  160. ];
  161. }
  162. } catch (error) {
  163. console.error('获取模型列表失败:', error);
  164. message.error('获取模型列表失败');
  165. } finally {
  166. loading.value = false;
  167. }
  168. };
  169. const getKeyPlaceholder = (type) => {
  170. const placeholders = {
  171. 'chatgpt': '请输入 OpenAI API Key',
  172. 'gpt4': '请输入 OpenAI API Key',
  173. 'claude': '请输入 Anthropic API Key',
  174. 'deepseek': '请输入 DeepSeek API Key',
  175. 'qianwen': '请输入阿里云 API Key',
  176. 'wenxin': '请输入百度 API Key',
  177. 'spark': '请输入讯飞 API Key',
  178. 'chatglm': '请输入智谱 API Key',
  179. 'ollama': '可选:请输入访问密钥',
  180. 'custom': '请输入 API Key'
  181. };
  182. return placeholders[type] || '请输入 API Key';
  183. };
  184. const handleModelChange = (value, index) => {
  185. const model = modelList.value[index];
  186. const selectedOption = modelGroups.value
  187. .flatMap(group => group.options)
  188. .find(option => option.value === value);
  189. if (selectedOption) {
  190. model.endpoint = selectedOption.endpoint;
  191. model.name = selectedOption.label;
  192. }
  193. };
  194. const togglePanel = () => {
  195. isPanelOpen.value = !isPanelOpen.value;
  196. };
  197. const addModel = () => {
  198. modelList.value.push({ type: '', key: '', endpoint: '', name: '' });
  199. };
  200. const removeModel = (index) => {
  201. if (modelList.value.length === 1) {
  202. message.warning('至少保留一个模型配置');
  203. return;
  204. }
  205. modelList.value.splice(index, 1);
  206. };
  207. const validateModel = (model) => {
  208. if (!model.type) return false;
  209. if (!model.key) return false;
  210. if (model.type === 'ollama' && !model.endpoint) return false;
  211. if (model.type === 'custom' && (!model.name || !model.endpoint)) return false;
  212. return true;
  213. };
  214. const saveConfig = async () => {
  215. const hasInvalidModel = modelList.value.some(model => !validateModel(model));
  216. if (hasInvalidModel) {
  217. message.error('请填写完整的模型信息');
  218. return;
  219. }
  220. saving.value = true;
  221. try {
  222. localStorage.setItem('modelConfig', JSON.stringify(modelList.value));
  223. message.success('配置保存成功');
  224. } catch (error) {
  225. message.error('配置保存失败');
  226. } finally {
  227. saving.value = false;
  228. }
  229. };
  230. // 初始化时从localStorage加载配置
  231. const initConfig = () => {
  232. const savedConfig = localStorage.getItem('modelConfig');
  233. if (savedConfig) {
  234. modelList.value = JSON.parse(savedConfig);
  235. }
  236. };
  237. // 组件挂载时加载配置和获取模型列表
  238. onMounted(async () => {
  239. initConfig();
  240. await fetchModels();
  241. });
  242. </script>
  243. <style scoped>
  244. .model-config {
  245. border-top: 1px solid rgba(0, 0, 0, 0.06);
  246. background: #fff;
  247. }
  248. .config-header {
  249. padding: 12px 20px;
  250. display: flex;
  251. align-items: center;
  252. gap: 8px;
  253. cursor: pointer;
  254. transition: background-color 0.2s;
  255. }
  256. .config-header:hover {
  257. background: rgba(0, 0, 0, 0.02);
  258. }
  259. .config-icon {
  260. width: 16px;
  261. height: 16px;
  262. }
  263. .toggle-icon {
  264. width: 12px;
  265. height: 12px;
  266. margin-left: auto;
  267. }
  268. .config-panel {
  269. padding: 12px 20px;
  270. border-top: 1px solid rgba(0, 0, 0, 0.06);
  271. }
  272. .model-list {
  273. margin-bottom: 16px;
  274. }
  275. .model-item {
  276. margin-bottom: 16px;
  277. padding: 12px;
  278. background: #fafafa;
  279. border-radius: 4px;
  280. }
  281. .model-header {
  282. display: flex;
  283. justify-content: space-between;
  284. align-items: center;
  285. margin-bottom: 12px;
  286. }
  287. .model-title {
  288. font-size: 13px;
  289. color: rgba(0, 0, 0, 0.85);
  290. }
  291. .model-form {
  292. display: flex;
  293. flex-direction: column;
  294. gap: 12px;
  295. }
  296. .form-item {
  297. margin-bottom: 12px;
  298. }
  299. .form-item label {
  300. display: block;
  301. font-size: 12px;
  302. color: rgba(0, 0, 0, 0.45);
  303. margin-bottom: 8px;
  304. }
  305. .add-model-btn {
  306. margin-bottom: 16px;
  307. }
  308. .action-buttons {
  309. display: flex;
  310. justify-content: flex-end;
  311. }
  312. :deep(.ant-select),
  313. :deep(.ant-input),
  314. :deep(.ant-input-password) {
  315. font-size: 14px;
  316. }
  317. :deep(.ant-select-selector) {
  318. border-radius: 4px !important;
  319. }
  320. :deep(.ant-input),
  321. :deep(.ant-input-password) {
  322. border-radius: 4px;
  323. }
  324. </style>