2026-04-28 18:38:02 +08:00
|
|
|
import { useEffect, useMemo, useRef, useState } from 'react'
|
|
|
|
|
import { VChart } from '@visactor/react-vchart'
|
|
|
|
|
import { AreaChart, BarChart3, WalletCards } from 'lucide-react'
|
|
|
|
|
import { useTranslation } from 'react-i18next'
|
|
|
|
|
import type { TimeGranularity } from '@/lib/time'
|
|
|
|
|
import { VCHART_OPTION } from '@/lib/vchart'
|
|
|
|
|
import { useTheme } from '@/context/theme-provider'
|
2026-04-30 13:57:10 +08:00
|
|
|
import {
|
|
|
|
|
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
|
|
|
|
|
DEFAULT_TIME_GRANULARITY,
|
|
|
|
|
} from '@/features/dashboard/constants'
|
2026-04-28 18:38:02 +08:00
|
|
|
import { processChartData } from '@/features/dashboard/lib'
|
2026-04-30 13:57:10 +08:00
|
|
|
import type {
|
|
|
|
|
ConsumptionDistributionChartType,
|
|
|
|
|
QuotaDataItem,
|
|
|
|
|
} from '@/features/dashboard/types'
|
2026-04-28 18:38:02 +08:00
|
|
|
|
|
|
|
|
let themeManagerPromise: Promise<
|
|
|
|
|
(typeof import('@visactor/vchart'))['ThemeManager']
|
|
|
|
|
> | null = null
|
|
|
|
|
|
|
|
|
|
interface ConsumptionDistributionChartProps {
|
|
|
|
|
data: QuotaDataItem[]
|
|
|
|
|
loading?: boolean
|
|
|
|
|
timeGranularity?: TimeGranularity
|
2026-04-30 13:57:10 +08:00
|
|
|
defaultChartType?: ConsumptionDistributionChartType
|
2026-04-28 18:38:02 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-30 13:57:10 +08:00
|
|
|
const CHART_TYPE_ICONS: Record<ConsumptionDistributionChartType, typeof BarChart3> =
|
|
|
|
|
{
|
|
|
|
|
bar: BarChart3,
|
|
|
|
|
area: AreaChart,
|
|
|
|
|
}
|
2026-04-28 18:38:02 +08:00
|
|
|
|
|
|
|
|
export function ConsumptionDistributionChart(
|
|
|
|
|
props: ConsumptionDistributionChartProps
|
|
|
|
|
) {
|
|
|
|
|
const { t } = useTranslation()
|
|
|
|
|
const { resolvedTheme } = useTheme()
|
2026-04-30 13:57:10 +08:00
|
|
|
const [chartType, setChartType] = useState<ConsumptionDistributionChartType>(
|
|
|
|
|
props.defaultChartType ?? 'bar'
|
|
|
|
|
)
|
2026-04-28 18:38:02 +08:00
|
|
|
const [themeReady, setThemeReady] = useState(false)
|
|
|
|
|
const themeManagerRef = useRef<
|
|
|
|
|
(typeof import('@visactor/vchart'))['ThemeManager'] | null
|
|
|
|
|
>(null)
|
|
|
|
|
const timeGranularity = props.timeGranularity ?? DEFAULT_TIME_GRANULARITY
|
|
|
|
|
|
2026-04-30 13:57:10 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
if (props.defaultChartType) setChartType(props.defaultChartType)
|
|
|
|
|
}, [props.defaultChartType])
|
|
|
|
|
|
2026-04-28 18:38:02 +08:00
|
|
|
useEffect(() => {
|
|
|
|
|
const updateTheme = async () => {
|
|
|
|
|
setThemeReady(false)
|
|
|
|
|
|
|
|
|
|
if (!themeManagerPromise) {
|
|
|
|
|
themeManagerPromise = import('@visactor/vchart').then(
|
|
|
|
|
(m) => m.ThemeManager
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const ThemeManager = await themeManagerPromise
|
|
|
|
|
themeManagerRef.current = ThemeManager
|
|
|
|
|
ThemeManager.setCurrentTheme(resolvedTheme === 'dark' ? 'dark' : 'light')
|
|
|
|
|
setThemeReady(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
updateTheme()
|
|
|
|
|
}, [resolvedTheme])
|
|
|
|
|
|
|
|
|
|
const chartData = useMemo(
|
|
|
|
|
() => processChartData(props.loading ? [] : props.data, timeGranularity, t),
|
|
|
|
|
[props.data, props.loading, timeGranularity, t]
|
|
|
|
|
)
|
|
|
|
|
const spec = chartType === 'bar' ? chartData.spec_line : chartData.spec_area
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className='overflow-hidden rounded-lg border'>
|
2026-04-30 19:53:02 +08:00
|
|
|
<div className='flex w-full flex-col gap-1.5 border-b px-3 py-2 sm:gap-3 sm:px-5 sm:py-3 lg:flex-row lg:items-center lg:justify-between'>
|
2026-04-28 18:38:02 +08:00
|
|
|
<div className='flex items-center gap-2'>
|
|
|
|
|
<WalletCards className='text-muted-foreground/60 size-4' />
|
|
|
|
|
<div className='text-sm font-semibold'>
|
|
|
|
|
{t('Quota Distribution')}
|
|
|
|
|
</div>
|
|
|
|
|
<span className='text-muted-foreground text-xs'>
|
|
|
|
|
{t('Total:')} {chartData.totalQuotaDisplay}
|
|
|
|
|
</span>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-30 19:53:02 +08:00
|
|
|
<div className='bg-muted/60 inline-flex h-7 w-full overflow-x-auto rounded-md border p-0.5 sm:h-8 sm:w-auto'>
|
2026-04-30 13:57:10 +08:00
|
|
|
{CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
|
|
|
|
|
const Icon = CHART_TYPE_ICONS[item.value]
|
2026-04-28 18:38:02 +08:00
|
|
|
return (
|
|
|
|
|
<button
|
|
|
|
|
key={item.value}
|
|
|
|
|
type='button'
|
|
|
|
|
onClick={() => setChartType(item.value)}
|
2026-04-30 19:53:02 +08:00
|
|
|
className={`inline-flex shrink-0 items-center gap-1.5 rounded-[5px] px-3 text-xs font-medium transition-colors ${
|
2026-04-28 18:38:02 +08:00
|
|
|
chartType === item.value
|
|
|
|
|
? 'bg-background text-foreground shadow-sm'
|
|
|
|
|
: 'text-muted-foreground hover:text-foreground'
|
|
|
|
|
}`}
|
|
|
|
|
>
|
|
|
|
|
<Icon className='size-3.5' />
|
|
|
|
|
{t(item.labelKey)}
|
|
|
|
|
</button>
|
|
|
|
|
)
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
2026-04-30 19:53:02 +08:00
|
|
|
<div className='h-[300px] p-1.5 sm:h-96 sm:p-2'>
|
2026-04-28 18:38:02 +08:00
|
|
|
{themeReady && spec && (
|
|
|
|
|
<VChart
|
|
|
|
|
key={`${chartType}-${resolvedTheme}`}
|
|
|
|
|
spec={{
|
|
|
|
|
...spec,
|
|
|
|
|
theme: resolvedTheme === 'dark' ? 'dark' : 'light',
|
|
|
|
|
background: 'transparent',
|
|
|
|
|
}}
|
|
|
|
|
option={VCHART_OPTION}
|
|
|
|
|
/>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|