初始提交: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
+106
View File
@@ -0,0 +1,106 @@
<script setup lang="ts">
import { NSelect, NSpin } from 'naive-ui'
import { computed, ref, watch } from 'vue'
import { getCosts } from '@/api/analytics'
import CostChart from '@/components/CostChart.vue'
import EmptyCard from '@/components/EmptyCard.vue'
import FootNote from '@/components/FootNote.vue'
import { useAsync } from '@/composables/useAsync'
const props = defineProps<{ cfgId: number }>()
const rangeDays = ref(14)
const rangeOptions = [
{ label: '近 14 天', value: 14 },
{ label: '近 30 天', value: 30 },
]
const costs = useAsync(() => getCosts(props.cfgId, { granularity: 'DAILY', groupBy: 'service' }))
watch(rangeDays, () => void costs.run())
const daily = computed(() => {
const byDay = new Map<string, number>()
for (const item of costs.data.value ?? []) {
const day = item.timeStart.slice(5, 10)
byDay.set(day, (byDay.get(day) ?? 0) + item.computedAmount)
}
const labels = [...byDay.keys()].sort()
return { labels, values: labels.map((l) => +(byDay.get(l) ?? 0).toFixed(2)) }
})
const total = computed(() => daily.value.values.reduce((s, v) => s + v, 0))
const byService = computed(() => {
const map = new Map<string, number>()
for (const item of costs.data.value ?? []) {
map.set(item.groupValue, (map.get(item.groupValue) ?? 0) + item.computedAmount)
}
const rows = [...map.entries()].sort((a, b) => b[1] - a[1])
const max = rows[0]?.[1] ?? 1
return rows.map(([name, amount]) => ({
name,
amount,
pct: total.value ? Math.round((amount / total.value) * 100) : 0,
barWidth: `${Math.round((amount / max) * 100)}%`,
}))
})
</script>
<template>
<div class="panel">
<div class="flex flex-wrap items-center gap-2.5 border-b border-line-soft px-4.5 py-3.5">
<div class="text-sm font-semibold">成本分析</div>
<div class="flex-1" />
<div class="w-28">
<NSelect v-model:value="rangeDays" size="small" :options="rangeOptions" />
</div>
</div>
<div v-if="costs.loading.value" class="flex items-center justify-center py-14">
<NSpin size="small" />
</div>
<template v-else-if="costs.data.value?.length">
<div class="flex flex-wrap items-baseline gap-2.5 px-4.5 pt-3.5">
<div class="text-[26px] font-semibold tabular-nums">${{ total.toFixed(2) }}</div>
<div class="text-[12.5px] text-ink-3">
{{ daily.labels[0] }} {{ daily.labels.at(-1) }} 合计
</div>
</div>
<div class="px-3 pb-1">
<CostChart :labels="daily.labels" :values="daily.values" />
</div>
<div class="border-t border-line-soft px-4.5 py-3">
<div class="mb-2 flex items-center justify-between">
<div class="text-[13px] font-semibold">按服务拆分</div>
<div class="text-[12.5px] text-ink-3">本期合计 ${{ total.toFixed(2) }}</div>
</div>
<div
v-for="svc in byService"
:key="svc.name"
class="flex items-center gap-3 py-1.5 max-md:gap-2"
>
<div class="w-32 flex-none truncate text-[13px] max-md:w-24" :title="svc.name">
{{ svc.name }}
</div>
<div class="h-1.5 min-w-0 flex-1 overflow-hidden rounded-full bg-line-soft">
<div class="h-full rounded-full bg-chart" :style="{ width: svc.barWidth }" />
</div>
<div class="w-17 flex-none text-right text-[13px] tabular-nums">
${{ svc.amount.toFixed(2) }}
</div>
<div class="w-10 flex-none text-right text-xs tabular-nums text-ink-3">{{ svc.pct }}%</div>
</div>
</div>
</template>
<EmptyCard
v-else
title="暂无成本数据"
note="试用租户无消费时成本为空;Usage API 数据通常延迟数小时至一天"
/>
<FootNote>
每日 03:30 自动同步 Usage API · 本地快照 · 免费类别租户不参与成本同步 · 点击刷新可即时拉取
</FootNote>
</div>
</template>