improve(channel):调整渠道编辑表单

This commit is contained in:
qian committed 2026-08-03 21:38:34 +08:00
1 parent a9f5e08889
commit 07369f2f30
4 files changed
+102 -50

No files matched your search

+4
View File
@@ -30,6 +30,7 @@ type ChannelItem struct {
Status string `json:"status"`
Priority int `json:"priority"`
Weight int `json:"weight"`
MaxConcurrency int `json:"max_concurrency"`
Tier string `json:"tier"`
StrictCapacity bool `json:"strict_capacity"`
TestModel string `json:"test_model"`
@@ -52,6 +53,7 @@ type ChannelCreateReq struct {
ApiKey string `json:"api_key" v:"required#请输入 API Key" dc:"API Key"`
Priority int `json:"priority" d:"0" dc:"优先级"`
Weight int `json:"weight" d:"100" v:"between:0,100" dc:"权重"`
MaxConcurrency int `json:"max_concurrency" d:"100" v:"min:0" dc:"最大并发请求数(0=按上游 429 水位自动估算)"`
Tier string `json:"tier" d:"primary" v:"in:primary,secondary,reserve" dc:"调度层级:primary=首选 secondary=备用 reserve=保底"`
StrictCapacity bool `json:"strict_capacity" d:"false" dc:"严格容量:Redis 故障时实例级保守限额(fail-closed),用于高成本渠道"`
TestModel string `json:"test_model" dc:"测试模型名"`
@@ -78,6 +80,7 @@ type ChannelUpdateReq struct {
ApiKey *string `json:"api_key" dc:"更新 API Key(留空不更新)"`
Priority int `json:"priority" dc:"优先级"`
Weight int `json:"weight" dc:"权重"`
MaxConcurrency *int `json:"max_concurrency" v:"min:0" dc:"最大并发请求数(0=按上游 429 水位自动估算,留空不更新)"`
TestModel string `json:"test_model" dc:"测试模型名"`
Remark string `json:"remark" dc:"备注"`
Status string `json:"status" v:"in:active,disabled,testing" dc:"状态"`
@@ -112,6 +115,7 @@ type ChannelDetailRes struct {
Status string `json:"status"`
Priority int `json:"priority"`
Weight int `json:"weight"`
MaxConcurrency int `json:"max_concurrency"`
Tier string `json:"tier"`
StrictCapacity bool `json:"strict_capacity"`
TestModel string `json:"test_model"`
+13 -3
View File
@@ -68,6 +68,7 @@ func (s *sAdmin) ListChannels(ctx context.Context, req *v1.ChannelListReq) (*v1.
Status string `json:"status"`
Priority int `json:"priority"`
Weight int `json:"weight"`
MaxConcurrency int `json:"max_concurrency"`
Tier string `json:"tier"`
StrictCapacity bool `json:"strict_capacity"`
TestModel string `json:"test_model"`
@@ -81,7 +82,7 @@ func (s *sAdmin) ListChannels(ctx context.Context, req *v1.ChannelListReq) (*v1.
Settings string `json:"settings"`
}
err := query.Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, h.health_score, chn_channels.settings").
err := query.Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.max_concurrency, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, h.health_score, chn_channels.settings").
// 列表默认按 id 倒序:新建渠道排在最前,便于运营查看最新配置。
// 注意分页场景下排序必须在 DB 层完成,前端只在当前页排序会导致跨页错乱。
OrderDesc("chn_channels.id").
@@ -109,6 +110,7 @@ func (s *sAdmin) ListChannels(ctx context.Context, req *v1.ChannelListReq) (*v1.
Status: ch.Status,
Priority: ch.Priority,
Weight: ch.Weight,
MaxConcurrency: ch.MaxConcurrency,
Tier: ch.Tier,
StrictCapacity: ch.StrictCapacity,
TestModel: ch.TestModel,
@@ -141,6 +143,7 @@ func (s *sAdmin) CloneChannel(ctx context.Context, req *v1.ChannelCloneReq) (*v1
BaseURL string `json:"base_url"`
Priority int `json:"priority"`
Weight int `json:"weight"`
MaxConcurrency int `json:"max_concurrency"`
TestModel string `json:"test_model"`
Remark string `json:"remark"`
Settings string `json:"settings"`
@@ -150,7 +153,7 @@ func (s *sAdmin) CloneChannel(ctx context.Context, req *v1.ChannelCloneReq) (*v1
BorrowingCooldownSeconds int `json:"borrowing_cooldown_seconds"`
}
err := dao.ChnChannels.Ctx(ctx).
Fields("id, name, type, base_url, priority, weight, test_model, remark, settings, is_vip, sharing_threshold, preemption_threshold, borrowing_cooldown_seconds").
Fields("id, name, type, base_url, priority, weight, max_concurrency, test_model, remark, settings, is_vip, sharing_threshold, preemption_threshold, borrowing_cooldown_seconds").
Where("id", req.ID).
Scan(&src)
if err != nil {
@@ -184,6 +187,7 @@ func (s *sAdmin) CloneChannel(ctx context.Context, req *v1.ChannelCloneReq) (*v1
Status: "active",
Priority: src.Priority,
Weight: src.Weight,
MaxConcurrency: src.MaxConcurrency,
TestModel: src.TestModel,
Remark: src.Remark,
Settings: src.Settings,
@@ -265,6 +269,7 @@ func (s *sAdmin) CreateChannel(ctx context.Context, req *v1.ChannelCreateReq) (*
Status: "active",
Priority: req.Priority,
Weight: req.Weight,
MaxConcurrency: req.MaxConcurrency,
Tier: tier,
StrictCapacity: req.StrictCapacity,
TestModel: req.TestModel,
@@ -317,6 +322,9 @@ func (s *sAdmin) UpdateChannel(ctx context.Context, req *v1.ChannelUpdateReq) (*
}
data.Priority = req.Priority
data.Weight = req.Weight
if req.MaxConcurrency != nil {
data.MaxConcurrency = *req.MaxConcurrency
}
if req.TestModel != "" {
data.TestModel = req.TestModel
}
@@ -426,6 +434,7 @@ func (s *sAdmin) GetChannelDetail(ctx context.Context, req *v1.ChannelDetailReq)
Status string `json:"status"`
Priority int `json:"priority"`
Weight int `json:"weight"`
MaxConcurrency int `json:"max_concurrency"`
Tier string `json:"tier"`
StrictCapacity bool `json:"strict_capacity"`
TestModel string `json:"test_model"`
@@ -442,7 +451,7 @@ func (s *sAdmin) GetChannelDetail(ctx context.Context, req *v1.ChannelDetailReq)
err := dao.ChnChannels.Ctx(ctx).
LeftJoin("chn_health_scores h ON chn_channels.id = h.channel_id").
Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.settings, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, chn_channels.updated_at, h.health_score").
Fields("chn_channels.id, chn_channels.name, chn_channels.type, chn_channels.base_url, chn_channels.status, chn_channels.priority, chn_channels.weight, chn_channels.max_concurrency, chn_channels.tier, chn_channels.strict_capacity, chn_channels.test_model, chn_channels.remark, chn_channels.is_vip, chn_channels.settings, chn_channels.sharing_threshold, chn_channels.preemption_threshold, chn_channels.borrowing_cooldown_seconds, chn_channels.created_at, chn_channels.updated_at, h.health_score").
Where("chn_channels.id", req.ID).
Scan(&ch)
if err != nil {
@@ -486,6 +495,7 @@ func (s *sAdmin) GetChannelDetail(ctx context.Context, req *v1.ChannelDetailReq)
Status: ch.Status,
Priority: ch.Priority,
Weight: ch.Weight,
MaxConcurrency: ch.MaxConcurrency,
Tier: ch.Tier,
StrictCapacity: ch.StrictCapacity,
TestModel: ch.TestModel,
+38 -21
View File
@@ -58,6 +58,7 @@ const editForm = reactive({
base_url: '',
priority: 0,
weight: 100,
max_concurrency: 100,
tier: 'primary',
strict_capacity: false,
test_model: '',
@@ -78,6 +79,7 @@ function openEditModal() {
base_url: detail.value.base_url || '',
priority: detail.value.priority || 0,
weight: detail.value.weight || 100,
max_concurrency: detail.value.max_concurrency ?? 100,
tier: detail.value.tier || 'primary',
strict_capacity: detail.value.strict_capacity || false,
test_model: detail.value.test_model || '',
@@ -511,6 +513,10 @@ function formatHeaders(headers: Record<string, string>): string {
</ADescriptionsItem>
<ADescriptionsItem label="优先级">{{ detail.priority }}</ADescriptionsItem>
<ADescriptionsItem label="权重">{{ detail.weight }}</ADescriptionsItem>
<ADescriptionsItem label="最大并发">
<span v-if="detail.max_concurrency > 0">{{ detail.max_concurrency }}</span>
<span v-else style="color: #94a3b8">自动</span>
</ADescriptionsItem>
<ADescriptionsItem label="调度层级">
<ATag :color="tierTagColor[detail.tier] || 'gray'" size="small">{{ tierLabel[detail.tier] || detail.tier || '-' }}</ATag>
</ADescriptionsItem>
@@ -731,41 +737,50 @@ function formatHeaders(headers: Record<string, string>): string {
</template>
</AFormItem>
<!-- 调度与状态 -->
<div class="form-group-title">调度与状态</div>
<!-- 调度与容量 -->
<div class="form-group-title">调度与容量</div>
<ARow :gutter="16">
<ACol :span="8">
<ACol :span="12">
<AFormItem label="调度层级">
<ASelect v-model="editForm.tier" :options="tierOptions" />
<template #extra><span class="field-help">三档固定层级,替代旧版数值优先级</span></template>
</AFormItem>
</ACol>
<ACol :span="8">
<ACol :span="12">
<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="8">
</ARow>
<ARow :gutter="16">
<ACol :span="12">
<AFormItem label="最大并发">
<AInputNumber v-model="editForm.max_concurrency" :min="0" class="w-full" />
<template #extra><span class="field-help">0 = 自动估算(按上游 429 水位动态调整)</span></template>
</AFormItem>
</ACol>
<ACol :span="12">
<AFormItem label="严格容量">
<ASwitch v-model="editForm.strict_capacity" />
<template #extra><span class="field-help">Redis 故障时按保守限额拒绝新请求(高成本渠道)</span></template>
</AFormItem>
</ACol>
</ARow>
<ARow :gutter="16">
<ACol :span="12">
<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>
<ACol :span="6">
<AFormItem label="严格容量">
<ASwitch v-model="editForm.strict_capacity" />
<template #extra><span class="field-help">Redis 故障时按保守限额拒绝新请求(高成本渠道)</span></template>
</ARow>
<ARow :gutter="16">
<ACol :span="12">
<AFormItem label="使用代理">
<ASwitch v-model="editForm.use_proxy" />
</AFormItem>
</ACol>
</ARow>
@@ -775,16 +790,18 @@ function formatHeaders(headers: Record<string, string>): string {
<ACollapse :bordered="false">
<ACollapseItem header="VIP 设置" key="vip" :header-style="{ fontSize: '13px' }">
<ARow :gutter="16">
<ACol :span="6">
<ACol :span="12">
<AFormItem label="VIP 渠道"><ASwitch v-model="editForm.is_vip" /></AFormItem>
</ACol>
<ACol v-if="editForm.is_vip" :span="6">
<ACol v-if="editForm.is_vip" :span="12">
<AFormItem label="共享阈值"><AInputNumber v-model="editForm.sharing_threshold" :min="0" :max="100" class="w-full" /></AFormItem>
</ACol>
<ACol v-if="editForm.is_vip" :span="6">
</ARow>
<ARow v-if="editForm.is_vip" :gutter="16">
<ACol :span="12">
<AFormItem label="抢占阈值"><AInputNumber v-model="editForm.preemption_threshold" :min="0" :max="100" class="w-full" /></AFormItem>
</ACol>
<ACol v-if="editForm.is_vip" :span="6">
<ACol :span="12">
<AFormItem label="借用冷却(秒)"><AInputNumber v-model="editForm.borrowing_cooldown_seconds" :min="0" class="w-full" /></AFormItem>
</ACol>
</ARow>
+47 -26
View File
@@ -175,10 +175,10 @@ const providerDefaultURLs: Record<number, string> = {
35: 'https://api.openai.com',
}
const form = reactive({ name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null as number | null, preemption_threshold: null as number | null, borrowing_cooldown_seconds: null as number | null })
const form = reactive({ name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, max_concurrency: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null as number | null, preemption_threshold: null as number | null, borrowing_cooldown_seconds: null as number | null })
function openCreate() {
Object.assign(form, { name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null, preemption_threshold: null, borrowing_cooldown_seconds: null })
Object.assign(form, { name: '', type: 1, base_url: '', api_key: '', priority: 0, weight: 100, max_concurrency: 100, tier: 'primary', strict_capacity: false, test_model: '', remark: '', status: 'active', is_vip: false, use_proxy: false, sharing_threshold: null, preemption_threshold: null, borrowing_cooldown_seconds: null })
showModal.value = true
}
@@ -372,7 +372,8 @@ const { exporting, exportFile } = useExport({
<!-- Create Channel Modal -->
<AModal v-model:visible="showModal" title="创建渠道" :width="640" :mask-closable="false" :on-before-ok="handleSubmit" :ok-loading="formLoading">
<AForm ref="formRef" :model="form" :rules="formRules" :auto-label-width="true" layout="vertical">
<!-- 基本信息 -->
<!-- 基础信息 -->
<div class="form-group-title">基础信息</div>
<ARow :gutter="16">
<ACol :span="12">
<AFormItem field="name" label="渠道名称" required><AInput v-model="form.name" placeholder="例如:OpenAI 主力" /></AFormItem>
@@ -392,40 +393,51 @@ const { exporting, exportFile } = useExport({
<AFormItem field="api_key" label="API Key" required>
<AInput v-model="form.api_key" type="textarea" :auto-size="{ minRows: 2, maxRows: 4 }" placeholder="sk-xxxx..." />
</AFormItem>
<!-- 调度 & 状态 -->
<!-- 调度与容量 -->
<div class="form-group-title">调度与容量</div>
<ARow :gutter="16">
<ACol :span="8">
<ACol :span="12">
<AFormItem label="调度层级">
<ASelect v-model="form.tier" :options="tierOptions" />
<template #extra><span class="field-help">三档固定层级,替代旧版数值优先级</span></template>
</AFormItem>
</ACol>
<ACol :span="8">
<ACol :span="12">
<AFormItem label="权重">
<AInputNumber v-model="form.weight" :min="0" :max="100" class="w-full" />
<template #extra><span class="field-help">同层级内按权重比例分配</span></template>
</AFormItem>
</ACol>
<ACol :span="8">
<AFormItem label="状态">
<ASelect v-model="form.status" :options="[{ label: '启用', value: 'active' }, { label: '禁用', value: 'disabled' }, { label: '测试中', value: 'testing' }]" />
</ARow>
<ARow :gutter="16">
<ACol :span="12">
<AFormItem label="最大并发">
<AInputNumber v-model="form.max_concurrency" :min="0" class="w-full" />
<template #extra><span class="field-help">0 = 自动估算(按上游 429 水位动态调整)</span></template>
</AFormItem>
</ACol>
<ACol :span="12">
<AFormItem label="严格容量">
<ASwitch v-model="form.strict_capacity" />
<template #extra><span class="field-help">Redis 故障时保守限流(高成本渠道)</span></template>
</AFormItem>
</ACol>
</ARow>
<ARow :gutter="16">
<ACol :span="8">
<AFormItem label="测试模型"><AInput v-model="form.test_model" placeholder="gpt-4o-mini" /></AFormItem>
</ACol>
<ACol :span="8">
<AFormItem label="使用代理">
<ASwitch v-model="form.use_proxy" />
<template #extra><span style="color:var(--color-text-3);font-size:12px">需先在系统设置中配置代理</span></template>
<ACol :span="12">
<AFormItem label="状态">
<ASelect v-model="form.status" :options="[{ label: '启用', value: 'active' }, { label: '禁用', value: 'disabled' }, { label: '测试中', value: 'testing' }]" />
</AFormItem>
</ACol>
<ACol :span="8">
<AFormItem label="严格容量">
<ASwitch v-model="form.strict_capacity" />
<template #extra><span style="color:var(--color-text-3);font-size:12px">Redis 故障时保守限流(高成本渠道)</span></template>
<ACol :span="12">
<AFormItem label="测试模型"><AInput v-model="form.test_model" placeholder="gpt-4o-mini" /></AFormItem>
</ACol>
</ARow>
<ARow :gutter="16">
<ACol :span="12">
<AFormItem label="使用代理">
<ASwitch v-model="form.use_proxy" />
<template #extra><span class="field-help">需先在系统设置中配置代理</span></template>
</AFormItem>
</ACol>
</ARow>
@@ -434,18 +446,18 @@ const { exporting, exportFile } = useExport({
<ACollapse :bordered="false" :default-active-key="form.is_vip ? ['vip'] : []">
<ACollapseItem header="VIP 设置" key="vip" :header-style="{ fontSize: '13px' }">
<ARow :gutter="16">
<ACol :span="8">
<ACol :span="12">
<AFormItem label="VIP 渠道"><ASwitch v-model="form.is_vip" /></AFormItem>
</ACol>
<ACol v-if="form.is_vip" :span="8">
<ACol v-if="form.is_vip" :span="12">
<AFormItem label="共享阈值"><AInputNumber v-model="form.sharing_threshold" :min="0" :max="100" placeholder="共享健康分阈值" class="w-full" /></AFormItem>
</ACol>
<ACol v-if="form.is_vip" :span="8">
<AFormItem label="抢占阈值"><AInputNumber v-model="form.preemption_threshold" :min="0" :max="100" placeholder="抢占优先级阈值" class="w-full" /></AFormItem>
</ACol>
</ARow>
<ARow v-if="form.is_vip" :gutter="16">
<ACol :span="8">
<ACol :span="12">
<AFormItem label="抢占阈值"><AInputNumber v-model="form.preemption_threshold" :min="0" :max="100" placeholder="抢占优先级阈值" class="w-full" /></AFormItem>
</ACol>
<ACol :span="12">
<AFormItem label="借用冷却(秒)"><AInputNumber v-model="form.borrowing_cooldown_seconds" :min="0" placeholder="非VIP冷却时间" class="w-full" /></AFormItem>
</ACol>
</ARow>
@@ -586,4 +598,13 @@ const { exporting, exportFile } = useExport({
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>