初始提交:OCI 面板前端(含 GenAI 网关一期)

This commit is contained in:
Wang Defa
2026-07-09 15:31:29 +08:00
commit db45a669e3
135 changed files with 25220 additions and 0 deletions
+308
View File
@@ -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>