feat(ui): improve table controls and analytics filters

This commit is contained in:
CaIon
2026-04-30 13:57:10 +08:00
parent 8bff691089
commit 8f3c41ae77
35 changed files with 858 additions and 472 deletions
@@ -5,43 +5,51 @@ import { useTranslation } from 'react-i18next'
import type { TimeGranularity } from '@/lib/time'
import { VCHART_OPTION } from '@/lib/vchart'
import { useTheme } from '@/context/theme-provider'
import { DEFAULT_TIME_GRANULARITY } from '@/features/dashboard/constants'
import {
CONSUMPTION_DISTRIBUTION_CHART_OPTIONS,
DEFAULT_TIME_GRANULARITY,
} from '@/features/dashboard/constants'
import { processChartData } from '@/features/dashboard/lib'
import type { QuotaDataItem } from '@/features/dashboard/types'
import type {
ConsumptionDistributionChartType,
QuotaDataItem,
} from '@/features/dashboard/types'
let themeManagerPromise: Promise<
(typeof import('@visactor/vchart'))['ThemeManager']
> | null = null
type DistributionChartType = 'bar' | 'area'
interface ConsumptionDistributionChartProps {
data: QuotaDataItem[]
loading?: boolean
timeGranularity?: TimeGranularity
defaultChartType?: ConsumptionDistributionChartType
}
const CHART_TYPES: Array<{
value: DistributionChartType
labelKey: string
icon: typeof BarChart3
}> = [
{ value: 'bar', labelKey: 'Bar Chart', icon: BarChart3 },
{ value: 'area', labelKey: 'Area Chart', icon: AreaChart },
]
const CHART_TYPE_ICONS: Record<ConsumptionDistributionChartType, typeof BarChart3> =
{
bar: BarChart3,
area: AreaChart,
}
export function ConsumptionDistributionChart(
props: ConsumptionDistributionChartProps
) {
const { t } = useTranslation()
const { resolvedTheme } = useTheme()
const [chartType, setChartType] = useState<DistributionChartType>('bar')
const [chartType, setChartType] = useState<ConsumptionDistributionChartType>(
props.defaultChartType ?? 'bar'
)
const [themeReady, setThemeReady] = useState(false)
const themeManagerRef = useRef<
(typeof import('@visactor/vchart'))['ThemeManager'] | null
>(null)
const timeGranularity = props.timeGranularity ?? DEFAULT_TIME_GRANULARITY
useEffect(() => {
if (props.defaultChartType) setChartType(props.defaultChartType)
}, [props.defaultChartType])
useEffect(() => {
const updateTheme = async () => {
setThemeReady(false)
@@ -81,8 +89,8 @@ export function ConsumptionDistributionChart(
</div>
<div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
{CHART_TYPES.map((item) => {
const Icon = item.icon
{CONSUMPTION_DISTRIBUTION_CHART_OPTIONS.map((item) => {
const Icon = CHART_TYPE_ICONS[item.value]
return (
<button
key={item.value}
@@ -5,47 +5,51 @@ import { useTranslation } from 'react-i18next'
import type { TimeGranularity } from '@/lib/time'
import { VCHART_OPTION } from '@/lib/vchart'
import { useTheme } from '@/context/theme-provider'
import { DEFAULT_TIME_GRANULARITY } from '@/features/dashboard/constants'
import {
DEFAULT_TIME_GRANULARITY,
MODEL_ANALYTICS_CHART_OPTIONS,
} from '@/features/dashboard/constants'
import { processChartData } from '@/features/dashboard/lib'
import type { QuotaDataItem } from '@/features/dashboard/types'
import type {
ModelAnalyticsChartTab,
QuotaDataItem,
} from '@/features/dashboard/types'
let themeManagerPromise: Promise<
(typeof import('@visactor/vchart'))['ThemeManager']
> | null = null
type ChartTab = 'trend' | 'proportion' | 'top'
type ChartSpecKey = 'spec_model_line' | 'spec_pie' | 'spec_rank_bar'
const CHART_TABS: {
value: ChartTab
labelKey: string
specKey: ChartSpecKey
}[] = [
{ value: 'trend', labelKey: 'Call Trend', specKey: 'spec_model_line' },
{
value: 'proportion',
labelKey: 'Call Count Distribution',
specKey: 'spec_pie',
},
{ value: 'top', labelKey: 'Call Count Ranking', specKey: 'spec_rank_bar' },
]
const CHART_SPEC_KEYS: Record<ModelAnalyticsChartTab, ChartSpecKey> = {
trend: 'spec_model_line',
proportion: 'spec_pie',
top: 'spec_rank_bar',
}
interface ModelChartsProps {
data: QuotaDataItem[]
loading?: boolean
timeGranularity?: TimeGranularity
defaultChartTab?: ModelAnalyticsChartTab
}
export function ModelCharts(props: ModelChartsProps) {
const { t } = useTranslation()
const { resolvedTheme } = useTheme()
const [activeTab, setActiveTab] = useState<ChartTab>('trend')
const [activeTab, setActiveTab] = useState<ModelAnalyticsChartTab>(
props.defaultChartTab ?? 'trend'
)
const [themeReady, setThemeReady] = useState(false)
const themeManagerRef = useRef<
(typeof import('@visactor/vchart'))['ThemeManager'] | null
>(null)
const timeGranularity = props.timeGranularity ?? DEFAULT_TIME_GRANULARITY
useEffect(() => {
if (props.defaultChartTab) setActiveTab(props.defaultChartTab)
}, [props.defaultChartTab])
useEffect(() => {
const updateTheme = async () => {
setThemeReady(false)
@@ -70,8 +74,7 @@ export function ModelCharts(props: ModelChartsProps) {
[props.data, props.loading, timeGranularity, t]
)
const activeSpec = CHART_TABS.find((tab) => tab.value === activeTab)
const spec = activeSpec ? chartData[activeSpec.specKey] : null
const spec = chartData[CHART_SPEC_KEYS[activeTab]]
return (
<div className='overflow-hidden rounded-lg border'>
@@ -87,7 +90,7 @@ export function ModelCharts(props: ModelChartsProps) {
</div>
<div className='bg-muted/60 inline-flex h-8 rounded-md border p-0.5'>
{CHART_TABS.map((tab) => (
{MODEL_ANALYTICS_CHART_OPTIONS.map((tab) => (
<button
key={tab.value}
type='button'
@@ -1,4 +1,4 @@
import { useState } from 'react'
import { useEffect, useState } from 'react'
import { Filter, RotateCcw, Calendar, Search } from 'lucide-react'
import { useTranslation } from 'react-i18next'
import { useAuthStore } from '@/stores/auth-store'
@@ -26,20 +26,20 @@ import {
} from '@/components/ui/select'
import { DateTimePicker } from '@/components/datetime-picker'
import {
DEFAULT_TIME_GRANULARITY,
TIME_GRANULARITY_OPTIONS,
TIME_RANGE_PRESETS,
EMPTY_DASHBOARD_FILTERS,
} from '@/features/dashboard/constants'
import {
buildDefaultDashboardFilters,
cleanFilters,
getSavedGranularity,
saveGranularity,
getDefaultDays,
} from '@/features/dashboard/lib'
import { type DashboardFilters } from '@/features/dashboard/types'
import type {
DashboardChartPreferences,
DashboardFilters,
} from '@/features/dashboard/types'
interface ModelsFilterProps {
preferences: DashboardChartPreferences
onFilterChange: (filters: DashboardFilters) => void
onReset: () => void
}
@@ -58,30 +58,27 @@ const SectionDivider = ({ label }: { label: string }) => (
</div>
)
export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) {
export function ModelsFilter(props: ModelsFilterProps) {
const { t } = useTranslation()
// 使用已缓存的用户数据,避免重复调用 API
const user = useAuthStore((state) => state.auth.user)
const isAdmin = user?.role && user.role >= 10
const [open, setOpen] = useState(false)
const [filters, setFilters] = useState<DashboardFilters>(() => {
const granularity = getSavedGranularity()
const days = getDefaultDays(granularity)
const { start, end } = getNormalizedDateRange(days)
return {
...EMPTY_DASHBOARD_FILTERS,
start_timestamp: start,
end_timestamp: end,
time_granularity: granularity,
}
})
const [filters, setFilters] = useState<DashboardFilters>(() =>
buildDefaultDashboardFilters(props.preferences)
)
const [selectedRange, setSelectedRange] = useState<number | null>(() =>
getDefaultDays()
props.preferences.defaultTimeRangeDays
)
useEffect(() => {
setFilters(buildDefaultDashboardFilters(props.preferences))
setSelectedRange(props.preferences.defaultTimeRangeDays)
}, [props.preferences])
const handleApply = () => {
onFilterChange(
props.onFilterChange(
cleanFilters(
filters as unknown as Record<string, unknown>
) as typeof filters
@@ -90,17 +87,15 @@ export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) {
}
const handleReset = () => {
const days = getDefaultDays(DEFAULT_TIME_GRANULARITY)
const days = props.preferences.defaultTimeRangeDays
const { start, end } = getNormalizedDateRange(days)
setFilters({
...EMPTY_DASHBOARD_FILTERS,
...buildDefaultDashboardFilters(props.preferences),
start_timestamp: start,
end_timestamp: end,
time_granularity: DEFAULT_TIME_GRANULARITY,
})
setSelectedRange(days)
saveGranularity(DEFAULT_TIME_GRANULARITY)
onReset()
props.onReset()
setOpen(false)
}
@@ -111,8 +106,6 @@ export function ModelsFilter({ onFilterChange, onReset }: ModelsFilterProps) {
setFilters((prev) => ({ ...prev, [field]: value }))
if (field === 'start_timestamp' || field === 'end_timestamp')
setSelectedRange(null)
if (field === 'time_granularity' && typeof value === 'string')
saveGranularity(value as TimeGranularity)
}
const handleQuickRange = (days: number) => {