feat: 用户订阅流量限速规则编辑器与列表信息增强

- 订阅表单新增卡片式流量限速规则编辑器(TrafficLimitRuleEditor),替换 ArrayInput
- 用户列表与订阅详情展示会员状态(member_status)、最近登录时间(last_login_time)
- ProTable 支持列级 meta.className 自定义样式
- 仪表盘统计与促销规则表单配套调整,用户文案双语同步
This commit is contained in:
2026-07-09 13:34:03 -07:00
parent 511ce4ff65
commit dfe1bafd1c
11 changed files with 367 additions and 158 deletions
@@ -28,6 +28,7 @@
"createSubscription": "Create Subscription",
"createSuccess": "Created successfully",
"createUser": "Create User",
"current": "Current",
"currentCommission": "Current Commission",
"delete": "Delete",
"deleted": "Deleted",
@@ -117,6 +118,8 @@
"permanent": "Permanent",
"prev": "Prev",
"pleaseEnterEmail": "Enter email",
"hour": "Hour",
"day": "Day",
"referer": "Referer",
"refererId": "Referer ID",
"refererIdPlaceholder": "Enter referer ID",
@@ -149,7 +152,12 @@
"save": "Save",
"shortCode": "Short Code",
"speedLimit": "Speed Limit",
"speedLimitMbps": "Target Speed Limit (Mbps)",
"startTime": "startTime",
"statType": "Statistics Type",
"statTypeDay": "Day",
"statTypeHour": "Hour",
"statValue": "Time Value",
"status": "Status",
"statusActive": "Active",
"statusDeducted": "Deducted",
@@ -171,6 +179,7 @@
"subscriptionInfo": "subscriptionInfo",
"subscriptionList": "Subscription List",
"subscriptionLogs": "Subscription Logs",
"subscriptionDefault": "Subscription Default",
"subscriptionName": "subscriptionName",
"subscriptionNotifications": "Subscription Notifications",
"telephone": "Phone",
@@ -181,17 +190,25 @@
"tradeNotifications": "Trade Notifications",
"trafficDetails": "Traffic Details",
"trafficLimit": "Traffic Limit",
"trafficLimitRule": "Rule",
"trafficLimitRules": "Traffic Limit Rules",
"trafficLimitRuleSummary": "{{traffic}} GB in {{value}} {{type}} -> {{speed}} Mbps",
"trafficStats": "Traffic Stats",
"trafficUsage": "Traffic Usage",
"trafficUsage": "Traffic Threshold (GB)",
"throttled": "Throttled",
"remainingTraffic": "Remaining Traffic",
"unlimited": "unlimited",
"unverified": "Unverified",
"update": "Update",
"updateSuccess": "Updated successfully",
"userOverride": "User Override",
"effectiveSpeed": "Effective Speed",
"groupUpdated": "Group updated successfully",
"upload": "Upload",
"uploadTraffic": "Upload Traffic",
"userStatus": "User Status",
"subscribed": "Subscribed",
"unsubscribed": "No active subscription",
"userAgent": "User Agent",
"userEmail": "Email",
"userEmailPlaceholder": "Enter email",
@@ -28,6 +28,7 @@
"createSubscription": "创建订阅",
"createSuccess": "创建成功",
"createUser": "创建用户",
"current": "当前",
"currentCommission": "当前佣金",
"delete": "删除",
"deleted": "已删除",
@@ -118,6 +119,8 @@
"permanent": "永久",
"prev": "上一页",
"pleaseEnterEmail": "输入邮箱",
"hour": "小时",
"day": "天",
"referer": "推荐人",
"refererId": "推荐人 ID",
"refererIdPlaceholder": "输入推荐人 ID",
@@ -150,7 +153,12 @@
"save": "保存",
"shortCode": "短码",
"speedLimit": "速度限制",
"speedLimitMbps": "目标限速 (Mbps)",
"startTime": "开始时间",
"statType": "统计周期",
"statTypeDay": "天",
"statTypeHour": "小时",
"statValue": "周期值",
"status": "状态",
"statusActive": "活跃",
"statusDeducted": "已扣除",
@@ -172,6 +180,7 @@
"subscriptionInfo": "订阅信息",
"subscriptionList": "订阅列表",
"subscriptionLogs": "订阅日志",
"subscriptionDefault": "套餐默认",
"subscriptionName": "订阅名称",
"subscriptionNotifications": "订阅通知",
"telephone": "电话",
@@ -182,17 +191,25 @@
"tradeNotifications": "交易通知",
"trafficDetails": "流量详情",
"trafficLimit": "流量限制",
"trafficLimitRule": "规则",
"trafficLimitRules": "流量限速规则",
"trafficLimitRuleSummary": "{{value}}{{type}}内达到 {{traffic}} GB 后限速到 {{speed}} Mbps",
"trafficStats": "流量统计",
"trafficUsage": "流量使用",
"trafficUsage": "触发流量 (GB)",
"throttled": "已触发限速",
"remainingTraffic": "剩余流量",
"unlimited": "无限制",
"unverified": "未验证",
"update": "更新",
"updateSuccess": "更新成功",
"userOverride": "用户覆盖",
"effectiveSpeed": "当前生效",
"groupUpdated": "分组更新成功",
"upload": "上传",
"uploadTraffic": "上传流量",
"userStatus": "用户状态",
"subscribed": "有有效订阅",
"unsubscribed": "无有效订阅",
"userAgent": "用户代理",
"userEmail": "邮箱",
"userEmailPlaceholder": "输入邮箱",
@@ -18,10 +18,8 @@ import { Separator } from "@workspace/ui/components/separator";
import { Tabs, TabsList, TabsTrigger } from "@workspace/ui/components/tabs";
import Empty from "@workspace/ui/composed/empty";
import { Icon } from "@workspace/ui/composed/icon";
import {
queryServerTotalData,
queryTicketWaitReply,
} from "@workspace/ui/services/admin/console";
import { queryServerTotalData } from "@workspace/ui/services/admin/console";
import { getWithdrawalList } from "@workspace/ui/services/admin/withdrawal";
import { formatBytes } from "@workspace/ui/utils/formatting";
import { useState } from "react";
import { useTranslation } from "react-i18next";
@@ -41,11 +39,15 @@ import { UserStatisticsCard } from "./user-statistics-card";
export default function Statistics() {
const { t } = useTranslation("dashboard");
const { data: TicketTotal } = useQuery({
queryKey: ["queryTicketWaitReply"],
const { data: PendingWithdrawalTotal } = useQuery({
queryKey: ["queryPendingWithdrawalTotal"],
queryFn: async () => {
const { data } = await queryTicketWaitReply();
return data.data?.count;
const { data } = await getWithdrawalList({
page: 1,
size: 1,
status: 0,
});
return data.data?.total || 0;
},
});
const { data: ServerTotal } = useQuery({
@@ -242,7 +244,7 @@ export default function Statistics() {
},
{
title: t("withdrawalManagement", "提现管理"),
value: TicketTotal || 0,
value: PendingWithdrawalTotal || 0,
subtitle: t("pending", "Pending"),
icon: "uil:clipboard-notes",
href: "/dashboard/withdrawal",
+4 -4
View File
@@ -93,8 +93,8 @@ function toFormValues(rule?: API.PromoRule): RuleFormValues {
window_hours: rule?.params?.window_hours,
priority: rule?.priority ?? 0,
enabled: rule?.enabled ?? false,
start_time: rule?.start_time,
end_time: rule?.end_time,
start_time: rule?.start_time ? rule.start_time * 1000 : undefined,
end_time: rule?.end_time ? rule.end_time * 1000 : undefined,
};
}
@@ -112,8 +112,8 @@ function toRequest(values: RuleFormValues): API.CreatePromoRuleRequest {
params,
priority: values.priority,
enabled: values.enabled,
start_time: values.start_time || undefined,
end_time: values.end_time || undefined,
start_time: values.start_time ? Math.floor(values.start_time / 1000) : undefined,
end_time: values.end_time ? Math.floor(values.end_time / 1000) : undefined,
};
}
+60 -23
View File
@@ -66,7 +66,6 @@ import { formatDate } from "@/utils/common";
import FamilyManagement from "./family";
import { RemarkForm } from "./remark-form";
import { buildUserBasicInfoPayload } from "./user-basic-info-payload";
import { UserDetail } from "./user-detail";
import UserForm from "./user-form";
import { UserIdentifier } from "./user-identifier";
import { UserInviteStatsSheet } from "./user-invite-stats-sheet";
@@ -336,23 +335,31 @@ export default function User() {
id: "referer_id",
accessorKey: "referer_id",
header: t("referer", "Referer"),
cell: ({ row }) => <UserDetail id={row.original.referer_id} />,
cell: ({ row }) => (
<RefererProfileTrigger
onUpdated={() => ref.current?.refresh()}
userId={row.original.referer_id}
/>
),
},
{
id: "last_login_time",
accessorKey: "last_login_time",
id: "member_status",
accessorKey: "member_status",
header: t("userStatus", "用户状态"),
cell: ({ row }) => {
const lastLoginTime = (
// member_status 由后端基于当前有效订阅计算:有有效订阅时为套餐名,否则为空
const memberStatus = (
row.original as API.User & {
last_login_time?: number;
member_status?: string;
}
).last_login_time;
const isOnline = Boolean(lastLoginTime);
const dotClassName = isOnline ? "bg-green-500" : "bg-gray-400";
const statusLabel = isOnline
? t("active", "Active")
: t("inactive", "Inactive");
).member_status;
const hasActiveSubscription = Boolean(memberStatus);
const dotClassName = hasActiveSubscription
? "bg-green-500"
: "bg-gray-400";
const tooltipLabel = hasActiveSubscription
? memberStatus
: t("unsubscribed", "No active subscription");
return (
<TooltipProvider>
@@ -363,18 +370,10 @@ export default function User() {
aria-hidden="true"
className={`h-2.5 w-2.5 rounded-full ${dotClassName}`}
/>
<span className="sr-only">
{isOnline
? `${statusLabel}: ${formatDate(lastLoginTime)}`
: statusLabel}
</span>
<span className="sr-only">{tooltipLabel}</span>
</div>
</TooltipTrigger>
<TooltipContent>
{isOnline
? formatDate(lastLoginTime)
: t("neverLoggedIn", "Never logged in")}
</TooltipContent>
<TooltipContent>{tooltipLabel}</TooltipContent>
</Tooltip>
</TooltipProvider>
);
@@ -439,9 +438,11 @@ export default function User() {
function ProfileSheet({
userId,
onUpdated,
trigger,
}: {
userId: number;
onUpdated?: () => void;
trigger?: React.ReactNode;
}) {
const { t } = useTranslation("user");
const [open, setOpen] = useState(false);
@@ -462,7 +463,7 @@ function ProfileSheet({
return (
<Sheet onOpenChange={setOpen} open={open}>
<SheetTrigger asChild>
<Button variant="default">{t("edit", "Edit")}</Button>
{trigger || <Button variant="default">{t("edit", "Edit")}</Button>}
</SheetTrigger>
<SheetContent
className="w-[700px] max-w-full md:max-w-screen-lg"
@@ -526,6 +527,42 @@ function SubscriptionSheet({ userId }: { userId: number }) {
);
}
function RefererProfileTrigger({
userId,
onUpdated,
}: {
userId: number;
onUpdated?: () => void;
}) {
const { t } = useTranslation("user");
const { data: user } = useQuery({
enabled: userId > 0,
queryKey: ["refererUserDetail", userId],
queryFn: async () => {
const { data } = await getUserDetail({ id: userId });
return data.data as API.User;
},
});
if (!userId) return "--";
return (
<ProfileSheet
onUpdated={onUpdated}
trigger={
<Button className="h-auto p-0 font-normal" variant="link">
{user ? (
<UserIdentifier className="flex items-center" user={user} />
) : (
t("loading", "Loading...")
)}
</Button>
}
userId={userId}
/>
);
}
function InviteStatsMenuItem({ userId }: { userId: number }) {
const { t } = useTranslation("user");
const [open, setOpen] = useState(false);
@@ -316,19 +316,22 @@ export default function UserSubscription({ userId }: { userId: number }) {
{
accessorKey: "speed_limit",
header: t("speedLimit", "Speed Limit"),
meta: {
className: "min-w-[220px] whitespace-normal",
},
cell: ({ row }) => {
const userSpeed = row.original.speed_limit ?? 0;
const effectiveSpeed = row.original.effective_speed ?? 0;
const planSpeed = resolvePlanSpeed(row.original);
const speed = effectiveSpeed || userSpeed || planSpeed;
const source = row.original.is_throttled
? t("effectiveSpeed", "Effective Speed")
? t("effectiveSpeed")
: userSpeed > 0
? t("userOverride", "User Override")
: t("subscriptionDefault", "Subscription Default");
? t("userOverride")
: t("subscriptionDefault");
return (
<div className="flex flex-col gap-1">
<div className="flex min-w-[200px] flex-col gap-1 whitespace-normal">
<div className="flex items-center gap-2">
<Display type="trafficSpeed" value={speed} />
{row.original.is_throttled && (
@@ -337,19 +340,18 @@ export default function UserSubscription({ userId }: { userId: number }) {
</Badge>
)}
</div>
<span className="text-muted-foreground text-xs">
{source}
</span>
<span className="text-muted-foreground text-xs">
{t("userOverride", "User Override")}:{" "}
<span className="inline-block">
<div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5 text-muted-foreground text-xs leading-5">
<span>{t("current")}</span>
<span>{source}</span>
<span>{t("userOverride")}</span>
<span>
<Display type="trafficSpeed" value={userSpeed} />
</span>{" "}
/ {t("subscriptionDefault", "Subscription Default")}:{" "}
<span className="inline-block">
</span>
<span>{t("subscriptionDefault")}</span>
<span>
<Display type="trafficSpeed" value={planSpeed} />
</span>
</span>
</div>
</div>
);
},
@@ -121,13 +121,13 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
<div className="flex flex-wrap items-center gap-2 font-medium">
{isThrottled ? (
<>
<span>{t("throttled", "Speed Throttled")}</span>
<span>{t("throttled")}</span>
<Badge className="text-xs" variant="destructive">
{effectiveSpeed} Mbps
</Badge>
{baseSpeed > 0 && (
<span className="text-muted-foreground text-xs line-through">
{t("subscriptionDefault", "Subscription Default")}:{" "}
{t("subscriptionDefault")}:{" "}
{baseSpeed} Mbps
</span>
)}
@@ -138,24 +138,22 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
{t("speedLimit", "Speed Limit")}
</span>
<Badge className="text-xs" variant="secondary">
{effectiveSpeed > 0
? `${effectiveSpeed} Mbps`
: t("unlimited", "Unlimited")}
{effectiveSpeed > 0 ? `${effectiveSpeed} Mbps` : t("unlimited")}
</Badge>
</>
)}
</div>
<div className="grid gap-2 sm:grid-cols-3">
<SpeedMetric
label={t("subscriptionDefault", "Subscription Default")}
label={t("subscriptionDefault")}
value={baseSpeed}
/>
<SpeedMetric
label={t("userOverride", "User Override")}
label={t("userOverride")}
value={userSpeed}
/>
<SpeedMetric
label={t("effectiveSpeed", "Effective Speed")}
label={t("effectiveSpeed")}
value={effectiveSpeed}
/>
</div>
@@ -173,7 +171,7 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
{rules.length > 0 && (
<div className="space-y-2">
<p className="font-medium text-foreground text-xs">
{t("trafficLimitRules", "Traffic Limit Rules")}
{t("trafficLimitRules")}
</p>
<div className="grid gap-2">
{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,
})}
</div>
@@ -209,7 +207,7 @@ function SpeedMetric({ label, value }: { label: string; value: number }) {
<div className="rounded-md border bg-background/60 p-2">
<div className="text-muted-foreground text-xs">{label}</div>
<div className="font-medium text-foreground">
{value > 0 ? `${value} Mbps` : t("unlimited", "Unlimited")}
{value > 0 ? `${value} Mbps` : t("unlimited")}
</div>
</div>
);
@@ -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<SubscriptionTrafficLimitRule>
) => {
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 (
<div className="space-y-3">
{rules.map((rule, index) => (
<Card className="gap-0 rounded-lg py-0 shadow-none" key={index}>
<CardHeader className="border-b px-4 py-3">
<div className="flex items-center justify-between gap-3">
<CardTitle className="text-sm">
{t("trafficLimitRule")} {index + 1}
</CardTitle>
<div className="flex items-center gap-1">
{rules.length > 1 && (
<Button
className="h-8 w-8 p-0 text-destructive"
onClick={() => removeRule(index)}
size="icon"
type="button"
variant="ghost"
>
<CircleMinusIcon className="h-4 w-4" />
</Button>
)}
{index === rules.length - 1 && (
<Button
className="h-8 w-8 p-0 text-primary"
onClick={addRule}
size="icon"
type="button"
variant="ghost"
>
<CirclePlusIcon className="h-4 w-4" />
</Button>
)}
</div>
</div>
</CardHeader>
<CardContent className="px-4 py-4">
<div className="grid gap-3 sm:grid-cols-2">
<div className="space-y-2">
<FormLabel className="text-xs">
{t("statType")}
</FormLabel>
<Combobox<string, false>
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}
/>
</div>
<div className="space-y-2">
<FormLabel className="text-xs">
{t("statValue")}
</FormLabel>
<EnhancedInput
min={1}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
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}
/>
</div>
<div className="space-y-2">
<FormLabel className="text-xs">
{t("trafficUsage")}
</FormLabel>
<EnhancedInput
min={0}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
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}
/>
</div>
<div className="space-y-2">
<FormLabel className="text-xs">
{t("speedLimitMbps")}
</FormLabel>
<EnhancedInput
min={0}
onKeyDown={(e: KeyboardEvent<HTMLInputElement>) => {
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}
/>
</div>
</div>
<div
className={cn(
"mt-3 rounded-md bg-muted/40 px-3 py-2 text-muted-foreground text-xs"
)}
>
{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,
})}
</div>
</CardContent>
</Card>
))}
</div>
);
}
export function SubscriptionForm({
trigger,
title,
@@ -317,98 +517,11 @@ export function SubscriptionForm({
{t("trafficLimitRules", "Traffic Limit Rules")}
</FormLabel>
<FormControl>
<ArrayInput<SubscriptionTrafficLimitRule>
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<HTMLInputElement>
) => {
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<HTMLInputElement>
) => {
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<HTMLInputElement>
) => {
if (e.key === "." || e.key === ",") {
e.preventDefault();
}
},
formatOutput: (value: string | number) => {
const num = Number(value);
return String(
Number.isNaN(num) ? 0 : Math.floor(num)
);
},
},
]}
<TrafficLimitRuleEditor
onChange={(items) => {
field.onChange(items.map(normalizeLimitRule));
}}
value={
field.value && field.value.length > 0
? field.value
: [defaultLimitRule]
}
value={field.value || []}
/>
</FormControl>
<FormDescription>
@@ -313,7 +313,8 @@ export function ProTable<
<TableHead
className={cn(
"!z-auto",
getTableHeaderClass(header.column.id)
getTableHeaderClass(header.column.id),
getColumnClassName(header)
)}
key={header.id}
>
@@ -362,7 +363,10 @@ export function ProTable<
.filter((cell) => cell.column.id !== "sortable")
.map((cell) => (
<TableCell
className={getTableCellClass(cell.column.id)}
className={cn(
getTableCellClass(cell.column.id),
getColumnClassName(cell)
)}
key={cell.id}
>
{flexRender(
@@ -381,7 +385,10 @@ export function ProTable<
>
{row.getVisibleCells().map((cell) => (
<TableCell
className={getTableCellClass(cell.column.id)}
className={cn(
getTableCellClass(cell.column.id),
getColumnClassName(cell)
)}
key={cell.id}
>
{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 || "";
}
+3
View File
@@ -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;
-1
View File
@@ -1,4 +1,3 @@
// @ts-expect-error
/* eslint-disable */
import request from "@workspace/ui/lib/request";