improve(channel):优化渠道编辑功能

This commit is contained in:
qian committed 2026-08-01 23:19:37 +08:00
1 parent 4bf835394d
commit 657c452ea1
2 files changed
+82 -52

No files matched your search

+81 -22
View File
@@ -1,10 +1,11 @@
<script setup lang="ts">
import { ref, reactive, computed, onMounted, h, nextTick, onBeforeUnmount } from 'vue'
import { useRoute, useRouter } from 'vue-router'
import { Tag, Button, Popconfirm, Message, RadioGroup, Radio, InputNumber } from '@arco-design/web-vue'
import { Tag, Button, Popconfirm, Message, RadioGroup, Radio, InputNumber, Input } from '@arco-design/web-vue'
import type { TableColumnData } from '@arco-design/web-vue'
import PageHeader from '@/components/PageHeader.vue'
import request from '@/utils/request'
import { providerTypeOptions, filterProviderOption } from '@/constants/channel'
import * as echarts from 'echarts'
const route = useRoute()
@@ -52,6 +53,7 @@ function goBack() {
const showEditModal = ref(false)
const editLoading = ref(false)
const editForm = reactive({
type: 0,
name: '',
base_url: '',
priority: 0,
@@ -71,6 +73,7 @@ const editForm = reactive({
function openEditModal() {
if (!detail.value) return
Object.assign(editForm, {
type: detail.value.type ?? 0,
name: detail.value.name || '',
base_url: detail.value.base_url || '',
priority: detail.value.priority || 0,
@@ -126,7 +129,17 @@ const abilitiesData = ref<any[]>([])
const abilityColumns: TableColumnData[] = [
{ title: 'ID', dataIndex: 'id', width: 60 },
{ title: '平台模型名', dataIndex: 'model_name', width: 200 },
{ title: '上游模型名', dataIndex: 'upstream_model', width: 200 },
{
title: '上游模型名', dataIndex: 'upstream_model', width: 200,
render({ record }) {
return h(Input, {
modelValue: record.upstream_model || '',
size: 'mini',
placeholder: '留空则同名',
onChange: (v: string) => handleUpstreamModelChange(record, v),
})
},
},
{
title: '成本比例', dataIndex: 'cost_ratio', width: 130,
render({ record }) {
@@ -191,18 +204,28 @@ async function handleToggleAbility(ab: any) {
} catch { /* error handled by interceptor */ }
}
// 成本比例内联编辑:600ms 防抖后整表提交
let costRatioSaveTimer: any = null
// 能力表内联编辑(成本比例 / 上游模型名):任意字段变更后统一 600ms 防抖整表提交,
// 连续改多字段只发一次请求。提交体 abilityPayload 已包含 upstream_model。
let abilitySaveTimer: any = null
function scheduleAbilitySave(successMsg: string) {
clearTimeout(abilitySaveTimer)
abilitySaveTimer = setTimeout(async () => {
try {
await request.put(`/admin/channels/${channelId}/abilities`, abilityPayload(abilitiesData.value))
Message.success(successMsg)
} catch { /* error handled by interceptor */ }
}, 600)
}
function handleCostRatioChange(record: any, value: number) {
if (!value || value <= 0) return
record.cost_ratio = value
clearTimeout(costRatioSaveTimer)
costRatioSaveTimer = setTimeout(async () => {
try {
await request.put(`/admin/channels/${channelId}/abilities`, abilityPayload(abilitiesData.value))
Message.success('成本比例已更新')
} catch { /* error handled by interceptor */ }
}, 600)
scheduleAbilitySave('成本比例已更新')
}
function handleUpstreamModelChange(record: any, value: string) {
record.upstream_model = value
scheduleAbilitySave('上游模型名已更新')
}
// CSV 批量导入成本比例:每行 "model_name,cost_ratio"(首行可为表头)
@@ -407,6 +430,7 @@ function handleTrendHoursChange(val: string | number | boolean) {
onBeforeUnmount(() => {
window.removeEventListener('resize', onWindowResize)
if (resizeTimer) clearTimeout(resizeTimer)
if (abilitySaveTimer) clearTimeout(abilitySaveTimer)
if (trendChart) { trendChart.dispose(); trendChart = null }
})
@@ -699,37 +723,61 @@ function formatHeaders(headers: Record<string, string>): string {
</ASpin>
<!-- Edit Channel Modal -->
<AModal v-model:visible="showEditModal" title="编辑渠道" :width="600" :mask-closable="false" :on-before-ok="handleEditSubmit" :ok-loading="editLoading">
<AModal v-model:visible="showEditModal" title="编辑渠道" :width="680" :mask-closable="false" :on-before-ok="handleEditSubmit" :ok-loading="editLoading">
<AForm :model="editForm" :auto-label-width="true" layout="vertical">
<!-- 基础信息 -->
<div class="form-group-title">基础信息</div>
<ARow :gutter="16">
<ACol :span="8">
<ACol :span="12">
<AFormItem label="供应商类型" required>
<ASelect
v-model="editForm.type"
:options="providerTypeOptions"
placeholder="搜索或选择供应商"
allow-search
:filter-option="filterProviderOption"
/>
</AFormItem>
</ACol>
<ACol :span="12">
<AFormItem label="渠道名称"><AInput v-model="editForm.name" /></AFormItem>
</ACol>
<ACol :span="16">
<AFormItem label="Base URL"><AInput v-model="editForm.base_url" /></AFormItem>
</ACol>
</ARow>
<AFormItem label="Base URL">
<AInput v-model="editForm.base_url" placeholder="留空使用供应商默认地址" />
<template #extra>
<span class="field-help">ⓘ 修改供应商类型后,请同步检查 Base URL 与「模型能力」配置</span>
</template>
</AFormItem>
<!-- 调度与状态 -->
<div class="form-group-title">调度与状态</div>
<ARow :gutter="16">
<ACol :span="6">
<ACol :span="8">
<AFormItem label="调度层级">
<ASelect v-model="editForm.tier" :options="tierOptions" />
<template #extra><span class="field-help">三档固定层级,替代旧版数值优先级</span></template>
</AFormItem>
</ACol>
<ACol :span="6">
<ACol :span="8">
<AFormItem label="权重">
<AInputNumber v-model="editForm.weight" :min="0" :max="100" class="w-full" />
<template #extra><span class="field-help">同层级内按权重比例分配</span></template>
</AFormItem>
</ACol>
<ACol :span="6">
<AFormItem label="测试模型"><AInput v-model="editForm.test_model" /></AFormItem>
</ACol>
<ACol :span="6">
<ACol :span="8">
<AFormItem label="状态">
<ASelect v-model="editForm.status" :options="[{ label: '启用', value: 'active' }, { label: '禁用', value: 'disabled' }, { label: '测试中', value: 'testing' }]" />
</AFormItem>
</ACol>
</ARow>
<!-- 功能与测试 -->
<div class="form-group-title">功能与测试</div>
<ARow :gutter="16">
<ACol :span="12">
<AFormItem label="测试模型"><AInput v-model="editForm.test_model" /></AFormItem>
</ACol>
<ACol :span="6">
<AFormItem label="使用代理"><ASwitch v-model="editForm.use_proxy" /></AFormItem>
</ACol>
@@ -740,7 +788,9 @@ function formatHeaders(headers: Record<string, string>): string {
</AFormItem>
</ACol>
</ARow>
<AFormItem label="备注"><AInput v-model="editForm.remark" type="textarea" :auto-size="{ minRows: 2, maxRows: 4 }" /></AFormItem>
<ACollapse :bordered="false">
<ACollapseItem header="VIP 设置" key="vip" :header-style="{ fontSize: '13px' }">
<ARow :gutter="16">
@@ -819,4 +869,13 @@ function formatHeaders(headers: Record<string, string>): string {
.test-debug-value { font-size: 12px; color: var(--color-text-2); word-break: break-all; background: var(--color-fill-1); padding: 2px 6px; border-radius: 3px; }
.test-debug-code { font-size: 12px; background: var(--color-fill-1); padding: 6px 8px; border-radius: 4px; max-height: 150px; overflow: auto; color: var(--color-text-3); white-space: pre-wrap; word-break: break-all; margin: 0; flex: 1; }
.field-help { color: var(--color-text-3); font-size: 12px; }
.form-group-title {
font-size: 13px;
font-weight: 600;
color: var(--color-text-2);
margin: 12px 0 12px;
padding-left: 8px;
border-left: 3px solid #165dff;
line-height: 1.2;
}
</style>
+1 -30
View File
@@ -8,40 +8,11 @@ import PageHeader from '@/components/PageHeader.vue'
import request from '@/utils/request'
import { useExport } from '@/composables/useExport'
import { hasPermission } from '@/utils/permission'
import { providerTypeOptions, providerTypeName, filterProviderOption } from '@/constants/channel'
const router = useRouter()
const message = Message
// === Provider types ===
const providerTypeOptions = [
{ label: 'OpenAI', value: 1 }, { label: 'Claude (Anthropic)', value: 2 },
{ label: 'Gemini (Google)', value: 3 }, { label: 'Ali (百炼)', value: 4 },
{ label: 'Tencent (混元)', value: 6 },
{ label: 'Zhipu (智谱)', value: 7 }, { label: 'DeepSeek', value: 8 },
{ label: 'Moonshot', value: 9 }, { label: 'Volcengine (火山)', value: 10 },
{ label: 'AWS Bedrock', value: 11 }, { label: 'Azure OpenAI', value: 12 },
{ label: 'Vertex AI', value: 13 },
{ label: 'Mistral', value: 15 }, { label: 'xAI (Grok)', value: 16 },
{ label: '360 智脑', value: 17 }, { label: 'Lingyi (零一万物)', value: 18 },
{ label: 'Baidu V2', value: 19 }, { label: 'Cloudflare Workers AI', value: 20 },
{ label: 'Ollama', value: 22 },
{ label: 'SiliconFlow (硅基流动)', value: 25 }, { label: 'Xunfei (讯飞)', value: 26 },
{ label: 'OpenRouter', value: 27 }, { label: 'XInference', value: 28 },
{ label: 'MiniMax', value: 29 }, { label: 'Submodel', value: 30 },
{ label: 'Coze (扣子)', value: 32 },
{ label: 'Dify', value: 33 }, { label: 'Jimeng (即梦)', value: 34 },
{ label: 'Codex', value: 35 },
{ label: 'New API', value: 41 }, { label: 'Sub2API', value: 42 },
]
const providerTypeName: Record<number, string> = {}
providerTypeOptions.forEach(o => { providerTypeName[o.value] = o.label })
function filterProviderOption(inputValue: string, option: { label: string; value: number }) {
const input = inputValue.toLowerCase()
return option.label.toLowerCase().includes(input)
}
const providerTagColor: Record<number, string> = {
1: 'green', 2: 'arcoblue', 3: 'orangered', 8: 'green', 15: 'arcoblue',
}