初始提交:OCI 面板前端(含 GenAI 网关一期)
This commit is contained in:
@@ -0,0 +1,233 @@
|
||||
<script setup lang="ts">
|
||||
import {
|
||||
NCheckbox,
|
||||
NDataTable,
|
||||
NInput,
|
||||
NInputGroup,
|
||||
NInputGroupLabel,
|
||||
NSelect,
|
||||
type DataTableColumns,
|
||||
} from 'naive-ui'
|
||||
import { computed, h, reactive, ref, watch } from 'vue'
|
||||
|
||||
import { listCachedRegions } from '@/api/configs'
|
||||
import { listLimitServices, listLimits } from '@/api/tenant'
|
||||
import FootNote from '@/components/FootNote.vue'
|
||||
import { regionCity, shortAd } from '@/composables/useFormat'
|
||||
import { useAsync } from '@/composables/useAsync'
|
||||
import type { LimitItem } from '@/types/api'
|
||||
|
||||
const props = defineProps<{ cfgId: number }>()
|
||||
|
||||
const service = ref('compute')
|
||||
const name = ref('')
|
||||
const region = ref('')
|
||||
const scopeType = ref<'' | 'GLOBAL' | 'REGION' | 'AD'>('')
|
||||
const withAvail = ref(false)
|
||||
const hideZero = ref(false)
|
||||
|
||||
const scopeOptions = [
|
||||
{ label: '全部作用域', value: '' },
|
||||
{ label: 'GLOBAL', value: 'GLOBAL' },
|
||||
{ label: 'REGION', value: 'REGION' },
|
||||
{ label: 'AD', value: 'AD' },
|
||||
]
|
||||
|
||||
const services = useAsync(() => listLimitServices(props.cfgId))
|
||||
/** 区域选项来自服务端缓存(未开启多区域的租户只返回默认区域) */
|
||||
const regions = useAsync(() => listCachedRegions(props.cfgId).catch(() => []))
|
||||
const limits = useAsync(() =>
|
||||
listLimits(props.cfgId, {
|
||||
service: service.value,
|
||||
name: name.value,
|
||||
region: region.value || undefined,
|
||||
scopeType: scopeType.value || undefined,
|
||||
// 下推服务端过滤:0 配额不占「查用量」的 100 条并发名额
|
||||
nonZero: hideZero.value || undefined,
|
||||
withAvailability: withAvail.value,
|
||||
}),
|
||||
)
|
||||
|
||||
// 区域列表加载后默认选中主区域;已选值失效(退订)时同样回退
|
||||
watch(
|
||||
() => regions.data.value,
|
||||
(subs) => {
|
||||
if (!subs?.length) return
|
||||
if (!region.value || !subs.some((s) => s.name === region.value))
|
||||
region.value = subs.find((s) => s.isHomeRegion)?.name ?? subs[0].name
|
||||
},
|
||||
)
|
||||
|
||||
/** 选项用括号内城市短名(与全局选择器一致),避免选中框被长名截断 */
|
||||
const regionOptions = computed(() =>
|
||||
(regions.data.value ?? []).map((s) => ({
|
||||
label: s.isHomeRegion ? `${regionCity(s.alias)} · 主` : regionCity(s.alias),
|
||||
value: s.name,
|
||||
})),
|
||||
)
|
||||
|
||||
watch([service, withAvail, region, scopeType, hideZero], () => void limits.run())
|
||||
|
||||
const rows = computed(() =>
|
||||
hideZero.value ? (limits.data.value ?? []).filter((r) => r.value !== 0) : (limits.data.value ?? []),
|
||||
)
|
||||
|
||||
const serviceOptions = computed(
|
||||
() =>
|
||||
services.data.value?.map((s) => ({ label: s.description || s.name, value: s.name })) ?? [
|
||||
{ label: 'Compute', value: 'compute' },
|
||||
],
|
||||
)
|
||||
|
||||
function usagePct(row: LimitItem): number | null {
|
||||
if (row.used === undefined || row.value === 0) return null
|
||||
return Math.round((row.used / row.value) * 100)
|
||||
}
|
||||
|
||||
/** 受控分页:字面量对象会在重渲染时被重建导致页大小切换失效 */
|
||||
const pagination = reactive({
|
||||
page: 1,
|
||||
pageSize: 10,
|
||||
showSizePicker: true,
|
||||
pageSizes: [10, 20, 50],
|
||||
onUpdatePage: (p: number) => {
|
||||
pagination.page = p
|
||||
},
|
||||
onUpdatePageSize: (ps: number) => {
|
||||
pagination.pageSize = ps
|
||||
pagination.page = 1
|
||||
},
|
||||
})
|
||||
|
||||
const baseColumns: DataTableColumns<LimitItem> = [
|
||||
{
|
||||
title: '配额项',
|
||||
key: 'name',
|
||||
minWidth: 230,
|
||||
render: (row) =>
|
||||
h('div', [
|
||||
h('div', { class: 'mono text-[12.5px] text-ink' }, row.name),
|
||||
h(
|
||||
'div',
|
||||
{ class: 'text-xs text-ink-3 mt-px' },
|
||||
row.availabilityDomain ? shortAd(row.availabilityDomain) : row.scopeType,
|
||||
),
|
||||
]),
|
||||
},
|
||||
{
|
||||
title: '上限',
|
||||
key: 'value',
|
||||
width: 130,
|
||||
render: (r) =>
|
||||
h(
|
||||
'span',
|
||||
{ class: 'tabular-nums whitespace-nowrap' },
|
||||
r.value.toLocaleString('en-US'),
|
||||
),
|
||||
},
|
||||
]
|
||||
|
||||
const availColumns: DataTableColumns<LimitItem> = [
|
||||
{
|
||||
title: '已用',
|
||||
key: 'used',
|
||||
width: 80,
|
||||
render: (r) => h('span', { class: 'tabular-nums' }, r.used === undefined ? '—' : String(r.used)),
|
||||
},
|
||||
{
|
||||
title: '可用',
|
||||
key: 'available',
|
||||
width: 80,
|
||||
render: (r) =>
|
||||
h('span', { class: 'tabular-nums' }, r.available === undefined ? '—' : String(r.available)),
|
||||
},
|
||||
{
|
||||
title: '用量',
|
||||
key: 'pct',
|
||||
minWidth: 160,
|
||||
render: (row) => {
|
||||
const pct = usagePct(row)
|
||||
if (pct === null) return h('span', { class: 'text-xs text-ink-3' }, '无用量数据')
|
||||
const full = pct >= 100
|
||||
return h('div', { class: 'flex items-center gap-2' }, [
|
||||
h('div', { class: 'h-1.5 w-24 overflow-hidden rounded-full bg-line-soft' }, [
|
||||
h('div', {
|
||||
class: 'h-full rounded-full',
|
||||
style: { width: `${Math.min(pct, 100)}%`, background: full ? 'var(--color-err)' : 'var(--color-ok)' },
|
||||
}),
|
||||
]),
|
||||
h(
|
||||
'span',
|
||||
{
|
||||
class: 'text-[12.5px] tabular-nums',
|
||||
style: full ? 'color:var(--color-err);font-weight:600' : 'color:var(--color-ink-2)',
|
||||
},
|
||||
`${pct}%`,
|
||||
),
|
||||
])
|
||||
},
|
||||
},
|
||||
]
|
||||
|
||||
const columns = computed(() => (withAvail.value ? [...baseColumns, ...availColumns] : baseColumns))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="panel">
|
||||
<div class="flex flex-wrap items-center gap-2 px-4.5 py-3">
|
||||
<div class="text-sm font-semibold">服务配额</div>
|
||||
<div class="flex-1" />
|
||||
<NInputGroup class="!w-60 max-md:!w-full">
|
||||
<NInputGroupLabel size="small">区域</NInputGroupLabel>
|
||||
<NSelect
|
||||
v-model:value="region"
|
||||
size="small"
|
||||
filterable
|
||||
:options="regionOptions"
|
||||
:loading="regions.loading.value"
|
||||
:consistent-menu-width="false"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NInputGroup class="!w-72 max-lg:!w-60 max-md:!w-full">
|
||||
<NInputGroupLabel size="small">服务</NInputGroupLabel>
|
||||
<NSelect
|
||||
v-model:value="service"
|
||||
size="small"
|
||||
filterable
|
||||
:options="serviceOptions"
|
||||
:loading="services.loading.value"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NInputGroup class="!w-52 max-lg:!w-44 max-md:!w-full">
|
||||
<NInputGroupLabel size="small">作用域</NInputGroupLabel>
|
||||
<NSelect v-model:value="scopeType" size="small" :options="scopeOptions" />
|
||||
</NInputGroup>
|
||||
<NInputGroup class="!w-56 max-lg:!w-44 max-md:!w-full">
|
||||
<NInputGroupLabel size="small">配额名</NInputGroupLabel>
|
||||
<NInput
|
||||
v-model:value="name"
|
||||
size="small"
|
||||
placeholder="如 core-count"
|
||||
clearable
|
||||
@keyup.enter="limits.run()"
|
||||
@clear="limits.run()"
|
||||
/>
|
||||
</NInputGroup>
|
||||
<NCheckbox v-model:checked="hideZero" size="small">忽略 0 配额</NCheckbox>
|
||||
<NCheckbox v-model:checked="withAvail" size="small">查用量</NCheckbox>
|
||||
</div>
|
||||
<NDataTable
|
||||
:columns="columns"
|
||||
:data="rows"
|
||||
:loading="limits.loading.value"
|
||||
:pagination="pagination"
|
||||
:scroll-x="withAvail ? 660 : 380"
|
||||
:max-height="440"
|
||||
:row-key="(r: LimitItem) => r.name + (r.availabilityDomain ?? '')"
|
||||
/>
|
||||
<FootNote>
|
||||
勾选「查用量」后单次限 100 条,超出请用配额名 / 作用域过滤收窄,配合「忽略 0
|
||||
配额」(服务端过滤)可大幅释放名额;不支持用量查询的配额自动跳过并显示「无用量数据」
|
||||
</FootNote>
|
||||
</div>
|
||||
</template>
|
||||
Reference in New Issue
Block a user