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
|
||||
.DS_Store
|
||||
*.pem
|
||||
.mcp.json
|
||||
|
||||
Binary file not shown.
@ -24,7 +24,7 @@ const notifySettingsSchema = z.object({
|
||||
enable_balance_notify: z.boolean(),
|
||||
enable_login_notify: z.boolean(),
|
||||
enable_subscribe_notify: z.boolean(),
|
||||
enable_trade_notify: z.boolean(),
|
||||
// enable_trade_notify: z.boolean(),
|
||||
});
|
||||
|
||||
type NotifySettingsValues = z.infer<typeof notifySettingsSchema>;
|
||||
@ -44,7 +44,7 @@ export function NotifySettingsForm({
|
||||
enable_balance_notify: user.enable_balance_notify,
|
||||
enable_login_notify: user.enable_login_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}
|
||||
name="enable_trade_notify"
|
||||
render={({ field }) => (
|
||||
@ -142,7 +142,7 @@ export function NotifySettingsForm({
|
||||
</FormControl>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
/> */}
|
||||
</div>
|
||||
</CardContent>
|
||||
</Card>
|
||||
|
||||
@ -451,7 +451,7 @@ function RowReadOnlyActions({
|
||||
"This action cannot be undone."
|
||||
)}
|
||||
onConfirm={async () => {
|
||||
await deleteUserSubscribe({ user_subscribe_id: String(row.id) });
|
||||
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||
refresh?.();
|
||||
}}
|
||||
@ -636,7 +636,7 @@ function RowMoreActions({
|
||||
"This action cannot be undone."
|
||||
)}
|
||||
onConfirm={async () => {
|
||||
await deleteUserSubscribe({ user_subscribe_id: String(row.id) });
|
||||
await deleteUserSubscribe({ user_subscribe_id: row.id });
|
||||
toast.success(t("deleteSuccess", "Deleted successfully"));
|
||||
refresh();
|
||||
}}
|
||||
|
||||
@ -11,16 +11,86 @@ import { Switch } from "@workspace/ui/components/switch";
|
||||
import { ConfirmButton } from "@workspace/ui/composed/confirm-button";
|
||||
import { ProTable } from "@workspace/ui/composed/pro-table/pro-table";
|
||||
import {
|
||||
getUserSubscribeById,
|
||||
getUserSubscribeDevices,
|
||||
kickOfflineByUserDevice,
|
||||
} from "@workspace/ui/services/admin/user";
|
||||
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 { toast } from "sonner";
|
||||
import { IpLink } from "@/components/ip-link";
|
||||
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({
|
||||
trigger,
|
||||
userId,
|
||||
@ -44,6 +114,7 @@ export function SubscriptionDetail({
|
||||
<SheetTitle>{t("onlineDevices", "Online Devices")}</SheetTitle>
|
||||
</SheetHeader>
|
||||
<div className="mt-4 max-h-[calc(100dvh-120px)] overflow-y-auto">
|
||||
{open && <SpeedLimitCard subscriptionId={subscriptionId} />}
|
||||
<ProTable<API.UserDevice, Record<string, unknown>>
|
||||
actions={{
|
||||
render: (row) => {
|
||||
|
||||
@ -27,7 +27,7 @@ const FormSchema = z.object({
|
||||
enable_balance_notify: z.boolean(),
|
||||
enable_login_notify: z.boolean(),
|
||||
enable_subscribe_notify: z.boolean(),
|
||||
enable_trade_notify: z.boolean(),
|
||||
// enable_trade_notify: z.boolean(),
|
||||
});
|
||||
|
||||
export default function NotifySettings() {
|
||||
@ -39,7 +39,7 @@ export default function NotifySettings() {
|
||||
enable_balance_notify: user?.enable_balance_notify ?? false,
|
||||
enable_login_notify: user?.enable_login_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",
|
||||
label: t("notify.subscribe", "Subscribe"),
|
||||
},
|
||||
{
|
||||
name: "enable_trade_notify",
|
||||
label: t("notify.finance", "Finance"),
|
||||
},
|
||||
// {
|
||||
// name: "enable_trade_notify",
|
||||
// label: t("notify.finance", "Finance"),
|
||||
// },
|
||||
].map(({ name, label }) => (
|
||||
<FormField
|
||||
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 = {
|
||||
user_subscribe_id: string;
|
||||
user_subscribe_id: number;
|
||||
};
|
||||
|
||||
type DeviceAuthticateConfig = {
|
||||
@ -1850,11 +1850,11 @@ declare namespace API {
|
||||
};
|
||||
|
||||
type ResetUserSubscribeTokenRequest = {
|
||||
user_subscribe_id: any;
|
||||
user_subscribe_id: number;
|
||||
};
|
||||
|
||||
type ResetUserSubscribeTrafficRequest = {
|
||||
user_subscribe_id: string;
|
||||
user_subscribe_id: number;
|
||||
};
|
||||
|
||||
type Response = {
|
||||
@ -2177,7 +2177,7 @@ declare namespace API {
|
||||
};
|
||||
|
||||
type ToggleUserSubscribeStatusRequest = {
|
||||
user_subscribe_id: any;
|
||||
user_subscribe_id: number;
|
||||
};
|
||||
|
||||
type TosConfig = {
|
||||
@ -2406,7 +2406,7 @@ declare namespace API {
|
||||
};
|
||||
|
||||
type UpdateUserSubscribeRequest = {
|
||||
user_subscribe_id: string;
|
||||
user_subscribe_id: number;
|
||||
subscribe_id: number;
|
||||
traffic: number;
|
||||
expired_at: number;
|
||||
@ -2431,7 +2431,7 @@ declare namespace API {
|
||||
enable_login_notify: boolean;
|
||||
enable_subscribe_notify: boolean;
|
||||
enable_trade_notify: boolean;
|
||||
user_group_id: string;
|
||||
user_group_id: number;
|
||||
group_locked: boolean;
|
||||
auth_methods: UserAuthMethod[];
|
||||
user_devices: UserDevice[];
|
||||
@ -2523,6 +2523,9 @@ declare namespace API {
|
||||
upload: number;
|
||||
token: string;
|
||||
status: number;
|
||||
effective_speed: number;
|
||||
is_throttled: boolean;
|
||||
throttle_rule?: string;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
};
|
||||
@ -2824,7 +2827,7 @@ declare namespace API {
|
||||
|
||||
type SubscribeGroupMapping = {
|
||||
subscribe_id: number;
|
||||
user_group_id: string;
|
||||
user_group_id: number;
|
||||
created_at: number;
|
||||
updated_at: number;
|
||||
};
|
||||
@ -2856,8 +2859,8 @@ declare namespace API {
|
||||
type GroupHistoryDetailItem = {
|
||||
id: number;
|
||||
history_id: number;
|
||||
user_group_id: string;
|
||||
node_group_id: string;
|
||||
user_group_id: number;
|
||||
node_group_id: number;
|
||||
user_count: number;
|
||||
node_count: number;
|
||||
user_data?: string;
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user