| | |
| | | font-size: 12px; |
| | | opacity: 0.9; |
| | | } |
| | | .hero-actions { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | flex-wrap: wrap; |
| | | justify-content: flex-end; |
| | | } |
| | | .summary-grid { |
| | | margin-top: 10px; |
| | | display: grid; |
| | |
| | | <div class="sub">支持多API、多模型、多Key,额度耗尽或故障自动切换</div> |
| | | </div> |
| | | </div> |
| | | <div> |
| | | <div class="hero-actions"> |
| | | <el-button type="primary" size="mini" @click="addRoute">新增路由</el-button> |
| | | <el-button size="mini" @click="exportRoutes">导出JSON</el-button> |
| | | <el-button size="mini" @click="triggerImport">导入JSON</el-button> |
| | | <el-button size="mini" @click="loadRoutes">刷新</el-button> |
| | | <el-button size="mini" @click="openLogDialog">调用日志</el-button> |
| | | </div> |
| | |
| | | </div> |
| | | </div> |
| | | </div> |
| | | <input ref="importFileInput" type="file" accept="application/json,.json" style="display:none;" @change="handleImportFileChange" /> |
| | | |
| | | <div class="route-board" v-loading="loading"> |
| | | <div v-if="!routes || routes.length === 0" class="empty-shell"> |
| | |
| | | authHeaders: function() { |
| | | return { 'token': localStorage.getItem('token') }; |
| | | }, |
| | | exportRoutes: function() { |
| | | var self = this; |
| | | fetch(baseUrl + '/ai/llm/config/export/auth', { headers: self.authHeaders() }) |
| | | .then(function(r){ return r.json(); }) |
| | | .then(function(res){ |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '导出失败'); |
| | | return; |
| | | } |
| | | var payload = res.data || {}; |
| | | var text = JSON.stringify(payload, null, 2); |
| | | var name = 'llm_routes_' + self.buildExportTimestamp() + '.json'; |
| | | var blob = new Blob([text], { type: 'application/json;charset=utf-8' }); |
| | | var a = document.createElement('a'); |
| | | a.href = URL.createObjectURL(blob); |
| | | a.download = name; |
| | | document.body.appendChild(a); |
| | | a.click(); |
| | | setTimeout(function() { |
| | | URL.revokeObjectURL(a.href); |
| | | document.body.removeChild(a); |
| | | }, 0); |
| | | self.$message.success('导出成功'); |
| | | }) |
| | | .catch(function(){ |
| | | self.$message.error('导出失败'); |
| | | }); |
| | | }, |
| | | buildExportTimestamp: function() { |
| | | var d = new Date(); |
| | | var pad = function(n) { return n < 10 ? ('0' + n) : String(n); }; |
| | | return d.getFullYear() |
| | | + pad(d.getMonth() + 1) |
| | | + pad(d.getDate()) |
| | | + '_' |
| | | + pad(d.getHours()) |
| | | + pad(d.getMinutes()) |
| | | + pad(d.getSeconds()); |
| | | }, |
| | | triggerImport: function() { |
| | | var input = this.$refs.importFileInput; |
| | | if (!input) return; |
| | | input.value = ''; |
| | | input.click(); |
| | | }, |
| | | handleImportFileChange: function(evt) { |
| | | var self = this; |
| | | var files = evt && evt.target && evt.target.files ? evt.target.files : null; |
| | | var file = files && files.length > 0 ? files[0] : null; |
| | | if (!file) return; |
| | | var reader = new FileReader(); |
| | | reader.onload = function(e) { |
| | | var text = e && e.target ? e.target.result : ''; |
| | | var parsed; |
| | | try { |
| | | parsed = JSON.parse(text || '{}'); |
| | | } catch (err) { |
| | | self.$message.error('JSON 格式不正确'); |
| | | return; |
| | | } |
| | | var routes = self.extractImportRoutes(parsed); |
| | | if (!routes || routes.length === 0) { |
| | | self.$message.warning('未找到可导入的 routes'); |
| | | return; |
| | | } |
| | | self.$confirm( |
| | | '请选择导入方式:覆盖导入会先清空现有路由;点击“合并导入”则按ID更新或新增。', |
| | | '导入确认', |
| | | { |
| | | type: 'warning', |
| | | distinguishCancelAndClose: true, |
| | | confirmButtonText: '覆盖导入', |
| | | cancelButtonText: '合并导入', |
| | | closeOnClickModal: false |
| | | } |
| | | ).then(function() { |
| | | self.doImportRoutes(routes, true); |
| | | }).catch(function(action) { |
| | | if (action === 'cancel') { |
| | | self.doImportRoutes(routes, false); |
| | | } |
| | | }); |
| | | }; |
| | | reader.onerror = function() { |
| | | self.$message.error('读取文件失败'); |
| | | }; |
| | | reader.readAsText(file, 'utf-8'); |
| | | }, |
| | | extractImportRoutes: function(parsed) { |
| | | if (Array.isArray(parsed)) return parsed; |
| | | if (!parsed || typeof parsed !== 'object') return []; |
| | | if (Array.isArray(parsed.routes)) return parsed.routes; |
| | | if (parsed.data && Array.isArray(parsed.data.routes)) return parsed.data.routes; |
| | | if (Array.isArray(parsed.data)) return parsed.data; |
| | | return []; |
| | | }, |
| | | doImportRoutes: function(routes, replace) { |
| | | var self = this; |
| | | fetch(baseUrl + '/ai/llm/config/import/auth', { |
| | | method: 'POST', |
| | | headers: Object.assign({ 'Content-Type': 'application/json' }, self.authHeaders()), |
| | | body: JSON.stringify({ |
| | | replace: replace === true, |
| | | routes: routes |
| | | }) |
| | | }) |
| | | .then(function(r){ return r.json(); }) |
| | | .then(function(res){ |
| | | if (!res || res.code !== 200) { |
| | | self.$message.error((res && res.msg) ? res.msg : '导入失败'); |
| | | return; |
| | | } |
| | | var d = res.data || {}; |
| | | var msg = '导入完成:新增 ' + (d.inserted || 0) |
| | | + ',更新 ' + (d.updated || 0) |
| | | + ',跳过 ' + (d.skipped || 0); |
| | | if (d.errorCount && d.errorCount > 0) { |
| | | msg += ',异常 ' + d.errorCount; |
| | | } |
| | | self.$message.success(msg); |
| | | if (Array.isArray(d.errors) && d.errors.length > 0) { |
| | | self.$alert(d.errors.join('\n'), '导入异常明细(最多20条)', { |
| | | confirmButtonText: '确定', |
| | | type: 'warning' |
| | | }); |
| | | } |
| | | self.loadRoutes(); |
| | | }) |
| | | .catch(function(){ |
| | | self.$message.error('导入失败'); |
| | | }); |
| | | }, |
| | | handleRouteCommand: function(command, route, idx) { |
| | | if (command === 'test') return this.testRoute(route); |
| | | if (command === 'save') return this.saveRoute(route); |