初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,229 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NButton,
|
||||
NDataTable,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NSelect,
|
||||
useMessage,
|
||||
type DataTableColumns,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, ref, watch } from 'vue'
|
||||
import { RouterLink } from 'vue-router'
|
||||
|
||||
import { instanceAction, listInstances } from '@/api/instances'
|
||||
import DeadPlaceholder from '@/components/DeadPlaceholder.vue'
|
||||
import CreateInstanceModal from '@/components/instance/CreateInstanceModal.vue'
|
||||
import LifecycleBadge from '@/components/LifecycleBadge.vue'
|
||||
import { useRegionAlias } from '@/composables/useRegionAlias'
|
||||
import { shortAd } from '@/composables/useFormat'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import { INSTANCE_TRANSIENT_STATES, useTransientPoll } from '@/composables/useTransientPoll'
|
||||
import { useScopeStore } from '@/stores/scope'
|
||||
import type { Instance, PowerAction } from '@/types/api'
|
||||
|
||||
interface InstanceRow extends Instance {
|
||||
cfgId: number
|
||||
cfgAlias: string
|
||||
}
|
||||
|
||||
const message = useMessage()
|
||||
const { alias } = useRegionAlias()
|
||||
|
||||
/** 租户 / 区域 / 区间来自侧栏全局作用域,页内只做区间切换与搜索 */
|
||||
const scope = useScopeStore()
|
||||
|
||||
const keyword = ref('')
|
||||
const showCreate = ref(false)
|
||||
|
||||
const rows = useAsync<InstanceRow[]>(async () => {
|
||||
const cfg = scope.currentConfig
|
||||
if (!cfg || cfg.aliveStatus === 'dead') return []
|
||||
const compartmentId = scope.compartmentId || undefined
|
||||
try {
|
||||
const list = await listInstances(cfg.id, scope.region || undefined, compartmentId)
|
||||
return list.map((item) => ({ ...item, cfgId: cfg.id, cfgAlias: cfg.alias }))
|
||||
} catch {
|
||||
return []
|
||||
}
|
||||
}, false)
|
||||
|
||||
/** 当前作用域租户失联时整页替换占位,不发起资源请求 */
|
||||
const isDead = computed(() => scope.currentConfig?.aliveStatus === 'dead')
|
||||
|
||||
// 同一 flush 内租户与区域先后变化只触发一次查询,避免旧区域请求覆盖新结果
|
||||
watch(
|
||||
[() => scope.currentConfig?.id, () => scope.region, () => scope.compartmentId],
|
||||
() => void rows.run(),
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// 创建 / 电源操作后存在过渡态行时每 5 秒静默刷新(不闪 loading),直到全部进入稳态
|
||||
useTransientPoll(
|
||||
() => rows.data.value,
|
||||
(list) => !!list?.some((r) => INSTANCE_TRANSIENT_STATES.includes(r.lifecycleState)),
|
||||
() => void rows.run({ silent: true }),
|
||||
)
|
||||
|
||||
const filtered = computed(() => {
|
||||
const kw = keyword.value.trim().toLowerCase()
|
||||
return (rows.data.value ?? []).filter((r) => {
|
||||
if (!kw) return true
|
||||
return [r.displayName, r.id, r.publicIp, r.privateIp].some((v) =>
|
||||
v?.toLowerCase().includes(kw),
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
function isEnded(row: InstanceRow): boolean {
|
||||
return row.lifecycleState === 'TERMINATED' || row.lifecycleState === 'TERMINATING'
|
||||
}
|
||||
|
||||
async function power(row: InstanceRow, action: PowerAction) {
|
||||
try {
|
||||
await instanceAction(row.cfgId, row.id, action, scope.region || undefined)
|
||||
message.success(`${action} 已提交:${row.displayName}`)
|
||||
void rows.run()
|
||||
} catch (e) {
|
||||
message.error(e instanceof Error ? e.message : '操作失败')
|
||||
}
|
||||
}
|
||||
|
||||
const columns = computed<DataTableColumns<InstanceRow>>(() => [
|
||||
{
|
||||
title: '实例',
|
||||
key: 'displayName',
|
||||
minWidth: 180,
|
||||
render: (row) =>
|
||||
h('div', { class: 'min-w-0' }, [
|
||||
h(
|
||||
RouterLink,
|
||||
{
|
||||
class: 'font-medium text-ink hover:text-accent',
|
||||
to: {
|
||||
name: 'instance-detail',
|
||||
params: { cfgId: row.cfgId, instanceId: row.id },
|
||||
query: { region: scope.region || undefined },
|
||||
},
|
||||
},
|
||||
{ default: () => row.displayName },
|
||||
),
|
||||
h('div', { class: 'text-xs text-ink-3 mt-px' }, row.cfgAlias),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: 'Shape',
|
||||
key: 'shape',
|
||||
minWidth: 190,
|
||||
render: (row) =>
|
||||
h('div', [
|
||||
h('div', { class: 'text-[13px]' }, row.shape),
|
||||
h('div', { class: 'text-xs text-ink-3 mt-px' }, `${row.ocpus} OCPU · ${row.memoryInGBs} GB`),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '区域 / AD',
|
||||
key: 'region',
|
||||
minWidth: 170,
|
||||
render: (row) =>
|
||||
h('div', [
|
||||
h('div', { class: 'text-[13px]' }, alias(row.region)),
|
||||
h('div', { class: 'text-xs text-ink-3 mt-px' }, shortAd(row.availabilityDomain)),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '公网 IP',
|
||||
key: 'publicIp',
|
||||
minWidth: 130,
|
||||
render: (row) => h('span', { class: 'mono' }, row.publicIp || '—'),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
key: 'lifecycleState',
|
||||
minWidth: 130,
|
||||
render: (row) => h(LifecycleBadge, { state: row.lifecycleState }),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'actions',
|
||||
width: 150,
|
||||
render: (row) =>
|
||||
h('div', { class: 'flex items-center gap-1' }, [
|
||||
row.lifecycleState === 'STOPPED'
|
||||
? h(
|
||||
NButton,
|
||||
{ size: 'tiny', quaternary: true, disabled: isEnded(row), onClick: () => power(row, 'START') },
|
||||
{ default: () => '启动' },
|
||||
)
|
||||
: h(
|
||||
NButton,
|
||||
{ size: 'tiny', quaternary: true, disabled: isEnded(row), onClick: () => power(row, 'STOP') },
|
||||
{ default: () => '停止' },
|
||||
),
|
||||
h(
|
||||
NButton,
|
||||
{ size: 'tiny', quaternary: true, disabled: isEnded(row), onClick: () => power(row, 'RESET') },
|
||||
{ default: () => '重启' },
|
||||
),
|
||||
]),
|
||||
},
|
||||
])
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex flex-col gap-4 p-6 pb-8 max-md:p-3">
|
||||
<div class="flex flex-wrap items-center justify-between gap-2">
|
||||
<div class="flex items-baseline gap-2.5">
|
||||
<h1 class="page-title">实例</h1>
|
||||
<span class="text-[13px] text-ink-3">
|
||||
{{ rows.data.value?.length ?? '…' }} 台 ·
|
||||
{{ scope.currentConfig?.alias ?? '…' }}
|
||||
</span>
|
||||
</div>
|
||||
<NButton v-if="!isDead" size="small" type="primary" @click="showCreate = true">
|
||||
创建实例
|
||||
</NButton>
|
||||
</div>
|
||||
|
||||
<DeadPlaceholder v-if="isDead" :last-error="scope.currentConfig?.lastError" />
|
||||
<div v-else class="panel">
|
||||
<div class="flex flex-wrap items-center gap-2 px-4.5 py-3">
|
||||
<NInputGroup class="!w-72 max-md:!w-full">
|
||||
<NInputGroupLabel size="small">区间</NInputGroupLabel>
|
||||
<NSelect
|
||||
v-model:value="scope.compartmentId"
|
||||
:options="scope.compOptions"
|
||||
:loading="scope.compartments.loading"
|
||||
:disabled="scope.compDisabled"
|
||||
size="small"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<div class="max-w-72 min-w-40 flex-1 max-md:max-w-full">
|
||||
<NInput
|
||||
v-model:value="keyword"
|
||||
size="small"
|
||||
placeholder="搜索实例名 / OCID / IP"
|
||||
clearable
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="filtered"
|
||||
:loading="rows.loading.value"
|
||||
:pagination="{ pageSize: 10 }"
|
||||
:scroll-x="960"
|
||||
:row-key="(r: InstanceRow) => r.id"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<CreateInstanceModal
|
||||
v-model:show="showCreate"
|
||||
:cfg-id="scope.currentConfig?.id ?? null"
|
||||
:compartment-id="scope.compartmentId"
|
||||
:region="scope.region"
|
||||
@created="rows.run()"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user