feat: refine api key permissions and admin routes

This commit is contained in:
2026-05-10 23:22:26 +08:00
parent 0fc23d7eb8
commit d86651ff55
23 changed files with 1683 additions and 532 deletions
+54
View File
@@ -0,0 +1,54 @@
import * as React from 'react';
import { ChevronDown, ChevronLeft, ChevronRight, ChevronUp } from 'lucide-react';
import { DayPicker, type ChevronProps, type DayPickerProps } from 'react-day-picker';
import { cn } from '../../lib/utils';
export function Calendar(props: DayPickerProps) {
const { className, classNames, showOutsideDays = true, ...rest } = props;
return (
<DayPicker
className={cn('shCalendar', className)}
classNames={{
root: 'shCalendarRoot',
months: 'shCalendarMonths',
month: 'shCalendarMonth',
month_caption: 'shCalendarCaption',
caption_label: 'shCalendarCaptionLabel',
nav: 'shCalendarNav',
button_previous: 'shCalendarNavButton',
button_next: 'shCalendarNavButton',
chevron: 'shCalendarChevron',
month_grid: 'shCalendarGrid',
weekdays: 'shCalendarWeekdays',
weekday: 'shCalendarWeekday',
weeks: 'shCalendarWeeks',
week: 'shCalendarWeek',
day: 'shCalendarDay',
day_button: 'shCalendarDayButton',
outside: 'shCalendarDayOutside',
disabled: 'shCalendarDayDisabled',
selected: 'shCalendarDaySelected',
today: 'shCalendarDayToday',
hidden: 'shCalendarDayHidden',
...classNames,
}}
components={{
Chevron: CalendarChevron,
...props.components,
}}
showOutsideDays={showOutsideDays}
{...rest}
/>
);
}
function CalendarChevron(props: ChevronProps) {
const Icon = props.orientation === 'left'
? ChevronLeft
: props.orientation === 'right'
? ChevronRight
: props.orientation === 'up'
? ChevronUp
: ChevronDown;
return <Icon className={cn('shCalendarChevronIcon', props.className)} size={props.size ?? 16} />;
}
@@ -0,0 +1,87 @@
import { useMemo, useState } from 'react';
import { format } from 'date-fns';
import { CalendarIcon, X } from 'lucide-react';
import { Button } from './button';
import { Calendar } from './calendar';
import { Input } from './input';
import { Label } from './label';
import { Popover, PopoverContent, PopoverTrigger } from './popover';
export function DateTimePicker(props: {
disabled?: boolean;
placeholder?: string;
value: string;
onChange: (value: string) => void;
}) {
const [open, setOpen] = useState(false);
const selectedDate = useMemo(() => parseLocalDateTime(props.value), [props.value]);
const timeValue = selectedDate ? formatTimeValue(selectedDate) : '';
function updateDate(nextDate: Date | undefined) {
if (!nextDate) return;
const current = selectedDate ?? new Date();
const next = new Date(nextDate);
next.setHours(current.getHours(), current.getMinutes(), 0, 0);
props.onChange(formatLocalDateTime(next));
}
function updateTime(value: string) {
const [hours, minutes] = value.split(':').map((item) => Number(item));
if (!Number.isFinite(hours) || !Number.isFinite(minutes)) return;
const next = selectedDate ? new Date(selectedDate) : new Date();
next.setHours(hours, minutes, 0, 0);
props.onChange(formatLocalDateTime(next));
}
return (
<Popover open={open} onOpenChange={setOpen}>
<PopoverTrigger asChild>
<Button
className="dateTimePickerTrigger"
data-empty={!selectedDate}
disabled={props.disabled}
type="button"
variant="outline"
>
<CalendarIcon size={15} />
{selectedDate ? format(selectedDate, 'yyyy-MM-dd HH:mm') : <span>{props.placeholder ?? '选择日期时间'}</span>}
</Button>
</PopoverTrigger>
<PopoverContent align="start" className="dateTimePickerPopover">
<Calendar mode="single" selected={selectedDate} onSelect={updateDate} />
<div className="dateTimePickerFooter">
<Label>
<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('')}>
<X size={14} />
</Button>
</div>
</PopoverContent>
</Popover>
);
}
function parseLocalDateTime(value: string) {
if (!value) return undefined;
const date = new Date(value);
return Number.isNaN(date.getTime()) ? undefined : date;
}
function formatLocalDateTime(date: Date) {
const year = date.getFullYear();
const month = pad(date.getMonth() + 1);
const day = pad(date.getDate());
const hours = pad(date.getHours());
const minutes = pad(date.getMinutes());
return `${year}-${month}-${day}T${hours}:${minutes}`;
}
function formatTimeValue(date: Date) {
return `${pad(date.getHours())}:${pad(date.getMinutes())}`;
}
function pad(value: number) {
return String(value).padStart(2, '0');
}
+3
View File
@@ -1,13 +1,16 @@
export * from './badge';
export * from './button';
export * from './calendar';
export * from './card';
export * from './checkbox';
export * from './confirm-dialog';
export * from './date-time-picker';
export * from './dialog';
export * from './form-item';
export * from './input';
export * from './label';
export * from './message';
export * from './popover';
export * from './select';
export * from './separator';
export * from './table';
+24
View File
@@ -0,0 +1,24 @@
import * as React from 'react';
import * as PopoverPrimitive from '@radix-ui/react-popover';
import { cn } from '../../lib/utils';
export const Popover = PopoverPrimitive.Root;
export const PopoverTrigger = PopoverPrimitive.Trigger;
export const PopoverAnchor = PopoverPrimitive.Anchor;
export const PopoverContent = React.forwardRef<
React.ElementRef<typeof PopoverPrimitive.Content>,
React.ComponentPropsWithoutRef<typeof PopoverPrimitive.Content>
>(({ align = 'center', className, sideOffset = 6, ...props }, ref) => (
<PopoverPrimitive.Portal>
<PopoverPrimitive.Content
align={align}
className={cn('shPopoverContent', className)}
ref={ref}
sideOffset={sideOffset}
{...props}
/>
</PopoverPrimitive.Portal>
));
PopoverContent.displayName = PopoverPrimitive.Content.displayName;