初始提交: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
+233
View File
@@ -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>