chore: checkpoint admin editor and perf work

This commit is contained in:
2026-03-31 00:12:02 +08:00
parent 92a85eef20
commit 99b308e800
45 changed files with 7265 additions and 833 deletions

View File

@@ -1,18 +1,473 @@
import * as React from 'react'
import * as ReactDOM from 'react-dom'
import { Check, ChevronDown } from 'lucide-react'
import { cn } from '@/lib/utils'
const Select = React.forwardRef<HTMLSelectElement, React.ComponentProps<'select'>>(
({ className, ...props }, ref) => (
<select
ref={ref}
className={cn(
'flex h-11 w-full rounded-xl border border-input bg-background/80 px-3 py-2 text-sm shadow-sm outline-none transition-colors focus-visible:ring-2 focus-visible:ring-ring/70 disabled:cursor-not-allowed disabled:opacity-50',
className,
)}
{...props}
/>
),
type NativeSelectProps = React.ComponentProps<'select'>
type SelectOption = {
value: string
label: React.ReactNode
disabled: boolean
}
type MenuPlacement = 'top' | 'bottom'
function normalizeValue(value: NativeSelectProps['value'] | NativeSelectProps['defaultValue']) {
if (Array.isArray(value)) {
return value[0] == null ? '' : String(value[0])
}
return value == null ? '' : String(value)
}
function extractOptions(children: React.ReactNode) {
const options: SelectOption[] = []
React.Children.forEach(children, (child) => {
if (!React.isValidElement(child) || child.type !== 'option') {
return
}
const props = child.props as React.OptionHTMLAttributes<HTMLOptionElement> & {
children?: React.ReactNode
}
options.push({
value: normalizeValue(props.value),
label: props.children,
disabled: Boolean(props.disabled),
})
})
return options
}
function getFirstEnabledIndex(options: SelectOption[]) {
return options.findIndex((option) => !option.disabled)
}
function getLastEnabledIndex(options: SelectOption[]) {
for (let index = options.length - 1; index >= 0; index -= 1) {
if (!options[index]?.disabled) {
return index
}
}
return -1
}
function getNextEnabledIndex(options: SelectOption[], currentIndex: number, direction: 1 | -1) {
if (options.length === 0) {
return -1
}
let index = currentIndex
for (let step = 0; step < options.length; step += 1) {
index = (index + direction + options.length) % options.length
if (!options[index]?.disabled) {
return index
}
}
return -1
}
const Select = React.forwardRef<HTMLSelectElement, NativeSelectProps>(
(
{
children,
className,
defaultValue,
disabled = false,
id,
onBlur,
onClick,
onFocus,
onKeyDown,
value,
...props
},
forwardedRef,
) => {
const options = React.useMemo(() => extractOptions(children), [children])
const isControlled = value !== undefined
const initialValue = React.useMemo(() => {
if (defaultValue !== undefined) {
return normalizeValue(defaultValue)
}
return options[0]?.value ?? ''
}, [defaultValue, options])
const [internalValue, setInternalValue] = React.useState(initialValue)
const [open, setOpen] = React.useState(false)
const [highlightedIndex, setHighlightedIndex] = React.useState(-1)
const [menuPlacement, setMenuPlacement] = React.useState<MenuPlacement>('bottom')
const [menuStyle, setMenuStyle] = React.useState<React.CSSProperties | null>(null)
const wrapperRef = React.useRef<HTMLDivElement>(null)
const triggerRef = React.useRef<HTMLButtonElement>(null)
const nativeSelectRef = React.useRef<HTMLSelectElement>(null)
const menuRef = React.useRef<HTMLDivElement>(null)
const optionRefs = React.useRef<Array<HTMLButtonElement | null>>([])
const menuId = React.useId()
const currentValue = isControlled ? normalizeValue(value) : internalValue
const selectedIndex = options.findIndex((option) => option.value === currentValue)
const selectedOption = selectedIndex >= 0 ? options[selectedIndex] : options[0] ?? null
React.useEffect(() => {
if (!isControlled && options.length > 0 && !options.some((option) => option.value === internalValue)) {
setInternalValue(options[0]?.value ?? '')
}
}, [internalValue, isControlled, options])
const updateMenuPosition = React.useCallback(() => {
const trigger = triggerRef.current
if (!trigger) {
return
}
const rect = trigger.getBoundingClientRect()
const viewportPadding = 12
const gutter = 6
const estimatedHeight = Math.min(Math.max(options.length, 1) * 44 + 18, 320)
const spaceBelow = window.innerHeight - rect.bottom - viewportPadding
const spaceAbove = rect.top - viewportPadding
const openToTop = spaceBelow < estimatedHeight && spaceAbove > spaceBelow
const maxHeight = Math.max(120, Math.min(openToTop ? spaceAbove : spaceBelow, 320))
const width = Math.min(rect.width, window.innerWidth - viewportPadding * 2)
const left = Math.min(Math.max(rect.left, viewportPadding), window.innerWidth - width - viewportPadding)
setMenuPlacement(openToTop ? 'top' : 'bottom')
setMenuStyle(
openToTop
? {
left,
width,
maxHeight,
bottom: window.innerHeight - rect.top + gutter,
}
: {
left,
width,
maxHeight,
top: rect.bottom + gutter,
},
)
}, [options.length])
const setOpenWithHighlight = React.useCallback(
(nextOpen: boolean, preferredIndex?: number) => {
if (disabled) {
return
}
if (nextOpen) {
const fallbackIndex =
preferredIndex ??
(selectedIndex >= 0 && !options[selectedIndex]?.disabled
? selectedIndex
: getFirstEnabledIndex(options))
setHighlightedIndex(fallbackIndex)
updateMenuPosition()
setOpen(true)
return
}
setOpen(false)
},
[disabled, options, selectedIndex, updateMenuPosition],
)
const commitValue = React.useCallback(
(nextIndex: number) => {
const option = options[nextIndex]
const nativeSelect = nativeSelectRef.current
if (!option || option.disabled) {
return
}
if (!isControlled) {
setInternalValue(option.value)
}
if (nativeSelect && currentValue !== option.value) {
nativeSelect.value = option.value
nativeSelect.dispatchEvent(new Event('change', { bubbles: true }))
}
setOpen(false)
window.requestAnimationFrame(() => {
triggerRef.current?.focus()
})
},
[currentValue, isControlled, options],
)
const handleKeyDown = React.useCallback(
(event: React.KeyboardEvent<HTMLButtonElement | HTMLDivElement>) => {
onKeyDown?.(event as unknown as React.KeyboardEvent<HTMLSelectElement>)
if (event.defaultPrevented || disabled) {
return
}
switch (event.key) {
case 'ArrowDown': {
event.preventDefault()
if (!open) {
const nextIndex =
selectedIndex >= 0
? getNextEnabledIndex(options, selectedIndex, 1)
: getFirstEnabledIndex(options)
setOpenWithHighlight(true, nextIndex >= 0 ? nextIndex : getFirstEnabledIndex(options))
return
}
setHighlightedIndex((current) => getNextEnabledIndex(options, current, 1))
return
}
case 'ArrowUp': {
event.preventDefault()
if (!open) {
const nextIndex =
selectedIndex >= 0
? getNextEnabledIndex(options, selectedIndex, -1)
: getLastEnabledIndex(options)
setOpenWithHighlight(true, nextIndex >= 0 ? nextIndex : getLastEnabledIndex(options))
return
}
setHighlightedIndex((current) => getNextEnabledIndex(options, current, -1))
return
}
case 'Home': {
event.preventDefault()
const firstIndex = getFirstEnabledIndex(options)
if (!open) {
setOpenWithHighlight(true, firstIndex)
return
}
setHighlightedIndex(firstIndex)
return
}
case 'End': {
event.preventDefault()
const lastIndex = getLastEnabledIndex(options)
if (!open) {
setOpenWithHighlight(true, lastIndex)
return
}
setHighlightedIndex(lastIndex)
return
}
case 'Enter':
case ' ': {
event.preventDefault()
if (!open) {
setOpenWithHighlight(true)
return
}
if (highlightedIndex >= 0) {
commitValue(highlightedIndex)
}
return
}
case 'Escape': {
if (!open) {
return
}
event.preventDefault()
setOpen(false)
return
}
case 'Tab': {
setOpen(false)
return
}
default:
return
}
},
[commitValue, disabled, highlightedIndex, onKeyDown, open, options, selectedIndex, setOpenWithHighlight],
)
React.useLayoutEffect(() => {
if (!open) {
return
}
updateMenuPosition()
}, [open, updateMenuPosition])
React.useEffect(() => {
if (!open) {
return
}
const handlePointerDown = (event: MouseEvent) => {
const target = event.target as Node
if (wrapperRef.current?.contains(target) || menuRef.current?.contains(target)) {
return
}
setOpen(false)
}
const handleWindowChange = () => updateMenuPosition()
document.addEventListener('mousedown', handlePointerDown)
window.addEventListener('resize', handleWindowChange)
window.addEventListener('scroll', handleWindowChange, true)
return () => {
document.removeEventListener('mousedown', handlePointerDown)
window.removeEventListener('resize', handleWindowChange)
window.removeEventListener('scroll', handleWindowChange, true)
}
}, [open, updateMenuPosition])
React.useEffect(() => {
if (!open || highlightedIndex < 0) {
return
}
optionRefs.current[highlightedIndex]?.scrollIntoView({ block: 'nearest' })
}, [highlightedIndex, open])
const triggerClasses = cn(
'flex h-11 w-full items-center justify-between gap-3 rounded-xl border border-input bg-background/80 px-3 py-2 text-left text-sm text-foreground shadow-sm outline-none transition-[border-color,box-shadow,background-color,transform] focus-visible:ring-2 focus-visible:ring-ring/70 disabled:cursor-not-allowed disabled:opacity-50 data-[state=open]:border-primary/40 data-[state=open]:bg-card data-[state=open]:shadow-[0_18px_40px_rgb(15_23_42_/_0.14)]',
className,
)
const menu = open && menuStyle
? ReactDOM.createPortal(
<div
ref={menuRef}
aria-orientation="vertical"
className={cn(
'custom-select-popover fixed z-[80] overflow-hidden rounded-2xl border border-border/70 bg-popover p-1.5 text-popover-foreground shadow-[0_18px_48px_rgb(15_23_42_/_0.18)] will-change-transform',
menuPlacement === 'top' ? 'origin-bottom' : 'origin-top',
)}
id={menuId}
onKeyDown={handleKeyDown}
role="listbox"
style={menuStyle}
tabIndex={-1}
>
<div className="max-h-full overflow-y-auto pr-0.5">
{options.map((option, index) => {
const selected = option.value === currentValue
const highlighted = index === highlightedIndex
return (
<button
key={`${option.value}-${index}`}
ref={(node) => {
optionRefs.current[index] = node
}}
aria-selected={selected}
className={cn(
'flex w-full items-center justify-between gap-3 rounded-xl px-3 py-2.5 text-left text-sm transition-colors',
option.disabled ? 'cursor-not-allowed opacity-45' : 'cursor-pointer',
selected
? 'bg-primary text-primary-foreground shadow-[0_12px_30px_rgb(37_99_235_/_0.22)]'
: highlighted
? 'bg-accent text-accent-foreground'
: 'text-foreground hover:bg-accent hover:text-accent-foreground',
)}
disabled={option.disabled}
onClick={() => commitValue(index)}
onMouseEnter={() => {
if (!option.disabled) {
setHighlightedIndex(index)
}
}}
role="option"
type="button"
>
<span className="truncate">{option.label}</span>
<Check className={cn('h-4 w-4 shrink-0', selected ? 'opacity-100' : 'opacity-0')} />
</button>
)
})}
</div>
</div>,
document.body,
)
: null
return (
<div className="relative w-full" ref={wrapperRef}>
<select
{...props}
aria-hidden="true"
className="pointer-events-none absolute h-0 w-0 opacity-0"
defaultValue={defaultValue}
disabled={disabled}
id={id}
onBlur={onBlur}
onFocus={onFocus}
ref={(node) => {
nativeSelectRef.current = node
if (typeof forwardedRef === 'function') {
forwardedRef(node)
} else if (forwardedRef) {
forwardedRef.current = node
}
}}
tabIndex={-1}
value={isControlled ? currentValue : internalValue}
>
{children}
</select>
<button
aria-controls={open ? menuId : undefined}
aria-expanded={open}
aria-haspopup="listbox"
className={triggerClasses}
data-state={open ? 'open' : 'closed'}
disabled={disabled}
onBlur={(event) => {
onBlur?.(event as unknown as React.FocusEvent<HTMLSelectElement>)
}}
onClick={(event) => {
onClick?.(event as unknown as React.MouseEvent<HTMLSelectElement>)
}}
onPointerDown={(event) => {
if (event.button !== 0 || disabled) {
return
}
event.preventDefault()
triggerRef.current?.focus()
setOpenWithHighlight(!open)
}}
onFocus={(event) => {
onFocus?.(event as unknown as React.FocusEvent<HTMLSelectElement>)
}}
onKeyDown={handleKeyDown}
ref={triggerRef}
role="combobox"
type="button"
>
<span className="min-w-0 flex-1 truncate">{selectedOption?.label ?? '请选择'}</span>
<span className="flex h-8 w-8 shrink-0 items-center justify-center rounded-lg bg-muted/70 text-muted-foreground transition-colors">
<ChevronDown className={cn('h-4 w-4 transition-transform duration-200', open && 'rotate-180')} />
</span>
</button>
{menu}
</div>
)
},
)
Select.displayName = 'Select'