feat(ui): improve table controls and analytics filters
This commit is contained in:
+23
-15
@@ -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'
|
||||
|
||||
+21
-28
@@ -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) => {
|
||||
|
||||
Reference in New Issue
Block a user