185 lines
5.6 KiB
Vue
185 lines
5.6 KiB
Vue
<script setup lang="ts">
|
||
import {
|
||
NButton,
|
||
NDataTable,
|
||
NInputGroup,
|
||
NInputGroupLabel,
|
||
type DataTableColumns,
|
||
} from 'naive-ui'
|
||
import { computed, h, ref, watch } from 'vue'
|
||
import { RouterLink } from 'vue-router'
|
||
|
||
import { listSubnets, listVcns } from '@/api/networks'
|
||
import CompartmentSelect from '@/components/CompartmentSelect.vue'
|
||
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
|
||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||
import CreateVcnModal from '@/components/network/CreateVcnModal.vue'
|
||
import ReservedIpPanel from '@/components/network/ReservedIpPanel.vue'
|
||
import StatusBadge from '@/components/StatusBadge.vue'
|
||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||
import { useAsync } from '@/composables/useAsync'
|
||
import { useScopeStore } from '@/stores/scope'
|
||
import type { Vcn } from '@/types/api'
|
||
|
||
interface VcnRow extends Vcn {
|
||
cfgId: number
|
||
region: string
|
||
subnetCount: number
|
||
}
|
||
|
||
/** 租户 / 区域 / 区间来自侧栏全局作用域 */
|
||
const scope = useScopeStore()
|
||
const { alias: regionAlias } = useRegionAlias()
|
||
const showCreate = ref(false)
|
||
|
||
/** 当前作用域租户失联时整页替换占位,不发起资源请求 */
|
||
const isDead = computed(() => scope.currentConfig?.aliveStatus === 'dead')
|
||
|
||
const rows = useAsync<VcnRow[]>(async () => {
|
||
const cfg = scope.currentConfig
|
||
if (!cfg || cfg.aliveStatus === 'dead') return []
|
||
const region = scope.region || undefined
|
||
try {
|
||
const vcns = await listVcns(cfg.id, region, scope.compartmentId || undefined)
|
||
return Promise.all(
|
||
vcns.map(async (vcn) => {
|
||
// 后端异常路径可能返回 null 而非 [],.catch 拦不住,需再判空
|
||
const subnets = (await listSubnets(cfg.id, vcn.id, region).catch(() => [])) ?? []
|
||
return {
|
||
...vcn,
|
||
cfgId: cfg.id,
|
||
region: scope.region || cfg.region,
|
||
subnetCount: subnets.length,
|
||
}
|
||
}),
|
||
)
|
||
} catch {
|
||
return []
|
||
}
|
||
}, false)
|
||
|
||
watch(
|
||
[() => scope.currentConfig?.id, () => scope.region, () => scope.compartmentId],
|
||
() => void rows.run(),
|
||
{ immediate: true },
|
||
)
|
||
|
||
const columns = computed<DataTableColumns<VcnRow>>(() => [
|
||
{
|
||
title: 'VCN',
|
||
key: 'displayName',
|
||
minWidth: 200,
|
||
render: (row) =>
|
||
h('div', { class: 'min-w-0' }, [
|
||
h(
|
||
RouterLink,
|
||
{
|
||
class: 'font-medium text-ink hover:text-accent',
|
||
to: {
|
||
name: 'vcn-detail',
|
||
params: { cfgId: row.cfgId, vcnId: row.id },
|
||
query: { region: scope.region || undefined },
|
||
},
|
||
},
|
||
{ default: () => row.displayName },
|
||
),
|
||
h('div', { class: 'mono text-xs text-ink-3 mt-px' }, row.dnsLabel),
|
||
]),
|
||
},
|
||
{
|
||
title: '区域',
|
||
key: 'region',
|
||
minWidth: 150,
|
||
render: (row) => h('span', { class: 'text-[13px]' }, regionAlias(row.region)),
|
||
},
|
||
{
|
||
title: 'IPv4 CIDR',
|
||
key: 'cidrBlocks',
|
||
minWidth: 130,
|
||
render: (row) => h('span', { class: 'mono' }, (row.cidrBlocks ?? []).join(', ')),
|
||
},
|
||
{
|
||
title: 'IPv6',
|
||
key: 'ipv6CidrBlocks',
|
||
minWidth: 120,
|
||
render: (row) =>
|
||
row.ipv6CidrBlocks?.length
|
||
? h(StatusBadge, { kind: 'prov', label: '/56 已分配', dot: false })
|
||
: h(StatusBadge, { kind: 'check', label: '未启用', dot: false }),
|
||
},
|
||
{
|
||
title: '状态',
|
||
key: 'lifecycleState',
|
||
width: 120,
|
||
render: (row) => h(LifecycleBadge, { state: row.lifecycleState }),
|
||
},
|
||
{
|
||
title: '子网',
|
||
key: 'subnetCount',
|
||
width: 70,
|
||
render: (row) => h('span', { class: 'tabular-nums' }, String(row.subnetCount)),
|
||
},
|
||
])
|
||
</script>
|
||
|
||
<template>
|
||
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
|
||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||
<div class="flex items-baseline gap-2.5">
|
||
<h1 class="page-title">网络</h1>
|
||
<span class="text-[13px] text-ink-3">
|
||
{{ rows.data.value?.length ?? '…' }} 个 VCN ·
|
||
{{ scope.currentConfig?.alias ?? '…' }}
|
||
</span>
|
||
</div>
|
||
<NInputGroup v-if="!isDead" class="!w-72 max-md:!w-full">
|
||
<NInputGroupLabel size="small">区间</NInputGroupLabel>
|
||
<CompartmentSelect
|
||
v-model:value="scope.compartmentId"
|
||
:compartments="scope.compartments.data ?? []"
|
||
:root-label="scope.rootLabel"
|
||
:loading="scope.compartments.loading"
|
||
:disabled="scope.compDisabled"
|
||
/>
|
||
</NInputGroup>
|
||
</div>
|
||
|
||
<DeadPlaceholder v-if="isDead" :last-error="scope.currentConfig?.lastError" />
|
||
<div v-else class="panel">
|
||
<div class="flex items-center justify-between gap-3 border-b border-line-soft px-4.5 py-3.5">
|
||
<div>
|
||
<div class="text-sm font-semibold">
|
||
VCN
|
||
</div>
|
||
<div class="mt-0.5 text-xs text-ink-3">
|
||
新建默认启用 IPv6 并放行全部出入流量,可在 VCN 详情收紧
|
||
</div>
|
||
</div>
|
||
<NButton size="medium" type="primary" @click="showCreate = true">创建 VCN</NButton>
|
||
</div>
|
||
<NDataTable
|
||
:columns="columns"
|
||
:data="rows.data.value ?? []"
|
||
:loading="rows.loading.value"
|
||
:scroll-x="920"
|
||
:row-key="(r: VcnRow) => r.id"
|
||
/>
|
||
</div>
|
||
|
||
<ReservedIpPanel
|
||
v-if="!isDead"
|
||
:cfg-id="scope.currentConfig?.id ?? null"
|
||
:region="scope.region || undefined"
|
||
:compartment-id="scope.compartmentId || undefined"
|
||
/>
|
||
|
||
<CreateVcnModal
|
||
v-model:show="showCreate"
|
||
:cfg-id="scope.currentConfig?.id ?? null"
|
||
:compartment-id="scope.compartmentId"
|
||
:region="scope.region"
|
||
@created="rows.run()"
|
||
/>
|
||
</div>
|
||
</template>
|