初始提交: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
+766
View File
@@ -0,0 +1,766 @@
<script setup lang="ts">
import {
NButton,
NDataTable,
NDropdown,
NPopconfirm,
NSpin,
useDialog,
useMessage,
type DataTableColumns,
} from 'naive-ui'
import { computed, h, ref, watch } from 'vue'
import { RouterLink, useRoute, useRouter } from 'vue-router'
import { getBootVolume } from '@/api/bootVolumes'
import { getConfig } from '@/api/configs'
import {
changePublicIp,
deleteConsoleConnection,
detachBootVolume,
detachVolume,
getImage,
getInstance,
instanceAction,
listBootVolumeAttachments,
listConsoleConnections,
listVolumeAttachments,
terminateInstance,
updateInstance,
} from '@/api/instances'
import FootNote from '@/components/FootNote.vue'
import AttachVolumeModal from '@/components/instance/AttachVolumeModal.vue'
import ConsoleConnModal from '@/components/instance/ConsoleConnModal.vue'
import EditBootVolumeModal from '@/components/instance/EditBootVolumeModal.vue'
import InstanceTrafficPanel from '@/components/instance/InstanceTrafficPanel.vue'
import ReplaceBootVolumeModal from '@/components/instance/ReplaceBootVolumeModal.vue'
import ResizeModal from '@/components/instance/ResizeModal.vue'
import VnicPanel from '@/components/instance/VnicPanel.vue'
import { useConsoleStore } from '@/stores/console'
import LifecycleBadge from '@/components/LifecycleBadge.vue'
import OcidText from '@/components/OcidText.vue'
import RenameModal from '@/components/RenameModal.vue'
import { fmtTime, shortAd, truncOcid, vpusLabel } from '@/composables/useFormat'
import { useRegionAlias } from '@/composables/useRegionAlias'
import { useAsync } from '@/composables/useAsync'
import {
ATTACHMENT_TRANSIENT_STATES,
INSTANCE_TRANSIENT_STATES,
useTransientPoll,
} from '@/composables/useTransientPoll'
import type { BootVolume, ConsoleConnection, PowerAction, VolumeAttachment } from '@/types/api'
const route = useRoute()
const router = useRouter()
const message = useMessage()
const dialog = useDialog()
const { alias: regionAlias } = useRegionAlias()
const cfgId = computed(() => Number(route.params.cfgId))
const instanceId = computed(() => String(route.params.instanceId))
/** 实例所在区域:列表页跳转时经 query 传入,缺失时回退实例对象上的短名(后端已归一化) */
const region = computed(
() => (route.query.region as string) || instance.data.value?.region || undefined,
)
const config = useAsync(() => getConfig(cfgId.value))
const instance = useAsync(() =>
getInstance(cfgId.value, instanceId.value, (route.query.region as string) || undefined),
)
const bvAtts = useAsync(() =>
listBootVolumeAttachments(cfgId.value, instanceId.value, region.value),
)
const volAtts = useAsync(() => listVolumeAttachments(cfgId.value, instanceId.value, region.value))
const consoles = useAsync(() =>
listConsoleConnections(cfgId.value, instanceId.value, region.value),
)
/** 按 imageId 补镜像名称,查询失败时回退 OCID 展示 */
const image = useAsync(
() => {
const inst = instance.data.value
if (!inst?.imageId) return Promise.resolve(null)
return getImage(cfgId.value, inst.imageId, inst.region).catch(() => null)
},
false,
)
watch(
() => instance.data.value?.imageId,
(v) => {
if (v) void image.run()
},
)
/** 引导卷挂载补齐卷详情(名称 / 大小 / VPU),支持就地调整 */
const bvDetails = useAsync<BootVolume[]>(async () => {
const atts = bvAtts.data.value ?? []
const list = await Promise.all(
atts.map((a) => getBootVolume(cfgId.value, a.bootVolumeId, region.value).catch(() => null)),
)
return list.filter((b): b is BootVolume => b !== null)
}, false)
watch(
() => bvAtts.data.value,
() => void bvDetails.run(),
)
const bvById = computed(() => new Map((bvDetails.data.value ?? []).map((b) => [b.id, b])))
// 电源 / 挂载操作提交后 OCI 状态异步迁移:过渡态期间每 5 秒静默刷新直到稳态
useTransientPoll(
() => instance.data.value,
(v) => !!v && INSTANCE_TRANSIENT_STATES.includes(v.lifecycleState),
() => void instance.run({ silent: true }),
)
useTransientPoll(
() => bvAtts.data.value,
(list) => !!list?.some((a) => ATTACHMENT_TRANSIENT_STATES.includes(a.lifecycleState)),
() => void bvAtts.run({ silent: true }),
)
useTransientPoll(
() => volAtts.data.value,
(list) => !!list?.some((a) => ATTACHMENT_TRANSIENT_STATES.includes(a.lifecycleState)),
() => void volAtts.run({ silent: true }),
)
useTransientPoll(
() => consoles.data.value,
(list) => !!list?.some((c) => ['CREATING', 'DELETING'].includes(c.lifecycleState)),
() => void consoles.run({ silent: true }),
)
const isStopped = computed(() => instance.data.value?.lifecycleState === 'STOPPED')
/** 终止 / 终止中:一切变更类操作不可用 */
const isEnded = computed(() =>
['TERMINATED', 'TERMINATING'].includes(instance.data.value?.lifecycleState ?? ''),
)
const attachedVolumeIds = computed(() => (volAtts.data.value ?? []).map((a) => a.volumeId))
/** 创建时若选了随机密码,密码写在 TAG RootPassword 中,这里回显 */
const rootPassword = computed(() => instance.data.value?.freeformTags?.RootPassword ?? '')
async function copyRootPassword() {
await navigator.clipboard.writeText(rootPassword.value)
message.success('已复制 ROOT 密码')
}
async function copyImageOcid() {
await navigator.clipboard.writeText(instance.data.value?.imageId ?? '')
message.success('已复制镜像 OCID')
}
const showRename = ref(false)
const renaming = ref(false)
const showResize = ref(false)
const showConsole = ref(false)
const showReplaceBv = ref(false)
const showAttachVol = ref(false)
const showEditBv = ref(false)
const editingBv = ref<BootVolume | null>(null)
function openEditBv(bv: BootVolume) {
editingBv.value = bv
showEditBv.value = true
}
const consoleStore = useConsoleStore()
/** VNC 与串行共用实例唯一的控制台连接资源(创建时互相清理),同时只开一个 */
function openVnc() {
consoleStore.close()
const href = router.resolve({
name: 'vnc-console',
params: { cfgId: cfgId.value, instanceId: instanceId.value },
query: { region: region.value, name: instance.data.value?.displayName },
}).href
window.open(href, '_blank')
}
function openSerial() {
void consoleStore.openSerial({
cfgId: cfgId.value,
instanceId: instanceId.value,
region: region.value,
instanceName: instance.data.value?.displayName,
rootPassword: rootPassword.value,
})
}
async function doRename(name: string) {
renaming.value = true
try {
await updateInstance(cfgId.value, instanceId.value, { displayName: name }, region.value)
message.success('已重命名')
showRename.value = false
void instance.run()
} catch (e) {
message.error(e instanceof Error ? e.message : '重命名失败')
} finally {
renaming.value = false
}
}
async function act(fn: () => Promise<unknown>, ok: string) {
try {
await fn()
message.success(ok)
void instance.run()
} catch (e) {
message.error(e instanceof Error ? e.message : '操作失败')
}
}
// 换 IP 请求期间的按钮 loading,操作发出立即有反馈
const changeIpBusy = ref(false)
function power(action: PowerAction) {
void act(
() => instanceAction(cfgId.value, instanceId.value, action, region.value),
`${action} 已提交`,
)
}
const moreOptions = [
{ label: 'SOFTSTOP(系统内关机)', key: 'SOFTSTOP' },
{ label: 'SOFTRESET(系统内重启)', key: 'SOFTRESET' },
]
function terminate() {
dialog.warning({
title: '终止实例',
content: '终止后实例不可恢复。是否同时保留引导卷?',
positiveText: '终止并删除引导卷',
negativeText: '终止但保留引导卷',
onPositiveClick: () =>
act(
() => terminateInstance(cfgId.value, instanceId.value, false, region.value),
'终止请求已提交',
),
onNegativeClick: () =>
act(
() => terminateInstance(cfgId.value, instanceId.value, true, region.value),
'终止请求已提交(保留引导卷)',
),
})
}
async function doChangeIp() {
changeIpBusy.value = true
await act(() => changePublicIp(cfgId.value, instanceId.value, region.value), '公网 IP 已更换')
changeIpBusy.value = false
}
const volColumns: DataTableColumns<VolumeAttachment> = [
{
title: '块卷',
key: 'volumeName',
minWidth: 170,
render: (r) =>
h('div', { class: 'min-w-0' }, [
h(
'div',
{ class: 'font-medium' },
r.volumeName || r.displayName || truncOcid(r.volumeId, 18),
),
r.volumeId.includes('.bootvolume.')
? h('div', { class: 'text-xs text-ink-3 mt-px' }, '引导卷 · 作数据卷附加')
: null,
]),
},
{
title: '设备路径',
key: 'device',
minWidth: 200,
render: (r) => h('span', { class: 'mono' }, r.device || '—'),
},
{
title: '读写',
key: 'isReadOnly',
width: 80,
render: (r) => h('span', { class: 'text-[13px]' }, r.isReadOnly ? '只读' : '读写'),
},
{
title: '状态',
key: 'lifecycleState',
width: 120,
render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
},
{
title: '操作',
key: 'actions',
width: 90,
render: (r) =>
h(
NPopconfirm,
{
onPositiveClick: () =>
act(() => detachVolume(cfgId.value, r.id, region.value), '分离请求已提交'),
},
{
trigger: () => h(NButton, { size: 'tiny', quaternary: true }, { default: () => '分离' }),
default: () => '分离该块卷?OS 内请先卸载文件系统',
},
),
},
]
/** DELETED 是 OCI 保留的终态记录,不可操作也无展示价值,直接过滤 */
const liveConsoles = computed(() =>
(consoles.data.value ?? []).filter((c) => c.lifecycleState !== 'DELETED'),
)
const deletingConn = ref('')
async function doDeleteConsoleConn(id: string) {
deletingConn.value = id
try {
await deleteConsoleConnection(cfgId.value, id, region.value)
message.success('删除请求已提交')
void consoles.run()
} catch (e) {
message.error(e instanceof Error ? e.message : '删除失败')
} finally {
deletingConn.value = ''
}
}
/** 连接串行:类型标签 + 截断命令 + 行内复制,避免整串撑爆表格 */
function connStringRow(label: string, value: string): ReturnType<typeof h> {
return h('div', { class: 'flex min-w-0 max-w-[640px] items-center gap-1.5' }, [
h('span', { class: 'shrink-0 rounded bg-wash px-1.5 text-[11px] leading-5 text-ink-2' }, label),
h('code', { class: 'mono min-w-0 flex-1 truncate text-xs', title: value }, value),
h(
NButton,
{
size: 'tiny',
quaternary: true,
onClick: async () => {
await navigator.clipboard.writeText(value)
message.success(`已复制${label}连接串`)
},
},
{ default: () => '复制' },
),
])
}
const consoleColumns: DataTableColumns<ConsoleConnection> = [
{
title: '状态',
key: 'lifecycleState',
width: 110,
render: (r) => h(LifecycleBadge, { state: r.lifecycleState }),
},
{
title: '连接串',
key: 'connectionString',
minWidth: 320,
render: (r) =>
h('div', { class: 'flex min-w-0 flex-col gap-0.5' }, [
connStringRow('串口', r.connectionString),
connStringRow('VNC', r.vncConnectionString),
]),
},
{
title: '操作',
key: 'actions',
width: 90,
render: (r) =>
h(
NPopconfirm,
{ onPositiveClick: () => doDeleteConsoleConn(r.id) },
{
trigger: () =>
h(
NButton,
{
size: 'tiny',
quaternary: true,
type: 'error',
loading: deletingConn.value === r.id,
disabled: deletingConn.value !== '' && deletingConn.value !== r.id,
},
{ default: () => '删除' },
),
default: () => '删除该控制台连接?',
},
),
},
]
</script>
<template>
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
<div
v-if="instance.loading.value && !instance.data.value"
class="panel flex items-center justify-center py-20"
>
<NSpin size="small" />
</div>
<div
v-else-if="instance.error.value"
class="panel px-5 py-10 text-center text-[13px] text-ink-3"
>
{{ instance.error.value }}
</div>
<div v-if="instance.data.value" class="panel px-5 py-4">
<RouterLink
class="mb-2 inline-flex items-center gap-1.5 text-[13px] text-ink-3 hover:text-ink"
:to="{ name: 'instances' }"
>
返回实例列表
</RouterLink>
<div class="flex flex-wrap items-center gap-3">
<h1 class="page-title">{{ instance.data.value.displayName }}</h1>
<LifecycleBadge :state="instance.data.value.lifecycleState" />
<div class="flex-1" />
<NButton v-if="isStopped" size="small" :disabled="isEnded" @click="power('START')">
启动
</NButton>
<NButton v-else size="small" :disabled="isEnded" @click="power('STOP')">停止</NButton>
<NButton size="small" :disabled="isEnded" @click="power('RESET')">重启</NButton>
<NButton size="small" type="error" ghost :disabled="isEnded" @click="terminate">
终止
</NButton>
<NDropdown
:options="moreOptions"
:disabled="isEnded"
@select="(key: string) => power(key as PowerAction)"
>
<NButton size="small" quaternary :disabled="isEnded"></NButton>
</NDropdown>
</div>
<div class="mt-3 grid grid-cols-5 gap-4 max-lg:grid-cols-3 max-md:grid-cols-1">
<div class="min-w-0">
<div class="text-xs text-ink-3">租户</div>
<div class="mt-0.5 truncate text-[13px]" :title="config.data.value?.tenancyName">
{{ config.data.value?.tenancyName || config.data.value?.alias || '…' }}
</div>
</div>
<div class="min-w-0">
<div class="text-xs text-ink-3">区域 / AD</div>
<div class="mt-0.5 truncate text-[13px]">
{{ regionAlias(instance.data.value.region) }} ·
{{ shortAd(instance.data.value.availabilityDomain) }}
</div>
</div>
<div class="min-w-0">
<div class="text-xs text-ink-3">Shape</div>
<div class="mono mt-0.5 truncate text-[13px]">{{ instance.data.value.shape }}</div>
</div>
<div class="min-w-0">
<div class="text-xs text-ink-3">规格</div>
<div class="mt-0.5 text-[13px]">
{{ instance.data.value.ocpus }} OCPU · {{ instance.data.value.memoryInGBs }} GB
</div>
</div>
<div class="min-w-0">
<div class="text-xs text-ink-3">创建时间</div>
<div class="mt-0.5 text-[13px]">{{ fmtTime(instance.data.value.timeCreated) }}</div>
</div>
</div>
</div>
<div class="grid grid-cols-2 items-start gap-4 max-lg:grid-cols-1">
<!-- 基本信息 -->
<div v-if="instance.data.value" class="panel">
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">基本信息</div>
<NButton size="tiny" quaternary :disabled="isEnded" @click="showRename = true">
重命名
</NButton>
</div>
<div class="flex flex-col px-4.5 py-1.5">
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">OCID</div>
<div class="min-w-0 flex-1"><OcidText :ocid="instance.data.value.id" /></div>
</div>
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">镜像</div>
<div class="flex min-w-0 flex-1 items-center gap-1.5">
<span class="truncate text-[13px]" :title="image.data.value?.displayName">
{{ image.data.value?.displayName ?? (image.loading.value ? '…' : '未知镜像') }}
</span>
<NButton size="tiny" quaternary type="primary" @click="copyImageOcid">复制</NButton>
</div>
</div>
<div
v-if="rootPassword"
class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5"
>
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">ROOT 密码</div>
<div class="flex min-w-0 flex-1 items-center gap-1.5">
<code class="mono truncate text-[13px]" :title="rootPassword">{{ rootPassword }}</code>
<NButton size="tiny" quaternary type="primary" @click="copyRootPassword">
复制
</NButton>
</div>
</div>
<div class="flex gap-3 py-2.5 max-md:flex-col max-md:gap-0.5">
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">规格调整</div>
<div class="min-w-0 flex-1 text-[13px]">
{{ instance.data.value.ocpus }} OCPU · {{ instance.data.value.memoryInGBs }} GB
<NButton
size="tiny"
quaternary
type="primary"
:disabled="isEnded"
@click="showResize = true"
>
调整
</NButton>
<div class="mt-0.5 text-xs text-ink-3">
可更换 shape 或调整 Flex 规格 · 运行中调整会自动重启
</div>
</div>
</div>
</div>
</div>
<!-- 网络 -->
<div v-if="instance.data.value" class="panel">
<div class="border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">网络</div>
<div class="mt-0.5 text-xs text-ink-3">
主网卡地址快捷视图全部网卡与各卡 IPv6 管理见下方网卡VNIC
</div>
</div>
<div class="flex flex-col px-4.5 py-1.5">
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">公网 IP</div>
<div class="min-w-0 flex-1">
<span class="mono">{{ instance.data.value.publicIp || '—' }}</span>
<NPopconfirm @positive-click="doChangeIp">
<template #trigger>
<NButton
size="tiny"
quaternary
type="primary"
:disabled="isEnded"
:loading="changeIpBusy"
>
更换
</NButton>
</template>
更换即删除旧临时公网 IP 再分配新的保留 IP 会被拒绝确认
</NPopconfirm>
<div class="mt-0.5 text-xs text-ink-3">临时 IP · 更换即删旧分新</div>
</div>
</div>
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">私有 IP</div>
<div class="mono min-w-0 flex-1">{{ instance.data.value.privateIp || '—' }}</div>
</div>
<div class="flex gap-3 border-b border-line-soft py-2.5 max-md:flex-col max-md:gap-0.5">
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">IPv6</div>
<div class="flex min-w-0 flex-1 flex-wrap items-center gap-1.5">
<span
v-for="addr in instance.data.value.ipv6Addresses"
:key="addr"
class="mono inline-flex max-w-full items-center rounded border border-line bg-white px-1.5 py-0.5 text-xs"
>
<span class="truncate">{{ addr }}</span>
</span>
<span v-if="!instance.data.value.ipv6Addresses?.length" class="text-[13px]"></span>
<div class="w-full text-xs text-ink-3">
主网卡 IPv6 · 添加 / 删除在下方网卡VNIC区块按卡操作
</div>
</div>
</div>
<div class="flex gap-3 py-2.5 max-md:flex-col max-md:gap-0.5">
<div class="w-22 flex-none text-xs text-ink-3 max-md:w-auto">子网</div>
<div class="min-w-0 flex-1"><OcidText :ocid="instance.data.value.subnetId" /></div>
</div>
</div>
</div>
</div>
<!-- 网卡(VNIC):每卡一主行 + IPv6 副行;变化后刷实例(主网卡快捷视图跟随) -->
<VnicPanel
v-if="instance.data.value"
:cfg-id="cfgId"
:instance-id="instanceId"
:region="region"
:disabled="isEnded"
@changed="instance.run({ silent: true })"
/>
<!-- 引导卷 -->
<div class="panel">
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">引导卷</div>
<NButton
size="small"
:disabled="!isStopped || isEnded"
:title="isStopped ? '' : '替换引导卷须实例处于 STOPPED'"
@click="showReplaceBv = true"
>
替换引导卷
</NButton>
</div>
<div
v-if="(bvAtts.loading.value || bvDetails.loading.value) && !bvDetails.data.value"
class="flex items-center justify-center py-8"
>
<NSpin size="small" />
</div>
<div v-else class="px-4.5 py-1.5">
<div
v-for="att in bvAtts.data.value ?? []"
:key="att.id"
class="flex flex-wrap items-center gap-3 border-b border-line-soft py-2.5 last:border-b-0"
>
<div class="min-w-0">
<div class="font-medium text-ink">
{{ bvById.get(att.bootVolumeId)?.displayName ?? truncOcid(att.bootVolumeId, 26) }}
</div>
<div class="text-xs text-ink-3">
<template v-if="bvById.get(att.bootVolumeId)">
{{ bvById.get(att.bootVolumeId)!.sizeInGBs }} GB · VPU
{{ vpusLabel(bvById.get(att.bootVolumeId)!.vpusPerGB) }}
</template>
<span v-else class="mono">{{ truncOcid(att.bootVolumeId, 26) }}</span>
</div>
</div>
<LifecycleBadge :state="att.lifecycleState" />
<div class="flex-1" />
<NButton
v-if="bvById.get(att.bootVolumeId)"
size="tiny"
quaternary
type="primary"
:disabled="isEnded"
@click="openEditBv(bvById.get(att.bootVolumeId)!)"
>
调整大小 / VPU
</NButton>
<NPopconfirm
@positive-click="act(() => detachBootVolume(cfgId, att.id, region), '分离请求已提交')"
>
<template #trigger>
<NButton size="tiny" quaternary :disabled="!isStopped || isEnded">分离</NButton>
</template>
分离引导卷?实例将无法启动直到挂载新引导卷
</NPopconfirm>
</div>
<div
v-if="!(bvAtts.data.value ?? []).length"
class="py-6 text-center text-[13px] text-ink-3"
>
无挂载引导卷
</div>
</div>
<FootNote>
大小仅支持扩容(在线生效,OS 内需扩展文件系统)· 挂载 / 分离 / 替换要求实例处于 STOPPED
</FootNote>
</div>
<!-- 块存储卷 -->
<div class="panel">
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">块存储卷</div>
<NButton size="small" type="primary" :disabled="isEnded" @click="showAttachVol = true">
附加块卷
</NButton>
</div>
<NDataTable
:columns="volColumns"
:data="volAtts.data.value ?? []"
:loading="volAtts.loading.value"
:scroll-x="700"
:row-key="(r: VolumeAttachment) => r.id"
/>
<FootNote>
半虚拟化附加,运行中实例可热插拔;附加后仍需在实例 OS 内分区挂载文件系统
</FootNote>
</div>
<!-- 控制台连接 -->
<div class="panel">
<div class="flex items-center justify-between border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">控制台连接(VNC / 串口)</div>
<div class="flex items-center gap-2">
<NButton size="small" type="primary" :disabled="isEnded" @click="openSerial">
串行终端
</NButton>
<NButton size="small" :disabled="isEnded" @click="openVnc">网页 VNC</NButton>
<NButton size="small" :disabled="isEnded" @click="showConsole = true">创建连接</NButton>
</div>
</div>
<NDataTable
:columns="consoleColumns"
:data="liveConsoles"
:loading="consoles.loading.value"
:scroll-x="640"
:row-key="(r: ConsoleConnection) => r.id"
/>
<FootNote>
串行终端 / 网页 VNC 在浏览器内直连(创建会话时自动清理旧连接,二者同时只能开一个)·
「创建连接」用于本地 ssh / VNC 客户端:公钥仅支持 RSA,串口直接执行串口连接串,VNC
执行后客户端连 localhost:5900 · 串口登录需实例本地账户密码
</FootNote>
</div>
<!-- 流量 -->
<InstanceTrafficPanel
v-if="instance.data.value"
:cfg-id="cfgId"
:instance-id="instanceId"
:region="region"
/>
<RenameModal
v-model:show="showRename"
title="重命名实例"
:current="instance.data.value?.displayName ?? ''"
:submitting="renaming"
@submit="doRename"
/>
<ResizeModal
v-if="instance.data.value"
v-model:show="showResize"
:cfg-id="cfgId"
:instance-id="instanceId"
:region="region"
:shape="instance.data.value.shape"
:ocpus="instance.data.value.ocpus"
:memory-in-g-bs="instance.data.value.memoryInGBs"
@resized="instance.run()"
/>
<ConsoleConnModal
v-model:show="showConsole"
:cfg-id="cfgId"
:instance-id="instanceId"
:region="region"
@created="consoles.run()"
/>
<ReplaceBootVolumeModal
v-if="instance.data.value"
v-model:show="showReplaceBv"
:cfg-id="cfgId"
:instance-id="instanceId"
:region="region"
:availability-domain="instance.data.value.availabilityDomain"
@replaced="bvAtts.run()"
/>
<AttachVolumeModal
v-if="instance.data.value"
v-model:show="showAttachVol"
:cfg-id="cfgId"
:instance-id="instanceId"
:region="region"
:availability-domain="instance.data.value.availabilityDomain"
:attached-volume-ids="attachedVolumeIds"
@attached="volAtts.run()"
/>
<EditBootVolumeModal
v-model:show="showEditBv"
:cfg-id="cfgId"
:region="region"
:boot-volume="editingBv"
@updated="bvDetails.run()"
/>
</div>
</template>