- 新增家庭共享订阅管理 - 新增用户邀请统计 - 新增签名和订阅模式设置表单 - 更新 API 服务层和国际化文件 - UI 组件优化(enhanced-input、pro-table)
This commit is contained in:
@@ -8,7 +8,15 @@ import {
|
||||
DropdownMenuItem,
|
||||
DropdownMenuTrigger,
|
||||
} from "@workspace/ui/components/dropdown-menu";
|
||||
import { Input } from "@workspace/ui/components/input";
|
||||
import { ScrollArea } from "@workspace/ui/components/scroll-area";
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from "@workspace/ui/components/select";
|
||||
import {
|
||||
Sheet,
|
||||
SheetContent,
|
||||
@@ -23,6 +31,7 @@ import {
|
||||
TabsList,
|
||||
TabsTrigger,
|
||||
} from "@workspace/ui/components/tabs";
|
||||
import { Combobox } from "@workspace/ui/composed/combobox";
|
||||
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
|
||||
import {
|
||||
ProTable,
|
||||
@@ -35,14 +44,16 @@ import {
|
||||
getUserList,
|
||||
updateUserBasicInfo,
|
||||
} from "@workspace/ui/services/admin/user";
|
||||
import { useRef, useState } from "react";
|
||||
import { useCallback, useRef, useState } from "react";
|
||||
import { useTranslation } from "react-i18next";
|
||||
import { toast } from "sonner";
|
||||
import { Display } from "@/components/display";
|
||||
import { useSubscribe } from "@/stores/subscribe";
|
||||
import { formatDate } from "@/utils/common";
|
||||
import FamilyManagement from "./family";
|
||||
import { UserDetail } from "./user-detail";
|
||||
import UserForm from "./user-form";
|
||||
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";
|
||||
@@ -56,13 +67,15 @@ export default function User() {
|
||||
|
||||
const { subscribes } = useSubscribe();
|
||||
|
||||
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,
|
||||
};
|
||||
const searchRef = useRef({
|
||||
type: sp.user_id ? "user_id" : "email",
|
||||
value: sp.search || sp.user_id || "",
|
||||
});
|
||||
|
||||
const handleSearch = useCallback((type: string, value: string) => {
|
||||
searchRef.current = { type, value };
|
||||
ref.current?.refresh();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<ProTable<API.User, API.GetUserListParams>
|
||||
@@ -75,6 +88,11 @@ export default function User() {
|
||||
userId={row.id}
|
||||
/>,
|
||||
<SubscriptionSheet key="subscription" userId={row.id} />,
|
||||
<DeviceGroupSheet
|
||||
key="device-group"
|
||||
onChanged={() => ref.current?.refresh()}
|
||||
userId={row.id}
|
||||
/>,
|
||||
<ConfirmButton
|
||||
cancelText={t("cancel", "Cancel")}
|
||||
confirmText={t("confirm", "Confirm")}
|
||||
@@ -98,6 +116,7 @@ export default function User() {
|
||||
<Button variant="outline">{t("more", "More")}</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end">
|
||||
<InviteStatsMenuItem userId={row.id} />
|
||||
<DropdownMenuItem asChild>
|
||||
<Link
|
||||
search={{ user_id: String(row.id) }}
|
||||
@@ -194,6 +213,10 @@ export default function User() {
|
||||
header: t("userName", "Username"),
|
||||
cell: ({ row }) => {
|
||||
const method = row.original.auth_methods?.[0];
|
||||
const identifier = method?.auth_identifier || "";
|
||||
const isDevice = method?.auth_type === "device";
|
||||
const deviceNo = (row.original.user_devices?.[0] as any)?.device_no;
|
||||
const display = isDevice ? deviceNo || identifier : identifier;
|
||||
return (
|
||||
<div>
|
||||
<Badge
|
||||
@@ -202,7 +225,7 @@ export default function User() {
|
||||
>
|
||||
{method?.auth_type}
|
||||
</Badge>
|
||||
{method?.auth_identifier}
|
||||
<span title={isDevice ? display : undefined}>{display}</span>
|
||||
</div>
|
||||
);
|
||||
},
|
||||
@@ -245,7 +268,14 @@ export default function User() {
|
||||
},
|
||||
]}
|
||||
header={{
|
||||
title: t("userList", "User List"),
|
||||
title: (
|
||||
<UserSearchBar
|
||||
initialType={searchRef.current.type}
|
||||
initialValue={searchRef.current.value}
|
||||
onSearch={handleSearch}
|
||||
subscribes={subscribes}
|
||||
/>
|
||||
),
|
||||
toolbar: (
|
||||
<UserForm<API.CreateUserRequest>
|
||||
key="create"
|
||||
@@ -270,39 +300,19 @@ export default function User() {
|
||||
/>
|
||||
),
|
||||
}}
|
||||
initialFilters={initialFilters}
|
||||
key={initialFilters.user_id}
|
||||
params={[
|
||||
{
|
||||
key: "subscribe_id",
|
||||
placeholder: t("subscription", "Subscription"),
|
||||
options: 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,
|
||||
});
|
||||
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);
|
||||
return {
|
||||
list: data.data?.list || [],
|
||||
total: data.data?.total || 0,
|
||||
@@ -401,3 +411,137 @@ function SubscriptionSheet({ userId }: { userId: number }) {
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function InviteStatsMenuItem({ userId }: { userId: number }) {
|
||||
const { t } = useTranslation("user");
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<DropdownMenuItem
|
||||
onSelect={(e) => {
|
||||
e.preventDefault();
|
||||
setOpen(true);
|
||||
}}
|
||||
>
|
||||
{t("inviteStats", "Invite Statistics")}
|
||||
</DropdownMenuItem>
|
||||
<UserInviteStatsSheet
|
||||
onOpenChange={setOpen}
|
||||
open={open}
|
||||
userId={userId}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
function DeviceGroupSheet({
|
||||
userId,
|
||||
onChanged,
|
||||
}: {
|
||||
userId: number;
|
||||
onChanged?: () => void;
|
||||
}) {
|
||||
const { t } = useTranslation("user");
|
||||
const [open, setOpen] = useState(false);
|
||||
return (
|
||||
<Sheet onOpenChange={setOpen} open={open}>
|
||||
<SheetTrigger asChild>
|
||||
<Button variant="outline">{t("deviceGroup", "Device Group")}</Button>
|
||||
</SheetTrigger>
|
||||
<SheetContent className="w-[1000px] max-w-full md:max-w-7xl" side="right">
|
||||
<SheetHeader>
|
||||
<SheetTitle>
|
||||
{t("deviceGroup", "Device Group")} · ID: {userId}
|
||||
</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-2 px-4">
|
||||
<FamilyManagement initialUserId={userId} onChanged={onChanged} />
|
||||
</div>
|
||||
</SheetContent>
|
||||
</Sheet>
|
||||
);
|
||||
}
|
||||
|
||||
function UserSearchBar({
|
||||
initialType,
|
||||
initialValue,
|
||||
onSearch,
|
||||
subscribes,
|
||||
}: {
|
||||
initialType: string;
|
||||
initialValue: string;
|
||||
onSearch: (type: string, value: string) => void;
|
||||
subscribes?: API.SubscribeItem[];
|
||||
}) {
|
||||
const { t } = useTranslation("user");
|
||||
const [searchType, setSearchType] = useState(initialType);
|
||||
const [searchValue, setSearchValue] = useState(initialValue);
|
||||
|
||||
return (
|
||||
<div className="flex items-center gap-2">
|
||||
<Select
|
||||
onValueChange={(v) => {
|
||||
setSearchType(v);
|
||||
setSearchValue("");
|
||||
}}
|
||||
value={searchType}
|
||||
>
|
||||
<SelectTrigger className="w-24">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="email">{t("email", "Email")}</SelectItem>
|
||||
<SelectItem value="device">{t("deviceSearch", "Device")}</SelectItem>
|
||||
<SelectItem value="user_id">{t("userId", "User ID")}</SelectItem>
|
||||
<SelectItem value="subscribe_id">
|
||||
{t("subscription", "Subscription")}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
{searchType === "subscribe_id" ? (
|
||||
<Combobox
|
||||
className="w-48"
|
||||
onChange={(value) => {
|
||||
setSearchValue(value);
|
||||
onSearch("subscribe_id", value);
|
||||
}}
|
||||
options={subscribes?.map((item) => ({
|
||||
label: item.name!,
|
||||
value: String(item.id!),
|
||||
}))}
|
||||
placeholder={t("subscription", "Subscription")}
|
||||
value={searchValue}
|
||||
/>
|
||||
) : (
|
||||
<>
|
||||
<Input
|
||||
className="w-48"
|
||||
onChange={(e) => setSearchValue(e.target.value)}
|
||||
onKeyDown={(e) =>
|
||||
e.key === "Enter" && onSearch(searchType, searchValue)
|
||||
}
|
||||
placeholder={t("searchInputPlaceholder", "Enter search term")}
|
||||
value={searchValue}
|
||||
/>
|
||||
<Button
|
||||
onClick={() => onSearch(searchType, searchValue)}
|
||||
variant="default"
|
||||
>
|
||||
{t("search", "Search")}
|
||||
</Button>
|
||||
{searchValue && (
|
||||
<Button
|
||||
onClick={() => {
|
||||
setSearchValue("");
|
||||
onSearch(searchType, "");
|
||||
}}
|
||||
variant="outline"
|
||||
>
|
||||
{t("resetSearch", "Reset")}
|
||||
</Button>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user