From f9c3b18249c4c43958789fb4a9dbd313f4054187 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Mon, 22 Jun 2026 09:06:25 -0700 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D(#52):=20=E8=B0=83=E6=95=B4?= =?UTF-8?q?=E9=A6=96=E9=A1=B5=E4=B8=8E=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E4=BA=A4=E4=BA=92?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dashboard/components/statistics.tsx | 4 +- apps/admin/src/sections/user/index.tsx | 283 ++++++++---------- apps/admin/src/sections/user/user-detail.tsx | 16 +- .../sections/user/user-identifier.test.tsx | 101 +++++++ .../src/sections/user/user-identifier.tsx | 66 ++++ .../sections/user/user-search-bar.test.tsx | 92 ++++++ .../src/sections/user/user-search-bar.tsx | 102 +++++++ 7 files changed, 489 insertions(+), 175 deletions(-) create mode 100644 apps/admin/src/sections/user/user-identifier.test.tsx create mode 100644 apps/admin/src/sections/user/user-identifier.tsx create mode 100644 apps/admin/src/sections/user/user-search-bar.test.tsx create mode 100644 apps/admin/src/sections/user/user-search-bar.tsx diff --git a/apps/admin/src/sections/dashboard/components/statistics.tsx b/apps/admin/src/sections/dashboard/components/statistics.tsx index db3b069..1009c41 100644 --- a/apps/admin/src/sections/dashboard/components/statistics.tsx +++ b/apps/admin/src/sections/dashboard/components/statistics.tsx @@ -241,11 +241,11 @@ export default function Statistics() { iconBg: "bg-green-100 dark:bg-green-900/30", }, { - title: t("pendingTickets", "Pending Tickets"), + title: t("withdrawalManagement", "提现管理"), value: TicketTotal || 0, subtitle: t("pending", "Pending"), icon: "uil:clipboard-notes", - href: "/dashboard/ticket", + href: "/dashboard/withdrawal", color: "text-red-600 dark:text-red-400", iconBg: "bg-red-100 dark:bg-red-900/30", }, diff --git a/apps/admin/src/sections/user/index.tsx b/apps/admin/src/sections/user/index.tsx index 74b9998..2117b19 100644 --- a/apps/admin/src/sections/user/index.tsx +++ b/apps/admin/src/sections/user/index.tsx @@ -1,34 +1,25 @@ 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, DropdownMenuItem, + DropdownMenuSeparator, DropdownMenuTrigger, } from "@workspace/ui/components/dropdown-menu"; -import { Input } from "@workspace/ui/components/input"; import { Popover, PopoverContent, PopoverTrigger, } from "@workspace/ui/components/popover"; import { ScrollArea } from "@workspace/ui/components/scroll-area"; -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@workspace/ui/components/select"; import { Sheet, SheetContent, @@ -43,7 +34,12 @@ import { TabsList, TabsTrigger, } from "@workspace/ui/components/tabs"; -import { Combobox } from "@workspace/ui/composed/combobox"; +import { + Tooltip, + TooltipContent, + TooltipProvider, + TooltipTrigger, +} from "@workspace/ui/components/tooltip"; // import { ConfirmButton } from "@workspace/ui/composed/confirm-button"; import { ProTable, @@ -60,9 +56,8 @@ import { getUserList, updateUserBasicInfo, } from "@workspace/ui/services/admin/user"; -import { parseDeviceType } from "@workspace/ui/utils/device"; import { FilePenLine } from "lucide-react"; -import { useCallback, useRef, useState } from "react"; +import { useCallback, useEffect, useRef, useState } from "react"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Display } from "@/components/display"; @@ -73,18 +68,16 @@ import { RemarkForm } from "./remark-form"; import { buildUserBasicInfoPayload } from "./user-basic-info-payload"; import { UserDetail } from "./user-detail"; import UserForm from "./user-form"; +import { UserIdentifier } from "./user-identifier"; import { UserInviteStatsSheet } from "./user-invite-stats-sheet"; 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 { UserSearchBar } from "./user-search-bar"; import UserSubscription from "./user-subscription"; // import EditUserGroupDialog from "./edit-user-group-dialog"; -type UserDeviceWithDeviceNo = API.UserDevice & { - device_no?: string; -}; - export default function User() { const { t } = useTranslation("user"); const [loading, setLoading] = useState(false); @@ -98,6 +91,14 @@ export default function User() { value: sp.search || sp.user_id || "", }); + useEffect(() => { + searchRef.current = { + type: sp.user_id ? "user_id" : "email", + value: sp.search || sp.user_id || "", + }; + ref.current?.refresh(); + }, [sp.search, sp.user_id]); + const handleSearch = useCallback((type: string, value: string) => { searchRef.current = { type, value }; ref.current?.refresh(); @@ -151,7 +152,6 @@ export default function User() { onChanged={() => ref.current?.refresh()} userId={row.id} />, - , /* + + {t("orderList", "Order List")} @@ -262,62 +264,43 @@ export default function User() { id: "auth_methods", accessorKey: "auth_methods", header: "设备码/邮箱", - cell: ({ row }) => { - const method = row.original.auth_methods?.[0]; - const identifier = method?.auth_identifier || ""; - const isDevice = method?.auth_type === "device"; - const firstDevice = row.original.user_devices?.[0] as - | UserDeviceWithDeviceNo - | undefined; - const deviceNo = firstDevice?.device_no; - const deviceType = parseDeviceType(firstDevice?.user_agent || ""); - const display = isDevice ? deviceNo || identifier : identifier; - return ( -
- {/* - {method?.auth_type} - */} - {deviceType && ( - - {deviceType} - - )} - {display} - - -
- {row.original?.remark ? `(${row.original.remark})` : ""} - -
-
- - { - const { data } = await getUserDetail({ - id: row.original.id, - }); - const user = data.data; + cell: ({ row }) => ( +
+ + + +
+ {row.original?.remark ? `(${row.original.remark})` : ""} + +
+
+ + { + const { data } = await getUserDetail({ + id: row.original.id, + }); + const user = data.data; - if (!user) { - throw new Error("User detail not found"); - } + if (!user) { + throw new Error("User detail not found"); + } - await updateUserBasicInfo( - buildUserBasicInfoPayload(user, remark) - ); - toast.success(t("updateSuccess")); - ref.current?.refresh(); - }} - /> - -
-
- ); - }, + await updateUserBasicInfo( + buildUserBasicInfoPayload(user, remark) + ); + toast.success(t("updateSuccess")); + ref.current?.refresh(); + }} + /> +
+
+
+ ), }, /* { id: "balance", @@ -355,6 +338,48 @@ export default function User() { header: t("referer", "Referer"), cell: ({ row }) => , }, + { + id: "last_login_time", + accessorKey: "last_login_time", + header: t("userStatus", "用户状态"), + cell: ({ row }) => { + const lastLoginTime = ( + row.original as API.User & { + last_login_time?: number; + } + ).last_login_time; + const isOnline = Boolean(lastLoginTime); + const dotClassName = isOnline ? "bg-green-500" : "bg-gray-400"; + const statusLabel = isOnline + ? t("active", "Active") + : t("inactive", "Inactive"); + + return ( + + + +
+
+
+ + {isOnline + ? formatDate(lastLoginTime) + : t("neverLoggedIn", "Never logged in")} + +
+
+ ); + }, + }, { id: "created_at", accessorKey: "created_at", @@ -523,6 +548,25 @@ function InviteStatsMenuItem({ userId }: { userId: number }) { ); } +function PreviewNodesMenuItem({ userId }: { userId: number }) { + const { t } = useTranslation("user"); + const [open, setOpen] = useState(false); + + return ( + <> + { + e.preventDefault(); + setOpen(true); + }} + > + {t("previewNodes", "Preview Nodes")} + + + + ); +} + function DeviceGroupSheet({ userId, onChanged, @@ -551,92 +595,16 @@ function DeviceGroupSheet({ ); } -function UserSearchBar({ - initialType, - initialValue, - onSearch, - subscribes, +function PreviewNodesDialog({ + userId, + open, + onOpenChange, }: { - initialType: string; - initialValue: string; - onSearch: (type: string, value: string) => void; - subscribes?: API.SubscribeItem[]; + userId: number; + open: boolean; + onOpenChange: (open: boolean) => void; }) { const { t } = useTranslation("user"); - const [searchType, setSearchType] = useState(initialType); - const [searchValue, setSearchValue] = useState(initialValue); - - return ( -
- - {searchType === "subscribe_id" ? ( - { - setSearchValue(value); - onSearch("subscribe_id", value); - }} - options={subscribes?.map((item) => ({ - label: item.name!, - value: String(item.id!), - }))} - placeholder={t("subscription", "Subscription")} - value={searchValue} - /> - ) : ( - <> - setSearchValue(e.target.value)} - onKeyDown={(e) => - e.key === "Enter" && onSearch(searchType, searchValue) - } - placeholder={t("searchInputPlaceholder", "Enter search term")} - value={searchValue} - /> - - {searchValue && ( - - )} - - )} -
- ); -} - -function PreviewNodesDialog({ userId }: { userId: number }) { - const { t } = useTranslation("user"); - const [open, setOpen] = useState(false); const { data: previewData, isLoading } = useQuery({ enabled: open, queryKey: ["previewUserNodes", userId], @@ -647,10 +615,7 @@ function PreviewNodesDialog({ userId }: { userId: number }) { }); return ( - - - - + diff --git a/apps/admin/src/sections/user/user-detail.tsx b/apps/admin/src/sections/user/user-detail.tsx index 7f530ca..03baeb3 100644 --- a/apps/admin/src/sections/user/user-detail.tsx +++ b/apps/admin/src/sections/user/user-detail.tsx @@ -16,6 +16,7 @@ import { formatBytes } from "@workspace/ui/utils/formatting"; import { useTranslation } from "react-i18next"; import { Display } from "@/components/display"; import { formatDate } from "@/utils/common"; +import { UserIdentifier } from "./user-identifier"; // import EditUserGroupDialog from "./edit-user-group-dialog"; // import { getUserGroupList } from "@workspace/ui/services/admin/group"; @@ -229,26 +230,13 @@ export function UserDetail({ id }: { id: number }) { if (!id) return "--"; - const firstMethod = data?.auth_methods[0]; - const isDevice = firstMethod?.auth_type === "device"; - const deviceNo = (data as any)?.user_devices?.[0]?.device_no; - const rawIdentifier = firstMethod?.auth_identifier || ""; - const identifier = isDevice ? deviceNo || rawIdentifier : rawIdentifier; - return ( + {searchValue && ( + + )} + + )} + + ); +}