初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,308 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NPopconfirm,
|
||||
NSpin,
|
||||
useMessage,
|
||||
type DataTableColumns,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, ref } from 'vue'
|
||||
import { RouterLink, useRoute, useRouter } from 'vue-router'
|
||||
|
||||
import { deleteVcn, enableVcnIpv6, getVcn, listSecurityLists, listSubnets, renameVcn } from '@/api/networks'
|
||||
import FootNote from '@/components/FootNote.vue'
|
||||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
import RenameModal from '@/components/RenameModal.vue'
|
||||
import StatusBadge from '@/components/StatusBadge.vue'
|
||||
import { fmtTime } from '@/composables/useFormat'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import type { Ipv6Step, SecurityList, Subnet } from '@/types/api'
|
||||
|
||||
const route = useRoute()
|
||||
const router = useRouter()
|
||||
const message = useMessage()
|
||||
|
||||
const cfgId = computed(() => Number(route.params.cfgId))
|
||||
const vcnId = computed(() => String(route.params.vcnId))
|
||||
/** 列表页跳转时带上查询区域,非主区域 VCN 的所有请求都要发往该区域 */
|
||||
const region = computed(() => (route.query.region as string) || undefined)
|
||||
|
||||
const vcn = useAsync(() => getVcn(cfgId.value, vcnId.value, region.value))
|
||||
const subnets = useAsync(() => listSubnets(cfgId.value, vcnId.value, region.value))
|
||||
const seclists = useAsync(() => listSecurityLists(cfgId.value, vcnId.value, region.value))
|
||||
|
||||
const ipv6Steps = ref<Ipv6Step[] | null>(null)
|
||||
const enabling = ref(false)
|
||||
|
||||
async function runEnableIpv6() {
|
||||
enabling.value = true
|
||||
try {
|
||||
const report = await enableVcnIpv6(cfgId.value, vcnId.value, region.value)
|
||||
ipv6Steps.value = report.steps
|
||||
const failed = report.steps.filter((s) => s.status === 'failed').length
|
||||
if (failed) message.warning(`${failed} 个步骤失败,详见执行报告`)
|
||||
else message.success('一键 IPv6 执行完成')
|
||||
void vcn.run()
|
||||
void subnets.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '执行失败')
|
||||
} finally {
|
||||
enabling.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const removing = ref(false)
|
||||
|
||||
async function removeVcn() {
|
||||
removing.value = true
|
||||
try {
|
||||
await deleteVcn(cfgId.value, vcnId.value, region.value)
|
||||
message.success('VCN 已删除')
|
||||
void router.push({ name: 'network' })
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败')
|
||||
} finally {
|
||||
removing.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const showRename = ref(false)
|
||||
const renaming = ref(false)
|
||||
|
||||
async function doRename(name: string) {
|
||||
renaming.value = true
|
||||
try {
|
||||
await renameVcn(cfgId.value, vcnId.value, name, region.value)
|
||||
message.success('已重命名')
|
||||
showRename.value = false
|
||||
void vcn.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '重命名失败')
|
||||
} finally {
|
||||
renaming.value = false
|
||||
}
|
||||
}
|
||||
|
||||
const subnetColumns: DataTableColumns<Subnet> = [
|
||||
{
|
||||
title: '子网',
|
||||
key: 'displayName',
|
||||
minWidth: 170,
|
||||
render: (row) =>
|
||||
h('div', [
|
||||
h('div', { class: 'font-medium' }, row.displayName),
|
||||
h('div', { class: 'mono text-xs text-ink-3 mt-px' }, row.dnsLabel),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: 'IPv4 CIDR',
|
||||
key: 'cidrBlock',
|
||||
minWidth: 120,
|
||||
render: (r) => h('span', { class: 'mono' }, r.cidrBlock),
|
||||
},
|
||||
{
|
||||
title: 'IPv6 /64',
|
||||
key: 'ipv6CidrBlock',
|
||||
minWidth: 200,
|
||||
render: (r) =>
|
||||
r.ipv6CidrBlock
|
||||
? h('span', { class: 'mono text-[12.5px]' }, r.ipv6CidrBlock)
|
||||
: h('span', { class: 'text-xs text-ink-3' }, '未启用'),
|
||||
},
|
||||
{
|
||||
title: '公网 IP',
|
||||
key: 'prohibitPublicIp',
|
||||
width: 100,
|
||||
render: (r) =>
|
||||
h(StatusBadge, {
|
||||
kind: r.prohibitPublicIp ? 'stop' : 'run',
|
||||
label: r.prohibitPublicIp ? '禁止' : '允许',
|
||||
dot: false,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'lifecycleState',
|
||||
width: 120,
|
||||
render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
|
||||
},
|
||||
]
|
||||
|
||||
function ruleSummary(list: SecurityList): string {
|
||||
const ingress = list.ingressRules ?? []
|
||||
const egress = list.egressRules ?? []
|
||||
const ing = ingress.length
|
||||
const eg = egress.length
|
||||
const allOpen =
|
||||
ingress.some((r) => r.protocol === 'all' && r.source === '0.0.0.0/0') &&
|
||||
egress.some((r) => r.protocol === 'all' && r.destination === '0.0.0.0/0')
|
||||
return allOpen ? `出入全放行 · 入 ${ing} / 出 ${eg}` : `入 ${ing} / 出 ${eg}`
|
||||
}
|
||||
|
||||
const seclistColumns: DataTableColumns<SecurityList> = [
|
||||
{
|
||||
title: '安全列表',
|
||||
key: 'displayName',
|
||||
minWidth: 240,
|
||||
render: (row) => h('span', { class: 'font-medium' }, row.displayName),
|
||||
},
|
||||
{
|
||||
title: '规则',
|
||||
key: 'rules',
|
||||
minWidth: 180,
|
||||
render: (row) => h('span', { class: 'text-[13px] text-ink-2' }, ruleSummary(row)),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'lifecycleState',
|
||||
width: 120,
|
||||
render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
|
||||
},
|
||||
]
|
||||
|
||||
const stepColumns: DataTableColumns<Ipv6Step> = [
|
||||
{
|
||||
title: '步骤',
|
||||
key: 'step',
|
||||
minWidth: 170,
|
||||
render: (r) => h('span', { class: 'mono text-[12.5px]' }, r.step),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'status',
|
||||
width: 110,
|
||||
render: (r) =>
|
||||
h(StatusBadge, {
|
||||
kind: r.status === 'done' ? 'run' : r.status === 'skipped' ? 'check' : 'term',
|
||||
label: r.status,
|
||||
}),
|
||||
},
|
||||
{
|
||||
title: '详情',
|
||||
key: 'detail',
|
||||
minWidth: 260,
|
||||
render: (r) => h('span', { class: 'mono text-xs break-all' }, r.detail),
|
||||
},
|
||||
]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
|
||||
<div
|
||||
v-if="vcn.loading.value && !vcn.data.value"
|
||||
class="panel flex items-center justify-center py-20"
|
||||
>
|
||||
<NSpin size="small" />
|
||||
</div>
|
||||
<div v-else-if="vcn.error.value" class="panel px-5 py-10 text-center text-[13px] text-ink-3">
|
||||
{{ vcn.error.value }}
|
||||
</div>
|
||||
|
||||
<div v-if="vcn.data.value" class="panel px-5 py-4">
|
||||
<RouterLink
|
||||
class="mb-2 inline-flex items-center gap-1.5 text-[13px] text-ink-3 hover:text-ink"
|
||||
:to="{ name: 'network' }"
|
||||
>
|
||||
← 返回网络列表
|
||||
</RouterLink>
|
||||
<div class="flex flex-wrap items-center gap-3">
|
||||
<h1 class="page-title">{{ vcn.data.value.displayName }}</h1>
|
||||
<LifecycleBadge :state="vcn.data.value.lifecycleState" />
|
||||
<StatusBadge
|
||||
v-if="vcn.data.value.ipv6CidrBlocks?.length"
|
||||
kind="prov"
|
||||
label="IPv6 已启用"
|
||||
:dot="false"
|
||||
/>
|
||||
<div class="flex-1" />
|
||||
<NButton size="small" @click="showRename = true">重命名</NButton>
|
||||
<NPopconfirm @positive-click="removeVcn">
|
||||
<template #trigger>
|
||||
<NButton size="small" type="error" ghost :loading="removing" :disabled="removing">
|
||||
{{ removing ? '删除中…' : '删除 VCN' }}
|
||||
</NButton>
|
||||
</template>
|
||||
删除 VCN?将级联删除其子网、网关、路由等关联资源;子网仍被实例占用时会失败,需先终止实例
|
||||
</NPopconfirm>
|
||||
</div>
|
||||
<div 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>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">IPv6 CIDR</div>
|
||||
<div class="mono mt-0.5 text-[13px] break-all">
|
||||
{{ (vcn.data.value.ipv6CidrBlocks ?? []).join(', ') || '未启用' }}
|
||||
</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">DNS Label</div>
|
||||
<div class="mono mt-0.5 text-[13px]">{{ vcn.data.value.dnsLabel || '—' }}</div>
|
||||
</div>
|
||||
<div class="min-w-0">
|
||||
<div class="text-xs text-ink-3">创建时间</div>
|
||||
<div class="mt-0.5 text-[13px]">{{ fmtTime(vcn.data.value.timeCreated) }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="border-b border-line-soft px-4.5 py-3.5 text-sm font-semibold">子网</div>
|
||||
<NDataTable
|
||||
:columns="subnetColumns"
|
||||
:data="subnets.data.value ?? []"
|
||||
:loading="subnets.loading.value"
|
||||
:scroll-x="760"
|
||||
:row-key="(r: Subnet) => r.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="border-b border-line-soft px-4.5 py-3.5 text-sm font-semibold">安全列表</div>
|
||||
<NDataTable
|
||||
:columns="seclistColumns"
|
||||
:data="seclists.data.value ?? []"
|
||||
:loading="seclists.loading.value"
|
||||
:scroll-x="600"
|
||||
:row-key="(r: SecurityList) => r.id"
|
||||
/>
|
||||
<FootNote>
|
||||
更新安全列表时规则字段一经提供即全量替换;protocol 取 all / 6(TCP) / 17(UDP) / 1(ICMP) /
|
||||
58(ICMPv6)
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="text-sm font-semibold">一键启用 IPv6</div>
|
||||
<NButton size="small" type="primary" :loading="enabling" @click="runEnableIpv6">
|
||||
{{ ipv6Steps ? '重新执行' : '执行' }}
|
||||
</NButton>
|
||||
</div>
|
||||
<NDataTable
|
||||
v-if="ipv6Steps"
|
||||
:columns="stepColumns"
|
||||
:data="ipv6Steps"
|
||||
:scroll-x="620"
|
||||
:row-key="(r: Ipv6Step) => r.step"
|
||||
/>
|
||||
<div v-else class="px-4.5 py-4 text-[13px] text-ink-3">
|
||||
依次执行:VCN 分配 /56 → 子网分配 /64 → 默认路由补 ::/0 → 默认安全列表放行 IPv6
|
||||
出方向;全部步骤幂等,已配置的自动跳过。
|
||||
</div>
|
||||
<FootNote>
|
||||
只改默认路由表与默认安全列表;实例要实际获得 IPv6 还需在创建实例 / VNIC 时启用 IPv6 分配
|
||||
</FootNote>
|
||||
</div>
|
||||
|
||||
<RenameModal
|
||||
v-model:show="showRename"
|
||||
title="重命名 VCN"
|
||||
:current="vcn.data.value?.displayName ?? ''"
|
||||
:submitting="renaming"
|
||||
@submit="doRename"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user