From 9f95cec876c9ac8bf00a6ca12a5c40243c7171af Mon Sep 17 00:00:00 2001 From: "web@ppanel" Date: Mon, 29 Dec 2025 07:02:31 +0000 Subject: [PATCH] =?UTF-8?q?=E2=9C=A8=20feat:=20Added=20localized=20support?= =?UTF-8?q?=20for=20user=20subscription=20and=20deletion=20status,=20and?= =?UTF-8?q?=20optimized=20the=20subscription=20form=20and=20user=20interfa?= =?UTF-8?q?ce.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../public/assets/locales/en-US/user.json | 3 + .../public/assets/locales/zh-CN/user.json | 3 + .../src/sections/product/subscribe-form.tsx | 2 +- apps/admin/src/sections/user/index.tsx | 19 +- .../sections/user/user-subscription/index.tsx | 257 ++++--- .../user-subscription/subscription-form.tsx | 2 +- .../src/sections/user/dashboard/content.tsx | 651 +++++++++--------- packages/ui/src/composed/date-picker.tsx | 27 +- 8 files changed, 520 insertions(+), 444 deletions(-) diff --git a/apps/admin/public/assets/locales/en-US/user.json b/apps/admin/public/assets/locales/en-US/user.json index d03c4e2..028fe70 100644 --- a/apps/admin/public/assets/locales/en-US/user.json +++ b/apps/admin/public/assets/locales/en-US/user.json @@ -25,9 +25,11 @@ "createSuccess": "Created successfully", "createUser": "Create User", "delete": "Delete", + "deleted": "Deleted", "deleteDescription": "This action cannot be undone.", "deleteSubscriptionDescription": "This action cannot be undone.", "deleteSuccess": "Deleted successfully", + "isDeleted": "Status", "deviceLimit": "Device Limit", "download": "Download", "downloadTraffic": "Download Traffic", @@ -51,6 +53,7 @@ "loginStatus": "Login Status", "manager": "Administrator", "more": "More", + "normal": "Normal", "notifySettingsTitle": "Notify Settings", "offline": "Offline", "online": "Online", diff --git a/apps/admin/public/assets/locales/zh-CN/user.json b/apps/admin/public/assets/locales/zh-CN/user.json index 5be625c..0de2b9c 100644 --- a/apps/admin/public/assets/locales/zh-CN/user.json +++ b/apps/admin/public/assets/locales/zh-CN/user.json @@ -25,9 +25,11 @@ "createSuccess": "创建成功", "createUser": "创建用户", "delete": "删除", + "deleted": "已删除", "deleteDescription": "此操作无法撤销。", "deleteSubscriptionDescription": "此操作无法撤销。", "deleteSuccess": "删除成功", + "isDeleted": "状态", "deviceLimit": "IP限制", "download": "下载", "downloadTraffic": "下载流量", @@ -51,6 +53,7 @@ "loginStatus": "登录状态", "manager": "管理员", "more": "更多", + "normal": "正常", "notifySettingsTitle": "通知设置", "offline": "离线", "online": "在线", diff --git a/apps/admin/src/sections/product/subscribe-form.tsx b/apps/admin/src/sections/product/subscribe-form.tsx index 198d124..495171b 100644 --- a/apps/admin/src/sections/product/subscribe-form.tsx +++ b/apps/admin/src/sections/product/subscribe-form.tsx @@ -277,7 +277,7 @@ export default function SubscribeForm>({ {trigger} - + {title} diff --git a/apps/admin/src/sections/user/index.tsx b/apps/admin/src/sections/user/index.tsx index b550f2f..a027b0b 100644 --- a/apps/admin/src/sections/user/index.tsx +++ b/apps/admin/src/sections/user/index.tsx @@ -172,6 +172,18 @@ export default function User() { accessorKey: "id", header: "ID", }, + { + accessorKey: "deleted_at", + header: t("isDeleted", "Deleted"), + cell: ({ row }) => { + const deletedAt = row.getValue("deleted_at") as number | undefined; + return deletedAt ? ( + {t("deleted", "Deleted")} + ) : ( + {t("normal", "Normal")} + ); + }, + }, { accessorKey: "auth_methods", header: t("userName", "Username"), @@ -355,16 +367,13 @@ function SubscriptionSheet({ userId }: { userId: number }) { - + {t("subscriptionList", "Subscription List")} · ID: {userId} -
+
diff --git a/apps/admin/src/sections/user/user-subscription/index.tsx b/apps/admin/src/sections/user/user-subscription/index.tsx index 7ad9554..5a2c755 100644 --- a/apps/admin/src/sections/user/user-subscription/index.tsx +++ b/apps/admin/src/sections/user/user-subscription/index.tsx @@ -33,7 +33,6 @@ export default function UserSubscription({ userId }: { userId: number }) { const { t } = useTranslation("user"); const [loading, setLoading] = useState(false); const ref = useRef(null); - const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore(); return ( > @@ -59,105 +58,13 @@ export default function UserSubscription({ userId }: { userId: number }) { title={t("editSubscription", "Edit Subscription")} trigger={t("edit", "Edit")} />, - , - { - await resetUserSubscribeToken({ user_subscribe_id: row.id }); - toast.success( - t( - "resetTokenSuccess", - "Subscription address reset successfully" - ) - ); - ref.current?.refresh(); - }} - title={t("confirmResetToken", "Confirm Reset Subscription Address")} - trigger={ - - } + ref.current?.refresh()} + row={row} + token={row.token} + userId={userId} />, - { - await toggleUserSubscribeStatus({ user_subscribe_id: row.id }); - toast.success( - row.status === 5 - ? t( - "resumeSubscribeSuccess", - "Subscription resumed successfully" - ) - : t( - "stopSubscribeSuccess", - "Subscription stopped successfully" - ) - ); - ref.current?.refresh(); - }} - title={ - row.status === 5 - ? t("confirmResumeSubscribe", "Confirm Resume Subscription") - : t("confirmStopSubscribe", "Confirm Stop Subscription") - } - trigger={ - - } - />, - { - await deleteUserSubscribe({ user_subscribe_id: row.id }); - toast.success(t("deleteSuccess", "Deleted successfully")); - ref.current?.refresh(); - }} - title={t("confirmDelete", "Confirm Delete")} - trigger={ - - } - />, - , ], }} columns={[ @@ -175,6 +82,11 @@ export default function UserSubscription({ userId }: { userId: number }) { header: t("status", "Status"), cell: ({ row }) => { const status = row.getValue("status") as number; + const expireTime = row.original.expire_time; + + // 如果过期时间为0,说明是永久订阅,应该显示为激活状态 + const displayStatus = status === 3 && expireTime === 0 ? 1 : status; + const statusMap: Record< number, { @@ -201,7 +113,7 @@ export default function UserSubscription({ userId }: { userId: number }) { variant: "destructive", }, }; - const statusInfo = statusMap[status] || { + const statusInfo = statusMap[displayStatus] || { label: "Unknown", variant: "outline", }; @@ -261,10 +173,12 @@ export default function UserSubscription({ userId }: { userId: number }) { { accessorKey: "expire_time", header: t("expireTime", "Expire Time"), - cell: ({ row }) => - row.getValue("expire_time") - ? formatDate(row.getValue("expire_time")) - : t("permanent", "Permanent"), + cell: ({ row }) => { + const expireTime = row.getValue("expire_time") as number; + return expireTime && expireTime !== 0 + ? formatDate(expireTime) + : t("permanent", "Permanent"); + }, }, { accessorKey: "created_at", @@ -308,9 +222,24 @@ export default function UserSubscription({ userId }: { userId: number }) { ); } -function RowMoreActions({ userId, subId }: { userId: number; subId: number }) { +function RowMoreActions({ + userId, + row, + token, + refresh, +}: { + userId: number; + row: API.UserSubscribe; + token: string; + refresh: () => void; +}) { const triggerRef = useRef(null); + const resetTokenRef = useRef(null); + const toggleStatusRef = useRef(null); + const deleteRef = useRef(null); const { t } = useTranslation("user"); + const { getUserSubscribe: getUserSubscribeUrls } = useGlobalStore(); + return (
@@ -318,9 +247,47 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) { + { + e.preventDefault(); + await navigator.clipboard.writeText( + getUserSubscribeUrls(token)[0] || "" + ); + toast.success(t("copySuccess", "Copied successfully")); + }} + > + {t("copySubscription", "Copy Subscription")} + + { + e.preventDefault(); + resetTokenRef.current?.click(); + }} + > + {t("resetToken", "Reset Subscription Address")} + + { + e.preventDefault(); + toggleStatusRef.current?.click(); + }} + > + {row.status === 5 + ? t("resumeSubscribe", "Resume Subscription") + : t("stopSubscribe", "Stop Subscription")} + + { + e.preventDefault(); + deleteRef.current?.click(); + }} + > + {t("delete", "Delete")} + {t("subscriptionLogs", "Subscription Logs")} @@ -328,7 +295,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) { {t("resetLogs", "Reset Logs")} @@ -336,7 +303,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) { {t("trafficStats", "Traffic Stats")} @@ -344,7 +311,7 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) { {t("trafficDetails", "Traffic Details")} @@ -361,8 +328,78 @@ function RowMoreActions({ userId, subId }: { userId: number; subId: number }) { + {/* Hidden triggers for confirm dialogs */} + { + await resetUserSubscribeToken({ + user_subscribe_id: row.subscribe_id, + }); + toast.success( + t("resetTokenSuccess", "Subscription address reset successfully") + ); + refresh(); + }} + title={t("confirmResetToken", "Confirm Reset Subscription Address")} + trigger={ - - - - - {t("prompt", "Prompt")} - - - {t( - "confirmResetSubscription", - "Are you sure you want to reset your subscription?" - )} - - - - - {t("cancel", "Cancel")} - - { - await resetUserSubscribeToken({ - user_subscribe_id: item.id, - }); - await refetch(); - toast.success(t("resetSuccess", "Reset Success")); + return ( + + ); + })} ) : ( <> diff --git a/packages/ui/src/composed/date-picker.tsx b/packages/ui/src/composed/date-picker.tsx index acbbfe6..690e8e7 100644 --- a/packages/ui/src/composed/date-picker.tsx +++ b/packages/ui/src/composed/date-picker.tsx @@ -34,6 +34,15 @@ export function DatePicker({ } }; + const handleClear = (e: React.MouseEvent) => { + e.preventDefault(); + e.stopPropagation(); + setDate(undefined); + if (onChange) { + onChange(0); + } + }; + return ( @@ -47,16 +56,14 @@ export function DatePicker({ {value ? intlFormat(value) : {placeholder}}
{value && ( - { - e.stopPropagation(); - setDate(undefined); - if (onChange) { - onChange(0); - } - }} - /> + )}