feat: improve model catalog aggregation

This commit is contained in:
2026-05-11 17:44:57 +08:00
parent ec87816c95
commit 0431cb8157
41 changed files with 4745 additions and 550 deletions
@@ -1,5 +1,11 @@
import { useMemo, useState } from 'react';
import { format } from 'date-fns';
import ConfigProvider from 'antd/es/config-provider';
import DatePicker from 'antd/es/date-picker';
import zhCN from 'antd/es/locale/zh_CN';
import dayjs, { type Dayjs } from 'dayjs';
import customParseFormat from 'dayjs/plugin/customParseFormat';
import 'dayjs/locale/zh-cn';
import { CalendarIcon, X } from 'lucide-react';
import { Button } from './button';
import { Calendar } from './calendar';
@@ -7,7 +13,11 @@ import { Input } from './input';
import { Label } from './label';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
dayjs.extend(customParseFormat);
dayjs.locale('zh-cn');
export function DateTimePicker(props: {
clearLabel?: string;
disabled?: boolean;
placeholder?: string;
value: string;
@@ -54,7 +64,7 @@ export function DateTimePicker(props: {
<Input type="time" value={timeValue} onChange={(event) => updateTime(event.target.value)} />
</Label>
<Button type="button" variant="ghost" size="icon" title="清除有效期" disabled={!props.value} onClick={() => props.onChange('')}>
<Button type="button" variant="ghost" size="icon" title={props.clearLabel ?? '清除有效期'} disabled={!props.value} onClick={() => props.onChange('')}>
<X size={14} />
</Button>
</div>
@@ -63,9 +73,68 @@ export function DateTimePicker(props: {
);
}
export function DateTimeRangePicker(props: {
disabled?: boolean;
from: string;
fromPlaceholder?: string;
to: string;
toPlaceholder?: string;
onChange: (value: { from: string; to: string }) => void;
}) {
const value = useMemo<[Dayjs | null, Dayjs | null] | null>(() => {
const from = parseDayjsDateTime(props.from);
const to = parseDayjsDateTime(props.to);
return from || to ? [from, to] : null;
}, [props.from, props.to]);
return (
<ConfigProvider locale={zhCN} theme={dateRangePickerTheme}>
<DatePicker.RangePicker
allowClear
className="shDateRangePicker"
classNames={{ popup: { root: 'shDateRangePickerPopup' } }}
disabled={props.disabled}
format={rangeDateTimeFormat}
needConfirm
placeholder={[props.fromPlaceholder ?? '开始日期', props.toPlaceholder ?? '结束日期']}
showTime={{ format: 'HH:mm:ss' }}
size="middle"
style={{ width: '100%' }}
value={value}
onChange={(nextValue) => {
props.onChange({
from: nextValue?.[0] ? nextValue[0].format(rangeDateTimeFormat) : '',
to: nextValue?.[1] ? nextValue[1].format(rangeDateTimeFormat) : '',
});
}}
/>
</ConfigProvider>
);
}
const rangeDateTimeFormat = 'YYYY-MM-DD HH:mm:ss';
const rangeParseFormats = [rangeDateTimeFormat, 'YYYY-MM-DDTHH:mm:ss', 'YYYY-MM-DDTHH:mm', 'YYYY-MM-DD'];
const dateRangePickerTheme = {
token: {
colorPrimary: '#18181b',
colorPrimaryHover: '#27272a',
colorBorder: '#e4e4e7',
colorText: '#27272a',
colorTextPlaceholder: '#71717a',
colorBgContainer: '#ffffff',
colorBgElevated: '#ffffff',
colorFillSecondary: '#f4f4f5',
borderRadius: 6,
controlHeight: 36,
fontFamily: 'inherit',
fontSize: 14,
},
};
function parseLocalDateTime(value: string) {
if (!value) return undefined;
const date = new Date(value);
const normalized = value.includes('T') ? value : value.replace(' ', 'T');
const date = new Date(normalized);
return Number.isNaN(date.getTime()) ? undefined : date;
}
@@ -82,6 +151,14 @@ function formatTimeValue(date: Date) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function parseDayjsDateTime(value: string): Dayjs | null {
if (!value) return null;
const parsed = dayjs(value, rangeParseFormats, true);
if (parsed.isValid()) return parsed;
const fallback = dayjs(value);
return fallback.isValid() ? fallback : null;
}
function pad(value: number) {
return String(value).padStart(2, '0');
}
+37 -6
View File
@@ -1,9 +1,40 @@
import * as React from 'react';
import { cn } from '../../lib/utils';
export function Table(props: React.HTMLAttributes<HTMLDivElement>) {
export type TableDensity = 'standard' | 'compact';
export interface TableProps extends React.HTMLAttributes<HTMLDivElement> {
density?: TableDensity;
}
export function Table(props: TableProps) {
const { className, density = 'standard', ...rest } = props;
return <div className={cn('shTable', density === 'compact' ? 'shTableCompact' : 'shTableStandard', className)} role="table" {...rest} />;
}
export function TableViewportLayout(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shTable', className)} role="table" {...rest} />;
return <div className={cn('shTableViewportLayout', className)} {...rest} />;
}
export function TableToolbar(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shTableToolbar', className)} {...rest} />;
}
export function TableSummary(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shTableSummary', className)} {...rest} />;
}
export function TableFooter(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shTableFooter', className)} {...rest} />;
}
export function TablePageActions(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <div className={cn('shTablePageActions', className)} {...rest} />;
}
export function TableRow(props: React.HTMLAttributes<HTMLDivElement>) {
@@ -11,14 +42,14 @@ export function TableRow(props: React.HTMLAttributes<HTMLDivElement>) {
return <div className={cn('shTableRow', className)} role="row" {...rest} />;
}
export function TableHead(props: React.HTMLAttributes<HTMLSpanElement>) {
export function TableHead(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <span className={cn('shTableHead', className)} role="columnheader" {...rest} />;
return <div className={cn('shTableHead', className)} role="columnheader" {...rest} />;
}
export function TableCell(props: React.HTMLAttributes<HTMLSpanElement>) {
export function TableCell(props: React.HTMLAttributes<HTMLDivElement>) {
const { className, ...rest } = props;
return <span className={cn('shTableCell', className)} role="cell" {...rest} />;
return <div className={cn('shTableCell', className)} role="cell" {...rest} />;
}
export function EmptyState(props: { title: string; description?: string }) {