初始提交: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
+146
View File
@@ -0,0 +1,146 @@
<script setup lang="ts">
import { NSelect, useMessage } 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'
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 = useMessage()
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 shapeA1 免费额度上限 4 OCPU / 24 GB
</div>
</FormModal>
</template>