diff --git a/src/api/user.ts b/src/api/user.ts index 3119d65..7b5fdfb 100644 --- a/src/api/user.ts +++ b/src/api/user.ts @@ -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; } @@ -99,6 +101,19 @@ export async function getUserInfoById(id: number) { return adminGet(`/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("/user/updateUserPlans", payload); +} + export interface UserLoginLogItem { id: number; ip: string; diff --git a/src/locales/en-US.ts b/src/locales/en-US.ts index 5f03bf2..1d1fea8 100644 --- a/src/locales/en-US.ts +++ b/src/locales/en-US.ts @@ -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", @@ -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", @@ -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", diff --git a/src/locales/ru-RU.ts b/src/locales/ru-RU.ts index 9417b63..61a79b4 100644 --- a/src/locales/ru-RU.ts +++ b/src/locales/ru-RU.ts @@ -376,6 +376,7 @@ const translations: Translations = { "online_count": "Устройства онлайн", "status": "Статус", "subscription": "Подписка", + "plan_list": "Пакеты", "group": "Группа", "used_traffic": "Использовано", "total_traffic": "Всего трафика", @@ -1403,6 +1404,10 @@ const translations: Translations = { "description": "Формат «порог:бонус» в основных единицах, например 100:10 — пополнение на 100 даёт 10 бонуса. Берётся наибольший подходящий уровень.", "placeholder": "100:10" }, + "multi_plan_enable": { + "title": "Разрешить несколько подписок", + "description": "При включении пользователи могут одновременно иметь несколько активных подписок. Новые покупки добавляются к существующим, а не заменяют их." + }, "saving": "Сохранение...", "plan": { "title": "Тарифные планы", diff --git a/src/locales/zh-CN.ts b/src/locales/zh-CN.ts index 5f3bf29..2889919 100644 --- a/src/locales/zh-CN.ts +++ b/src/locales/zh-CN.ts @@ -376,6 +376,7 @@ const translations: Translations = { "online_count": "在线设备", "status": "状态", "subscription": "订阅", + "plan_list": "套餐列表", "group": "权限组", "used_traffic": "已用流量", "total_traffic": "总流量", @@ -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": "用户可以正常使用所有服务", @@ -1292,6 +1302,10 @@ const translations: Translations = { "description": "格式为「门槛元:赠送元」,例如 100:10 表示充值满 100 元赠送 10 元。取满足门槛的最大赠送。", "placeholder": "100:10" }, + "multi_plan_enable": { + "title": "允许用户持有多个订阅套餐", + "description": "开启后用户可同时拥有多个有效套餐,新购订单会追加到现有套餐而非覆盖。" + }, "saving": "保存中...", "plan": { "title": "订阅套餐", diff --git a/src/locales/zh-TW.ts b/src/locales/zh-TW.ts index abb5b2d..231e2f4 100644 --- a/src/locales/zh-TW.ts +++ b/src/locales/zh-TW.ts @@ -376,6 +376,7 @@ const translations: Translations = { "online_count": "在線設備", "status": "狀態", "subscription": "訂閱", + "plan_list": "套餐列表", "group": "權限組", "used_traffic": "已用流量", "total_traffic": "總流量", @@ -1292,6 +1293,10 @@ const translations: Translations = { "description": "格式為「門檻元:贈送元」,例如 100:10 表示充值滿 100 元贈送 10 元。取滿足門檻的最大贈送。", "placeholder": "100:10" }, + "multi_plan_enable": { + "title": "允許用戶持有多個訂閱套餐", + "description": "開啟後用戶可同時擁有多个有效套餐,新購訂單會追加到現有套餐而非覆蓋。" + }, "saving": "保存中...", "plan": { "title": "訂閱套餐", diff --git a/src/pages/config/sections/subscribe-section.tsx b/src/pages/config/sections/subscribe-section.tsx index 42acf00..54004cd 100644 --- a/src/pages/config/sections/subscribe-section.tsx +++ b/src/pages/config/sections/subscribe-section.tsx @@ -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[] = [ diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx index 209b76e..f39002f 100644 --- a/src/pages/user/index.tsx +++ b/src/pages/user/index.tsx @@ -398,8 +398,8 @@ export function UserListPage() { {t("user.columns.status")} - - {t("user.columns.subscription")} + + {t("user.columns.plan_list")} {t("user.columns.group")} @@ -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; @@ -509,14 +513,24 @@ export function UserListPage() { ); - const planBadge = planName ? ( - - {planName} - + const planBadge = planNames.length > 0 ? ( +
+ {planNames.map((name, i) => ( + + {name} + + ))} +
) : ( ); @@ -659,15 +673,16 @@ export function UserListPage() { {/* 窄屏:在独立列出现前,把关键信息并入邮箱列 */}
{statusBadge} - {planName ? ( + {planNames.map((name, i) => ( - {planName} + {name} - ) : null} + ))}
+ + +
+ {planList.map((item, index) => ( +
+
+ + +
+ +
+ + + updatePlan(index, "expired_at", e.target.value) + } + className="mt-1" + /> +
+ +
+ + + updatePlan(index, "speed_limit", e.target.value ? Number(e.target.value) : null) + } + className="mt-1" + /> +
+ + +
+ ))} +
+ + )} +