feat: 优化分组表单
This commit is contained in:
@@ -2,18 +2,18 @@
|
|||||||
import { keysApi } from "@/api/keys";
|
import { keysApi } from "@/api/keys";
|
||||||
import { settingsApi } from "@/api/settings";
|
import { settingsApi } from "@/api/settings";
|
||||||
import type { Group, GroupConfigOption, UpstreamInfo } from "@/types/models";
|
import type { Group, GroupConfigOption, UpstreamInfo } from "@/types/models";
|
||||||
import { Add, Close, Remove } from "@vicons/ionicons5";
|
import { Add, Close, HelpCircleOutline, Remove } from "@vicons/ionicons5";
|
||||||
import {
|
import {
|
||||||
NButton,
|
NButton,
|
||||||
NCard,
|
NCard,
|
||||||
NCollapse,
|
|
||||||
NCollapseItem,
|
|
||||||
NForm,
|
NForm,
|
||||||
NFormItem,
|
NFormItem,
|
||||||
|
NIcon,
|
||||||
NInput,
|
NInput,
|
||||||
NInputNumber,
|
NInputNumber,
|
||||||
NModal,
|
NModal,
|
||||||
NSelect,
|
NSelect,
|
||||||
|
NTooltip,
|
||||||
useMessage,
|
useMessage,
|
||||||
type FormRules,
|
type FormRules,
|
||||||
} from "naive-ui";
|
} from "naive-ui";
|
||||||
@@ -27,6 +27,7 @@ interface Props {
|
|||||||
interface Emits {
|
interface Emits {
|
||||||
(e: "update:show", value: boolean): void;
|
(e: "update:show", value: boolean): void;
|
||||||
(e: "success", value: Group): void;
|
(e: "success", value: Group): void;
|
||||||
|
(e: "switchToGroup", groupId: number): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 配置项类型
|
// 配置项类型
|
||||||
@@ -51,7 +52,7 @@ interface GroupFormData {
|
|||||||
display_name: string;
|
display_name: string;
|
||||||
description: string;
|
description: string;
|
||||||
upstreams: UpstreamInfo[];
|
upstreams: UpstreamInfo[];
|
||||||
channel_type: "openai" | "gemini" | "anthropic";
|
channel_type: "anthropic" | "gemini" | "openai";
|
||||||
sort: number;
|
sort: number;
|
||||||
test_model: string;
|
test_model: string;
|
||||||
validation_endpoint: string;
|
validation_endpoint: string;
|
||||||
@@ -89,11 +90,11 @@ const configOptionsFetched = ref(false);
|
|||||||
const testModelPlaceholder = computed(() => {
|
const testModelPlaceholder = computed(() => {
|
||||||
switch (formData.channel_type) {
|
switch (formData.channel_type) {
|
||||||
case "openai":
|
case "openai":
|
||||||
return "如:gpt-4.1-nano";
|
return "gpt-4.1-nano";
|
||||||
case "gemini":
|
case "gemini":
|
||||||
return "如:gemini-2.0-flash-lite";
|
return "gemini-2.0-flash-lite";
|
||||||
case "anthropic":
|
case "anthropic":
|
||||||
return "如:claude-3-haiku-20240307";
|
return "claude-3-haiku-20240307";
|
||||||
default:
|
default:
|
||||||
return "请输入模型名称";
|
return "请输入模型名称";
|
||||||
}
|
}
|
||||||
@@ -112,6 +113,19 @@ const upstreamPlaceholder = computed(() => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const validationEndpointPlaceholder = computed(() => {
|
||||||
|
switch (formData.channel_type) {
|
||||||
|
case "openai":
|
||||||
|
return "/v1/chat/completions";
|
||||||
|
case "anthropic":
|
||||||
|
return "/v1/messages";
|
||||||
|
case "gemini":
|
||||||
|
return ""; // Gemini 不显示此字段
|
||||||
|
default:
|
||||||
|
return "请输入验证端点路径";
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
// 表单验证规则
|
// 表单验证规则
|
||||||
const rules: FormRules = {
|
const rules: FormRules = {
|
||||||
name: [
|
name: [
|
||||||
@@ -326,6 +340,10 @@ async function handleSubmit() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
emit("success", res);
|
emit("success", res);
|
||||||
|
// 如果是新建模式,发出切换到新分组的事件
|
||||||
|
if (!props.group?.id && res.id) {
|
||||||
|
emit("switchToGroup", res.id);
|
||||||
|
}
|
||||||
handleClose();
|
handleClose();
|
||||||
} finally {
|
} finally {
|
||||||
loading.value = false;
|
loading.value = false;
|
||||||
@@ -363,52 +381,152 @@ async function handleSubmit() {
|
|||||||
<div class="form-section">
|
<div class="form-section">
|
||||||
<h4 class="section-title">基础信息</h4>
|
<h4 class="section-title">基础信息</h4>
|
||||||
|
|
||||||
<n-form-item label="分组名称" path="name">
|
<!-- 分组名称和显示名称在同一行 -->
|
||||||
<n-input v-model:value="formData.name" placeholder="作为路由的一部分,如:gemini" />
|
<div class="form-row">
|
||||||
</n-form-item>
|
<n-form-item label="分组名称" path="name" class="form-item-half">
|
||||||
|
<template #label>
|
||||||
|
<div class="form-label-with-tooltip">
|
||||||
|
分组名称
|
||||||
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
|
</template>
|
||||||
|
作为API路由的一部分,只能包含小写字母、数字、中划线或下划线,长度3-30位。例如:gemini、openai-2
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n-input v-model:value="formData.name" placeholder="gemini" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item label="显示名称" path="display_name">
|
<n-form-item label="显示名称" path="display_name" class="form-item-half">
|
||||||
<n-input v-model:value="formData.display_name" placeholder="可选,用于显示的友好名称" />
|
<template #label>
|
||||||
</n-form-item>
|
<div class="form-label-with-tooltip">
|
||||||
|
显示名称
|
||||||
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
|
</template>
|
||||||
|
用于在界面上显示的友好名称,可以包含中文和特殊字符。如果不填写,将使用分组名称作为显示名称
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n-input v-model:value="formData.display_name" placeholder="Google Gemini" />
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-form-item label="渠道类型" path="channel_type">
|
<!-- 渠道类型和排序在同一行 -->
|
||||||
<n-select
|
<div class="form-row">
|
||||||
v-model:value="formData.channel_type"
|
<n-form-item label="渠道类型" path="channel_type" class="form-item-half">
|
||||||
:options="channelTypeOptions"
|
<template #label>
|
||||||
placeholder="请选择渠道类型"
|
<div class="form-label-with-tooltip">
|
||||||
/>
|
渠道类型
|
||||||
</n-form-item>
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
|
</template>
|
||||||
|
选择API提供商类型,决定了请求格式和认证方式。支持OpenAI、Gemini、Anthropic等主流AI服务商
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n-select
|
||||||
|
v-model:value="formData.channel_type"
|
||||||
|
:options="channelTypeOptions"
|
||||||
|
placeholder="请选择渠道类型"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item label="测试模型" path="test_model">
|
<n-form-item label="排序" path="sort" class="form-item-half">
|
||||||
<n-input v-model:value="formData.test_model" :placeholder="testModelPlaceholder" />
|
<template #label>
|
||||||
</n-form-item>
|
<div class="form-label-with-tooltip">
|
||||||
|
排序
|
||||||
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
|
</template>
|
||||||
|
决定分组在列表中的显示顺序,数字越小越靠前。建议使用10、20、30这样的间隔数字,便于后续调整
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n-input-number
|
||||||
|
v-model:value="formData.sort"
|
||||||
|
:min="0"
|
||||||
|
placeholder="排序值"
|
||||||
|
style="width: 100%"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
</div>
|
||||||
|
|
||||||
<n-form-item
|
<!-- 测试模型和测试路径在同一行 -->
|
||||||
label="测试路径"
|
<div class="form-row">
|
||||||
path="validation_endpoint"
|
<n-form-item label="测试模型" path="test_model" class="form-item-half">
|
||||||
v-if="formData.channel_type !== 'gemini'"
|
<template #label>
|
||||||
>
|
<div class="form-label-with-tooltip">
|
||||||
<n-input
|
测试模型
|
||||||
v-model:value="formData.validation_endpoint"
|
<n-tooltip trigger="hover" placement="top">
|
||||||
placeholder="可选,自定义用于验证key的API路径"
|
<template #trigger>
|
||||||
/>
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
</n-form-item>
|
</template>
|
||||||
|
用于验证API密钥有效性的模型名称。系统会使用这个模型发送测试请求来检查密钥是否可用
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n-input v-model:value="formData.test_model" :placeholder="testModelPlaceholder" />
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
<n-form-item label="排序" path="sort">
|
<n-form-item
|
||||||
<n-input-number
|
label="测试路径"
|
||||||
v-model:value="formData.sort"
|
path="validation_endpoint"
|
||||||
:min="0"
|
class="form-item-half"
|
||||||
placeholder="排序值,数字越小越靠前"
|
v-if="formData.channel_type !== 'gemini'"
|
||||||
/>
|
>
|
||||||
</n-form-item>
|
<template #label>
|
||||||
|
<div class="form-label-with-tooltip">
|
||||||
|
测试路径
|
||||||
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
|
</template>
|
||||||
|
<div>
|
||||||
|
自定义用于验证密钥的API端点路径。如果不填写,将使用默认路径:
|
||||||
|
<br />
|
||||||
|
• OpenAI: /v1/chat/completions
|
||||||
|
<br />
|
||||||
|
• Anthropic: /v1/messages
|
||||||
|
<br />
|
||||||
|
如需使用非标准路径,请在此填写完整的API路径
|
||||||
|
</div>
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.validation_endpoint"
|
||||||
|
:placeholder="validationEndpointPlaceholder || '可选,自定义用于验证key的API路径'"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
|
|
||||||
|
<!-- 当gemini渠道时,测试路径不显示,需要一个占位div保持布局 -->
|
||||||
|
<div v-else class="form-item-half" />
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 描述独占一行 -->
|
||||||
<n-form-item label="描述" path="description">
|
<n-form-item label="描述" path="description">
|
||||||
|
<template #label>
|
||||||
|
<div class="form-label-with-tooltip">
|
||||||
|
描述
|
||||||
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
|
</template>
|
||||||
|
分组的详细说明,帮助团队成员了解该分组的用途和特点。支持多行文本
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
<n-input
|
<n-input
|
||||||
v-model:value="formData.description"
|
v-model:value="formData.description"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
placeholder="可选,分组描述信息"
|
placeholder=""
|
||||||
:rows="2"
|
:rows="1"
|
||||||
:autosize="{ minRows: 2, maxRows: 2 }"
|
:autosize="{ minRows: 1, maxRows: 5 }"
|
||||||
style="resize: none"
|
style="resize: none"
|
||||||
/>
|
/>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
@@ -417,7 +535,6 @@ async function handleSubmit() {
|
|||||||
<!-- 上游地址 -->
|
<!-- 上游地址 -->
|
||||||
<div class="form-section" style="margin-top: 10px">
|
<div class="form-section" style="margin-top: 10px">
|
||||||
<h4 class="section-title">上游地址</h4>
|
<h4 class="section-title">上游地址</h4>
|
||||||
|
|
||||||
<n-form-item
|
<n-form-item
|
||||||
v-for="(upstream, index) in formData.upstreams"
|
v-for="(upstream, index) in formData.upstreams"
|
||||||
:key="index"
|
:key="index"
|
||||||
@@ -429,27 +546,38 @@ async function handleSubmit() {
|
|||||||
trigger: ['blur', 'input'],
|
trigger: ['blur', 'input'],
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="flex items-center gap-2" style="width: 100%">
|
<template #label>
|
||||||
<n-input
|
<div class="form-label-with-tooltip">
|
||||||
v-model:value="upstream.url"
|
上游 {{ index + 1 }}
|
||||||
:placeholder="upstreamPlaceholder"
|
<n-tooltip trigger="hover" placement="top">
|
||||||
style="flex: 1"
|
<template #trigger>
|
||||||
/>
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
<span class="form-label">权重</span>
|
</template>
|
||||||
<n-input-number
|
API服务器的完整URL地址。多个上游可以实现负载均衡和故障转移,提高服务可用性
|
||||||
v-model:value="upstream.weight"
|
</n-tooltip>
|
||||||
:min="1"
|
</div>
|
||||||
placeholder="权重"
|
</template>
|
||||||
style="width: 100px"
|
<div class="upstream-row">
|
||||||
/>
|
<div class="upstream-url">
|
||||||
<div style="width: 40px">
|
<n-input v-model:value="upstream.url" :placeholder="upstreamPlaceholder" />
|
||||||
|
</div>
|
||||||
|
<div class="upstream-weight">
|
||||||
|
<span class="weight-label">权重</span>
|
||||||
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-input-number v-model:value="upstream.weight" :min="1" placeholder="权重" />
|
||||||
|
</template>
|
||||||
|
负载均衡权重,数值越大被选中的概率越高。例如:权重为2的上游被选中的概率是权重为1的两倍
|
||||||
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
<div class="upstream-actions">
|
||||||
<n-button
|
<n-button
|
||||||
v-if="formData.upstreams.length > 1"
|
v-if="formData.upstreams.length > 1"
|
||||||
@click="removeUpstream(index)"
|
@click="removeUpstream(index)"
|
||||||
type="error"
|
type="error"
|
||||||
quaternary
|
quaternary
|
||||||
circle
|
circle
|
||||||
style="margin-left: 10px"
|
size="small"
|
||||||
>
|
>
|
||||||
<template #icon>
|
<template #icon>
|
||||||
<n-icon :component="Remove" />
|
<n-icon :component="Remove" />
|
||||||
@@ -472,15 +600,24 @@ async function handleSubmit() {
|
|||||||
<!-- 高级配置 -->
|
<!-- 高级配置 -->
|
||||||
<div class="form-section" style="margin-top: 10px">
|
<div class="form-section" style="margin-top: 10px">
|
||||||
<n-collapse>
|
<n-collapse>
|
||||||
<n-collapse-item title="高级配置" name="advanced">
|
<n-collapse-item name="advanced">
|
||||||
|
<template #header>高级配置</template>
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h5 class="config-title">分组配置</h5>
|
<h5 class="config-title-with-tooltip">
|
||||||
|
分组配置
|
||||||
|
<n-tooltip trigger="hover" placement="top">
|
||||||
|
<template #trigger>
|
||||||
|
<n-icon :component="HelpCircleOutline" class="help-icon config-help" />
|
||||||
|
</template>
|
||||||
|
针对此分组的专用配置参数,如超时时间、重试次数等。这些配置会覆盖全局默认设置
|
||||||
|
</n-tooltip>
|
||||||
|
</h5>
|
||||||
|
|
||||||
<div class="config-items">
|
<div class="config-items">
|
||||||
<n-form-item
|
<n-form-item
|
||||||
v-for="(configItem, index) in formData.configItems"
|
v-for="(configItem, index) in formData.configItems"
|
||||||
:key="index"
|
:key="index"
|
||||||
class="flex config-item"
|
class="config-item-row"
|
||||||
:label="`配置 ${index + 1}`"
|
:label="`配置 ${index + 1}`"
|
||||||
:path="`configItems[${index}].key`"
|
:path="`configItems[${index}].key`"
|
||||||
:rule="{
|
:rule="{
|
||||||
@@ -489,42 +626,56 @@ async function handleSubmit() {
|
|||||||
trigger: ['blur', 'change'],
|
trigger: ['blur', 'change'],
|
||||||
}"
|
}"
|
||||||
>
|
>
|
||||||
<div class="flex items-center" style="width: 100%">
|
<template #label>
|
||||||
<n-select
|
<div class="form-label-with-tooltip">
|
||||||
v-model:value="configItem.key"
|
配置 {{ index + 1 }}
|
||||||
:options="
|
<n-tooltip trigger="hover" placement="top">
|
||||||
configOptions.map(opt => ({
|
<template #trigger>
|
||||||
label: opt.name,
|
<n-icon :component="HelpCircleOutline" class="help-icon" />
|
||||||
value: opt.key,
|
</template>
|
||||||
disabled:
|
选择要配置的参数类型,然后设置对应的数值。不同参数有不同的作用和取值范围
|
||||||
formData.configItems
|
</n-tooltip>
|
||||||
.map((item: ConfigItem) => item.key)
|
</div>
|
||||||
?.includes(opt.key) && opt.key !== configItem.key,
|
</template>
|
||||||
}))
|
<div class="config-item-content">
|
||||||
"
|
<div class="config-select">
|
||||||
placeholder="请选择配置参数"
|
<n-select
|
||||||
style="min-width: 200px"
|
v-model:value="configItem.key"
|
||||||
@update:value="value => handleConfigKeyChange(index, value)"
|
:options="
|
||||||
clearable
|
configOptions.map(opt => ({
|
||||||
/>
|
label: opt.name,
|
||||||
<n-input-number
|
value: opt.key,
|
||||||
v-model:value="configItem.value"
|
disabled:
|
||||||
placeholder="参数值"
|
formData.configItems
|
||||||
style="width: 180px; margin-left: 15px"
|
.map((item: ConfigItem) => item.key)
|
||||||
:precision="0"
|
?.includes(opt.key) && opt.key !== configItem.key,
|
||||||
/>
|
}))
|
||||||
<n-button
|
"
|
||||||
@click="removeConfigItem(index)"
|
placeholder="请选择配置参数"
|
||||||
type="error"
|
@update:value="value => handleConfigKeyChange(index, value)"
|
||||||
quaternary
|
clearable
|
||||||
circle
|
/>
|
||||||
size="small"
|
</div>
|
||||||
style="margin-left: 10px"
|
<div class="config-value">
|
||||||
>
|
<n-input-number
|
||||||
<template #icon>
|
v-model:value="configItem.value"
|
||||||
<n-icon :component="Remove" />
|
placeholder="参数值"
|
||||||
</template>
|
:precision="0"
|
||||||
</n-button>
|
/>
|
||||||
|
</div>
|
||||||
|
<div class="config-actions">
|
||||||
|
<n-button
|
||||||
|
@click="removeConfigItem(index)"
|
||||||
|
type="error"
|
||||||
|
quaternary
|
||||||
|
circle
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<template #icon>
|
||||||
|
<n-icon :component="Remove" />
|
||||||
|
</template>
|
||||||
|
</n-button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</div>
|
</div>
|
||||||
@@ -544,17 +695,26 @@ async function handleSubmit() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="config-section">
|
<div class="config-section">
|
||||||
<h5 class="config-title">参数覆盖</h5>
|
<n-form-item path="param_overrides">
|
||||||
<div class="config-items">
|
<template #label>
|
||||||
<n-form-item path="param_overrides">
|
<div class="form-label-with-tooltip">
|
||||||
<n-input
|
参数覆盖
|
||||||
v-model:value="formData.param_overrides"
|
<n-tooltip trigger="hover" placement="top">
|
||||||
type="textarea"
|
<template #trigger>
|
||||||
placeholder="JSON 格式的参数覆盖配置"
|
<n-icon :component="HelpCircleOutline" class="help-icon config-help" />
|
||||||
:rows="4"
|
</template>
|
||||||
/>
|
使用JSON格式定义要覆盖的API请求参数。例如: {"temperature": 0.7,
|
||||||
</n-form-item>
|
"max_tokens": 2000}。这些参数会在发送请求时合并到原始参数中
|
||||||
</div>
|
</n-tooltip>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<n-input
|
||||||
|
v-model:value="formData.param_overrides"
|
||||||
|
type="textarea"
|
||||||
|
placeholder='{"temperature": 0.7, "max_tokens": 2000}'
|
||||||
|
:rows="4"
|
||||||
|
/>
|
||||||
|
</n-form-item>
|
||||||
</div>
|
</div>
|
||||||
</n-collapse-item>
|
</n-collapse-item>
|
||||||
</n-collapse>
|
</n-collapse>
|
||||||
@@ -649,10 +809,187 @@ async function handleSubmit() {
|
|||||||
font-weight: 500;
|
font-weight: 500;
|
||||||
}
|
}
|
||||||
|
|
||||||
.config-item {
|
/* Tooltip相关样式 */
|
||||||
margin-bottom: 12px;
|
.form-label-with-tooltip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.help-icon {
|
||||||
|
color: #9ca3af;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: help;
|
||||||
|
transition: color 0.2s ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.help-icon:hover {
|
||||||
|
color: #667eea;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-title-with-tooltip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
margin-bottom: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.section-help {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-header-with-tooltip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.collapse-help {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-title-with-tooltip {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 6px;
|
||||||
|
font-size: 0.9rem;
|
||||||
|
font-weight: 600;
|
||||||
|
color: #374151;
|
||||||
|
margin: 0 0 12px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-help {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 增强表单样式 */
|
||||||
|
:deep(.n-form-item-label) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input) {
|
||||||
|
--n-border-radius: 8px;
|
||||||
|
--n-border: 1px solid #e5e7eb;
|
||||||
|
--n-border-hover: 1px solid #667eea;
|
||||||
|
--n-border-focus: 1px solid #667eea;
|
||||||
|
--n-box-shadow-focus: 0 0 0 2px rgba(102, 126, 234, 0.1);
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-select) {
|
||||||
|
--n-border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-input-number) {
|
||||||
|
--n-border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-button) {
|
||||||
|
--n-border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 美化tooltip */
|
||||||
|
:deep(.n-tooltip__trigger) {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tooltip) {
|
||||||
|
--n-font-size: 13px;
|
||||||
|
--n-border-radius: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tooltip .n-tooltip__content) {
|
||||||
|
max-width: 320px;
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-tooltip .n-tooltip__content div) {
|
||||||
|
white-space: pre-line;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 折叠面板样式优化 */
|
||||||
|
:deep(.n-collapse-item__header) {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
|
|
||||||
|
:deep(.n-collapse-item) {
|
||||||
|
--n-title-padding: 16px 0;
|
||||||
|
}
|
||||||
|
|
||||||
:deep(.n-base-selection-label) {
|
:deep(.n-base-selection-label) {
|
||||||
height: 40px;
|
height: 40px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 表单行布局 */
|
||||||
|
.form-row {
|
||||||
|
display: flex;
|
||||||
|
gap: 20px;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-item-half {
|
||||||
|
flex: 1;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上游地址行布局 */
|
||||||
|
.upstream-row {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upstream-url {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upstream-weight {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
flex: 0 0 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.weight-label {
|
||||||
|
font-weight: 500;
|
||||||
|
color: #374151;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.upstream-actions {
|
||||||
|
flex: 0 0 32px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 配置项行布局 */
|
||||||
|
.config-item-row {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-item-content {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 12px;
|
||||||
|
width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-select {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-value {
|
||||||
|
flex: 0 0 140px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.config-actions {
|
||||||
|
flex: 0 0 32px;
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -295,27 +295,27 @@ function resetPage() {
|
|||||||
<n-grid :cols="2">
|
<n-grid :cols="2">
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-form-item label="分组名称:">
|
<n-form-item label="分组名称:">
|
||||||
{{ group?.name || "-" }}
|
{{ group?.name }}
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-form-item label="显示名称:">
|
<n-form-item label="显示名称:">
|
||||||
{{ group?.display_name || "-" }}
|
{{ group?.display_name }}
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-form-item label="渠道类型:">
|
<n-form-item label="渠道类型:">
|
||||||
{{ group?.channel_type || "-" }}
|
{{ group?.channel_type }}
|
||||||
</n-form-item>
|
|
||||||
</n-grid-item>
|
|
||||||
<n-grid-item>
|
|
||||||
<n-form-item label="测试模型:">
|
|
||||||
{{ group?.test_model || "-" }}
|
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-form-item label="排序:">
|
<n-form-item label="排序:">
|
||||||
{{ group?.sort || 0 }}
|
{{ group?.sort }}
|
||||||
|
</n-form-item>
|
||||||
|
</n-grid-item>
|
||||||
|
<n-grid-item>
|
||||||
|
<n-form-item label="测试模型:">
|
||||||
|
{{ group?.test_model }}
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item v-if="group?.channel_type !== 'gemini'">
|
<n-grid-item v-if="group?.channel_type !== 'gemini'">
|
||||||
@@ -325,7 +325,9 @@ function resetPage() {
|
|||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
<n-grid-item>
|
<n-grid-item>
|
||||||
<n-form-item label="描述:">
|
<n-form-item label="描述:">
|
||||||
{{ group?.description || "-" }}
|
<div class="description-content">
|
||||||
|
{{ group?.description }}
|
||||||
|
</div>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-grid-item>
|
</n-grid-item>
|
||||||
</n-grid>
|
</n-grid>
|
||||||
@@ -521,4 +523,13 @@ function resetPage() {
|
|||||||
:deep(.n-form-item-feedback-wrapper) {
|
:deep(.n-form-item-feedback-wrapper) {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 描述内容样式 */
|
||||||
|
.description-content {
|
||||||
|
white-space: pre-wrap;
|
||||||
|
word-wrap: break-word;
|
||||||
|
line-height: 1.5;
|
||||||
|
min-height: 20px;
|
||||||
|
color: #374151;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
@@ -61,10 +61,22 @@ function openCreateGroupModal() {
|
|||||||
showGroupModal.value = true;
|
showGroupModal.value = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
function handleGroupCreated() {
|
function handleGroupCreated(_group: Group) {
|
||||||
showGroupModal.value = false;
|
showGroupModal.value = false;
|
||||||
emit("refresh");
|
emit("refresh");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function handleSwitchToGroup(groupId: number) {
|
||||||
|
// 创建成功后,等待列表刷新完成,然后切换到新创建的分组
|
||||||
|
emit("refresh");
|
||||||
|
// 延迟选择新分组,确保列表已更新
|
||||||
|
setTimeout(() => {
|
||||||
|
const newGroup = props.groups.find(g => g.id === groupId);
|
||||||
|
if (newGroup) {
|
||||||
|
emit("group-select", newGroup);
|
||||||
|
}
|
||||||
|
}, 100);
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -123,7 +135,11 @@ function handleGroupCreated() {
|
|||||||
</n-button>
|
</n-button>
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
<group-form-modal v-model:show="showGroupModal" @success="handleGroupCreated" />
|
<group-form-modal
|
||||||
|
v-model:show="showGroupModal"
|
||||||
|
@success="handleGroupCreated"
|
||||||
|
@switch-to-group="handleSwitchToGroup"
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
Reference in New Issue
Block a user