Files
oci-portal-dash/src/views/NetworkListView.vue
T
2026-07-30 12:23:16 +08:00

185 lines
5.6 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<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>