Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions src/api/user.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,8 @@ export interface UserListItem {
discount?: number | null;
telegram_id?: string | null;
phone?: string | null;
/** 多套餐列表(multi_plan_enable 开启时返回) */
plan_list?: Array<{ id: number; name: string; expired_at: number | null; speed_limit?: number | null }>;
[k: string]: any;
}

Expand Down Expand Up @@ -99,6 +101,19 @@ export async function getUserInfoById(id: number) {
return adminGet<any>(`/user/getUserInfoById`, { id });
}

export interface PlanListItem {
plan_id: number;
expired_at: number | null;
speed_limit?: number | null;
}

export async function updateUserPlans(payload: {
id: number;
plan_list: PlanListItem[];
}) {
return adminPost<any>("/user/updateUserPlans", payload);
}

export interface UserLoginLogItem {
id: number;
ip: string;
Expand Down
14 changes: 14 additions & 0 deletions src/locales/en-US.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ const translations: Translations = {
"online_count": "Online Devices",
"status": "Status",
"subscription": "Subscription",
"plan_list": "Plans",
"group": "Group",
"used_traffic": "Used Traffic",
"total_traffic": "Total Traffic",
Expand Down Expand Up @@ -563,6 +564,15 @@ const translations: Translations = {
"expire_time_confirm": "Confirm",
"subscription": "Subscription Plan",
"subscription_none": "None",
"multi_plan_title": "Plan List (Multi-Plan Mode)",
"multi_plan_hint": "When multi-plan mode is enabled, multiple active plans can be added. In single-plan mode only the first plan is shown.",
"plan_item_plan": "Plan",
"plan_item_expire": "Expires",
"plan_item_speed_limit": "Speed Limit (Mbps)",
"plan_item_speed_limit_placeholder": "Leave empty for no limit",
"plan_item_permanent": "Permanent",
"plan_item_add": "Add Plan",
"plan_item_remove": "Remove",
"account_status": "Account Status",
"banned_hint": "User cannot log in or use subscription services",
"normal_hint": "User can use all services normally",
Expand Down Expand Up @@ -1403,6 +1413,10 @@ const translations: Translations = {
"description": "Format is threshold:bonus in major units, e.g. 100:10 means deposit 100 get 10 bonus. The highest matching tier is used.",
"placeholder": "100:10"
},
"multi_plan_enable": {
"title": "Allow Multiple Subscription Plans",
"description": "When enabled, users can hold multiple active plans simultaneously. New purchases will be added alongside existing plans instead of replacing them."
},
"saving": "Saving...",
"plan": {
"title": "Subscription Plans",
Expand Down
5 changes: 5 additions & 0 deletions src/locales/ru-RU.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ const translations: Translations = {
"online_count": "Устройства онлайн",
"status": "Статус",
"subscription": "Подписка",
"plan_list": "Пакеты",
"group": "Группа",
"used_traffic": "Использовано",
"total_traffic": "Всего трафика",
Expand Down Expand Up @@ -1403,6 +1404,10 @@ const translations: Translations = {
"description": "Формат «порог:бонус» в основных единицах, например 100:10 — пополнение на 100 даёт 10 бонуса. Берётся наибольший подходящий уровень.",
"placeholder": "100:10"
},
"multi_plan_enable": {
"title": "Разрешить несколько подписок",
"description": "При включении пользователи могут одновременно иметь несколько активных подписок. Новые покупки добавляются к существующим, а не заменяют их."
},
"saving": "Сохранение...",
"plan": {
"title": "Тарифные планы",
Expand Down
14 changes: 14 additions & 0 deletions src/locales/zh-CN.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ const translations: Translations = {
"online_count": "在线设备",
"status": "状态",
"subscription": "订阅",
"plan_list": "套餐列表",
"group": "权限组",
"used_traffic": "已用流量",
"total_traffic": "总流量",
Expand Down Expand Up @@ -563,6 +564,15 @@ const translations: Translations = {
"expire_time_confirm": "确定",
"subscription": "订阅计划",
"subscription_none": "无",
"multi_plan_title": "套餐列表(多套餐模式)",
"multi_plan_hint": "开启多套餐模式后,可添加多个有效套餐。单套餐模式下仅显示第一个套餐。",
"plan_item_plan": "套餐",
"plan_item_expire": "到期时间",
"plan_item_speed_limit": "限速 (Mbps)",
"plan_item_speed_limit_placeholder": "留空则不限速",
"plan_item_permanent": "长期有效",
"plan_item_add": "添加套餐",
"plan_item_remove": "移除",
"account_status": "账户状态",
"banned_hint": "用户无法登录、无法使用订阅服务",
"normal_hint": "用户可以正常使用所有服务",
Expand Down Expand Up @@ -1292,6 +1302,10 @@ const translations: Translations = {
"description": "格式为「门槛元:赠送元」,例如 100:10 表示充值满 100 元赠送 10 元。取满足门槛的最大赠送。",
"placeholder": "100:10"
},
"multi_plan_enable": {
"title": "允许用户持有多个订阅套餐",
"description": "开启后用户可同时拥有多个有效套餐,新购订单会追加到现有套餐而非覆盖。"
},
"saving": "保存中...",
"plan": {
"title": "订阅套餐",
Expand Down
5 changes: 5 additions & 0 deletions src/locales/zh-TW.ts
Original file line number Diff line number Diff line change
Expand Up @@ -376,6 +376,7 @@ const translations: Translations = {
"online_count": "在線設備",
"status": "狀態",
"subscription": "訂閱",
"plan_list": "套餐列表",
"group": "權限組",
"used_traffic": "已用流量",
"total_traffic": "總流量",
Expand Down Expand Up @@ -1292,6 +1293,10 @@ const translations: Translations = {
"description": "格式為「門檻元:贈送元」,例如 100:10 表示充值滿 100 元贈送 10 元。取滿足門檻的最大贈送。",
"placeholder": "100:10"
},
"multi_plan_enable": {
"title": "允許用戶持有多個訂閱套餐",
"description": "開啟後用戶可同時擁有多个有效套餐,新購訂單會追加到現有套餐而非覆蓋。"
},
"saving": "保存中...",
"plan": {
"title": "訂閱套餐",
Expand Down
1 change: 1 addition & 0 deletions src/pages/config/sections/subscribe-section.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ const switchKeys: Array<{ key: string; i18n: string }> = [
{ key: "default_remind_traffic", i18n: "default_remind_traffic" },
{ key: "deposit_enable", i18n: "deposit_enable" },
{ key: "deposit_commission_enable", i18n: "deposit_commission_enable" },
{ key: "multi_plan_enable", i18n: "multi_plan_enable" },
];

const baseFields: FieldDef[] = [
Expand Down
49 changes: 32 additions & 17 deletions src/pages/user/index.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -398,8 +398,8 @@ export function UserListPage() {
<TableHead className="hidden w-[108px] text-center sm:table-cell">
{t("user.columns.status")}
</TableHead>
<TableHead className="hidden min-w-[120px] md:table-cell">
{t("user.columns.subscription")}
<TableHead className="hidden min-w-[140px] md:table-cell">
{t("user.columns.plan_list")}
</TableHead>
<TableHead className="hidden min-w-[100px] xl:table-cell">
{t("user.columns.group")}
Expand Down Expand Up @@ -480,9 +480,13 @@ export function UserListPage() {
const expire = expireMeta(u.expired_at);
const online = Number(u.online_count || 0);
const selectedRow = selected.includes(u.id);
const planName = u.plan_id
? plansMap[u.plan_id] || `#${u.plan_id}`
: null;
// plan_list 优先(多套餐模式);否则回退 plan_id
const planNames: string[] = u.plan_list && u.plan_list.length > 0
? u.plan_list.map((p) => plansMap[p.id] || p.name || `#${p.id}`)
: u.plan_id
? [plansMap[u.plan_id] || `#${u.plan_id}`]
: [];
const planName = planNames[0] || null;
const groupName = u.group_id
? groupsMap[u.group_id] || `#${u.group_id}`
: null;
Expand All @@ -509,14 +513,24 @@ export function UserListPage() {
</Badge>
);

const planBadge = planName ? (
<Badge
variant="secondary"
className="max-w-[160px] truncate font-normal"
title={planName}
>
{planName}
</Badge>
const planBadge = planNames.length > 0 ? (
<div className="flex flex-wrap gap-1">
{planNames.map((name, i) => (
<Badge
key={i}
variant="secondary"
className={cn(
"font-normal",
i === planNames.length - 1
? "max-w-[140px] truncate"
: "max-w-[80px] truncate",
)}
title={name}
>
{name}
</Badge>
))}
</div>
) : (
<span className="text-xs text-muted-foreground">—</span>
);
Expand Down Expand Up @@ -659,15 +673,16 @@ export function UserListPage() {
{/* 窄屏:在独立列出现前,把关键信息并入邮箱列 */}
<div className="flex flex-wrap items-center gap-1 pt-0.5 md:hidden">
<span className="sm:hidden">{statusBadge}</span>
{planName ? (
{planNames.map((name, i) => (
<Badge
key={i}
variant="secondary"
className="max-w-[9rem] truncate px-1.5 text-[10px] font-normal"
title={planName}
title={name}
>
{planName}
{name}
</Badge>
) : null}
))}
</div>
<button
type="button"
Expand Down
Loading