148 lines
4.5 KiB
Vue
148 lines
4.5 KiB
Vue
<script setup lang="ts">
|
||
import { NSelect } from 'naive-ui'
|
||
import { computed, reactive, ref, watch } from 'vue'
|
||
|
||
import { listShapes, updateInstance } from '@/api/instances'
|
||
import AppInputNumber from '@/components/AppInputNumber.vue'
|
||
import FormField from '@/components/FormField.vue'
|
||
import FormModal from '@/components/FormModal.vue'
|
||
import { useAsync } from '@/composables/useAsync'
|
||
import { useToast } from '@/composables/useToast'
|
||
|
||
const props = defineProps<{
|
||
show: boolean
|
||
cfgId: number
|
||
instanceId: string
|
||
region?: string
|
||
shape: string
|
||
ocpus: number
|
||
memoryInGBs: number
|
||
}>()
|
||
|
||
const emit = defineEmits<{ 'update:show': [boolean]; resized: [] }>()
|
||
|
||
const message = useToast()
|
||
const submitting = ref(false)
|
||
const form = reactive({ shape: props.shape, ocpus: props.ocpus, memoryInGBs: props.memoryInGBs })
|
||
|
||
const shapes = useAsync(() => listShapes(props.cfgId, props.region).catch(() => []), false)
|
||
|
||
watch(
|
||
() => props.show,
|
||
(v) => {
|
||
if (v) {
|
||
Object.assign(form, { shape: props.shape, ocpus: props.ocpus, memoryInGBs: props.memoryInGBs })
|
||
void shapes.run()
|
||
}
|
||
},
|
||
)
|
||
|
||
const BILLING_LABEL: Record<string, string> = {
|
||
ALWAYS_FREE: '永久免费',
|
||
LIMITED_FREE: '免费额度',
|
||
}
|
||
|
||
const shapeOptions = computed(() => {
|
||
const list = shapes.data.value ?? []
|
||
if (!list.length) return [{ label: form.shape, value: form.shape }]
|
||
return list.map((s) => {
|
||
const free = BILLING_LABEL[s.billingType]
|
||
return { label: s.name + (free ? `(${free})` : ''), value: s.name }
|
||
})
|
||
})
|
||
|
||
const selectedShape = computed(
|
||
() => shapes.data.value?.find((s) => s.name === form.shape) ?? null,
|
||
)
|
||
const isFlex = computed(() => selectedShape.value?.isFlexible ?? form.shape.endsWith('.Flex'))
|
||
|
||
const ocpuRange = computed(() => ({
|
||
min: selectedShape.value?.ocpusMin || 1,
|
||
max: selectedShape.value?.ocpusMax || 128,
|
||
}))
|
||
const memRange = computed(() => ({
|
||
min: selectedShape.value?.memoryMinGBs || 1,
|
||
max: selectedShape.value?.memoryMaxGBs || 1024,
|
||
}))
|
||
|
||
// 换 shape 后把规格收敛到该 shape 的合法范围
|
||
watch(selectedShape, (s) => {
|
||
if (!s?.isFlexible) return
|
||
form.ocpus = Math.min(Math.max(form.ocpus, ocpuRange.value.min), ocpuRange.value.max)
|
||
form.memoryInGBs = Math.min(Math.max(form.memoryInGBs, memRange.value.min), memRange.value.max)
|
||
})
|
||
|
||
const changed = computed(
|
||
() =>
|
||
form.shape !== props.shape ||
|
||
(isFlex.value && (form.ocpus !== props.ocpus || form.memoryInGBs !== props.memoryInGBs)),
|
||
)
|
||
|
||
async function submit() {
|
||
submitting.value = true
|
||
try {
|
||
await updateInstance(
|
||
props.cfgId,
|
||
props.instanceId,
|
||
{
|
||
shape: form.shape !== props.shape ? form.shape : undefined,
|
||
ocpus: isFlex.value ? form.ocpus : undefined,
|
||
memoryInGBs: isFlex.value ? form.memoryInGBs : undefined,
|
||
},
|
||
props.region,
|
||
)
|
||
message.success('规格调整已提交,运行中实例将自动重启')
|
||
emit('update:show', false)
|
||
emit('resized')
|
||
} catch (e) {
|
||
message.error(e instanceof Error ? e.message : '调整失败')
|
||
} finally {
|
||
submitting.value = false
|
||
}
|
||
}
|
||
</script>
|
||
|
||
<template>
|
||
<FormModal
|
||
:show="show"
|
||
title="调整规格"
|
||
:submitting="submitting"
|
||
:submit-disabled="!changed"
|
||
submit-text="提交调整"
|
||
@update:show="emit('update:show', $event)"
|
||
@submit="submit"
|
||
>
|
||
<FormField label="Shape" required hint="可更换为租户当前区域提供的其他 shape,需目标容量可用">
|
||
<NSelect
|
||
v-model:value="form.shape"
|
||
filterable
|
||
tag
|
||
:options="shapeOptions"
|
||
:loading="shapes.loading.value"
|
||
/>
|
||
</FormField>
|
||
<div v-if="isFlex" class="grid grid-cols-2 gap-3 max-md:grid-cols-1">
|
||
<FormField label="OCPU" required :hint="`可选 ${ocpuRange.min}–${ocpuRange.max}`">
|
||
<AppInputNumber
|
||
v-model:value="form.ocpus"
|
||
:min="ocpuRange.min"
|
||
:max="ocpuRange.max"
|
||
class="w-full"
|
||
/>
|
||
</FormField>
|
||
<FormField label="内存(GB)" required :hint="`可选 ${memRange.min}–${memRange.max} GB`">
|
||
<AppInputNumber
|
||
v-model:value="form.memoryInGBs"
|
||
:min="memRange.min"
|
||
:max="memRange.max"
|
||
class="w-full"
|
||
/>
|
||
</FormField>
|
||
</div>
|
||
<div class="text-xs leading-relaxed text-ink-3">
|
||
运行中实例 OCI 会自动重启完成变更;更换 shape 要求镜像与目标 shape 架构兼容(如 aarch64
|
||
镜像不能换到 x86 shape)。A1 免费额度上限 4 OCPU / 24 GB。
|
||
</div>
|
||
</FormModal>
|
||
</template>
|