新增对象存储/账单/保留IP页面,成本今天视图与样式收敛
CI / test (push) Successful in 42s

This commit is contained in:
2026-07-21 12:11:22 +08:00
parent d813225ac4
commit ac457282ce
91 changed files with 6672 additions and 652 deletions
+53 -2
View File
@@ -4,6 +4,8 @@ import { computed, onMounted, reactive, ref, watch } from 'vue'
import { listAvailabilityDomains, listImages, listShapes } from '@/api/instances'
import { listSubnets, listVcns } from '@/api/networks'
import { listReservedIps } from '@/api/reservedips'
import { freeReservedIpOptions, renderReservedIpLabel } from '@/components/network/reservedIpSelect'
import { listLimits } from '@/api/tenant'
import AppInputNumber from '@/components/AppInputNumber.vue'
import FormField from '@/components/FormField.vue'
@@ -42,6 +44,8 @@ const blank = {
bootVolumeSizeGBs: null as number | null,
bootVolumeVpusPerGB: 10,
assignPublicIp: true,
publicIpMode: 'ephemeral' as 'ephemeral' | 'reserved' | 'none',
reservedPublicIpId: null as string | null,
assignIpv6: true,
authMode: 'password' as 'password' | 'ssh',
randomPassword: true,
@@ -173,6 +177,12 @@ function applySpec(spec: Record<string, unknown>) {
if (typeof s.bootVolumeSizeGBs === 'number') form.bootVolumeSizeGBs = s.bootVolumeSizeGBs
if (typeof s.bootVolumeVpusPerGB === 'number') form.bootVolumeVpusPerGB = s.bootVolumeVpusPerGB
form.assignPublicIp = s.assignPublicIp !== false
if (typeof s.reservedPublicIpId === 'string' && s.reservedPublicIpId) {
form.publicIpMode = 'reserved'
form.reservedPublicIpId = s.reservedPublicIpId
} else {
form.publicIpMode = s.assignPublicIp !== false ? 'ephemeral' : 'none'
}
form.assignIpv6 = s.assignIpv6 !== false
form.authMode = typeof s.sshPublicKey === 'string' && s.sshPublicKey ? 'ssh' : 'password'
form.sshPublicKey = typeof s.sshPublicKey === 'string' ? s.sshPublicKey : ''
@@ -376,9 +386,33 @@ watch(ipv6Supported, (v) => {
const valid = computed(() => {
if (!form.imageId) return false
if (form.vcnId !== AUTO_VCN && !form.subnetId) return false
if (form.publicIpMode === 'reserved' && !form.reservedPublicIpId) return false
return !isFlex.value || form.ocpus > 0
})
// ---- 公网 IPv4 分配方式:临时 / 保留 / 不分配 ----
const publicIpModes = [
{ label: '临时 IP', value: 'ephemeral' },
{ label: '保留 IP', value: 'reserved' },
{ label: '不分配', value: 'none' },
]
const reservedIps = useAsync(async () => {
if (props.cfgId === null) return []
try {
return await listReservedIps(props.cfgId, props.region || undefined, props.compartmentId || undefined)
} catch {
return []
}
}, false)
const freeReservedOptions = computed(() => freeReservedIpOptions(reservedIps.data.value ?? []))
watch(
() => form.publicIpMode,
(mode) => {
if (mode === 'reserved') void reservedIps.run()
else form.reservedPublicIpId = null
},
)
/** 规格摘要(父表单底部预估条用):shape 短名 + 弹性核内存 */
const summary = computed(() => {
const parts = [form.shape.replace(/^VM\.Standard\.?/, '') || form.shape]
@@ -399,7 +433,9 @@ function buildSpec(): Record<string, unknown> {
bootVolumeSizeGBs: form.bootVolumeSizeGBs ?? undefined,
// 性能独立于大小:留空按镜像默认大小时同样生效(后端仅镜像启动源使用)
bootVolumeVpusPerGB: form.bootVolumeVpusPerGB || undefined,
assignPublicIp: form.assignPublicIp,
assignPublicIp: form.publicIpMode === 'ephemeral',
reservedPublicIpId:
form.publicIpMode === 'reserved' ? (form.reservedPublicIpId ?? undefined) : undefined,
assignIpv6: form.assignIpv6,
generateRootPassword: usePwd && form.randomPassword ? true : undefined,
rootPassword:
@@ -578,8 +614,23 @@ defineExpose({ valid, buildSpec, reset, applySpec, summary })
placeholder="ssh-ed25519 AAAA…"
/>
</FormField>
<div class="grid grid-cols-2 gap-3 max-md:grid-cols-1">
<FormField label="公网 IPv4" hint="保留 IP:实例就绪后自动绑定,期间短暂无公网地址;仅单台创建可用">
<NSelect v-model:value="form.publicIpMode" :options="publicIpModes" />
</FormField>
<FormField v-if="form.publicIpMode === 'reserved'" label="保留 IP" required>
<NSelect
v-model:value="form.reservedPublicIpId"
:options="freeReservedOptions"
:loading="reservedIps.loading.value"
:render-label="renderReservedIpLabel"
:consistent-menu-width="false"
placeholder="选择未绑定的保留 IP"
filterable
/>
</FormField>
</div>
<div class="flex flex-wrap gap-5">
<NCheckbox v-model:checked="form.assignPublicIp">分配公网 IPv4</NCheckbox>
<NCheckbox v-model:checked="form.assignIpv6" :disabled="!ipv6Supported">
分配 IPv6{{ ipv6Supported ? '' : '(所选子网未启用)' }}
</NCheckbox>