模型黑名单与密钥模型白名单界面、分组填入、删除弹窗排版
This commit is contained in:
@@ -4,6 +4,7 @@ import { computed, h, reactive, ref } from 'vue'
|
||||
|
||||
import { createAiChannel, deleteAiChannel, probeAiChannel, syncAiChannelModels, updateAiChannel } from '@/api/aigateway'
|
||||
import { listCachedRegions } from '@/api/configs'
|
||||
import GroupChips from '@/components/ai/GroupChips.vue'
|
||||
import AppInputNumber from '@/components/AppInputNumber.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import FormModal from '@/components/FormModal.vue'
|
||||
@@ -15,9 +16,11 @@ import { useToast } from '@/composables/useToast'
|
||||
import { useScopeStore } from '@/stores/scope'
|
||||
import type { AiChannel, AiProbeStatus } from '@/types/api'
|
||||
|
||||
defineProps<{ channels: AiChannel[]; loading: boolean }>()
|
||||
const props = defineProps<{ channels: AiChannel[]; loading: boolean }>()
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
|
||||
const groupHints = computed(() => [...new Set(props.channels.map((c) => c.group).filter(Boolean))])
|
||||
|
||||
const toast = useToast()
|
||||
const dialog = useDialog()
|
||||
const scope = useScopeStore()
|
||||
@@ -233,6 +236,7 @@ const columns = computed<DataTableColumns<AiChannel>>(() => [
|
||||
<div class="grid grid-cols-3 gap-3">
|
||||
<FormField label="分组" hint="分组供密钥定向路由;留空归入默认分组">
|
||||
<NInput v-model:value="form.group" placeholder="默认" />
|
||||
<GroupChips :groups="groupHints" @pick="form.group = $event" />
|
||||
</FormField>
|
||||
<FormField label="优先级" hint="数字小优先">
|
||||
<AppInputNumber v-model:value="form.priority" :min="1" />
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NDataTable, NInput, useDialog, type DataTableColumns } from 'naive-ui'
|
||||
import { NButton, NDataTable, NInput, NSelect, NTooltip, useDialog, type DataTableColumns } from 'naive-ui'
|
||||
import { computed, h, reactive, ref } from 'vue'
|
||||
|
||||
import { createAiKey, deleteAiKey, updateAiKey, updateAiKeyContentLog } from '@/api/aigateway'
|
||||
import GroupChips from '@/components/ai/GroupChips.vue'
|
||||
import AppInputNumber from '@/components/AppInputNumber.vue'
|
||||
import FormField from '@/components/FormField.vue'
|
||||
import FormModal from '@/components/FormModal.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import { fmtTime } from '@/composables/useFormat'
|
||||
import type { AiKey } from '@/types/api'
|
||||
import type { AiKey, AiModel } from '@/types/api'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
const props = defineProps<{ keys: AiKey[]; loading: boolean; groups: string[] }>()
|
||||
const props = defineProps<{ keys: AiKey[]; loading: boolean; groups: string[]; models: AiModel[] }>()
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
|
||||
const message = useToast()
|
||||
@@ -21,20 +22,23 @@ const dialog = useDialog()
|
||||
const showForm = ref(false)
|
||||
const editing = ref<AiKey | null>(null)
|
||||
const submitting = ref(false)
|
||||
const form = reactive({ name: '', value: '', group: '' })
|
||||
const form = reactive({ name: '', value: '', group: '', models: [] as string[] })
|
||||
|
||||
function openCreate() {
|
||||
editing.value = null
|
||||
Object.assign(form, { name: '', value: '', group: '' })
|
||||
Object.assign(form, { name: '', value: '', group: '', models: [] })
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
function openEdit(key: AiKey) {
|
||||
editing.value = key
|
||||
Object.assign(form, { name: key.name, value: '', group: key.group })
|
||||
Object.assign(form, { name: key.name, value: '', group: key.group, models: key.models ?? [] })
|
||||
showForm.value = true
|
||||
}
|
||||
|
||||
/** 限制模型下拉选项:聚合模型列表;tag 模式允许手输未同步模型 */
|
||||
const modelOptions = computed(() => props.models.map((m) => ({ label: m.id, value: m.id })))
|
||||
|
||||
// 创建成功后明文只在此弹窗展示一次
|
||||
const createdKey = ref('')
|
||||
const showCreated = ref(false)
|
||||
@@ -43,10 +47,13 @@ async function submit() {
|
||||
submitting.value = true
|
||||
try {
|
||||
if (editing.value) {
|
||||
await updateAiKey(editing.value.id, { name: form.name, group: form.group })
|
||||
await updateAiKey(editing.value.id, { name: form.name, group: form.group, models: form.models })
|
||||
message.success('已更新密钥')
|
||||
} else {
|
||||
const { key } = await createAiKey({ name: form.name, value: form.value || undefined, group: form.group || undefined })
|
||||
const { key } = await createAiKey({
|
||||
name: form.name, value: form.value || undefined, group: form.group || undefined,
|
||||
models: form.models.length ? form.models : undefined,
|
||||
})
|
||||
createdKey.value = key
|
||||
showCreated.value = true
|
||||
}
|
||||
@@ -118,6 +125,21 @@ function confirmRemove(key: AiKey) {
|
||||
|
||||
const groupHints = computed(() => props.groups.filter(Boolean))
|
||||
|
||||
// modelsCell 渲染模型白名单列:空 = 不限;非空显示数量,悬浮列明细
|
||||
function modelsCell(r: AiKey) {
|
||||
const list = r.models ?? []
|
||||
if (!list.length) return h('span', { class: 'text-xs text-ink-3' }, '不限')
|
||||
return h(
|
||||
NTooltip,
|
||||
{ style: { maxWidth: '320px' } },
|
||||
{
|
||||
trigger: () =>
|
||||
h('span', { class: 'cursor-help rounded-[5px] bg-info/15 px-2 py-0.5 text-xs font-medium text-info' }, `${list.length} 个`),
|
||||
default: () => h('span', { class: 'text-xs leading-relaxed whitespace-pre-wrap break-all' }, list.join('\n')),
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
const columns = computed<DataTableColumns<AiKey>>(() => [
|
||||
{ title: '名称', key: 'name', width: 220, ellipsis: { tooltip: true }, render: (r) => h('span', { class: 'text-[13px] font-medium' }, r.name) },
|
||||
{ title: '密钥', key: 'tail', width: 120, render: (r) => h('span', { class: 'mono text-[13px] text-ink-2' }, `……${r.tail}`) },
|
||||
@@ -127,6 +149,7 @@ const columns = computed<DataTableColumns<AiKey>>(() => [
|
||||
? h('span', { class: 'rounded-[5px] bg-info/15 px-2 py-0.5 text-xs font-medium text-info' }, r.group)
|
||||
: h('span', { class: 'text-xs text-ink-3' }, '不限'),
|
||||
},
|
||||
{ title: '模型', key: 'models', width: 90, render: modelsCell },
|
||||
{
|
||||
title: '状态', key: 'enabled', width: 100,
|
||||
render: (r) => h(StatusBadge, r.enabled ? { kind: 'run', label: '启用' } : { kind: 'stop', label: '已禁用' }),
|
||||
@@ -170,18 +193,21 @@ const columns = computed<DataTableColumns<AiKey>>(() => [
|
||||
</FormField>
|
||||
<FormField label="渠道分组" hint="选择分组后,该密钥的调用只在该分组的渠道内负载均衡;留空不限分组">
|
||||
<NInput v-model:value="form.group" placeholder="留空 = 不限(全部渠道)" />
|
||||
<div v-if="groupHints.length" class="mt-1.5 flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-xs text-ink-3">现有分组:</span>
|
||||
<button
|
||||
v-for="g in groupHints"
|
||||
:key="g"
|
||||
type="button"
|
||||
class="cursor-pointer rounded-[5px] border border-line bg-card px-2 py-0.5 text-xs text-ink-2 hover:border-accent hover:text-accent"
|
||||
@click="form.group = g"
|
||||
>
|
||||
{{ g }}
|
||||
</button>
|
||||
</div>
|
||||
<GroupChips :groups="groupHints" @pick="form.group = $event" />
|
||||
</FormField>
|
||||
<FormField
|
||||
label="限制模型"
|
||||
hint="非空时该密钥仅能调用所选模型,/v1/models 同步只返回交集;可手输未同步的模型名;留空不限"
|
||||
>
|
||||
<NSelect
|
||||
v-model:value="form.models"
|
||||
:options="modelOptions"
|
||||
multiple
|
||||
filterable
|
||||
clearable
|
||||
tag
|
||||
placeholder="留空 = 不限(全部模型)"
|
||||
/>
|
||||
</FormField>
|
||||
<FormField v-if="!editing" label="自定义密钥值" hint="至少 8 位;仅测试场景建议自定义">
|
||||
<NInput v-model:value="form.value" placeholder="留空自动生成 sk- 开头随机密钥(推荐)" />
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
<script setup lang="ts">
|
||||
// 现有分组提示行:点击 chip 将分组名填入外部输入框;无分组时整行不渲染
|
||||
defineProps<{ groups: string[] }>()
|
||||
defineEmits<{ pick: [group: string] }>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div v-if="groups.length" class="mt-1.5 flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-xs text-ink-3">现有分组:</span>
|
||||
<button
|
||||
v-for="g in groups"
|
||||
:key="g"
|
||||
type="button"
|
||||
class="cursor-pointer rounded-[5px] border border-line bg-card px-2 py-0.5 text-xs text-ink-2 hover:border-accent hover:text-accent"
|
||||
@click="$emit('pick', g)"
|
||||
>
|
||||
{{ g }}
|
||||
</button>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user