251 lines
7.4 KiB
Vue
251 lines
7.4 KiB
Vue
<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>
|