Files
oci-portal-dash/src/components/network/ReservedIpPanel.vue
T
2026-07-21 19:26:22 +08:00

251 lines
7.4 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, NModal, NSelect, type DataTableColumns } from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
import { listInstances } from '@/api/instances'
import { assignReservedIp, createReservedIp, deleteReservedIp, listReservedIps } from '@/api/reservedips'
import ConfirmPop from '@/components/ConfirmPop.vue'
import StatusBadge from '@/components/StatusBadge.vue'
import { fmtTime } from '@/composables/useFormat'
import { useAsync } from '@/composables/useAsync'
import { useMobileModal } from '@/composables/useMobileModal'
import type { ReservedIp } from '@/types/api'
import { useToast } from '@/composables/useToast'
const props = defineProps<{ cfgId: number | null; region?: string; compartmentId?: string }>()
const message = useToast()
const { modalStyle, modalClass } = useMobileModal(420)
const ips = useAsync<ReservedIp[]>(async () => {
if (!props.cfgId) return []
try {
return await listReservedIps(props.cfgId, props.region, props.compartmentId)
} catch {
return []
}
}, false)
watch([() => props.cfgId, () => props.region, () => props.compartmentId], () => void ips.run(), {
immediate: true,
})
const creating = ref(false)
async function create() {
if (!props.cfgId) return
creating.value = true
try {
await createReservedIp(props.cfgId, props.region, undefined, props.compartmentId)
message.success('保留 IP 已创建')
void ips.run()
} catch (e) {
message.error(e instanceof Error ? e.message : '创建失败')
} finally {
creating.value = false
}
}
// ---- 绑定实例弹窗 ----
const showBind = ref(false)
const bindTarget = ref<ReservedIp | null>(null)
const bindInstanceId = ref<string | null>(null)
const binding = ref(false)
const instances = useAsync(async () => {
if (!props.cfgId) return []
try {
return await listInstances(props.cfgId, props.region, props.compartmentId)
} catch {
return []
}
}, false)
const instanceOptions = computed(() =>
(instances.data.value ?? [])
.filter((i) => !['TERMINATED', 'TERMINATING'].includes(i.lifecycleState))
.map((i) => ({ label: `${i.displayName}(${i.publicIp || '无公网 IP'})`, value: i.id })),
)
function openBind(row: ReservedIp) {
bindTarget.value = row
bindInstanceId.value = null
showBind.value = true
void instances.run()
}
async function doBind() {
if (!props.cfgId || !bindTarget.value || !bindInstanceId.value) return
binding.value = true
try {
await assignReservedIp(props.cfgId, bindTarget.value.id, bindInstanceId.value, props.region)
message.success('已绑定;实例原临时公网 IP 已释放')
showBind.value = false
void ips.run()
} catch (e) {
message.error(e instanceof Error ? e.message : '绑定失败')
} finally {
binding.value = false
}
}
async function unbind(row: ReservedIp) {
if (!props.cfgId) return
try {
await assignReservedIp(props.cfgId, row.id, '', props.region)
message.success('已解绑,实例暂无公网 IP')
void ips.run()
} catch (e) {
message.error(e instanceof Error ? e.message : '解绑失败')
}
}
async function remove(row: ReservedIp) {
if (!props.cfgId) return
try {
await deleteReservedIp(props.cfgId, row.id, props.region)
message.success('保留 IP 已删除')
void ips.run()
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败')
}
}
const columns = computed<DataTableColumns<ReservedIp>>(() => [
{
title: 'IP 地址',
key: 'ipAddress',
minWidth: 140,
render: (r) => h('span', { class: 'mono' }, r.ipAddress || '分配中…'),
},
{
title: '名称',
key: 'displayName',
minWidth: 130,
render: (r) => h('span', { class: 'text-[13px]' }, r.displayName),
},
{
title: '状态',
key: 'assigned',
minWidth: 110,
render: (r) =>
r.assignedInstanceId
? h(StatusBadge, { kind: 'run', label: '已绑定', dot: false })
: h(StatusBadge, { kind: 'check', label: '未绑定', dot: false }),
},
{
title: '绑定实例',
key: 'assignedInstanceName',
minWidth: 150,
render: (r) =>
h(
'span',
{ class: 'text-[13px] text-ink-2' },
r.assignedInstanceName || (r.assignedInstanceId ? r.assignedInstanceId.slice(-8) : '—'),
),
},
{
title: '创建时间',
key: 'timeCreated',
width: 150,
render: (r) => h('span', { class: 'text-[13px] text-ink-2' }, fmtTime(r.timeCreated)),
},
{
title: '操作',
key: 'actions',
width: 150,
render: (r) =>
h('div', { class: 'flex items-center gap-1' }, [
r.assignedInstanceId
? h(
ConfirmPop,
{
title: '解绑该保留 IP?',
content: '解绑后实例将暂无公网 IP,直到重新绑定或分配临时 IP。',
kind: 'plain',
confirmText: '解绑',
onConfirm: () => unbind(r),
},
{ trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '解绑' }) },
)
: h(
NButton,
{ size: 'tiny', quaternary: true, onClick: () => openBind(r) },
{ default: () => '绑定实例' },
),
h(
ConfirmPop,
{
title: '删除该保留 IP?',
content: '地址将归还 Oracle,无法找回。',
confirmText: '删除',
onConfirm: () => remove(r),
},
{
trigger: () =>
h(
NButton,
{ size: 'tiny', quaternary: true, type: 'error', disabled: !!r.assignedInstanceId },
{ default: () => '删除' },
),
},
),
]),
},
])
</script>
<template>
<div class="panel">
<div class="flex flex-wrap items-center justify-between gap-y-2 border-b border-line-soft px-4.5 py-3.5">
<div>
<div class="text-sm font-semibold">保留 IP</div>
<div class="mt-0.5 text-xs text-ink-3">
区域级固定公网 IP,可换绑实例或创建实例时选用
</div>
</div>
<NButton size="small" type="primary" :loading="creating" @click="create">创建保留 IP</NButton>
</div>
<NDataTable
:columns="columns"
:data="ips.data.value ?? []"
:loading="ips.loading.value"
:scroll-x="820"
:row-key="(r: ReservedIp) => r.id"
/>
<NModal
v-model:show="showBind"
preset="card"
title="绑定保留 IP 到实例"
:style="modalStyle"
:class="modalClass"
>
<div class="flex flex-col gap-3">
<div class="text-[13px] text-ink-2">
将 <span class="mono">{{ bindTarget?.ipAddress }}</span> 绑定到实例主网卡;
实例现有临时公网 IP 会被释放,公网地址变更为该保留 IP。
</div>
<NSelect
v-model:value="bindInstanceId"
:options="instanceOptions"
:loading="instances.loading.value"
placeholder="选择实例"
filterable
/>
<div class="flex justify-end gap-2">
<NButton size="small" @click="showBind = false">取消</NButton>
<NButton
size="small"
type="primary"
:disabled="!bindInstanceId"
:loading="binding"
@click="doBind"
>
绑定
</NButton>
</div>
</div>
</NModal>
</div>
</template>