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:
@@ -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>
|
||||
)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user