+26
-15
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NTabPane, NTabs } from 'naive-ui'
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import {
|
||||
@@ -14,6 +15,9 @@ import { useToast } from '@/composables/useToast'
|
||||
|
||||
const message = useToast()
|
||||
|
||||
/** 页面重构为双 Tab:管理面(密钥+渠道)与使用面(接入+模型)不再同屏堆叠 */
|
||||
const tab = ref('manage')
|
||||
|
||||
const keys = useAsync(listAiKeys)
|
||||
const channels = useAsync(listAiChannels)
|
||||
const models = useAsync(listAiModels)
|
||||
@@ -90,11 +94,16 @@ function refreshChannels() {
|
||||
<div class="flex flex-wrap items-baseline gap-2.5">
|
||||
<h1 class="page-title">AI 网关</h1>
|
||||
<span class="text-[13px] text-ink-3">
|
||||
把有 Generative AI 配额的租户接入号池,对外提供 OpenAI / Claude 格式的标准接口
|
||||
对外提供 OpenAI / Claude 格式的标准接口
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="grid grid-cols-2 items-start gap-4 max-lg:grid-cols-1">
|
||||
<NTabs v-model:value="tab" type="line">
|
||||
<NTabPane name="manage" tab="密钥与渠道" />
|
||||
<NTabPane name="access" tab="接入与模型" />
|
||||
</NTabs>
|
||||
|
||||
<div v-if="tab === 'access'" class="grid grid-cols-2 items-start gap-4 max-lg:grid-cols-1">
|
||||
<div class="panel">
|
||||
<PanelHeader title="接入信息" />
|
||||
<div class="px-4.5 py-2">
|
||||
@@ -114,7 +123,7 @@ function refreshChannels() {
|
||||
</div>
|
||||
</div>
|
||||
<div class="border-t border-line-soft bg-wash px-4.5 py-2.5 text-xs leading-relaxed text-ink-3">
|
||||
鉴权:Authorization: Bearer ‹密钥› 或 x-api-key: ‹密钥› · 两种头都支持,OpenAI / Anthropic SDK 可直接指向 Base URL
|
||||
鉴权支持 Authorization: Bearer 与 x-api-key 两种头;SDK 直接指向 Base URL 即可
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -174,18 +183,20 @@ function refreshChannels() {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AiKeyPanel
|
||||
:keys="keys.data.value ?? []"
|
||||
:loading="keys.loading.value"
|
||||
:groups="groups"
|
||||
:models="models.data.value ?? []"
|
||||
@changed="keys.run({ silent: true })"
|
||||
/>
|
||||
<template v-if="tab === 'manage'">
|
||||
<AiKeyPanel
|
||||
:keys="keys.data.value ?? []"
|
||||
:loading="keys.loading.value"
|
||||
:groups="groups"
|
||||
:models="models.data.value ?? []"
|
||||
@changed="keys.run({ silent: true })"
|
||||
/>
|
||||
|
||||
<AiChannelPanel
|
||||
:channels="channels.data.value ?? []"
|
||||
:loading="channels.loading.value"
|
||||
@changed="refreshChannels"
|
||||
/>
|
||||
<AiChannelPanel
|
||||
:channels="channels.data.value ?? []"
|
||||
:loading="channels.loading.value"
|
||||
@changed="refreshChannels"
|
||||
/>
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -137,7 +137,7 @@ function renderActions(row: BvRow) {
|
||||
NButton,
|
||||
{
|
||||
size: 'tiny',
|
||||
tertiary: true,
|
||||
quaternary: true,
|
||||
disabled: gone || attached || row.lifecycleState !== 'AVAILABLE',
|
||||
title: attached ? '已挂载到实例' : undefined,
|
||||
onClick: () => openAttach(row),
|
||||
@@ -148,7 +148,7 @@ function renderActions(row: BvRow) {
|
||||
attached || gone
|
||||
? h(
|
||||
NButton,
|
||||
{ size: 'tiny', type: 'error', ghost: true, disabled: true, title: attached ? '仅未挂载状态可删除' : undefined },
|
||||
{ size: 'tiny', type: 'error', quaternary: true, disabled: true, title: attached ? '仅未挂载状态可删除' : undefined },
|
||||
{ default: () => '删除' },
|
||||
)
|
||||
: h(
|
||||
@@ -161,13 +161,13 @@ function renderActions(row: BvRow) {
|
||||
},
|
||||
{
|
||||
trigger: () =>
|
||||
h(NButton, { size: 'tiny', type: 'error', ghost: true }, { default: () => '删除' }),
|
||||
h(NButton, { size: 'tiny', type: 'error', quaternary: true }, { default: () => '删除' }),
|
||||
},
|
||||
)
|
||||
return h('div', { class: 'flex items-center gap-1.5' }, [
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'tiny', tertiary: true, disabled: gone, onClick: () => openEdit(row) },
|
||||
{ size: 'tiny', quaternary: true, disabled: gone, onClick: () => openEdit(row) },
|
||||
{ default: () => '编辑' },
|
||||
),
|
||||
attachBtn,
|
||||
@@ -276,7 +276,7 @@ const columns = computed<DataTableColumns<BvRow>>(() => [
|
||||
:row-key="(r: BvRow) => r.id"
|
||||
/>
|
||||
<FootNote>
|
||||
引导卷随实例创建产生,不提供单独创建 · 挂载须实例同可用域且处于 STOPPED · 仅未挂载状态可删除
|
||||
挂载须实例同可用域且处于 STOPPED · 仅未挂载状态可删除
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -40,6 +40,7 @@ import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
import OcidText from '@/components/OcidText.vue'
|
||||
import RenameModal from '@/components/RenameModal.vue'
|
||||
import { fmtTime, shortAd, truncOcid, vpusLabel } from '@/composables/useFormat'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import {
|
||||
@@ -56,6 +57,10 @@ const message = useToast()
|
||||
const dialog = useDialog()
|
||||
const { alias: regionAlias } = useRegionAlias()
|
||||
|
||||
/** 移动端 hero 元信息默认收起省首屏 */
|
||||
const isMobile = useIsMobile()
|
||||
const heroExpanded = ref(false)
|
||||
|
||||
const cfgId = computed(() => Number(route.params.cfgId))
|
||||
const instanceId = computed(() => String(route.params.instanceId))
|
||||
/** 实例所在区域:列表页跳转时经 query 传入,缺失时回退实例对象上的短名(后端已归一化) */
|
||||
@@ -419,24 +424,47 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
<h1 class="page-title">{{ instance.data.value.displayName }}</h1>
|
||||
<LifecycleBadge :state="instance.data.value.lifecycleState" />
|
||||
<div class="flex-1" />
|
||||
<NButton v-if="isStopped" size="small" :disabled="isEnded" @click="power('START')">
|
||||
启动
|
||||
</NButton>
|
||||
<NButton v-else size="small" :disabled="isEnded" @click="power('STOP')">停止</NButton>
|
||||
<NButton size="small" :disabled="isEnded" @click="power('RESET')">重启</NButton>
|
||||
<NButton size="small" type="error" ghost :disabled="isEnded" @click="terminate">
|
||||
终止
|
||||
</NButton>
|
||||
<NDropdown
|
||||
:options="moreOptions"
|
||||
:disabled="isEnded"
|
||||
@select="(key: string) => power(key as PowerAction)"
|
||||
>
|
||||
<NButton size="small" quaternary :disabled="isEnded">⋯</NButton>
|
||||
</NDropdown>
|
||||
<!-- 操作按钮组:移动端整组独立成行,不随标题换行散落 -->
|
||||
<div class="flex items-center gap-2 max-md:w-full">
|
||||
<NButton v-if="isStopped" size="small" :disabled="isEnded" @click="power('START')">
|
||||
启动
|
||||
</NButton>
|
||||
<NButton v-else size="small" :disabled="isEnded" @click="power('STOP')">停止</NButton>
|
||||
<NButton size="small" :disabled="isEnded" @click="power('RESET')">重启</NButton>
|
||||
<NButton size="small" type="error" ghost :disabled="isEnded" @click="terminate">
|
||||
终止
|
||||
</NButton>
|
||||
<NDropdown
|
||||
:options="moreOptions"
|
||||
:disabled="isEnded"
|
||||
@select="(key: string) => power(key as PowerAction)"
|
||||
>
|
||||
<NButton size="small" quaternary :disabled="isEnded">⋯</NButton>
|
||||
</NDropdown>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-5 gap-4 max-lg:grid-cols-3 max-md:grid-cols-1">
|
||||
<button
|
||||
v-if="isMobile"
|
||||
class="mt-2 flex cursor-pointer items-center gap-1 text-xs text-ink-3"
|
||||
@click="heroExpanded = !heroExpanded"
|
||||
>
|
||||
{{ heroExpanded ? '收起详情' : '展开详情' }}
|
||||
<svg
|
||||
class="h-3 w-3 transition-transform"
|
||||
:class="heroExpanded ? 'rotate-180' : ''"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="!isMobile || heroExpanded"
|
||||
class="mt-3 grid grid-cols-5 gap-4 max-lg:grid-cols-3 max-md:grid-cols-1"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">租户</div>
|
||||
<div class="mt-0.5 truncate text-[13px]" :title="config.data.value?.tenancyName">
|
||||
@@ -470,7 +498,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
<div class="grid grid-cols-2 items-start gap-4 max-lg:grid-cols-1">
|
||||
<!-- 基本信息 -->
|
||||
<div v-if="instance.data.value" class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">基本信息</div>
|
||||
<NButton size="tiny" quaternary :disabled="isEnded" @click="showRename = true">
|
||||
重命名
|
||||
@@ -546,7 +574,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
<div class="border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">网络</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
主网卡地址快捷视图;全部网卡与各卡 IPv6 管理见下方「网卡(VNIC)」
|
||||
主网卡地址快捷视图
|
||||
</div>
|
||||
</div>
|
||||
<div class="flex flex-col px-4.5 py-1.5">
|
||||
@@ -599,7 +627,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
|
||||
<!-- 引导卷 -->
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">引导卷</div>
|
||||
<NButton
|
||||
size="small"
|
||||
@@ -671,7 +699,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
|
||||
<!-- 块存储卷 -->
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">块存储卷</div>
|
||||
<NButton
|
||||
size="small"
|
||||
@@ -696,7 +724,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
|
||||
<!-- 控制台连接 -->
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">控制台连接(VNC / 串口)</div>
|
||||
<div class="flex items-center gap-2">
|
||||
<NButton
|
||||
@@ -723,9 +751,7 @@ const consoleColumns: DataTableColumns<ConsoleConnection> = [
|
||||
:row-key="(r: ConsoleConnection) => r.id"
|
||||
/>
|
||||
<FootNote>
|
||||
串行终端 / 网页 VNC 在浏览器内直连(创建会话时自动清理旧连接,二者同时只能开一个)·
|
||||
「创建连接」用于本地 ssh / VNC 客户端:公钥仅支持 RSA,串口直接执行串口连接串,VNC
|
||||
执行后客户端连 localhost:5900 · 串口登录需实例本地账户密码
|
||||
串行终端 / 网页 VNC 浏览器内直连,同时只能开一个 · 「创建连接」供本地 ssh / VNC 客户端使用(公钥仅支持 RSA)
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
|
||||
@@ -16,7 +16,8 @@ import CompartmentSelect from '@/components/CompartmentSelect.vue'
|
||||
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
|
||||
import CreateInstanceModal from '@/components/instance/CreateInstanceModal.vue'
|
||||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import MobileCardList from '@/components/MobileCardList.vue'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
import { shortAd } from '@/composables/useFormat'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { INSTANCE_TRANSIENT_STATES, useTransientPoll } from '@/composables/useTransientPoll'
|
||||
@@ -30,10 +31,11 @@ interface InstanceRow extends Instance {
|
||||
}
|
||||
|
||||
const message = useToast()
|
||||
const { alias } = useRegionAlias()
|
||||
|
||||
/** 租户 / 区域 / 区间来自侧栏全局作用域,页内只做区间切换与搜索 */
|
||||
const scope = useScopeStore()
|
||||
/** ≤767px 表格换卡片列表(设计稿 B2) */
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
const keyword = ref('')
|
||||
const showCreate = ref(false)
|
||||
@@ -148,14 +150,11 @@ const columns = computed<DataTableColumns<InstanceRow>>(() => [
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '区域 / AD',
|
||||
key: 'region',
|
||||
minWidth: 170,
|
||||
render: (row) =>
|
||||
h('div', [
|
||||
h('div', { class: 'text-[13px]' }, alias(row.region)),
|
||||
h('div', { class: 'text-xs text-ink-3 mt-px' }, shortAd(row.availabilityDomain)),
|
||||
]),
|
||||
// 区域随顶栏全局作用域已确定,列内只标可用域
|
||||
title: 'AD',
|
||||
key: 'availabilityDomain',
|
||||
minWidth: 100,
|
||||
render: (row) => h('span', { class: 'text-[13px]' }, shortAd(row.availabilityDomain)),
|
||||
},
|
||||
{
|
||||
title: '公网 IP',
|
||||
@@ -239,6 +238,7 @@ const columns = computed<DataTableColumns<InstanceRow>>(() => [
|
||||
</div>
|
||||
</div>
|
||||
<NDataTable
|
||||
v-if="!isMobile"
|
||||
:columns="columns"
|
||||
:data="filtered"
|
||||
:loading="rows.loading.value"
|
||||
@@ -248,6 +248,63 @@ const columns = computed<DataTableColumns<InstanceRow>>(() => [
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MobileCardList
|
||||
v-if="isMobile && !isDead"
|
||||
:items="filtered"
|
||||
:loading="rows.loading.value"
|
||||
:item-key="(r: InstanceRow) => r.id"
|
||||
empty-text="当前作用域下没有实例"
|
||||
>
|
||||
<template #default="{ item: r }">
|
||||
<div class="flex min-w-0 items-center justify-between gap-2">
|
||||
<RouterLink
|
||||
class="min-w-0 truncate text-[14px] font-medium text-ink"
|
||||
:to="{
|
||||
name: 'instance-detail',
|
||||
params: { cfgId: r.cfgId, instanceId: r.id },
|
||||
query: { region: scope.region || undefined },
|
||||
}"
|
||||
>
|
||||
{{ r.displayName }}
|
||||
</RouterLink>
|
||||
<LifecycleBadge :state="r.lifecycleState" class="flex-none" />
|
||||
</div>
|
||||
<div class="mt-0.5 truncate text-xs text-ink-3">{{ r.cfgAlias }}</div>
|
||||
<div class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-ink-2">
|
||||
<span>{{ r.shape }}</span>
|
||||
<span>{{ r.ocpus }} OCPU · {{ r.memoryInGBs }} GB</span>
|
||||
<span>{{ shortAd(r.availabilityDomain) }}</span>
|
||||
<span class="mono">{{ r.publicIp || '—' }}</span>
|
||||
</div>
|
||||
<div class="mt-2.5 flex items-center gap-1 border-t border-line-soft pt-2">
|
||||
<NButton
|
||||
v-if="r.lifecycleState === 'STOPPED'"
|
||||
size="tiny"
|
||||
quaternary
|
||||
:disabled="isEnded(r)"
|
||||
@click="power(r, 'START')"
|
||||
>
|
||||
启动
|
||||
</NButton>
|
||||
<NButton v-else size="tiny" quaternary :disabled="isEnded(r)" @click="power(r, 'STOP')">
|
||||
停止
|
||||
</NButton>
|
||||
<NButton size="tiny" quaternary :disabled="isEnded(r)" @click="power(r, 'RESET')">
|
||||
重启
|
||||
</NButton>
|
||||
<NButton
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="error"
|
||||
:disabled="isEnded(r)"
|
||||
@click="confirmTerminate(r)"
|
||||
>
|
||||
终止
|
||||
</NButton>
|
||||
</div>
|
||||
</template>
|
||||
</MobileCardList>
|
||||
|
||||
<CreateInstanceModal
|
||||
v-model:show="showCreate"
|
||||
:cfg-id="scope.currentConfig?.id ?? null"
|
||||
|
||||
@@ -152,7 +152,7 @@ const columns = computed<DataTableColumns<VcnRow>>(() => [
|
||||
VCN
|
||||
</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
创建 VCN 默认启用 IPv6(Oracle GUA /56)并将默认安全列表替换为出入全放行;如需收紧请再更新安全列表
|
||||
新建默认启用 IPv6 并放行全部出入流量,可在 VCN 详情收紧
|
||||
</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" @click="showCreate = true">创建 VCN</NButton>
|
||||
|
||||
@@ -306,10 +306,9 @@ const columns = computed<DataTableColumns<Bucket>>(() => [
|
||||
:cfg-id="cfgId"
|
||||
:region="scope.region || undefined"
|
||||
:bucket="activeBucket"
|
||||
:versioning-on="activeBucketInfo?.versioningOn"
|
||||
:visibility="activeBucketInfo?.visibility"
|
||||
:namespace="activeBucketInfo?.namespace"
|
||||
:info="activeBucketInfo"
|
||||
@back="backToList"
|
||||
@settings="activeBucketInfo && openSettings(activeBucketInfo)"
|
||||
/>
|
||||
|
||||
<FormModal
|
||||
|
||||
@@ -156,7 +156,7 @@ const currencySymbol = computed(() => {
|
||||
|
||||
<div class="panel">
|
||||
<div
|
||||
class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5"
|
||||
class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-3.5"
|
||||
>
|
||||
<div class="text-sm font-semibold">租户测活状态</div>
|
||||
<div class="text-[12.5px] text-ink-3">
|
||||
@@ -202,7 +202,7 @@ const currencySymbol = computed(() => {
|
||||
|
||||
<!-- 最近任务执行 -->
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">最近任务执行</div>
|
||||
<RouterLink
|
||||
class="text-[13px] font-medium text-accent hover:text-accent-hover"
|
||||
|
||||
@@ -9,10 +9,10 @@ const panel = ref<InstanceType<typeof ProxyPanel> | null>(null)
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
|
||||
<div class="flex flex-wrap items-center gap-2.5">
|
||||
<div class="flex items-center gap-2.5">
|
||||
<h1 class="page-title">代理</h1>
|
||||
<span class="text-[13px] text-ink-3">出站代理管理,租户关联后 SDK 请求经代理发出</span>
|
||||
<div class="ml-auto flex items-center gap-2">
|
||||
<span class="truncate text-[13px] text-ink-3">出站代理管理</span>
|
||||
<div class="ml-auto flex flex-none items-center gap-2">
|
||||
<NButton size="small" @click="panel?.openImport()">批量导入</NButton>
|
||||
<NButton size="small" type="primary" @click="panel?.openCreate()">新增代理</NButton>
|
||||
</div>
|
||||
|
||||
@@ -89,11 +89,11 @@ const eventRows: { key: keyof NotifyEventsSetting; label: string; hint: string }
|
||||
{ key: 'taskStop', label: '任务停止', hint: '抢机连续鉴权失败熔断停止时推送' },
|
||||
{ key: 'loginLock', label: '登录锁定', hint: '同一 IP+用户名连续失败达阈值时推送' },
|
||||
{ key: 'modelDeprecated', label: '模型弃用预警', hint: 'AI 网关在池模型 30 天内被 OCI 弃用时随每日探测推送' },
|
||||
{ key: 'logEventInstance', label: '实例生命周期', hint: '终止 / 电源操作(Terminate、InstanceAction);创建不回传,避免抢机噪声' },
|
||||
{ key: 'logEventInstance', label: '实例生命周期', hint: '终止 / 电源操作;创建不回传,避免抢机噪声' },
|
||||
{ key: 'logEventIdentity', label: '用户与凭据', hint: '用户增删改、API Key 增删、能力变更' },
|
||||
{ key: 'logEventPolicy', label: '策略变更', hint: 'IAM Policy 创建 / 修改 / 删除' },
|
||||
{ key: 'logEventRegion', label: '区域订阅', hint: '订阅新区域(CreateRegionSubscription)' },
|
||||
{ key: 'logEventLogin', label: '控制台登录', hint: 'OCI 控制台交互登录(InteractiveLogin)' },
|
||||
{ key: 'logEventRegion', label: '区域订阅', hint: '订阅新区域' },
|
||||
{ key: 'logEventLogin', label: '控制台登录', hint: 'OCI 控制台登录' },
|
||||
]
|
||||
|
||||
/** 通知管理按业务域分组展示;新事件类型加入对应组即可 */
|
||||
@@ -206,10 +206,10 @@ const savingTask = ref(false)
|
||||
|
||||
/** 抢机熔断流程步骤条;hot 标记可配置的关键一步 */
|
||||
const CIRCUIT_STEPS = [
|
||||
{ t: '连续失败', d: '抢机请求连续返回 NotAuthenticated', hot: false },
|
||||
{ t: '连续失败', d: '抢机连续 NotAuthenticated', hot: false },
|
||||
{ t: '达到阈值', d: '失败次数达到下方配置值', hot: true },
|
||||
{ t: '熔断停止', d: '任务置为已熔断并退出调度', hot: false },
|
||||
{ t: '推送与恢复', d: '发送「任务停止」通知,修复 Key 后在任务页重新启用', hot: false },
|
||||
{ t: '推送与恢复', d: '发送「任务停止」通知', hot: false },
|
||||
]
|
||||
|
||||
watch(
|
||||
@@ -387,7 +387,7 @@ async function sendTest() {
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold">通知管理</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
按事件类型控制是否推送;开关即时生效,事件仅在状态变化时触发
|
||||
按事件类型控制是否推送;事件仅在状态变化时触发
|
||||
</div>
|
||||
</div>
|
||||
<span
|
||||
@@ -588,7 +588,7 @@ async function sendTest() {
|
||||
<div class="border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">访问防护</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
登录守卫与全局 IP 限速,超限返回 429;锁定事件可推送告警(通知管理 → 登录锁定)
|
||||
登录守卫与全局 IP 限速,超限返回 429
|
||||
</div>
|
||||
</div>
|
||||
<div class="grid grid-cols-2 gap-x-4 px-4.5 py-3.5 max-md:grid-cols-1">
|
||||
@@ -694,8 +694,7 @@ async function sendTest() {
|
||||
</FormField>
|
||||
</div>
|
||||
<FootNote>
|
||||
登录锁定为内存计数,重启服务即清零;真实IP解析对 X-Forwarded-For
|
||||
取链尾(反代追加的直连对端),链首可被客户端伪造不采信
|
||||
登录锁定为内存计数,重启即清零
|
||||
</FootNote>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -21,6 +21,7 @@ import SubsTab from '@/components/tenant/SubsTab.vue'
|
||||
import UsersTab from '@/components/tenant/UsersTab.vue'
|
||||
import { aliveMeta } from '@/composables/useAliveStatus'
|
||||
import { fmtTime } from '@/composables/useFormat'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { useScopeStore } from '@/stores/scope'
|
||||
@@ -36,6 +37,10 @@ const cfgId = computed(() => Number(route.params.id))
|
||||
const tab = ref('quota')
|
||||
const verifying = ref(false)
|
||||
|
||||
/** 移动端 hero 元信息默认收起省首屏;域选择器移出 Tab suffix 独立成行 */
|
||||
const isMobile = useIsMobile()
|
||||
const heroExpanded = ref(false)
|
||||
|
||||
const config = useAsync(() => getConfig(cfgId.value))
|
||||
|
||||
/** 失联/暂停租户各 tab 拉不到云端数据,内容区整体换占位 */
|
||||
@@ -135,22 +140,45 @@ async function remove() {
|
||||
/>
|
||||
<AccountTypeBadge :type="config.data.value.accountType" />
|
||||
<div class="flex-1" />
|
||||
<NButton size="small" :loading="verifying" @click="verify">立即测活</NButton>
|
||||
<ConfirmPop
|
||||
:title="`删除「${config.data.value.alias}」?`"
|
||||
content="将清理面板内关联数据。"
|
||||
note="云端日志回传链路请先在「其他」页销毁"
|
||||
confirm-text="删除"
|
||||
placement="bottom-end"
|
||||
:on-confirm="remove"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton size="small" type="error" ghost>删除租户</NButton>
|
||||
</template>
|
||||
</ConfirmPop>
|
||||
<!-- 操作按钮组:移动端整组独立成行,不随标题换行散落 -->
|
||||
<div class="flex items-center gap-2 max-md:w-full">
|
||||
<NButton size="small" :loading="verifying" @click="verify">立即测活</NButton>
|
||||
<ConfirmPop
|
||||
:title="`删除「${config.data.value.alias}」?`"
|
||||
content="将清理面板内关联数据。"
|
||||
note="云端日志回传链路请先在「其他」页销毁"
|
||||
confirm-text="删除"
|
||||
placement="bottom-end"
|
||||
:on-confirm="remove"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton size="small" type="error" ghost>删除租户</NButton>
|
||||
</template>
|
||||
</ConfirmPop>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="mt-3 grid grid-cols-4 gap-4 pb-4 max-lg:grid-cols-2 max-md:grid-cols-1">
|
||||
<button
|
||||
v-if="isMobile"
|
||||
class="mt-2 flex cursor-pointer items-center gap-1 text-xs text-ink-3"
|
||||
@click="heroExpanded = !heroExpanded"
|
||||
>
|
||||
{{ heroExpanded ? '收起详情' : '展开详情' }}
|
||||
<svg
|
||||
class="h-3 w-3 transition-transform"
|
||||
:class="heroExpanded ? 'rotate-180' : ''"
|
||||
viewBox="0 0 24 24"
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="!isMobile || heroExpanded"
|
||||
class="mt-3 grid grid-cols-4 gap-4 pb-4 max-lg:grid-cols-2 max-md:grid-cols-1"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">租户名</div>
|
||||
<div class="mono mt-0.5 truncate text-[13px]">{{ config.data.value.tenancyName }}</div>
|
||||
@@ -168,6 +196,7 @@ async function remove() {
|
||||
<div class="mt-0.5 text-[13px]">{{ fmtTime(config.data.value.createdAt).slice(0, 10) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div v-if="isMobile" class="pt-2"></div>
|
||||
|
||||
<NTabs v-model:value="tab" type="line" class="tenant-tabs">
|
||||
<NTabPane name="quota" tab="配额" />
|
||||
@@ -178,13 +207,18 @@ async function remove() {
|
||||
<NTabPane name="subs" tab="订阅" />
|
||||
<NTabPane name="audit" tab="审计日志" />
|
||||
<NTabPane name="misc" tab="其他" />
|
||||
<template #suffix>
|
||||
<template v-if="!isMobile" #suffix>
|
||||
<!-- 多域租户:用户/IDP/其他 tab 按选中域读写 -->
|
||||
<div v-if="showDomainPicker" class="w-64 max-md:w-44">
|
||||
<div v-if="showDomainPicker" class="w-64">
|
||||
<NSelect v-model:value="domainId" size="small" :options="domainOptions" placeholder="身份域" />
|
||||
</div>
|
||||
</template>
|
||||
</NTabs>
|
||||
<!-- 移动端:域选择器独立成行,不与 Tab 条抢横向空间 -->
|
||||
<div v-if="isMobile && showDomainPicker" class="flex items-center gap-2 pt-2.5 pb-3">
|
||||
<span class="flex-none text-xs text-ink-3">身份域</span>
|
||||
<NSelect v-model:value="domainId" size="small" :options="domainOptions" placeholder="身份域" />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<template v-if="config.data.value">
|
||||
|
||||
@@ -10,10 +10,12 @@ import ConfirmPop from '@/components/ConfirmPop.vue'
|
||||
import AccountTypeBadge from '@/components/AccountTypeBadge.vue'
|
||||
import EditConfigModal from '@/components/config/EditConfigModal.vue'
|
||||
import ImportConfigModal from '@/components/config/ImportConfigModal.vue'
|
||||
import MobileCardList from '@/components/MobileCardList.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import { parseSnatch, scopeCfgIds, STATUS_META, TYPE_LABEL } from '@/components/task/taskDisplay'
|
||||
import { aliveMeta } from '@/composables/useAliveStatus'
|
||||
import { fmtRelative, fmtTime } from '@/composables/useFormat'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { useScopeStore } from '@/stores/scope'
|
||||
@@ -24,6 +26,8 @@ const message = useToast()
|
||||
const router = useRouter()
|
||||
const { alias: regionAlias } = useRegionAlias()
|
||||
const scope = useScopeStore()
|
||||
/** ≤767px 表格换卡片列表(设计稿 B2) */
|
||||
const isMobile = useIsMobile()
|
||||
|
||||
/** 租户增删改 / 测活后本页列表与全局作用域选择器一起刷新 */
|
||||
function reloadConfigs() {
|
||||
@@ -353,6 +357,7 @@ const columns = computed<DataTableColumns<OciConfigSummary>>(() => [
|
||||
</div>
|
||||
</div>
|
||||
<NDataTable
|
||||
v-if="!isMobile"
|
||||
:columns="columns"
|
||||
:data="filtered"
|
||||
:loading="configs.loading.value"
|
||||
@@ -362,6 +367,66 @@ const columns = computed<DataTableColumns<OciConfigSummary>>(() => [
|
||||
/>
|
||||
</div>
|
||||
|
||||
<MobileCardList
|
||||
v-if="isMobile"
|
||||
:items="filtered"
|
||||
:loading="configs.loading.value"
|
||||
:item-key="(c: OciConfigSummary) => c.id"
|
||||
empty-text="没有匹配的租户"
|
||||
>
|
||||
<template #default="{ item: c }">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<RouterLink
|
||||
class="min-w-0 truncate text-[14px] font-medium text-ink"
|
||||
:to="{ name: 'tenant-detail', params: { id: c.id } }"
|
||||
>
|
||||
{{ c.alias }}
|
||||
</RouterLink>
|
||||
<AccountTypeBadge :type="c.accountType" class="flex-none" />
|
||||
<StatusBadge
|
||||
:kind="aliveMeta(c.aliveStatus).kind"
|
||||
:label="aliveMeta(c.aliveStatus).label"
|
||||
class="flex-none"
|
||||
/>
|
||||
</div>
|
||||
<div class="mono mt-0.5 truncate text-xs text-ink-3">{{ c.tenancyName }}</div>
|
||||
<div class="mt-1.5 flex flex-wrap items-center gap-x-3 gap-y-1 text-xs text-ink-2">
|
||||
<span>{{ regionAlias(c.region) }}</span>
|
||||
<span v-if="c.group">{{ c.group }}</span>
|
||||
<span class="text-ink-3">导入 {{ fmtTime(c.createdAt).slice(0, 10) }}</span>
|
||||
<span class="text-ink-3">测活 {{ fmtRelative(c.lastVerifiedAt) }}</span>
|
||||
<span v-if="tasksFor(c.id).length" class="text-ink-3">
|
||||
任务 {{ tasksFor(c.id).length }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="mt-2.5 flex items-center gap-1 border-t border-line-soft pt-2">
|
||||
<NButton
|
||||
size="tiny"
|
||||
quaternary
|
||||
:disabled="scope.cfgId === c.id && scope.region === c.region"
|
||||
@click="setGlobal(c)"
|
||||
>
|
||||
设为全局
|
||||
</NButton>
|
||||
<NButton size="tiny" quaternary :loading="verifying === c.id" @click="verify(c)">
|
||||
测活
|
||||
</NButton>
|
||||
<NButton size="tiny" quaternary @click="edit(c)">修改</NButton>
|
||||
<ConfirmPop
|
||||
:title="`删除「${c.alias}」?`"
|
||||
content="将清理面板内关联数据。"
|
||||
note="云端日志回传链路请先在「其他」页销毁"
|
||||
confirm-text="删除"
|
||||
:on-confirm="() => remove(c)"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton size="tiny" quaternary type="error">删除</NButton>
|
||||
</template>
|
||||
</ConfirmPop>
|
||||
</div>
|
||||
</template>
|
||||
</MobileCardList>
|
||||
|
||||
<ImportConfigModal v-model:show="showImport" @imported="reloadConfigs()" />
|
||||
<EditConfigModal v-model:show="showEdit" :config="editing" @updated="reloadConfigs()" />
|
||||
</div>
|
||||
|
||||
+39
-15
@@ -25,12 +25,16 @@ import RenameModal from '@/components/RenameModal.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import { fmtTime } from '@/composables/useFormat'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { useIsMobile } from '@/composables/useIsMobile'
|
||||
import type { Ipv6Step, SecurityList, SecurityRule, Subnet } from '@/types/api'
|
||||
import { useToast } from '@/composables/useToast'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useToast()
|
||||
/** 移动端 hero meta 默认收起,与租户/实例详情一致 */
|
||||
const isMobile = useIsMobile()
|
||||
const heroExpanded = ref(false)
|
||||
|
||||
const cfgId = computed(() => Number(route.params.cfgId))
|
||||
const vcnId = computed(() => String(route.params.vcnId))
|
||||
@@ -279,21 +283,41 @@ const stepColumns: DataTableColumns<Ipv6Step> = [
|
||||
:dot="false"
|
||||
/>
|
||||
<div class="flex-1" />
|
||||
<NButton size="small" @click="showRename = true">重命名</NButton>
|
||||
<ConfirmPop
|
||||
title="删除 VCN?"
|
||||
content="其关联资源将一并删除。"
|
||||
note="子网仍被实例占用时会失败,需先终止实例"
|
||||
confirm-text="删除"
|
||||
placement="bottom-end"
|
||||
:on-confirm="removeVcn"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton size="small" type="error" ghost>删除 VCN</NButton>
|
||||
</template>
|
||||
</ConfirmPop>
|
||||
<!-- 操作按钮组:移动端整组独立成行,不随标题换行散落 -->
|
||||
<div class="flex items-center gap-2 max-md:w-full">
|
||||
<NButton size="small" @click="showRename = true">重命名</NButton>
|
||||
<ConfirmPop
|
||||
title="删除 VCN?"
|
||||
content="其关联资源将一并删除。"
|
||||
note="子网仍被实例占用时会失败,需先终止实例"
|
||||
confirm-text="删除"
|
||||
placement="bottom-end"
|
||||
:on-confirm="removeVcn"
|
||||
>
|
||||
<template #trigger>
|
||||
<NButton size="small" type="error" ghost>删除 VCN</NButton>
|
||||
</template>
|
||||
</ConfirmPop>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3 grid grid-cols-4 gap-4 max-lg:grid-cols-2 max-md:grid-cols-1">
|
||||
<button
|
||||
v-if="isMobile"
|
||||
class="mt-2 flex cursor-pointer items-center gap-1 text-xs text-ink-3"
|
||||
@click="heroExpanded = !heroExpanded"
|
||||
>
|
||||
{{ heroExpanded ? '收起详情' : '展开详情' }}
|
||||
<svg
|
||||
class="h-3 w-3 transition-transform"
|
||||
:class="heroExpanded ? 'rotate-180' : ''"
|
||||
viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2"
|
||||
>
|
||||
<path d="m6 9 6 6 6-6" />
|
||||
</svg>
|
||||
</button>
|
||||
<div
|
||||
v-if="!isMobile || heroExpanded"
|
||||
class="mt-3 grid grid-cols-4 gap-4 max-lg:grid-cols-2 max-md:grid-cols-1"
|
||||
>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">IPv4 CIDR</div>
|
||||
<div class="mono mt-0.5 text-[13px]">{{ (vcn.data.value.cidrBlocks ?? []).join(', ') }}</div>
|
||||
@@ -316,7 +340,7 @@ const stepColumns: DataTableColumns<Ipv6Step> = [
|
||||
</div>
|
||||
|
||||
<div v-if="vcn.data.value" class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-2.5">
|
||||
<div class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-2.5">
|
||||
<div class="text-[13.5px] font-semibold">便捷功能</div>
|
||||
<div class="text-xs text-ink-3">执行前均会二次确认</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user