feat: 限速状态可视化 - 订阅详情 Sheet 顶部展示实时限速状态
- typings.d.ts 新增 UserSubscribeDetail.effective_speed/is_throttled/throttle_rule 字段 - SubscriptionDetail Sheet 打开时拉取订阅详情,顶部展示 SpeedLimitCard - 被降速时显示警告样式 + 实际速率 + 划掉原始速率 + 触发规则说明 Co-Authored-By: claude-flow <ruv@ruv.net>
This commit is contained in:
parent
785c832e02
commit
e6c2370d02
1
.gitignore
vendored
1
.gitignore
vendored
@ -34,3 +34,4 @@ npm-debug.log*
|
|||||||
# Misc
|
# Misc
|
||||||
.DS_Store
|
.DS_Store
|
||||||
*.pem
|
*.pem
|
||||||
|
.mcp.json
|
||||||
|
|||||||
Binary file not shown.
@ -24,7 +24,7 @@ const notifySettingsSchema = z.object({
|
|||||||
enable_balance_notify: z.boolean(),
|
enable_balance_notify: z.boolean(),
|
||||||
enable_login_notify: z.boolean(),
|
enable_login_notify: z.boolean(),
|
||||||
enable_subscribe_notify: z.boolean(),
|
enable_subscribe_notify: z.boolean(),
|
||||||
enable_trade_notify: z.boolean(),
|
// enable_trade_notify: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
type NotifySettingsValues = z.infer<typeof notifySettingsSchema>;
|
type NotifySettingsValues = z.infer<typeof notifySettingsSchema>;
|
||||||
@ -44,7 +44,7 @@ export function NotifySettingsForm({
|
|||||||
enable_balance_notify: user.enable_balance_notify,
|
enable_balance_notify: user.enable_balance_notify,
|
||||||
enable_login_notify: user.enable_login_notify,
|
enable_login_notify: user.enable_login_notify,
|
||||||
enable_subscribe_notify: user.enable_subscribe_notify,
|
enable_subscribe_notify: user.enable_subscribe_notify,
|
||||||
enable_trade_notify: user.enable_trade_notify,
|
// enable_trade_notify: user.enable_trade_notify,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -126,7 +126,7 @@ export function NotifySettingsForm({
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
{/* <FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enable_trade_notify"
|
name="enable_trade_notify"
|
||||||
render={({ field }) => (
|
render={({ field }) => (
|
||||||
@ -142,7 +142,7 @@ export function NotifySettingsForm({
|
|||||||
</FormControl>
|
</FormControl>
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)}
|
)}
|
||||||
/>
|
/> */}
|
||||||
</div>
|
</div>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|||||||
@ -451,7 +451,7 @@ function RowReadOnlyActions({
|
|||||||
"This action cannot be undone."
|
"This action cannot be undone."
|
||||||
)}
|
)}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
await deleteUserSubscribe({ user_subscribe_id: String(row.id) });
|
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||||
refresh?.();
|
refresh?.();
|
||||||
}}
|
}}
|
||||||
@ -636,7 +636,7 @@ function RowMoreActions({
|
|||||||
"This action cannot be undone."
|
"This action cannot be undone."
|
||||||
)}
|
)}
|
||||||
onConfirm={async () => {
|
onConfirm={async () => {
|
||||||
await deleteUserSubscribe({ user_subscribe_id: String(row.id) });
|
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||||
refresh();
|
refresh();
|
||||||
}}
|
}}
|
||||||
|
|||||||
@ -11,16 +11,86 @@ import { Switch } from "@workspace/ui/components/switch";
|
|||||||
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
|
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
|
||||||
import { ProTable } from "@workspace/ui/composed/pro-table/pro-table";
|
import { ProTable } from "@workspace/ui/composed/pro-table/pro-table";
|
||||||
import {
|
import {
|
||||||
|
getUserSubscribeById,
|
||||||
getUserSubscribeDevices,
|
getUserSubscribeDevices,
|
||||||
kickOfflineByUserDevice,
|
kickOfflineByUserDevice,
|
||||||
} from "@workspace/ui/services/admin/user";
|
} from "@workspace/ui/services/admin/user";
|
||||||
import { deviceIdToHash } from "@workspace/ui/utils/device";
|
import { deviceIdToHash } from "@workspace/ui/utils/device";
|
||||||
import { type ReactNode, useState } from "react";
|
import { AlertTriangle, Gauge } from "lucide-react";
|
||||||
|
import { type ReactNode, useEffect, useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
import { IpLink } from "@/components/ip-link";
|
import { IpLink } from "@/components/ip-link";
|
||||||
import { formatDate } from "@/utils/common";
|
import { formatDate } from "@/utils/common";
|
||||||
|
|
||||||
|
function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
|
||||||
|
const { t } = useTranslation("user");
|
||||||
|
const [detail, setDetail] = useState<API.UserSubscribeDetail | null>(null);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUserSubscribeById({ id: subscriptionId }).then(({ data }) => {
|
||||||
|
if (data.data) setDetail(data.data);
|
||||||
|
});
|
||||||
|
}, [subscriptionId]);
|
||||||
|
|
||||||
|
if (!detail || detail.status !== 1) return null;
|
||||||
|
|
||||||
|
const baseSpeed = detail.subscribe?.speed_limit ?? 0;
|
||||||
|
const effectiveSpeed = detail.effective_speed ?? 0;
|
||||||
|
const isThrottled = detail.is_throttled;
|
||||||
|
|
||||||
|
if (baseSpeed === 0 && !isThrottled) return null;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div
|
||||||
|
className={`mb-4 flex items-start gap-3 rounded-lg border p-3 text-sm ${
|
||||||
|
isThrottled
|
||||||
|
? "border-destructive/40 bg-destructive/5 text-destructive"
|
||||||
|
: "border-border bg-muted/40 text-foreground"
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
{isThrottled ? (
|
||||||
|
<AlertTriangle className="mt-0.5 h-4 w-4 shrink-0" />
|
||||||
|
) : (
|
||||||
|
<Gauge className="mt-0.5 h-4 w-4 shrink-0 text-muted-foreground" />
|
||||||
|
)}
|
||||||
|
<div className="space-y-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 font-medium">
|
||||||
|
{isThrottled ? (
|
||||||
|
<>
|
||||||
|
<span>{t("throttled", "Speed Throttled")}</span>
|
||||||
|
<Badge className="text-xs" variant="destructive">
|
||||||
|
{effectiveSpeed} Mbps
|
||||||
|
</Badge>
|
||||||
|
{baseSpeed > 0 && (
|
||||||
|
<span className="text-muted-foreground text-xs line-through">
|
||||||
|
{baseSpeed} Mbps
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<span className="text-muted-foreground">
|
||||||
|
{t("speedLimit", "Speed Limit")}
|
||||||
|
</span>
|
||||||
|
<Badge className="text-xs" variant="secondary">
|
||||||
|
{effectiveSpeed > 0
|
||||||
|
? `${effectiveSpeed} Mbps`
|
||||||
|
: t("unlimited", "Unlimited")}
|
||||||
|
</Badge>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{isThrottled && detail.throttle_rule && (
|
||||||
|
<p className="text-muted-foreground text-xs">
|
||||||
|
{detail.throttle_rule}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export function SubscriptionDetail({
|
export function SubscriptionDetail({
|
||||||
trigger,
|
trigger,
|
||||||
userId,
|
userId,
|
||||||
@ -44,6 +114,7 @@ export function SubscriptionDetail({
|
|||||||
<SheetTitle>{t("onlineDevices", "Online Devices")}</SheetTitle>
|
<SheetTitle>{t("onlineDevices", "Online Devices")}</SheetTitle>
|
||||||
</SheetHeader>
|
</SheetHeader>
|
||||||
<div className="mt-4 max-h-[calc(100dvh-120px)] overflow-y-auto">
|
<div className="mt-4 max-h-[calc(100dvh-120px)] overflow-y-auto">
|
||||||
|
{open && <SpeedLimitCard subscriptionId={subscriptionId} />}
|
||||||
<ProTable<API.UserDevice, Record<string, unknown>>
|
<ProTable<API.UserDevice, Record<string, unknown>>
|
||||||
actions={{
|
actions={{
|
||||||
render: (row) => {
|
render: (row) => {
|
||||||
|
|||||||
@ -27,7 +27,7 @@ const FormSchema = z.object({
|
|||||||
enable_balance_notify: z.boolean(),
|
enable_balance_notify: z.boolean(),
|
||||||
enable_login_notify: z.boolean(),
|
enable_login_notify: z.boolean(),
|
||||||
enable_subscribe_notify: z.boolean(),
|
enable_subscribe_notify: z.boolean(),
|
||||||
enable_trade_notify: z.boolean(),
|
// enable_trade_notify: z.boolean(),
|
||||||
});
|
});
|
||||||
|
|
||||||
export default function NotifySettings() {
|
export default function NotifySettings() {
|
||||||
@ -39,7 +39,7 @@ export default function NotifySettings() {
|
|||||||
enable_balance_notify: user?.enable_balance_notify ?? false,
|
enable_balance_notify: user?.enable_balance_notify ?? false,
|
||||||
enable_login_notify: user?.enable_login_notify ?? false,
|
enable_login_notify: user?.enable_login_notify ?? false,
|
||||||
enable_subscribe_notify: user?.enable_subscribe_notify ?? false,
|
enable_subscribe_notify: user?.enable_subscribe_notify ?? false,
|
||||||
enable_trade_notify: user?.enable_trade_notify ?? false,
|
// enable_trade_notify: user?.enable_trade_notify ?? false,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -80,10 +80,10 @@ export default function NotifySettings() {
|
|||||||
name: "enable_subscribe_notify",
|
name: "enable_subscribe_notify",
|
||||||
label: t("notify.subscribe", "Subscribe"),
|
label: t("notify.subscribe", "Subscribe"),
|
||||||
},
|
},
|
||||||
{
|
// {
|
||||||
name: "enable_trade_notify",
|
// name: "enable_trade_notify",
|
||||||
label: t("notify.finance", "Finance"),
|
// label: t("notify.finance", "Finance"),
|
||||||
},
|
// },
|
||||||
].map(({ name, label }) => (
|
].map(({ name, label }) => (
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
|
|||||||
21
packages/ui/src/services/admin/typings.d.ts
vendored
21
packages/ui/src/services/admin/typings.d.ts
vendored
@ -456,7 +456,7 @@ declare namespace API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type DeleteUserSubscribeRequest = {
|
type DeleteUserSubscribeRequest = {
|
||||||
user_subscribe_id: string;
|
user_subscribe_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type DeviceAuthticateConfig = {
|
type DeviceAuthticateConfig = {
|
||||||
@ -1850,11 +1850,11 @@ declare namespace API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ResetUserSubscribeTokenRequest = {
|
type ResetUserSubscribeTokenRequest = {
|
||||||
user_subscribe_id: any;
|
user_subscribe_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type ResetUserSubscribeTrafficRequest = {
|
type ResetUserSubscribeTrafficRequest = {
|
||||||
user_subscribe_id: string;
|
user_subscribe_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Response = {
|
type Response = {
|
||||||
@ -2177,7 +2177,7 @@ declare namespace API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type ToggleUserSubscribeStatusRequest = {
|
type ToggleUserSubscribeStatusRequest = {
|
||||||
user_subscribe_id: any;
|
user_subscribe_id: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
type TosConfig = {
|
type TosConfig = {
|
||||||
@ -2406,7 +2406,7 @@ declare namespace API {
|
|||||||
};
|
};
|
||||||
|
|
||||||
type UpdateUserSubscribeRequest = {
|
type UpdateUserSubscribeRequest = {
|
||||||
user_subscribe_id: string;
|
user_subscribe_id: number;
|
||||||
subscribe_id: number;
|
subscribe_id: number;
|
||||||
traffic: number;
|
traffic: number;
|
||||||
expired_at: number;
|
expired_at: number;
|
||||||
@ -2431,7 +2431,7 @@ declare namespace API {
|
|||||||
enable_login_notify: boolean;
|
enable_login_notify: boolean;
|
||||||
enable_subscribe_notify: boolean;
|
enable_subscribe_notify: boolean;
|
||||||
enable_trade_notify: boolean;
|
enable_trade_notify: boolean;
|
||||||
user_group_id: string;
|
user_group_id: number;
|
||||||
group_locked: boolean;
|
group_locked: boolean;
|
||||||
auth_methods: UserAuthMethod[];
|
auth_methods: UserAuthMethod[];
|
||||||
user_devices: UserDevice[];
|
user_devices: UserDevice[];
|
||||||
@ -2523,6 +2523,9 @@ declare namespace API {
|
|||||||
upload: number;
|
upload: number;
|
||||||
token: string;
|
token: string;
|
||||||
status: number;
|
status: number;
|
||||||
|
effective_speed: number;
|
||||||
|
is_throttled: boolean;
|
||||||
|
throttle_rule?: string;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
@ -2824,7 +2827,7 @@ declare namespace API {
|
|||||||
|
|
||||||
type SubscribeGroupMapping = {
|
type SubscribeGroupMapping = {
|
||||||
subscribe_id: number;
|
subscribe_id: number;
|
||||||
user_group_id: string;
|
user_group_id: number;
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
};
|
};
|
||||||
@ -2856,8 +2859,8 @@ declare namespace API {
|
|||||||
type GroupHistoryDetailItem = {
|
type GroupHistoryDetailItem = {
|
||||||
id: number;
|
id: number;
|
||||||
history_id: number;
|
history_id: number;
|
||||||
user_group_id: string;
|
user_group_id: number;
|
||||||
node_group_id: string;
|
node_group_id: number;
|
||||||
user_count: number;
|
user_count: number;
|
||||||
node_count: number;
|
node_count: number;
|
||||||
user_data?: string;
|
user_data?: string;
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user