初始提交: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
+235
View File
@@ -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 内需自行配置 IPdhclient / 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>