Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
94fbe62e4c |
@@ -2,6 +2,18 @@
|
||||
|
||||
格式遵循 [Keep a Changelog](https://keepachangelog.com/zh-CN/1.1.0/),版本号遵循语义化版本。
|
||||
|
||||
## [0.2.0] - 2026-07-12
|
||||
|
||||
### Added
|
||||
|
||||
- AI 网关页「模型黑名单」管理:模型列表按厂商手风琴分组展示,可一键拉黑(全渠道剔除)、移除后重新同步渠道恢复
|
||||
- AI 密钥模型白名单编辑:创建 / 编辑密钥时可限定放行的模型列表(空 = 不限)
|
||||
- 渠道 / 密钥表单「现有分组」chips:点击即填入分组输入框
|
||||
|
||||
### Changed
|
||||
|
||||
- 租户删除确认弹窗分段排版(标题 / 面板清理范围 / 云端提示),列表与详情页文案一致
|
||||
|
||||
## [0.1.0] - 2026-07-10
|
||||
|
||||
### Added
|
||||
|
||||
+1
-1
@@ -1,7 +1,7 @@
|
||||
{
|
||||
"name": "oci-portal-dash",
|
||||
"private": true,
|
||||
"version": "0.1.0",
|
||||
"version": "0.2.0",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite",
|
||||
|
||||
+28
-2
@@ -1,5 +1,13 @@
|
||||
import { request } from './request'
|
||||
import type { AiCallLog, AiChannel, AiContentLog, AiKey, AiModel, AiModelCacheItem } from '@/types/api'
|
||||
import type {
|
||||
AiCallLog,
|
||||
AiChannel,
|
||||
AiContentLog,
|
||||
AiKey,
|
||||
AiModel,
|
||||
AiModelBlacklistItem,
|
||||
AiModelCacheItem,
|
||||
} from '@/types/api'
|
||||
|
||||
// ---- 网关密钥 ----
|
||||
|
||||
@@ -11,6 +19,8 @@ export interface CreateAiKeyRequest {
|
||||
name: string
|
||||
value?: string
|
||||
group?: string
|
||||
/** 模型白名单;缺省/空 = 不限 */
|
||||
models?: string[]
|
||||
}
|
||||
|
||||
/** 创建密钥;key 为明文,仅本次响应返回 */
|
||||
@@ -20,7 +30,7 @@ export function createAiKey(body: CreateAiKeyRequest): Promise<{ key: string; it
|
||||
|
||||
export function updateAiKey(
|
||||
id: number,
|
||||
body: { name?: string; enabled?: boolean; group?: string },
|
||||
body: { name?: string; enabled?: boolean; group?: string; models?: string[] },
|
||||
): Promise<void> {
|
||||
return request(`/ai-keys/${id}`, { method: 'PUT', body })
|
||||
}
|
||||
@@ -78,6 +88,22 @@ export function listAiModels(): Promise<AiModel[]> {
|
||||
return request<{ items: AiModel[] }>('/ai-models').then((r) => r.items)
|
||||
}
|
||||
|
||||
// ---- 模型黑名单 ----
|
||||
|
||||
export function listAiBlacklist(): Promise<AiModelBlacklistItem[]> {
|
||||
return request<{ items: AiModelBlacklistItem[] }>('/ai-blacklist').then((r) => r.items)
|
||||
}
|
||||
|
||||
/** 拉黑模型:删除全部渠道缓存中的同名条目,后续同步 / 探测均过滤 */
|
||||
export function addAiBlacklist(name: string): Promise<void> {
|
||||
return request('/ai-blacklist', { method: 'POST', body: { name } })
|
||||
}
|
||||
|
||||
/** 移出黑名单:缓存不回填,重新同步 / 探测后恢复入池 */
|
||||
export function removeAiBlacklist(id: number): Promise<void> {
|
||||
return request(`/ai-blacklist/${id}`, { method: 'DELETE' })
|
||||
}
|
||||
|
||||
export function listAiCallLogs(params: {
|
||||
page: number
|
||||
size: number
|
||||
|
||||
@@ -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>
|
||||
@@ -946,6 +946,8 @@ export interface AiKey {
|
||||
name: string
|
||||
tail: string
|
||||
group: string
|
||||
/** 模型白名单;null/空 = 不限,非空时网关仅放行列表内模型 */
|
||||
models: string[] | null
|
||||
enabled: boolean
|
||||
lastUsedAt: string | null
|
||||
/** 内容日志开启截止时间;null = 永关(默认,红线),开启必须限时到期自动停写 */
|
||||
@@ -991,6 +993,13 @@ export interface AiModelCacheItem {
|
||||
syncedAt: string
|
||||
}
|
||||
|
||||
/** 模型黑名单条目:拉黑即全渠道剔除,同步 / 探测不再入库 */
|
||||
export interface AiModelBlacklistItem {
|
||||
id: number
|
||||
name: string
|
||||
createdAt: string
|
||||
}
|
||||
|
||||
/** 调用日志:仅元数据与 token 用量 */
|
||||
export interface AiCallLog {
|
||||
id: number
|
||||
|
||||
+111
-13
@@ -1,23 +1,32 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton } from 'naive-ui'
|
||||
import { NButton, useDialog } from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { listAiChannels, listAiKeys, listAiModels } from '@/api/aigateway'
|
||||
import {
|
||||
addAiBlacklist,
|
||||
listAiBlacklist,
|
||||
listAiChannels,
|
||||
listAiKeys,
|
||||
listAiModels,
|
||||
removeAiBlacklist,
|
||||
} from '@/api/aigateway'
|
||||
import AiChannelPanel from '@/components/ai/AiChannelPanel.vue'
|
||||
import AiKeyPanel from '@/components/ai/AiKeyPanel.vue'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
import type { AiModelBlacklistItem } from '@/types/api'
|
||||
|
||||
const message = useToast()
|
||||
const dialog = useDialog()
|
||||
|
||||
const keys = useAsync(listAiKeys)
|
||||
const channels = useAsync(listAiChannels)
|
||||
const models = useAsync(listAiModels)
|
||||
const blacklist = useAsync(listAiBlacklist)
|
||||
|
||||
const baseUrl = `${window.location.origin}/ai/v1`
|
||||
|
||||
const endpoints = [
|
||||
{ method: 'POST', path: '/chat/completions', note: 'OpenAI · 流式/非流式' },
|
||||
{ method: 'POST', path: '/responses', note: 'OpenAI Responses · 无状态子集' },
|
||||
{ method: 'POST', path: '/messages', note: 'Claude · 流式/非流式' },
|
||||
{ method: 'POST', path: '/embeddings', note: 'OpenAI Embeddings · 向量化' },
|
||||
@@ -43,9 +52,12 @@ const modelGroups = computed(() => {
|
||||
.map(([vendor, ids]) => ({ vendor, label: VENDOR_LABEL[vendor] ?? vendor, ids: ids.sort() }))
|
||||
})
|
||||
|
||||
/** 手风琴:同时只展开一个厂商,配合展开区限高控制卡片总高度 */
|
||||
/** 手风琴:同时只展开一个分组(厂商或黑名单),配合展开区限高控制卡片总高度 */
|
||||
const expandedVendor = ref<string | null>(null)
|
||||
|
||||
/** 黑名单在手风琴中的分组 key(厂商 key 取自模型名前缀,不会与此冲突) */
|
||||
const BLACKLIST_KEY = '__blacklist__'
|
||||
|
||||
function toggleVendor(vendor: string) {
|
||||
expandedVendor.value = expandedVendor.value === vendor ? null : vendor
|
||||
}
|
||||
@@ -71,9 +83,43 @@ async function copyModel(id: string) {
|
||||
}
|
||||
}
|
||||
|
||||
function refreshModels() {
|
||||
void models.run({ silent: true })
|
||||
void blacklist.run({ silent: true })
|
||||
}
|
||||
|
||||
function refreshChannels() {
|
||||
void channels.run({ silent: true })
|
||||
void models.run({ silent: true })
|
||||
refreshModels()
|
||||
}
|
||||
|
||||
/** 拉黑:删除全部渠道缓存中的该模型,后续同步 / 探测均过滤 */
|
||||
function banModel(id: string) {
|
||||
dialog.warning({
|
||||
title: '加入模型黑名单',
|
||||
content: `拉黑「${id}」?全部渠道立即剔除该模型,后续同步与探测不再收录。`,
|
||||
positiveText: '拉黑',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
try {
|
||||
await addAiBlacklist(id)
|
||||
message.success(`已拉黑:${id}`)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败')
|
||||
}
|
||||
refreshModels()
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
async function unbanModel(b: AiModelBlacklistItem) {
|
||||
try {
|
||||
await removeAiBlacklist(b.id)
|
||||
message.success(`已移出黑名单:${b.name},重新同步渠道后恢复`)
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败')
|
||||
}
|
||||
refreshModels()
|
||||
}
|
||||
</script>
|
||||
|
||||
@@ -114,7 +160,7 @@ function refreshChannels() {
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3">
|
||||
<div>
|
||||
<div class="text-sm font-semibold">可用模型</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">按厂商分组;点击厂商展开,点击模型名复制</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">按厂商分组;点击厂商展开,点击模型名复制,✕ 加入黑名单</div>
|
||||
</div>
|
||||
<span class="text-xs text-ink-3">{{ models.data.value?.length ?? '…' }} 个</span>
|
||||
</div>
|
||||
@@ -144,16 +190,67 @@ function refreshChannels() {
|
||||
v-if="expandedVendor === g.vendor"
|
||||
class="flex max-h-36 flex-wrap content-start gap-1.5 overflow-y-auto pb-2 pl-5"
|
||||
>
|
||||
<button
|
||||
<span
|
||||
v-for="id in g.ids"
|
||||
:key="id"
|
||||
type="button"
|
||||
class="mono cursor-pointer rounded-[5px] border border-line bg-card px-2 py-0.5 text-[11.5px] hover:border-accent hover:text-accent"
|
||||
:title="`复制 ${id}`"
|
||||
@click="copyModel(id)"
|
||||
class="mono inline-flex items-center overflow-hidden rounded-[5px] border border-line bg-card text-[11.5px]"
|
||||
>
|
||||
{{ id }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer py-0.5 pr-1 pl-2 hover:text-accent"
|
||||
:title="`复制 ${id}`"
|
||||
@click="copyModel(id)"
|
||||
>
|
||||
{{ id }}
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
class="cursor-pointer self-stretch pr-1.5 pl-0.5 text-ink-3 hover:text-err"
|
||||
:title="`加入黑名单:${id}`"
|
||||
@click="banModel(id)"
|
||||
>
|
||||
<svg
|
||||
class="h-2.5 w-2.5"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.4"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
>
|
||||
<path d="M18 6 6 18M6 6l12 12" />
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="blacklist.data.value?.length" class="border-b border-line-soft py-1 last:border-b-0">
|
||||
<button
|
||||
type="button"
|
||||
class="flex w-full cursor-pointer items-center gap-2 py-1.5 text-left"
|
||||
@click="toggleVendor(BLACKLIST_KEY)"
|
||||
>
|
||||
<svg
|
||||
class="h-3 w-3 flex-none text-ink-3 transition-transform"
|
||||
:class="expandedVendor === BLACKLIST_KEY ? 'rotate-90' : ''"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.2"
|
||||
stroke-linecap="round" stroke-linejoin="round"
|
||||
>
|
||||
<path d="m9 18 6-6-6-6" />
|
||||
</svg>
|
||||
<span class="text-[13px] font-medium">模型黑名单</span>
|
||||
<span class="text-xs text-ink-3">{{ blacklist.data.value.length }} 个</span>
|
||||
</button>
|
||||
<div v-if="expandedVendor === BLACKLIST_KEY" class="pb-2 pl-5">
|
||||
<div class="mb-1.5 text-xs text-ink-3">同步与探测均过滤;点击移出,重新同步渠道后恢复入池</div>
|
||||
<div class="flex max-h-36 flex-wrap content-start gap-1.5 overflow-y-auto">
|
||||
<button
|
||||
v-for="b in blacklist.data.value"
|
||||
:key="b.id"
|
||||
type="button"
|
||||
class="mono cursor-pointer rounded-[5px] border border-err/30 bg-err/5 px-2 py-0.5 text-[11.5px] text-ink-3 line-through hover:border-err hover:text-err"
|
||||
:title="`移出黑名单:${b.name}`"
|
||||
@click="unbanModel(b)"
|
||||
>
|
||||
{{ b.name }}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="models.data.value && !models.data.value.length" class="py-2 text-xs text-ink-3">
|
||||
@@ -170,6 +267,7 @@ function refreshChannels() {
|
||||
:keys="keys.data.value ?? []"
|
||||
:loading="keys.loading.value"
|
||||
:groups="groups"
|
||||
:models="models.data.value ?? []"
|
||||
@changed="keys.run({ silent: true })"
|
||||
/>
|
||||
|
||||
|
||||
@@ -140,8 +140,15 @@ async function remove() {
|
||||
<template #trigger>
|
||||
<NButton size="small" type="error" ghost>删除租户</NButton>
|
||||
</template>
|
||||
删除「{{ config.data.value.alias }}」?将清理本面板中的关联任务、快照、回传事件与 Webhook
|
||||
密钥、告警及 AI 渠道数据;OCI 云端资源不会被删除(云端日志回传链路请先在“其他”页销毁)。
|
||||
<div class="max-w-80 text-[13px] leading-relaxed">
|
||||
<div class="font-medium">删除「{{ config.data.value.alias }}」?</div>
|
||||
<div class="mt-1 text-xs text-ink-3">
|
||||
将清理面板内关联数据:后台任务、测活 / 成本快照、回传事件、Webhook 密钥、告警规则与 AI 渠道。
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
OCI 云端资源不受影响;云端日志回传链路请先在「其他」页销毁。
|
||||
</div>
|
||||
</div>
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -222,7 +222,19 @@ function renderActions(row: OciConfigSummary): VNodeChild {
|
||||
trigger: () =>
|
||||
h(NButton, { size: 'tiny', quaternary: true, type: 'error' }, { default: () => '删除' }),
|
||||
default: () =>
|
||||
`删除「${row.alias}」?将清理本面板中的关联任务、快照、回传事件与 Webhook 密钥、告警及 AI 渠道数据;OCI 云端资源不会被删除(云端日志回传链路请先在“其他”页销毁)。`,
|
||||
h('div', { class: 'max-w-80 text-[13px] leading-relaxed' }, [
|
||||
h('div', { class: 'font-medium' }, `删除「${row.alias}」?`),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'mt-1 text-xs text-ink-3' },
|
||||
'将清理面板内关联数据:后台任务、测活 / 成本快照、回传事件、Webhook 密钥、告警规则与 AI 渠道。',
|
||||
),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'mt-0.5 text-xs text-ink-3' },
|
||||
'OCI 云端资源不受影响;云端日志回传链路请先在「其他」页销毁。',
|
||||
),
|
||||
]),
|
||||
},
|
||||
),
|
||||
])
|
||||
|
||||
Reference in New Issue
Block a user