初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,235 @@
|
||||
<script setup lang="ts">
|
||||
import { NButton, NPopconfirm, NSpin, useMessage } from 'naive-ui'
|
||||
|
||||
import { computed, ref } from 'vue'
|
||||
|
||||
import { addVnicIpv6, detachVnic, listVnics, removeIpv6 } from '@/api/instances'
|
||||
import FootNote from '@/components/FootNote.vue'
|
||||
import AttachVnicModal from '@/components/instance/AttachVnicModal.vue'
|
||||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
import OcidText from '@/components/OcidText.vue'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { ATTACHMENT_TRANSIENT_STATES, useTransientPoll } from '@/composables/useTransientPoll'
|
||||
|
||||
/** 网卡(VNIC)区块:每卡一主行 + IPv6 副行;IPv6 增删按卡操作。
|
||||
* 删除复用实例级接口(后端按地址遍历全部网卡),添加走 per-VNIC 接口。 */
|
||||
const props = defineProps<{
|
||||
cfgId: number
|
||||
instanceId: string
|
||||
region?: string
|
||||
/** 实例已终止 / 终止中:全部变更操作不可用 */
|
||||
disabled?: boolean
|
||||
}>()
|
||||
|
||||
const emit = defineEmits<{ changed: [] }>()
|
||||
|
||||
const message = useMessage()
|
||||
const vnics = useAsync(() => listVnics(props.cfgId, props.instanceId, props.region))
|
||||
|
||||
useTransientPoll(
|
||||
() => vnics.data.value,
|
||||
(list) => !!list?.some((v) => ATTACHMENT_TRANSIENT_STATES.includes(v.lifecycleState)),
|
||||
() => void vnics.run({ silent: true }),
|
||||
)
|
||||
|
||||
const showAttach = ref(false)
|
||||
const detaching = ref('')
|
||||
const addingIpv6 = ref('')
|
||||
const removingIpv6 = ref('')
|
||||
const busy = computed(() => !!detaching.value || !!addingIpv6.value || !!removingIpv6.value)
|
||||
|
||||
function refresh() {
|
||||
void vnics.run()
|
||||
emit('changed')
|
||||
}
|
||||
|
||||
async function doDetach(attachmentId: string) {
|
||||
detaching.value = attachmentId
|
||||
try {
|
||||
await detachVnic(props.cfgId, attachmentId, props.region)
|
||||
message.success('分离请求已提交')
|
||||
refresh()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '分离失败')
|
||||
} finally {
|
||||
detaching.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function doAddIpv6(vnicId: string) {
|
||||
addingIpv6.value = vnicId
|
||||
try {
|
||||
await addVnicIpv6(props.cfgId, vnicId, '', props.region)
|
||||
message.success('IPv6 已添加')
|
||||
refresh()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '添加失败,请确认子网已启用 IPv6')
|
||||
} finally {
|
||||
addingIpv6.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
async function doRemoveIpv6(addr: string) {
|
||||
removingIpv6.value = addr
|
||||
try {
|
||||
await removeIpv6(props.cfgId, props.instanceId, addr, props.region)
|
||||
message.success('IPv6 已取消分配')
|
||||
refresh()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '删除失败')
|
||||
} finally {
|
||||
removingIpv6.value = ''
|
||||
}
|
||||
}
|
||||
|
||||
defineExpose({ refresh: () => vnics.run() })
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="flex items-center justify-between gap-3 border-b border-line-soft px-4.5 py-3.5">
|
||||
<div class="min-w-0">
|
||||
<div class="text-sm font-semibold">网卡(VNIC)</div>
|
||||
<div class="mt-0.5 text-xs text-ink-3">
|
||||
实例的全部虚拟网卡;每张网卡可各自附加多个 IPv6
|
||||
</div>
|
||||
</div>
|
||||
<NButton size="small" type="primary" :disabled="disabled" @click="showAttach = true">
|
||||
附加 VNIC
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="vnics.loading.value && !vnics.data.value"
|
||||
class="flex items-center justify-center py-8"
|
||||
>
|
||||
<NSpin size="small" />
|
||||
</div>
|
||||
<div v-else class="overflow-x-auto">
|
||||
<table class="w-full min-w-[760px] border-collapse text-[13px]">
|
||||
<thead>
|
||||
<tr class="text-left text-xs text-ink-3">
|
||||
<th class="w-[26%] border-b border-line-soft py-2 pr-3 pl-4.5 font-medium">名称</th>
|
||||
<th class="border-b border-line-soft px-3 py-2 font-medium">私网 IP</th>
|
||||
<th class="border-b border-line-soft px-3 py-2 font-medium">公网 IP</th>
|
||||
<th class="border-b border-line-soft px-3 py-2 font-medium">子网</th>
|
||||
<th class="w-28 border-b border-line-soft px-3 py-2 font-medium">状态</th>
|
||||
<th class="w-18 border-b border-line-soft py-2 pr-4.5 pl-3 font-medium">操作</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<template v-for="v in vnics.data.value ?? []" :key="v.attachmentId">
|
||||
<tr>
|
||||
<td class="py-2.5 pr-3 pb-1.5 pl-4.5">
|
||||
<div class="flex min-w-0 items-center gap-2">
|
||||
<span class="truncate font-medium" :title="v.displayName">
|
||||
{{ v.displayName || '(未命名)' }}
|
||||
</span>
|
||||
<span
|
||||
v-if="v.isPrimary"
|
||||
class="flex-none rounded bg-accent/10 px-1.5 py-px text-[10.5px] font-semibold text-accent"
|
||||
>
|
||||
主网卡
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="mono px-3 py-2.5 pb-1.5">{{ v.privateIp || '—' }}</td>
|
||||
<td class="mono px-3 py-2.5 pb-1.5">{{ v.publicIp || '—' }}</td>
|
||||
<td class="px-3 py-2.5 pb-1.5">
|
||||
<span v-if="v.subnetName" class="truncate" :title="v.subnetName">
|
||||
{{ v.subnetName }}
|
||||
</span>
|
||||
<OcidText v-else-if="v.subnetId" :ocid="v.subnetId" />
|
||||
<span v-else>—</span>
|
||||
</td>
|
||||
<td class="px-3 py-2.5 pb-1.5"><LifecycleBadge :state="v.lifecycleState" /></td>
|
||||
<td class="py-2.5 pr-4.5 pb-1.5 pl-3">
|
||||
<NButton
|
||||
v-if="v.isPrimary"
|
||||
size="tiny"
|
||||
quaternary
|
||||
disabled
|
||||
title="主网卡不可分离"
|
||||
>
|
||||
分离
|
||||
</NButton>
|
||||
<NPopconfirm v-else @positive-click="doDetach(v.attachmentId)">
|
||||
<template #trigger>
|
||||
<NButton
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="error"
|
||||
:loading="detaching === v.attachmentId"
|
||||
:disabled="disabled || v.lifecycleState !== 'ATTACHED' || busy"
|
||||
>
|
||||
分离
|
||||
</NButton>
|
||||
</template>
|
||||
分离该网卡?其上全部 IP(含 IPv6)将一并释放
|
||||
</NPopconfirm>
|
||||
</td>
|
||||
</tr>
|
||||
<tr class="border-b border-line-soft last:border-b-0">
|
||||
<td colspan="6" class="px-4.5 pt-0 pb-2.5">
|
||||
<div class="flex flex-wrap items-center gap-1.5">
|
||||
<span class="text-[10.5px] font-medium tracking-wider text-ink-3">IPV6</span>
|
||||
<template v-if="v.vnicId">
|
||||
<span
|
||||
v-for="addr in v.ipv6Addresses ?? []"
|
||||
:key="addr"
|
||||
class="mono inline-flex max-w-full items-center gap-1 rounded border border-line bg-white px-1.5 py-0.5 text-xs"
|
||||
>
|
||||
<span class="truncate">{{ addr }}</span>
|
||||
<button
|
||||
class="flex-none cursor-pointer text-ink-3 hover:text-err disabled:cursor-wait"
|
||||
:aria-label="`删除 ${addr}`"
|
||||
:disabled="disabled || busy"
|
||||
@click="doRemoveIpv6(addr)"
|
||||
>
|
||||
{{ removingIpv6 === addr ? '…' : '×' }}
|
||||
</button>
|
||||
</span>
|
||||
<span v-if="!(v.ipv6Addresses ?? []).length" class="text-xs text-ink-3">
|
||||
无
|
||||
</span>
|
||||
<NButton
|
||||
size="tiny"
|
||||
quaternary
|
||||
type="primary"
|
||||
:loading="addingIpv6 === v.vnicId"
|
||||
:disabled="disabled || busy"
|
||||
@click="doAddIpv6(v.vnicId)"
|
||||
>
|
||||
+ IPv6
|
||||
</NButton>
|
||||
</template>
|
||||
<span v-else class="text-xs text-ink-3">无 · 附加完成后可添加</span>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</tbody>
|
||||
</table>
|
||||
<div
|
||||
v-if="!(vnics.data.value ?? []).length && !vnics.loading.value"
|
||||
class="py-6 text-center text-[13px] text-ink-3"
|
||||
>
|
||||
{{ vnics.error.value || '无网卡记录' }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<FootNote>
|
||||
附加须实例运行中且 shape 有空余 VNIC 配额 · 主网卡不可分离 · 每卡 IPv6
|
||||
可多个(要求所在子网已启用 IPv6)· 次要网卡在 OS 内需自行配置 IP(dhclient / netplan)
|
||||
</FootNote>
|
||||
|
||||
<AttachVnicModal
|
||||
v-model:show="showAttach"
|
||||
:cfg-id="cfgId"
|
||||
:instance-id="instanceId"
|
||||
:region="region"
|
||||
:attached-count="(vnics.data.value ?? []).length"
|
||||
@attached="refresh"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user