+
{row.original.is_throttled && (
@@ -337,19 +340,18 @@ export default function UserSubscription({ userId }: { userId: number }) {
)}
-
- {source}
-
-
- {t("userOverride", "User Override")}:{" "}
-
+
+ {t("current")}
+ {source}
+ {t("userOverride")}
+
- {" "}
- / {t("subscriptionDefault", "Subscription Default")}:{" "}
-
+
+ {t("subscriptionDefault")}
+
-
+
);
},
diff --git a/apps/admin/src/sections/user/user-subscription/subscription-detail.tsx b/apps/admin/src/sections/user/user-subscription/subscription-detail.tsx
index 48c8d15..c280a09 100644
--- a/apps/admin/src/sections/user/user-subscription/subscription-detail.tsx
+++ b/apps/admin/src/sections/user/user-subscription/subscription-detail.tsx
@@ -121,13 +121,13 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
{isThrottled ? (
<>
- {t("throttled", "Speed Throttled")}
+ {t("throttled")}
{effectiveSpeed} Mbps
{baseSpeed > 0 && (
- {t("subscriptionDefault", "Subscription Default")}:{" "}
+ {t("subscriptionDefault")}:{" "}
{baseSpeed} Mbps
)}
@@ -138,24 +138,22 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
{t("speedLimit", "Speed Limit")}
- {effectiveSpeed > 0
- ? `${effectiveSpeed} Mbps`
- : t("unlimited", "Unlimited")}
+ {effectiveSpeed > 0 ? `${effectiveSpeed} Mbps` : t("unlimited")}
>
)}
@@ -173,7 +171,7 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
{rules.length > 0 && (
- {t("trafficLimitRules", "Traffic Limit Rules")}
+ {t("trafficLimitRules")}
{rules.map((rule, index) => (
@@ -188,8 +186,8 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
value: rule.stat_value,
type:
rule.stat_type === "hour"
- ? t("hour", "Hour")
- : t("day", "Day"),
+ ? t("hour")
+ : t("day"),
speed: rule.speed_limit,
})}
@@ -209,7 +207,7 @@ function SpeedMetric({ label, value }: { label: string; value: number }) {
{label}
- {value > 0 ? `${value} Mbps` : t("unlimited", "Unlimited")}
+ {value > 0 ? `${value} Mbps` : t("unlimited")}
);
diff --git a/apps/admin/src/sections/user/user-subscription/subscription-form.tsx b/apps/admin/src/sections/user/user-subscription/subscription-form.tsx
index 9b81260..9bcaf44 100644
--- a/apps/admin/src/sections/user/user-subscription/subscription-form.tsx
+++ b/apps/admin/src/sections/user/user-subscription/subscription-form.tsx
@@ -1,5 +1,11 @@
import { zodResolver } from "@hookform/resolvers/zod";
import { Button } from "@workspace/ui/components/button";
+import {
+ Card,
+ CardContent,
+ CardHeader,
+ CardTitle,
+} from "@workspace/ui/components/card";
import {
Form,
FormControl,
@@ -20,10 +26,11 @@ import {
} from "@workspace/ui/components/sheet";
import { Combobox } from "@workspace/ui/composed/combobox";
import { DatePicker } from "@workspace/ui/composed/date-picker";
-import { ArrayInput } from "@workspace/ui/composed/dynamic-inputs";
import { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
import { Icon } from "@workspace/ui/composed/icon";
+import { cn } from "@workspace/ui/lib/utils";
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
+import { CircleMinusIcon, CirclePlusIcon } from "lucide-react";
import { type KeyboardEvent, type ReactNode, useState } from "react";
import { useForm } from "react-hook-form";
import { useTranslation } from "react-i18next";
@@ -87,6 +94,199 @@ const normalizeLimitRule = (
speed_limit: Math.max(0, Number(item.speed_limit) || 0),
});
+function TrafficLimitRuleEditor({
+ value,
+ onChange,
+}: {
+ value: SubscriptionTrafficLimitRule[];
+ onChange: (value: SubscriptionTrafficLimitRule[]) => void;
+}) {
+ const { t } = useTranslation("user");
+ const rules = value.length > 0 ? value : [defaultLimitRule];
+
+ const updateRule = (
+ index: number,
+ patch: Partial
+ ) => {
+ const next = rules.map((rule, i) =>
+ i === index ? normalizeLimitRule({ ...rule, ...patch }) : rule
+ );
+ onChange(next);
+ };
+
+ const addRule = () => {
+ onChange([...rules, defaultLimitRule]);
+ };
+
+ const removeRule = (index: number) => {
+ const next = rules.filter((_, i) => i !== index);
+ onChange(next.length > 0 ? next : []);
+ };
+
+ return (
+
+ {rules.map((rule, index) => (
+
+
+
+
+ {t("trafficLimitRule")} {index + 1}
+
+
+ {rules.length > 1 && (
+
+ )}
+ {index === rules.length - 1 && (
+
+ )}
+
+
+
+
+
+
+
+ {t("statType")}
+
+
+ onChange={(nextValue) => {
+ updateRule(index, {
+ stat_type: nextValue || "day",
+ });
+ }}
+ options={[
+ {
+ label: t("statTypeHour"),
+ value: "hour",
+ },
+ {
+ label: t("statTypeDay"),
+ value: "day",
+ },
+ ]}
+ placeholder={t("statType")}
+ value={rule.stat_type}
+ />
+
+
+
+
+ {t("statValue")}
+
+ ) => {
+ if (e.key === "." || e.key === ",") {
+ e.preventDefault();
+ }
+ }}
+ onValueChange={(nextValue) => {
+ updateRule(index, {
+ stat_value: Math.max(
+ 1,
+ Math.floor(Number(nextValue) || 1)
+ ),
+ });
+ }}
+ placeholder={t("statValue")}
+ type="number"
+ value={rule.stat_value}
+ />
+
+
+
+
+ {t("trafficUsage")}
+
+ ) => {
+ if (e.key === "." || e.key === ",") {
+ e.preventDefault();
+ }
+ }}
+ onValueChange={(nextValue) => {
+ updateRule(index, {
+ traffic_usage: Math.max(
+ 0,
+ Math.floor(Number(nextValue) || 0)
+ ),
+ });
+ }}
+ placeholder={t("trafficUsage")}
+ suffix="GB"
+ type="number"
+ value={rule.traffic_usage}
+ />
+
+
+
+
+ {t("speedLimitMbps")}
+
+ ) => {
+ if (e.key === "." || e.key === ",") {
+ e.preventDefault();
+ }
+ }}
+ onValueChange={(nextValue) => {
+ updateRule(index, {
+ speed_limit: Math.max(
+ 0,
+ Math.floor(Number(nextValue) || 0)
+ ),
+ });
+ }}
+ placeholder={t("speedLimitMbps")}
+ suffix="Mbps"
+ type="number"
+ value={rule.speed_limit}
+ />
+
+
+
+
+ {t("trafficLimitRuleSummary", {
+ defaultValue:
+ "{{traffic}} GB in {{value}} {{type}} -> {{speed}} Mbps",
+ traffic: rule.traffic_usage,
+ value: rule.stat_value,
+ type:
+ rule.stat_type === "hour"
+ ? t("hour")
+ : t("day"),
+ speed: rule.speed_limit,
+ })}
+
+
+
+ ))}
+
+ );
+}
+
export function SubscriptionForm({
trigger,
title,
@@ -317,98 +517,11 @@ export function SubscriptionForm({
{t("trafficLimitRules", "Traffic Limit Rules")}
-
- className="grid grid-cols-2 gap-3 lg:grid-cols-4"
- fields={[
- {
- name: "stat_type",
- type: "select",
- placeholder: t("statType", "Statistics Type"),
- value: "day",
- options: [
- {
- label: t("statTypeHour", "Hour"),
- value: "hour",
- },
- {
- label: t("statTypeDay", "Day"),
- value: "day",
- },
- ],
- },
- {
- name: "stat_value",
- type: "number",
- placeholder: t("statValue", "Time Value"),
- min: 1,
- step: 1,
- onKeyDown: (
- e: KeyboardEvent
- ) => {
- if (e.key === "." || e.key === ",") {
- e.preventDefault();
- }
- },
- formatOutput: (value: string | number) => {
- const num = Number(value);
- return String(
- Number.isNaN(num) ? 1 : Math.floor(num)
- );
- },
- },
- {
- name: "traffic_usage",
- type: "number",
- placeholder: t(
- "trafficUsage",
- "Traffic Usage (GB)"
- ),
- min: 0,
- onKeyDown: (
- e: KeyboardEvent
- ) => {
- if (e.key === "." || e.key === ",") {
- e.preventDefault();
- }
- },
- formatOutput: (value: string | number) => {
- const num = Number(value);
- return String(
- Number.isNaN(num) ? 0 : Math.floor(num)
- );
- },
- },
- {
- name: "speed_limit",
- type: "number",
- placeholder: t(
- "speedLimitMbps",
- "Speed Limit (Mbps)"
- ),
- min: 0,
- onKeyDown: (
- e: KeyboardEvent
- ) => {
- if (e.key === "." || e.key === ",") {
- e.preventDefault();
- }
- },
- formatOutput: (value: string | number) => {
- const num = Number(value);
- return String(
- Number.isNaN(num) ? 0 : Math.floor(num)
- );
- },
- },
- ]}
+ {
field.onChange(items.map(normalizeLimitRule));
}}
- value={
- field.value && field.value.length > 0
- ? field.value
- : [defaultLimitRule]
- }
+ value={field.value || []}
/>
diff --git a/packages/ui/src/composed/pro-table/pro-table.tsx b/packages/ui/src/composed/pro-table/pro-table.tsx
index a111b09..d5b1d40 100644
--- a/packages/ui/src/composed/pro-table/pro-table.tsx
+++ b/packages/ui/src/composed/pro-table/pro-table.tsx
@@ -313,7 +313,8 @@ export function ProTable<
@@ -362,7 +363,10 @@ export function ProTable<
.filter((cell) => cell.column.id !== "sortable")
.map((cell) => (
{flexRender(
@@ -381,7 +385,10 @@ export function ProTable<
>
{row.getVisibleCells().map((cell) => (
{flexRender(
@@ -476,3 +483,17 @@ function getTableCellClass(columnId: string) {
}
return "truncate";
}
+
+function getColumnClassName(
+ columnLike: {
+ column: {
+ columnDef: {
+ meta?: {
+ className?: string;
+ };
+ };
+ };
+ }
+) {
+ return columnLike.column.columnDef.meta?.className || "";
+}
diff --git a/packages/ui/src/services/admin/typings.d.ts b/packages/ui/src/services/admin/typings.d.ts
index 0280f6e..eb9650a 100644
--- a/packages/ui/src/services/admin/typings.d.ts
+++ b/packages/ui/src/services/admin/typings.d.ts
@@ -2598,6 +2598,9 @@ declare namespace API {
user_devices: UserDevice[];
remark: string;
rules: string[];
+ /** 当前有效订阅的套餐名(由后端计算),无有效订阅时为空 */
+ member_status?: string;
+ last_login_time?: number;
created_at: number;
updated_at: number;
deleted_at?: number;
diff --git a/packages/ui/src/services/user/user.ts b/packages/ui/src/services/user/user.ts
index 16a6950..f48f01a 100644
--- a/packages/ui/src/services/user/user.ts
+++ b/packages/ui/src/services/user/user.ts
@@ -1,4 +1,3 @@
-// @ts-expect-error
/* eslint-disable */
import request from "@workspace/ui/lib/request";