merge: 同步 upstream/main 新功能到定制版本
Build and Release / Build (push) Has been cancelled
Issue Close Require / issue-close-require (push) Has been cancelled

- 分流规则 + 用户流量统计
- 验证码功能
- 节点分组管理 UI
- 重构用户分组组件
This commit is contained in:
2026-03-19 03:16:59 -07:00
89 changed files with 7036 additions and 383 deletions
@@ -17,6 +17,8 @@ import { formatBytes } from "@workspace/ui/utils/formatting";
import { useTranslation } from "react-i18next";
import { Display } from "@/components/display";
import { formatDate } from "@/utils/common";
// import EditUserGroupDialog from "./edit-user-group-dialog";
// import { getUserGroupList } from "@workspace/ui/services/admin/group";
export function UserSubscribeDetail({
id,
@@ -38,10 +40,33 @@ export function UserSubscribeDetail({
},
});
// Fetch user groups for display
// const { data: groupsData } = useQuery({
// enabled: id !== 0 && enabled,
// queryKey: ["getUserGroupList"],
// queryFn: async () => {
// const { data } = await getUserGroupList({
// page: 1,
// size: 100,
// });
// return data.data?.list || [];
// },
// });
if (!id) return "--";
const usedTraffic = data ? data.upload + data.download : 0;
const totalTraffic = data?.traffic || 0;
const remainingTraffic = totalTraffic > 0 ? totalTraffic - usedTraffic : 0;
// Get user group info from data.user
// const userGroupId = typeof data?.user?.user_group_id === 'number' ? data?.user?.user_group_id : 0;
// const groupLocked = data?.user?.group_locked || false;
// const groupIds = userGroupId > 0 ? [userGroupId] : [];
// const groupNames = userGroupId > 0
// ? groupsData?.find((g: API.UserGroup) => g.id === userGroupId)?.name || "--"
// : "--";
const subscribeContent = (
<div className="space-y-4">
@@ -77,6 +102,16 @@ export function UserSubscribeDetail({
: "--"}
</span>
</li>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">{t("remainingTraffic")}</span>
<span>
{data
? totalTraffic === 0
? t("unlimited")
: formatBytes(remainingTraffic)
: "--"}
</span>
</li>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">{t("startTime")}</span>
<span>
@@ -89,6 +124,35 @@ export function UserSubscribeDetail({
{data?.expire_time ? formatDate(data.expire_time) : "--"}
</span>
</li>
{/* <li className="flex items-center justify-between">
<span className="text-muted-foreground">{t("userGroup")}</span>
<div className="flex items-center gap-2">
<span>{groupNames || "--"}</span>
{data?.id && (
<EditUserGroupDialog
userId={data?.user_id || 0}
userSubscribeId={data?.id}
currentGroupIds={groupIds}
currentLocked={groupLocked}
trigger={
<Button variant="ghost" size="sm" className="h-6 px-2">
{t("edit", "Edit")}
</Button>
}
onSubmit={async () => {
window.location.reload();
return true;
}}
/>
)}
</div>
</li>
<li className="flex items-center justify-between">
<span className="text-muted-foreground">{t("groupLocked")}</span>
<span>
{groupLocked ? t("yes", "Yes") : t("no", "No")}
</span>
</li> */}
</ul>
</div>
</div>