merge: 同步 upstream/main 新功能到定制版本
- 分流规则 + 用户流量统计 - 验证码功能 - 节点分组管理 UI - 重构用户分组组件
This commit is contained in:
@@ -2,6 +2,13 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { Link, useSearch } from "@tanstack/react-router";
|
||||
import { Badge } from "@workspace/ui/components/badge";
|
||||
import { Button } from "@workspace/ui/components/button";
|
||||
import {
|
||||
Dialog,
|
||||
DialogContent,
|
||||
DialogHeader,
|
||||
DialogTitle,
|
||||
DialogTrigger,
|
||||
} from "@workspace/ui/components/dialog";
|
||||
import {
|
||||
DropdownMenu,
|
||||
DropdownMenuContent,
|
||||
@@ -44,6 +51,10 @@ import {
|
||||
getUserList,
|
||||
updateUserBasicInfo,
|
||||
} from "@workspace/ui/services/admin/user";
|
||||
import {
|
||||
// getUserGroupList,
|
||||
previewUserNodes,
|
||||
} from "@workspace/ui/services/admin/group";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
@@ -58,6 +69,7 @@ import { AuthMethodsForm } from "./user-profile/auth-methods-form";
|
||||
import { BasicInfoForm } from "./user-profile/basic-info-form";
|
||||
import { NotifySettingsForm } from "./user-profile/notify-settings-form";
|
||||
import UserSubscription from "./user-subscription";
|
||||
// import EditUserGroupDialog from "./edit-user-group-dialog";
|
||||
|
||||
export default function User() {
|
||||
const { t } = useTranslation("user");
|
||||
@@ -77,6 +89,23 @@ export default function User() {
|
||||
ref.current?.refresh();
|
||||
}, []);
|
||||
|
||||
// const { data: userGroupsData } = useQuery({
|
||||
// queryKey: ["userGroups"],
|
||||
// queryFn: async () => {
|
||||
// const { data } = await getUserGroupList({ page: 1, size: 1000 });
|
||||
// return data.data?.list || [];
|
||||
// },
|
||||
// });
|
||||
|
||||
const initialFilters = {
|
||||
search: sp.search || undefined,
|
||||
user_id: sp.user_id || undefined,
|
||||
subscribe_id: sp.subscribe_id || undefined,
|
||||
user_subscribe_id: sp.user_subscribe_id || undefined,
|
||||
short_code: sp.short_code || undefined,
|
||||
// user_group_id: sp.user_group_id || undefined,
|
||||
};
|
||||
|
||||
return (
|
||||
<ProTable<API.User, API.GetUserListParams>
|
||||
action={ref}
|
||||
@@ -93,6 +122,7 @@ export default function User() {
|
||||
onChanged={() => ref.current?.refresh()}
|
||||
userId={row.id}
|
||||
/>,
|
||||
<PreviewNodesDialog key="preview-nodes" userId={row.id} />,
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
@@ -163,6 +193,7 @@ export default function User() {
|
||||
}}
|
||||
columns={[
|
||||
{
|
||||
id: "enable",
|
||||
accessorKey: "enable",
|
||||
header: t("enable", "Enable"),
|
||||
cell: ({ row }) => (
|
||||
@@ -193,10 +224,12 @@ export default function User() {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "id",
|
||||
accessorKey: "id",
|
||||
header: "ID",
|
||||
},
|
||||
{
|
||||
id: "deleted_at",
|
||||
accessorKey: "deleted_at",
|
||||
header: t("isDeleted", "Deleted"),
|
||||
cell: ({ row }) => {
|
||||
@@ -209,6 +242,7 @@ export default function User() {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "auth_methods",
|
||||
accessorKey: "auth_methods",
|
||||
header: t("userName", "Username"),
|
||||
cell: ({ row }) => {
|
||||
@@ -231,6 +265,7 @@ export default function User() {
|
||||
},
|
||||
},
|
||||
{
|
||||
id: "balance",
|
||||
accessorKey: "balance",
|
||||
header: t("balance", "Balance"),
|
||||
cell: ({ row }) => (
|
||||
@@ -238,6 +273,7 @@ export default function User() {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "gift_amount",
|
||||
accessorKey: "gift_amount",
|
||||
header: t("giftAmount", "Gift Amount"),
|
||||
cell: ({ row }) => (
|
||||
@@ -245,6 +281,7 @@ export default function User() {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "commission",
|
||||
accessorKey: "commission",
|
||||
header: t("commission", "Commission"),
|
||||
cell: ({ row }) => (
|
||||
@@ -252,16 +289,19 @@ export default function User() {
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "refer_code",
|
||||
accessorKey: "refer_code",
|
||||
header: t("inviteCode", "Invite Code"),
|
||||
cell: ({ row }) => row.getValue("refer_code") || "--",
|
||||
},
|
||||
{
|
||||
id: "referer_id",
|
||||
accessorKey: "referer_id",
|
||||
header: t("referer", "Referer"),
|
||||
cell: ({ row }) => <UserDetail id={row.original.referer_id} />,
|
||||
},
|
||||
{
|
||||
id: "created_at",
|
||||
accessorKey: "created_at",
|
||||
header: t("createdAt", "Created At"),
|
||||
cell: ({ row }) => formatDate(row.getValue("created_at")),
|
||||
@@ -300,19 +340,42 @@ export default function User() {
|
||||
/>
|
||||
),
|
||||
}}
|
||||
request={async (pagination) => {
|
||||
const { type, value } = searchRef.current;
|
||||
const params: Record<string, unknown> = { ...pagination };
|
||||
if (value) {
|
||||
if (type === "user_id") {
|
||||
params.user_id = value;
|
||||
} else if (type === "subscribe_id") {
|
||||
params.subscribe_id = value;
|
||||
} else {
|
||||
params.search = value;
|
||||
}
|
||||
}
|
||||
const { data } = await getUserList(params as API.GetUserListParams);
|
||||
initialFilters={initialFilters}
|
||||
key={initialFilters.user_id}
|
||||
params={[
|
||||
{
|
||||
key: "subscribe_id",
|
||||
placeholder: t("subscription", "Subscription"),
|
||||
options: [
|
||||
{ label: t("all", "All"), value: "" },
|
||||
...(subscribes?.map((item) => ({
|
||||
label: item.name!,
|
||||
value: String(item.id!),
|
||||
})) || []),
|
||||
],
|
||||
},
|
||||
{
|
||||
key: "search",
|
||||
placeholder: "Search",
|
||||
},
|
||||
{
|
||||
key: "user_id",
|
||||
placeholder: t("userId", "User ID"),
|
||||
},
|
||||
{
|
||||
key: "user_subscribe_id",
|
||||
placeholder: t("subscriptionId", "Subscription ID"),
|
||||
},
|
||||
{
|
||||
key: "short_code",
|
||||
placeholder: t("shortCode", "Short Code"),
|
||||
},
|
||||
]}
|
||||
request={async (pagination, filter) => {
|
||||
const { data } = await getUserList({
|
||||
...pagination,
|
||||
...filter,
|
||||
});
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
@@ -545,3 +608,85 @@ function UserSearchBar({
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function PreviewNodesDialog({ userId }: { userId: number }) {
|
||||
const { t } = useTranslation("user");
|
||||
const [open, setOpen] = useState(false);
|
||||
const { data: previewData, isLoading } = useQuery({
|
||||
enabled: open,
|
||||
queryKey: ["previewUserNodes", userId],
|
||||
queryFn: async () => {
|
||||
const { data } = await previewUserNodes({ user_id: userId });
|
||||
return data.data;
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<Dialog onOpenChange={setOpen} open={open}>
|
||||
<DialogTrigger asChild>
|
||||
<Button variant="outline">{t("previewNodes", "Preview Nodes")}</Button>
|
||||
</DialogTrigger>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t("previewNodes", "Preview Nodes")} · ID: {userId}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
{isLoading ? (
|
||||
<div className="py-8 text-center text-muted-foreground">
|
||||
{t("loading", "Loading...")}
|
||||
</div>
|
||||
) : previewData ? (
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<span className="text-sm font-medium text-muted-foreground">
|
||||
{t("availableNodes", "Available Nodes")}:
|
||||
</span>{" "}
|
||||
{previewData.node_groups?.reduce((sum, group) => sum + (group.nodes?.length || 0), 0) || 0}
|
||||
</div>
|
||||
{previewData.node_groups && previewData.node_groups.length > 0 ? (
|
||||
<div className="max-h-[400px] overflow-y-auto space-y-4">
|
||||
{previewData.node_groups.map((group) => (
|
||||
<div key={group.id}>
|
||||
<h4 className="text-sm font-semibold mb-2">
|
||||
{group.name ||
|
||||
(group.id === -1
|
||||
? t("subscriptionNodes", "Subscription Nodes")
|
||||
: group.id === 0
|
||||
? t("publicNodes", "Public Nodes")
|
||||
: `${t("nodeGroup", "Node Group")} ${group.id}`)}
|
||||
</h4>
|
||||
{group.nodes && group.nodes.length > 0 ? (
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b">
|
||||
<th className="p-2 text-left font-medium">ID</th>
|
||||
<th className="p-2 text-left font-medium">{t("name", "Name")}</th>
|
||||
<th className="p-2 text-left font-medium">{t("address", "Address")}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{group.nodes.map((node) => (
|
||||
<tr key={node.id} className="border-b">
|
||||
<td className="p-2">{node.id}</td>
|
||||
<td className="p-2">{node.name}</td>
|
||||
<td className="p-2">{node.address}:{node.port}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
) : null}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="py-4 text-center text-muted-foreground">
|
||||
{t("noNodesAvailable", "No nodes available")}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : null}
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user