fix: support default and named export for useDebounce hook

Ensure compatibility with both default and named imports for hook.

#VERCEL_SKIP

Co-authored-by: null <4804959+fnvtk@users.noreply.github.com>
This commit is contained in:
v0
2025-09-23 07:09:47 +00:00
parent 7bb91b447e
commit ce0a716d02
30 changed files with 1682 additions and 2474 deletions

View File

@@ -1,19 +1,19 @@
"use client"
import { useMemo, useState } from "react"
import { Dialog, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
import { useEffect, useState } from "react"
import { X } from 'lucide-react'
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Checkbox } from "@/components/ui/checkbox"
import { Label } from "@/components/ui/label"
import { Input } from "@/components/ui/input"
export type FilterValues = {
tags: string[]
status: string[]
status: Array<"活跃" | "沉睡" | "已封禁">
rfm: [number, number]
}
type Props = {
interface Props {
open: boolean
onOpenChange: (v: boolean) => void
allTags: string[]
@@ -21,122 +21,131 @@ type Props = {
onApply: (v: FilterValues) => void
}
const STATUS_OPTIONS = ["活跃", "沉睡", "已封禁"] as const
export default function FilterDrawer({
open,
onOpenChange,
allTags,
value,
onApply,
}: Props) {
export default function FilterDrawer({ open, onOpenChange, allTags, value, onApply }: Props) {
const [local, setLocal] = useState<FilterValues>(value)
// 同步外部变更
useMemo(() => setLocal(value), [value])
useEffect(() => setLocal(value), [value, open])
const toggleArrayVal = (arr: string[], val: string, checked: boolean) =>
checked ? Array.from(new Set([...arr, val])) : arr.filter((x) => x !== val)
const toggleTag = (t: string, checked: boolean) => {
setLocal((prev) => ({
...prev,
tags: checked ? Array.from(new Set([...prev.tags, t])) : prev.tags.filter((x) => x !== t),
}))
}
const toggleStatus = (s: "活跃" | "沉睡" | "已封禁", checked: boolean) => {
setLocal((prev) => ({
...prev,
status: checked ? Array.from(new Set([...prev.status, s])) : prev.status.filter((x) => x !== s),
}))
}
const apply = () => {
onApply(local)
onOpenChange(false)
}
const reset = () => {
const init: FilterValues = { tags: [], status: [], rfm: [0, 100] }
setLocal(init)
onApply(init)
}
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-lg">
<DialogHeader>
<DialogTitle></DialogTitle>
</DialogHeader>
<div
className={`fixed inset-0 z-50 ${open ? "" : "pointer-events-none"} aria-modal`}
role="dialog"
aria-hidden={!open}
>
<div
className={`absolute inset-0 bg-black/40 transition-opacity ${open ? "opacity-100" : "opacity-0"}`}
onClick={() => onOpenChange(false)}
/>
<aside
className={`absolute right-0 top-0 h-full w-full max-w-md bg-white shadow-xl transition-transform duration-300
${open ? "translate-x-0" : "translate-x-full"}`}
aria-label="筛选"
>
<div className="flex items-center justify-between p-4 border-b">
<h2 className="text-lg font-semibold"></h2>
<Button variant="ghost" size="icon" onClick={() => onOpenChange(false)} aria-label="关闭筛选">
<X className="h-5 w-5" />
</Button>
</div>
<div className="grid gap-5 py-2">
<section className="space-y-2">
<Label></Label>
<div className="grid grid-cols-3 gap-2">
{STATUS_OPTIONS.map((s) => {
const checked = local.status.includes(s)
return (
<label key={s} className="flex items-center gap-2 text-sm">
<Checkbox
checked={checked}
onCheckedChange={(ck) =>
setLocal((p) => ({ ...p, status: toggleArrayVal(p.status, s, !!ck) }))
}
/>
<span>{s}</span>
</label>
)
})}
<div className="p-4 space-y-6 overflow-y-auto h-[calc(100%-120px)]">
{/* RFM 区间 */}
<section>
<h3 className="text-sm font-medium mb-3">RFM </h3>
<div className="grid grid-cols-2 gap-2 items-center">
<div>
<Label htmlFor="rfmMin" className="text-xs"></Label>
<Input
id="rfmMin"
type="number"
min={0}
max={100}
value={local.rfm[0]}
onChange={(e) => {
const v = Math.max(0, Math.min(100, Number(e.target.value) || 0))
setLocal((p) => ({ ...p, rfm: [Math.min(v, p.rfm[1]), p.rfm[1]] }))
}}
/>
</div>
<div>
<Label htmlFor="rfmMax" className="text-xs"></Label>
<Input
id="rfmMax"
type="number"
min={0}
max={100}
value={local.rfm[1]}
onChange={(e) => {
const v = Math.max(0, Math.min(100, Number(e.target.value) || 100))
setLocal((p) => ({ ...p, rfm: [p.rfm[0], Math.max(v, p.rfm[0])] }))
}}
/>
</div>
</div>
</section>
<section className="space-y-2">
<Label>RFM </Label>
<div className="flex items-center gap-2">
<Input
type="number"
min={0}
max={100}
value={local.rfm[0]}
aria-label="RFM最小值"
onChange={(e) =>
setLocal((p) => {
const v = Math.max(0, Math.min(100, Number(e.target.value)))
return { ...p, rfm: [v, Math.max(v, p.rfm[1])] }
})
}
/>
<span className="text-muted-foreground">{'—'}</span>
<Input
type="number"
min={0}
max={100}
value={local.rfm[1]}
aria-label="RFM最大值"
onChange={(e) =>
setLocal((p) => {
const v = Math.max(0, Math.min(100, Number(e.target.value)))
return { ...p, rfm: [Math.min(p.rfm[0], v), v] }
})
}
/>
{/* 状态 */}
<section>
<h3 className="text-sm font-medium mb-3"></h3>
<div className="grid gap-2">
{(["活跃", "沉睡", "已封禁"] as const).map((s) => (
<label key={s} className="flex items-center gap-2 text-sm">
<Checkbox checked={local.status.includes(s)} onCheckedChange={(ck) => toggleStatus(s, Boolean(ck))} />
<span>{s}</span>
</label>
))}
</div>
</section>
<section className="space-y-2">
<Label></Label>
<div className="grid grid-cols-2 gap-2 max-h-48 overflow-auto pr-1">
{allTags.length === 0 && (
<div className="text-sm text-muted-foreground col-span-2"></div>
)}
{allTags.map((t) => {
const checked = local.tags.includes(t)
return (
<label key={t} className="flex items-center gap-2 text-sm">
<Checkbox
checked={checked}
onCheckedChange={(ck) =>
setLocal((p) => ({ ...p, tags: toggleArrayVal(p.tags, t, !!ck) }))
}
/>
<span className="truncate">{t}</span>
</label>
)
})}
{/* 标签 */}
<section>
<h3 className="text-sm font-medium mb-3"></h3>
<div className="grid grid-cols-2 gap-2 max-h-56 overflow-auto">
{allTags.map((t) => (
<label key={t} className="flex items-center gap-2 text-sm">
<Checkbox checked={local.tags.includes(t)} onCheckedChange={(ck) => toggleTag(t, Boolean(ck))} />
<span className="truncate">{t}</span>
</label>
))}
{!allTags.length && <div className="text-xs text-muted-foreground col-span-2"></div>}
</div>
</section>
</div>
<div className="flex justify-end gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button>
<Button
onClick={() => {
onApply(local)
onOpenChange(false)
}}
>
</Button>
{/* Footer */}
<div className="p-4 border-t flex items-center justify-between gap-2">
<Button variant="outline" onClick={reset}></Button>
<div className="flex gap-2">
<Button variant="outline" onClick={() => onOpenChange(false)}></Button>
<Button onClick={apply}></Button>
</div>
</div>
</DialogContent>
</Dialog>
</aside>
</div>
)
}