233 lines
7.1 KiB
Vue
233 lines
7.1 KiB
Vue
<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"
|
||
:row-key="(r: LimitItem) => r.name + (r.availabilityDomain ?? '')"
|
||
/>
|
||
<FootNote>
|
||
勾选「查用量」后单次限 100 条,超出请用配额名 / 作用域过滤收窄,配合「忽略 0
|
||
配额」(服务端过滤)可大幅释放名额;不支持用量查询的配额自动跳过并显示「无用量数据」
|
||
</FootNote>
|
||
</div>
|
||
</template>
|