koishi-plugin-chatluna-affinity 0.3.3 → 0.3.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client/components/ui/input-group.tsx +108 -0
- package/client/components/ui/input.tsx +18 -0
- package/client/dashboard/AffinityDashboard.tsx +121 -12
- package/dist/index.js +7140 -6944
- package/dist/style.css +1 -1
- package/package.json +1 -1
- package/readme.md +9 -0
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { cva, type VariantProps } from "class-variance-authority";
|
|
2
|
+
import * as React from "react";
|
|
3
|
+
import { cn } from "../../lib/utils";
|
|
4
|
+
import { Button } from "./button";
|
|
5
|
+
import { Input } from "./input";
|
|
6
|
+
|
|
7
|
+
function InputGroup({ className, ...props }: React.ComponentProps<"div">) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
className={cn(
|
|
11
|
+
"group/input-group relative flex h-9 w-full min-w-0 items-center rounded-xl border border-input bg-background outline-none transition-[color,box-shadow] focus-within:border-ring focus-within:ring-2 focus-within:ring-ring/50",
|
|
12
|
+
className,
|
|
13
|
+
)}
|
|
14
|
+
data-slot="input-group"
|
|
15
|
+
role="group"
|
|
16
|
+
{...props}
|
|
17
|
+
/>
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const inputGroupAddonVariants = cva(
|
|
22
|
+
"flex h-auto cursor-text items-center justify-center gap-2 py-1.5 text-sm font-medium text-muted-foreground select-none [&>svg]:pointer-events-none [&>svg]:size-4 [&>svg]:shrink-0",
|
|
23
|
+
{
|
|
24
|
+
variants: {
|
|
25
|
+
align: {
|
|
26
|
+
"inline-start": "order-first pl-3",
|
|
27
|
+
"inline-end": "order-last pr-2",
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
defaultVariants: {
|
|
31
|
+
align: "inline-start",
|
|
32
|
+
},
|
|
33
|
+
},
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
function InputGroupAddon({
|
|
37
|
+
className,
|
|
38
|
+
align = "inline-start",
|
|
39
|
+
...props
|
|
40
|
+
}: React.ComponentProps<"div"> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
41
|
+
return (
|
|
42
|
+
<div
|
|
43
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
44
|
+
data-align={align}
|
|
45
|
+
data-slot="input-group-addon"
|
|
46
|
+
onClick={(event) => {
|
|
47
|
+
if ((event.target as HTMLElement).closest("button")) return;
|
|
48
|
+
event.currentTarget.parentElement?.querySelector("input")?.focus();
|
|
49
|
+
}}
|
|
50
|
+
role="group"
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const inputGroupButtonVariants = cva("flex items-center gap-2 shadow-none", {
|
|
57
|
+
variants: {
|
|
58
|
+
size: {
|
|
59
|
+
xs: "h-6 gap-1 rounded-lg px-2 text-xs [&>svg]:size-3.5",
|
|
60
|
+
"icon-xs": "size-6 rounded-lg p-0 [&>svg]:size-3.5",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
defaultVariants: {
|
|
64
|
+
size: "xs",
|
|
65
|
+
},
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
function InputGroupButton({
|
|
69
|
+
className,
|
|
70
|
+
type = "button",
|
|
71
|
+
variant = "ghost",
|
|
72
|
+
size = "xs",
|
|
73
|
+
...props
|
|
74
|
+
}: Omit<React.ComponentProps<typeof Button>, "size"> &
|
|
75
|
+
VariantProps<typeof inputGroupButtonVariants>) {
|
|
76
|
+
return (
|
|
77
|
+
<Button
|
|
78
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
79
|
+
data-size={size}
|
|
80
|
+
type={type}
|
|
81
|
+
variant={variant}
|
|
82
|
+
{...props}
|
|
83
|
+
/>
|
|
84
|
+
);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function InputGroupInput({
|
|
88
|
+
className,
|
|
89
|
+
...props
|
|
90
|
+
}: React.ComponentProps<"input">) {
|
|
91
|
+
return (
|
|
92
|
+
<Input
|
|
93
|
+
className={cn(
|
|
94
|
+
"flex-1 rounded-none border-0 bg-transparent px-2 shadow-none focus-visible:border-transparent focus-visible:ring-0",
|
|
95
|
+
className,
|
|
96
|
+
)}
|
|
97
|
+
data-slot="input-group-control"
|
|
98
|
+
{...props}
|
|
99
|
+
/>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
InputGroup,
|
|
105
|
+
InputGroupAddon,
|
|
106
|
+
InputGroupButton,
|
|
107
|
+
InputGroupInput,
|
|
108
|
+
};
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import * as React from "react";
|
|
2
|
+
import { cn } from "../../lib/utils";
|
|
3
|
+
|
|
4
|
+
function Input({ className, type, ...props }: React.ComponentProps<"input">) {
|
|
5
|
+
return (
|
|
6
|
+
<input
|
|
7
|
+
className={cn(
|
|
8
|
+
"h-9 w-full min-w-0 rounded-xl border border-input bg-transparent px-3 py-1 text-base outline-none transition-[color,box-shadow] placeholder:text-muted-foreground selection:bg-primary selection:text-primary-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/50 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm",
|
|
9
|
+
className,
|
|
10
|
+
)}
|
|
11
|
+
data-slot="input"
|
|
12
|
+
type={type}
|
|
13
|
+
{...props}
|
|
14
|
+
/>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export { Input };
|
|
@@ -7,8 +7,10 @@ import {
|
|
|
7
7
|
IconLoader2,
|
|
8
8
|
IconMinus,
|
|
9
9
|
IconRefresh,
|
|
10
|
+
IconSearch,
|
|
10
11
|
IconTrendingDown,
|
|
11
12
|
IconTrendingUp,
|
|
13
|
+
IconX,
|
|
12
14
|
} from "@tabler/icons-react";
|
|
13
15
|
import React, {
|
|
14
16
|
useCallback,
|
|
@@ -45,6 +47,12 @@ import {
|
|
|
45
47
|
CardHeader,
|
|
46
48
|
CardTitle,
|
|
47
49
|
} from "../components/ui/card";
|
|
50
|
+
import {
|
|
51
|
+
InputGroup,
|
|
52
|
+
InputGroupAddon,
|
|
53
|
+
InputGroupButton,
|
|
54
|
+
InputGroupInput,
|
|
55
|
+
} from "../components/ui/input-group";
|
|
48
56
|
import {
|
|
49
57
|
ChartContainer,
|
|
50
58
|
ChartTooltip,
|
|
@@ -85,6 +93,7 @@ type SortColumn =
|
|
|
85
93
|
| "user"
|
|
86
94
|
| "userId";
|
|
87
95
|
type SortDirection = "ascending" | "descending";
|
|
96
|
+
type RankingTab = "ranking" | "blacklist";
|
|
88
97
|
type TrendRange = "week" | "month" | "all";
|
|
89
98
|
|
|
90
99
|
interface TopUserSortDescriptor {
|
|
@@ -239,6 +248,20 @@ function compareText(left: string, right: string): number {
|
|
|
239
248
|
});
|
|
240
249
|
}
|
|
241
250
|
|
|
251
|
+
function normalizeSearchText(value: string): string {
|
|
252
|
+
return value.trim().toLocaleLowerCase("zh-CN");
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
function matchesSearchQuery(
|
|
256
|
+
query: string,
|
|
257
|
+
values: Array<string | null | undefined>,
|
|
258
|
+
): boolean {
|
|
259
|
+
if (!query) return true;
|
|
260
|
+
return values.some((value) =>
|
|
261
|
+
value ? normalizeSearchText(value).includes(query) : false,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
|
|
242
265
|
function compareTopUsers(
|
|
243
266
|
left: DashboardTopUser,
|
|
244
267
|
right: DashboardTopUser,
|
|
@@ -384,6 +407,40 @@ function SortHeader({
|
|
|
384
407
|
);
|
|
385
408
|
}
|
|
386
409
|
|
|
410
|
+
function RankingSearch({
|
|
411
|
+
value,
|
|
412
|
+
onValueChange,
|
|
413
|
+
}: {
|
|
414
|
+
value: string;
|
|
415
|
+
onValueChange: (value: string) => void;
|
|
416
|
+
}) {
|
|
417
|
+
return (
|
|
418
|
+
<InputGroup className="ml-auto w-64 max-w-full sm:w-80">
|
|
419
|
+
<InputGroupAddon>
|
|
420
|
+
<IconSearch aria-hidden="true" />
|
|
421
|
+
</InputGroupAddon>
|
|
422
|
+
<InputGroupInput
|
|
423
|
+
aria-label="搜索用户"
|
|
424
|
+
placeholder="搜索 QQ 号或昵称"
|
|
425
|
+
type="search"
|
|
426
|
+
value={value}
|
|
427
|
+
onChange={(event) => onValueChange(event.target.value)}
|
|
428
|
+
/>
|
|
429
|
+
{value ? (
|
|
430
|
+
<InputGroupAddon align="inline-end">
|
|
431
|
+
<InputGroupButton
|
|
432
|
+
aria-label="清空搜索"
|
|
433
|
+
size="icon-xs"
|
|
434
|
+
onClick={() => onValueChange("")}
|
|
435
|
+
>
|
|
436
|
+
<IconX aria-hidden="true" />
|
|
437
|
+
</InputGroupButton>
|
|
438
|
+
</InputGroupAddon>
|
|
439
|
+
) : null}
|
|
440
|
+
</InputGroup>
|
|
441
|
+
);
|
|
442
|
+
}
|
|
443
|
+
|
|
387
444
|
function StatCard({
|
|
388
445
|
label,
|
|
389
446
|
value,
|
|
@@ -562,10 +619,12 @@ function RelationshipDistribution({
|
|
|
562
619
|
}
|
|
563
620
|
|
|
564
621
|
function TopUserTable({
|
|
622
|
+
emptyMessage,
|
|
565
623
|
users,
|
|
566
624
|
selectedUserId,
|
|
567
625
|
onSelectUser,
|
|
568
626
|
}: {
|
|
627
|
+
emptyMessage: string;
|
|
569
628
|
users: DashboardTopUser[];
|
|
570
629
|
selectedUserId: string | null;
|
|
571
630
|
onSelectUser: (user: DashboardTopUser) => void;
|
|
@@ -601,7 +660,7 @@ function TopUserTable({
|
|
|
601
660
|
}, [pageCount]);
|
|
602
661
|
|
|
603
662
|
if (!users.length) {
|
|
604
|
-
return <p className="affinity-dashboard__empty"
|
|
663
|
+
return <p className="affinity-dashboard__empty">{emptyMessage}</p>;
|
|
605
664
|
}
|
|
606
665
|
|
|
607
666
|
return (
|
|
@@ -788,9 +847,15 @@ function TopUserTable({
|
|
|
788
847
|
);
|
|
789
848
|
}
|
|
790
849
|
|
|
791
|
-
function BlacklistTable({
|
|
850
|
+
function BlacklistTable({
|
|
851
|
+
emptyMessage,
|
|
852
|
+
items,
|
|
853
|
+
}: {
|
|
854
|
+
emptyMessage: string;
|
|
855
|
+
items: DashboardBlacklistItem[];
|
|
856
|
+
}) {
|
|
792
857
|
if (!items.length) {
|
|
793
|
-
return <p className="affinity-dashboard__empty"
|
|
858
|
+
return <p className="affinity-dashboard__empty">{emptyMessage}</p>;
|
|
794
859
|
}
|
|
795
860
|
|
|
796
861
|
return (
|
|
@@ -964,13 +1029,36 @@ function RankingPanel({
|
|
|
964
1029
|
}: {
|
|
965
1030
|
data: DashboardData;
|
|
966
1031
|
}) {
|
|
1032
|
+
const [activeTab, setActiveTab] = useState<RankingTab>("ranking");
|
|
1033
|
+
const [searchQuery, setSearchQuery] = useState("");
|
|
967
1034
|
const [selectedUserId, setSelectedUserId] = useState<string | null>(null);
|
|
1035
|
+
const normalizedSearchQuery = useMemo(
|
|
1036
|
+
() => normalizeSearchText(searchQuery),
|
|
1037
|
+
[searchQuery],
|
|
1038
|
+
);
|
|
1039
|
+
const filteredTopUsers = useMemo(
|
|
1040
|
+
() =>
|
|
1041
|
+
data.topUsers.filter((user) =>
|
|
1042
|
+
matchesSearchQuery(normalizedSearchQuery, [user.userId, user.name]),
|
|
1043
|
+
),
|
|
1044
|
+
[data.topUsers, normalizedSearchQuery],
|
|
1045
|
+
);
|
|
1046
|
+
const filteredBlacklistItems = useMemo(
|
|
1047
|
+
() =>
|
|
1048
|
+
data.blacklistItems.filter((item) =>
|
|
1049
|
+
matchesSearchQuery(normalizedSearchQuery, [item.userId, item.name]),
|
|
1050
|
+
),
|
|
1051
|
+
[data.blacklistItems, normalizedSearchQuery],
|
|
1052
|
+
);
|
|
1053
|
+
const hasSearchQuery = normalizedSearchQuery.length > 0;
|
|
968
1054
|
|
|
969
1055
|
useEffect(() => {
|
|
970
1056
|
setSelectedUserId((current) =>
|
|
971
|
-
current &&
|
|
1057
|
+
current && filteredTopUsers.some((user) => user.userId === current)
|
|
1058
|
+
? current
|
|
1059
|
+
: null,
|
|
972
1060
|
);
|
|
973
|
-
}, [
|
|
1061
|
+
}, [filteredTopUsers]);
|
|
974
1062
|
|
|
975
1063
|
return (
|
|
976
1064
|
<Card>
|
|
@@ -979,16 +1067,30 @@ function RankingPanel({
|
|
|
979
1067
|
<CardDescription>用户好感与黑名单记录</CardDescription>
|
|
980
1068
|
</CardHeader>
|
|
981
1069
|
<CardContent>
|
|
982
|
-
<Tabs
|
|
983
|
-
|
|
984
|
-
|
|
985
|
-
|
|
986
|
-
|
|
1070
|
+
<Tabs
|
|
1071
|
+
value={activeTab}
|
|
1072
|
+
onValueChange={(value) => setActiveTab(value as RankingTab)}
|
|
1073
|
+
>
|
|
1074
|
+
<div className="flex flex-wrap items-center justify-between gap-3">
|
|
1075
|
+
<TabsList className="self-start">
|
|
1076
|
+
<TabsTrigger value="ranking">好感度</TabsTrigger>
|
|
1077
|
+
<TabsTrigger value="blacklist">黑名单</TabsTrigger>
|
|
1078
|
+
</TabsList>
|
|
1079
|
+
<RankingSearch
|
|
1080
|
+
value={searchQuery}
|
|
1081
|
+
onValueChange={setSearchQuery}
|
|
1082
|
+
/>
|
|
1083
|
+
</div>
|
|
987
1084
|
<TabsContent value="ranking">
|
|
988
1085
|
<div className="grid gap-4">
|
|
989
1086
|
<TopUserTable
|
|
1087
|
+
emptyMessage={
|
|
1088
|
+
hasSearchQuery
|
|
1089
|
+
? "没有找到匹配的好感度用户。"
|
|
1090
|
+
: "当前 scopeId 暂无好感度记录。"
|
|
1091
|
+
}
|
|
990
1092
|
selectedUserId={selectedUserId}
|
|
991
|
-
users={
|
|
1093
|
+
users={filteredTopUsers}
|
|
992
1094
|
onSelectUser={(user) =>
|
|
993
1095
|
setSelectedUserId((current) =>
|
|
994
1096
|
current === user.userId ? null : user.userId,
|
|
@@ -998,7 +1100,14 @@ function RankingPanel({
|
|
|
998
1100
|
</div>
|
|
999
1101
|
</TabsContent>
|
|
1000
1102
|
<TabsContent value="blacklist">
|
|
1001
|
-
<BlacklistTable
|
|
1103
|
+
<BlacklistTable
|
|
1104
|
+
emptyMessage={
|
|
1105
|
+
hasSearchQuery
|
|
1106
|
+
? "没有找到匹配的黑名单用户。"
|
|
1107
|
+
: "当前 scopeId 暂无黑名单记录。"
|
|
1108
|
+
}
|
|
1109
|
+
items={filteredBlacklistItems}
|
|
1110
|
+
/>
|
|
1002
1111
|
</TabsContent>
|
|
1003
1112
|
</Tabs>
|
|
1004
1113
|
</CardContent>
|