first commit

This commit is contained in:
2026-07-25 15:42:10 +08:00
commit e302f4dfe4
250 changed files with 24504 additions and 0 deletions
@@ -0,0 +1,130 @@
import * as React from 'react'
import { Badge } from '@/components/ui/badge'
import { Button } from '@/components/ui/button'
import { AdminActionDialog, type StatusTone } from '@/components/admin'
export type BatchActionId = 'hide' | 'delete' | 'restore' | string
export interface BatchActionConfig {
id: BatchActionId
label: string
/** When true, the confirmation requires a note before submit. */
destructive?: boolean
tone?: StatusTone
confirmTitle: string
confirmDescription: string
confirmLabel?: string
}
interface BatchOperationsToolbarProps {
selectedCount: number
totalCount?: number
onSelectAll?: () => void
onClearSelection?: () => void
actions: BatchActionConfig[]
onAction: (action: BatchActionConfig, remark: string) => void
isLoading?: boolean
pendingAction?: BatchActionId | null
}
export function BatchOperationsToolbar({
selectedCount,
totalCount,
onSelectAll,
onClearSelection,
actions,
onAction,
isLoading = false,
pendingAction = null,
}: BatchOperationsToolbarProps) {
const [pending, setPending] = React.useState<BatchActionConfig | null>(null)
const [remark, setRemark] = React.useState('')
if (selectedCount === 0) return null
function handleConfirm() {
if (!pending) return
if (pending.destructive && !remark.trim()) {
return
}
onAction(pending, remark.trim())
setPending(null)
setRemark('')
}
return (
<>
<div className='flex flex-wrap items-center gap-3 rounded-md border border-primary/30 bg-primary/5 px-3 py-2'>
<Badge variant='default' className='gap-1'>
Selected {selectedCount}
{typeof totalCount === 'number' ? ` / ${totalCount}` : ''}
</Badge>
{onSelectAll && (
<Button variant='ghost' size='sm' onClick={onSelectAll}>
Select current page
</Button>
)}
{onClearSelection && (
<Button variant='ghost' size='sm' onClick={onClearSelection}>
Clear
</Button>
)}
<div className='ml-auto flex flex-wrap items-center gap-2'>
{actions.map((action) => (
<Button
key={action.id}
size='sm'
variant={action.destructive ? 'destructive' : 'outline'}
disabled={isLoading}
onClick={() => {
setRemark('')
setPending(action)
}}
>
{action.label}
</Button>
))}
</div>
</div>
<AdminActionDialog
open={pending !== null}
onOpenChange={(open) => {
if (!open) {
setPending(null)
setRemark('')
}
}}
tone={pending?.tone ?? (pending?.destructive ? 'danger' : 'info')}
title={pending?.confirmTitle ?? 'Confirm bulk action'}
description={pending?.confirmDescription ?? ''}
confirmLabel={pending?.confirmLabel ?? 'Confirm'}
isLoading={isLoading && pendingAction === pending?.id}
onConfirm={handleConfirm}
>
{pending?.destructive ? (
<div className='space-y-2'>
<label className='text-sm font-medium'>Note (required)</label>
<textarea
className='min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
placeholder='Add a short reason for this action'
value={remark}
onChange={(e) => setRemark(e.target.value)}
required
/>
</div>
) : (
<div className='space-y-2'>
<label className='text-sm font-medium'>Note (optional)</label>
<textarea
className='min-h-[80px] w-full rounded-md border border-input bg-background px-3 py-2 text-sm ring-offset-background placeholder:text-muted-foreground focus-visible:ring-2 focus-visible:ring-ring focus-visible:outline-none'
placeholder='Add a short note'
value={remark}
onChange={(e) => setRemark(e.target.value)}
/>
</div>
)}
</AdminActionDialog>
</>
)
}