初始提交: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
@@ -0,0 +1,97 @@
<script setup lang="ts">
import { NSelect, useMessage } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { attachBootVolume, listInstances } from '@/api/instances'
import FormField from '@/components/FormField.vue'
import FormModal from '@/components/FormModal.vue'
import { shortAd } from '@/composables/useFormat'
import { useAsync } from '@/composables/useAsync'
import type { BootVolume } from '@/types/api'
const props = defineProps<{
show: boolean
cfgId: number | null
volume: BootVolume | null
region?: string
}>()
const emit = defineEmits<{ 'update:show': [boolean]; attached: [] }>()
const message = useMessage()
const submitting = ref(false)
const instanceId = ref<string | null>(null)
const instances = useAsync(
() => (props.cfgId === null ? Promise.resolve([]) : listInstances(props.cfgId, props.region)),
false,
)
watch(
() => props.show,
(v) => {
if (v) {
instanceId.value = null
void instances.run()
}
},
)
/** 仅同可用域且 STOPPED 的实例可选,其余给出禁用原因 */
const instanceOptions = computed(() =>
(instances.data.value ?? []).map((i) => {
const sameAd = i.availabilityDomain === props.volume?.availabilityDomain
const stopped = i.lifecycleState === 'STOPPED'
const reason = !sameAd ? ` · ${shortAd(i.availabilityDomain)} 可用域不同` : !stopped ? ` · ${i.lifecycleState}` : ''
return {
label: `${i.displayName}${reason}`,
value: i.id,
disabled: !sameAd || !stopped,
}
}),
)
async function submit() {
if (props.cfgId === null || !props.volume || !instanceId.value) return
submitting.value = true
try {
await attachBootVolume(props.cfgId, instanceId.value, props.volume.id, props.region)
message.success('挂载已提交,稍后自动刷新')
emit('update:show', false)
emit('attached')
} catch (e) {
message.error(e instanceof Error ? e.message : '挂载失败')
} finally {
submitting.value = false
}
}
</script>
<template>
<FormModal
:show="show"
title="挂载引导卷"
:width="440"
:submitting="submitting"
:submit-disabled="!instanceId"
submit-text="挂载"
@update:show="emit('update:show', $event)"
@submit="submit"
>
<div class="mb-3 text-[13px] text-ink-2">
<span class="font-medium">{{ volume?.displayName }}</span> 挂载为实例引导盘
</div>
<FormField
label="目标实例"
required
hint="仅可挂到同可用域、处于 STOPPED 的实例;实例开机后从该卷启动"
>
<NSelect
v-model:value="instanceId"
filterable
:options="instanceOptions"
:loading="instances.loading.value"
placeholder="选择实例"
/>
</FormField>
</FormModal>
</template>
@@ -0,0 +1,110 @@
<script setup lang="ts">
import { NInput, useMessage } from 'naive-ui'
import AppInputNumber from '@/components/AppInputNumber.vue'
import { computed, reactive, ref, watch } from 'vue'
import { updateBootVolume } from '@/api/bootVolumes'
import FormField from '@/components/FormField.vue'
import FormModal from '@/components/FormModal.vue'
import type { BootVolume, UpdateBootVolumeRequest } from '@/types/api'
const props = defineProps<{
show: boolean
cfgId: number | null
volume: BootVolume | null
region?: string
}>()
const emit = defineEmits<{ 'update:show': [boolean]; updated: [] }>()
const message = useMessage()
const submitting = ref(false)
const form = reactive({ displayName: '', sizeInGBs: 50, vpusPerGB: 10 })
watch(
() => props.show,
(v) => {
if (v && props.volume)
Object.assign(form, {
displayName: props.volume.displayName,
sizeInGBs: props.volume.sizeInGBs,
vpusPerGB: props.volume.vpusPerGB,
})
},
)
const sizeError = computed(() =>
props.volume && form.sizeInGBs < props.volume.sizeInGBs ? '仅支持扩容,不能缩小' : '',
)
const canSubmit = computed(() => Boolean(form.displayName.trim()) && !sizeError.value)
/** 零值字段不更新:只提交与当前值不同的字段 */
function buildBody(v: BootVolume): UpdateBootVolumeRequest {
const body: UpdateBootVolumeRequest = {}
if (form.displayName.trim() !== v.displayName) body.displayName = form.displayName.trim()
if (form.sizeInGBs !== v.sizeInGBs) body.sizeInGBs = form.sizeInGBs
if (form.vpusPerGB !== v.vpusPerGB) body.vpusPerGB = form.vpusPerGB
return body
}
async function submit() {
if (props.cfgId === null || !props.volume) return
const body = buildBody(props.volume)
if (!Object.keys(body).length) {
emit('update:show', false)
return
}
submitting.value = true
try {
await updateBootVolume(props.cfgId, props.volume.id, body, props.region)
message.success(body.sizeInGBs ? '修改已提交;扩容后需在实例 OS 内扩展文件系统' : '修改已提交')
emit('update:show', false)
emit('updated')
} catch (e) {
message.error(e instanceof Error ? e.message : '保存失败')
} finally {
submitting.value = false
}
}
</script>
<template>
<FormModal
:show="show"
title="编辑引导卷"
:width="440"
:submitting="submitting"
:submit-disabled="!canSubmit"
submit-text="保存修改"
@update:show="emit('update:show', $event)"
@submit="submit"
>
<FormField label="显示名" required>
<NInput v-model:value="form.displayName" placeholder="引导卷显示名" />
</FormField>
<div class="grid grid-cols-2 gap-3 max-md:grid-cols-1">
<FormField
label="大小(GB"
hint="仅支持扩容,不能缩小;扩容后需在实例 OS 内扩展文件系统"
:error="sizeError"
>
<AppInputNumber
v-model:value="form.sizeInGBs"
:min="volume?.sizeInGBs ?? 47"
:max="32768"
class="w-full"
/>
</FormField>
<FormField label="性能(VPU/GB" hint="10 均衡 / 20 高性能 / 30120 超高性能,步进 10">
<AppInputNumber
v-model:value="form.vpusPerGB"
:min="10"
:max="120"
:step="10"
class="w-full"
/>
</FormField>
</div>
</FormModal>
</template>