68 lines
2.4 KiB
TypeScript
68 lines
2.4 KiB
TypeScript
import { ArrowDown, ArrowDownUp, ArrowUp } from 'lucide-react';
|
||
|
||
export type SortDirection = 'asc' | 'desc';
|
||
|
||
export function SortableColumnHeader<Key extends string>({
|
||
label,
|
||
sortKey,
|
||
activeSortKey,
|
||
sortDirection,
|
||
onSort,
|
||
align = 'left',
|
||
className = '',
|
||
}: {
|
||
label: string;
|
||
sortKey: Key;
|
||
activeSortKey: Key;
|
||
sortDirection: SortDirection;
|
||
onSort: (sortKey: Key) => void;
|
||
align?: 'left' | 'right' | 'center';
|
||
className?: string;
|
||
}) {
|
||
const active = activeSortKey === sortKey;
|
||
const Icon = active ? (sortDirection === 'asc' ? ArrowUp : ArrowDown) : ArrowDownUp;
|
||
const order = active ? (sortDirection === 'asc' ? '升序' : '降序') : '未排序';
|
||
const justify = align === 'right' ? 'justify-end' : align === 'center' ? 'justify-center' : 'justify-start';
|
||
|
||
return (
|
||
<button
|
||
type="button"
|
||
onClick={() => onSort(sortKey)}
|
||
className={`inline-flex w-full items-center ${justify} gap-1 rounded px-1 py-0.5 transition-colors ${active ? 'text-blue-600' : 'text-slate-400 hover:bg-slate-100 hover:text-slate-600'} ${className}`}
|
||
title={`${label}:${order},点击切换`}
|
||
aria-label={`${label}:${order},点击切换`}
|
||
aria-sort={active ? (sortDirection === 'asc' ? 'ascending' : 'descending') : 'none'}
|
||
>
|
||
<span>{label}</span>
|
||
<Icon size={12} strokeWidth={active ? 2.5 : 2} />
|
||
</button>
|
||
);
|
||
}
|
||
|
||
export function toggleSort<Key extends string>(
|
||
currentKey: Key,
|
||
currentDirection: SortDirection,
|
||
nextKey: Key,
|
||
): { key: Key; direction: SortDirection } {
|
||
return nextKey === currentKey
|
||
? { key: currentKey, direction: currentDirection === 'asc' ? 'desc' : 'asc' }
|
||
: { key: nextKey, direction: 'desc' };
|
||
}
|
||
|
||
export function sortBy<Key extends string, Row>(
|
||
rows: Row[],
|
||
sortKey: Key,
|
||
sortDirection: SortDirection,
|
||
valueOf: (row: Row, key: Key) => string | number | null | undefined,
|
||
): Row[] {
|
||
const multiplier = sortDirection === 'asc' ? 1 : -1;
|
||
// Keep the caller's row order immutable while remaining compatible with the
|
||
// ES2022 target used by this dashboard (Array.prototype.toSorted is ES2023).
|
||
return [...rows].sort((left, right) => {
|
||
const leftValue = valueOf(left, sortKey) ?? '';
|
||
const rightValue = valueOf(right, sortKey) ?? '';
|
||
if (typeof leftValue === 'number' && typeof rightValue === 'number') return (leftValue - rightValue) * multiplier;
|
||
return String(leftValue).localeCompare(String(rightValue), 'zh-CN', { numeric: true }) * multiplier;
|
||
});
|
||
}
|