699ea6e8
杨鑫
完善打印逻辑
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
"use client";
import * as React from "react";
import { useMemo, useState } from "react";
import { ChevronsUpDown, Search } from "lucide-react";
import { cn } from "./utils";
import { Button } from "./button";
import { Checkbox } from "./checkbox";
import { Input } from "./input";
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
import type { SearchableSelectOption } from "./searchable-select";
export type { SearchableSelectOption };
function dedupeIds(ids: string[]): string[] {
const seen = new Set<string>();
const out: string[] = [];
for (const id of ids) {
const t = id.trim();
if (!t || seen.has(t)) continue;
seen.add(t);
out.push(t);
}
return out;
}
export function SearchableMultiSelect({
values,
onValuesChange,
options,
placeholder = "Select…",
searchPlaceholder = "Search…",
emptyText = "No matching results.",
disabled,
className,
|
91821909
杨鑫
最新
|
36
37
|
/** 顶栏/模板编辑器等紧凑行:与 SearchableSelect h-7、text-xs 对齐 */
compact = false,
|
ef6b3255
杨鑫
修改BUG
|
38
39
|
/** 在列表顶部增加一行「全选」:针对当前搜索结果对应的选项值全选 / 清除 */
selectAllRowLabel,
|
699ea6e8
杨鑫
完善打印逻辑
|
40
41
42
43
44
45
46
47
48
|
}: {
values: string[];
onValuesChange: (next: string[]) => void;
options: SearchableSelectOption[];
placeholder?: string;
searchPlaceholder?: string;
emptyText?: string;
disabled?: boolean;
className?: string;
|
91821909
杨鑫
最新
|
49
|
compact?: boolean;
|
ef6b3255
杨鑫
修改BUG
|
50
|
selectAllRowLabel?: string;
|
699ea6e8
杨鑫
完善打印逻辑
|
51
52
53
54
55
56
57
58
59
60
61
62
63
64
|
}) {
const [open, setOpen] = useState(false);
const [q, setQ] = useState("");
const normalized = useMemo(() => dedupeIds(values), [values]);
const filtered = useMemo(() => {
const s = q.trim().toLowerCase();
if (!s) return options;
return options.filter(
(o) =>
o.label.toLowerCase().includes(s) || o.value.toLowerCase().includes(s),
);
}, [options, q]);
|
ef6b3255
杨鑫
修改BUG
|
65
66
67
68
69
70
|
const filteredValues = useMemo(() => filtered.map((o) => o.value).filter(Boolean), [filtered]);
const allFilteredSelected =
filteredValues.length > 0 && filteredValues.every((v) => normalized.includes(v));
const someFilteredSelected = filteredValues.some((v) => normalized.includes(v));
|
699ea6e8
杨鑫
完善打印逻辑
|
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
|
const labelByValue = useMemo(() => {
const m = new Map<string, string>();
for (const o of options) m.set(o.value, o.label);
return m;
}, [options]);
const summary = useMemo(() => {
if (normalized.length === 0) return null;
if (normalized.length === 1) {
return labelByValue.get(normalized[0]) ?? normalized[0];
}
const first = labelByValue.get(normalized[0]) ?? normalized[0];
const second = labelByValue.get(normalized[1]) ?? normalized[1];
if (normalized.length === 2) return `${first}, ${second}`;
return `${first}, ${second} +${normalized.length - 2}`;
}, [normalized, labelByValue]);
const toggle = (id: string) => {
const t = id.trim();
if (!t) return;
if (normalized.includes(t)) {
onValuesChange(normalized.filter((x) => x !== t));
} else {
onValuesChange([...normalized, t]);
}
};
|
ef6b3255
杨鑫
修改BUG
|
98
99
100
101
102
103
104
105
106
|
const toggleSelectAllFiltered = () => {
if (filteredValues.length === 0) return;
if (allFilteredSelected) {
onValuesChange(normalized.filter((x) => !filteredValues.includes(x)));
} else {
onValuesChange(dedupeIds([...normalized, ...filteredValues]));
}
};
|
699ea6e8
杨鑫
完善打印逻辑
|
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
|
return (
<Popover
open={open}
onOpenChange={(o) => {
setOpen(o);
if (!o) setQ("");
}}
>
<PopoverTrigger asChild>
<Button
type="button"
variant="outline"
role="combobox"
aria-expanded={open}
disabled={disabled}
className={cn(
|
91821909
杨鑫
最新
|
123
124
125
|
compact
? "w-full justify-between overflow-hidden border border-gray-300 bg-white font-normal shadow-none hover:bg-white"
: "h-auto min-h-10 w-full justify-between overflow-hidden px-3 py-2 font-normal border border-gray-300 bg-white",
|
699ea6e8
杨鑫
完善打印逻辑
|
126
127
128
129
130
|
className,
)}
>
<span
className={cn(
|
91821909
杨鑫
最新
|
131
132
|
"min-w-0 flex-1 truncate text-left",
compact ? "text-xs leading-tight" : "text-sm",
|
699ea6e8
杨鑫
完善打印逻辑
|
133
134
|
!summary && "text-gray-500",
)}
|
91821909
杨鑫
最新
|
135
|
title={summary ?? placeholder}
|
699ea6e8
杨鑫
完善打印逻辑
|
136
137
138
|
>
{summary ?? placeholder}
</span>
|
91821909
杨鑫
最新
|
139
140
141
142
143
144
|
<ChevronsUpDown
className={cn(
"ml-2 shrink-0 self-center opacity-50",
compact ? "h-3 w-3" : "h-4 w-4",
)}
/>
|
699ea6e8
杨鑫
完善打印逻辑
|
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
|
</Button>
</PopoverTrigger>
<PopoverContent
className="w-[var(--radix-popover-trigger-width)] max-w-[min(100vw-2rem,400px)] p-0"
align="start"
>
<div className="flex items-center gap-2 border-b border-gray-100 px-2 py-2">
<Search className="h-4 w-4 shrink-0 text-gray-400" />
<Input
value={q}
onChange={(e) => setQ(e.target.value)}
placeholder={searchPlaceholder}
className="h-8 border-0 bg-transparent px-0 text-sm shadow-none focus-visible:ring-0"
/>
</div>
<div className="max-h-[min(240px,50vh)] overflow-y-auto p-1">
{filtered.length === 0 ? (
<div className="px-2 py-6 text-center text-sm text-gray-500">{emptyText}</div>
) : (
|
ef6b3255
杨鑫
修改BUG
|
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
|
<>
{selectAllRowLabel ? (
<label className="flex cursor-pointer items-center gap-2 rounded-md border-b border-gray-100 px-2 py-2 text-sm font-medium hover:bg-gray-100">
<Checkbox
checked={
allFilteredSelected ? true : someFilteredSelected ? "indeterminate" : false
}
onCheckedChange={() => toggleSelectAllFiltered()}
onClick={(e) => e.stopPropagation()}
/>
<span className="min-w-0 flex-1 truncate">{selectAllRowLabel}</span>
</label>
) : null}
{filtered.map((opt) => (
<label
key={opt.value}
className="flex cursor-pointer items-center gap-2 rounded-md px-2 py-2 text-sm hover:bg-gray-100"
>
<Checkbox
checked={normalized.includes(opt.value)}
onCheckedChange={() => toggle(opt.value)}
onClick={(e) => e.stopPropagation()}
/>
<span className="min-w-0 flex-1 truncate">{opt.label}</span>
</label>
))}
</>
|
699ea6e8
杨鑫
完善打印逻辑
|
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
|
)}
</div>
{normalized.length > 0 ? (
<div className="border-t border-gray-100 px-2 py-1.5">
<button
type="button"
className="text-xs text-gray-500 underline-offset-2 hover:text-gray-900 hover:underline"
onClick={() => {
onValuesChange([]);
setQ("");
}}
>
Clear all ({normalized.length})
</button>
</div>
) : null}
</PopoverContent>
</Popover>
);
}
|