Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 497f654cdf | |||
| a98c7ecb52 | |||
| 0d79e9a6fd | |||
| 93d4091254 | |||
| fd94fb80c1 | |||
| ccb81d62a3 | |||
| 6e1e424eb2 | |||
| a30d83505f | |||
| 01591adb86 | |||
| 39bc0c5c7b | |||
| 7505d37af6 | |||
| f9c3b18249 | |||
| 1834b97fe2 | |||
| 23f51a1ef4 | |||
| c46944aae1 |
@@ -68,26 +68,9 @@
|
|||||||
"Read(//Users/Apple/.claude/**)",
|
"Read(//Users/Apple/.claude/**)",
|
||||||
"Bash(2)",
|
"Bash(2)",
|
||||||
"Bash(claude mcp:*)",
|
"Bash(claude mcp:*)",
|
||||||
"Bash(mysql:*)",
|
"Bash(mysql:*)"
|
||||||
"Bash(curl *)",
|
|
||||||
"mcp__mysql-ppanel__mysql_query",
|
|
||||||
"Read(//Users/Apple/code_vpn/vpn/**)",
|
|
||||||
"Read(//Users/Apple/code_vpn/**)",
|
|
||||||
"Bash(perl -0pi -e 's/onValueChange=\\\\{\\\\\\(value\\\\\\) =>\\\\n\\( +\\)field\\\\.onChange\\\\\\(value === \"\" \\\\? undefined : value\\\\\\)\\\\n\\( +\\)\\\\}/onValueChange={\\(value\\) => field.onChange\\(value\\)}/g' apps/admin/src/sections/lottery/rule-editor.tsx apps/admin/src/sections/lottery/grant-dialog.tsx)",
|
|
||||||
"Bash(pnpm --filter ppanel-admin-web exec vite build)",
|
|
||||||
"Bash(echo \"EXIT:$?\")",
|
|
||||||
"Read(//tmp/**)",
|
|
||||||
"Bash(git -C /Users/Apple/code_vpn/vpn/frontend status --short apps/admin/dist apps/admin/src/routeTree.gen.ts)",
|
|
||||||
"Bash(bun x *)",
|
|
||||||
"Bash(git *)",
|
|
||||||
"Bash(echo \"tsc exit: $?\")",
|
|
||||||
"Bash(echo \"biome exit: $?\")",
|
|
||||||
"Bash(unzip -l apps/admin/dist.zip)",
|
|
||||||
"Bash(awk '/^@@ -3297/{exit} {print}' /tmp/typings-full.patch)"
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"enableAllProjectMcpServers": true,
|
"enabledMcpjsonServers": ["mysql"],
|
||||||
"enabledMcpjsonServers": [
|
"enableAllProjectMcpServers": true
|
||||||
"mysql"
|
|
||||||
]
|
|
||||||
}
|
}
|
||||||
|
|||||||
Binary file not shown.
@@ -49,8 +49,8 @@
|
|||||||
"encryption_rtt": "RTT",
|
"encryption_rtt": "RTT",
|
||||||
"encryption_server_padding": "Server Padding",
|
"encryption_server_padding": "Server Padding",
|
||||||
"encryption_ticket": "Ticket Time",
|
"encryption_ticket": "Ticket Time",
|
||||||
"expireTime": "Expire Time",
|
|
||||||
"expired": "Expired",
|
"expired": "Expired",
|
||||||
|
"expireTime": "Expire Time",
|
||||||
"extra": "Extra",
|
"extra": "Extra",
|
||||||
"flow": "Flow",
|
"flow": "Flow",
|
||||||
"generate_quantum_resistant_key": "Generate Quantum-Resistant Key",
|
"generate_quantum_resistant_key": "Generate Quantum-Resistant Key",
|
||||||
@@ -141,52 +141,6 @@
|
|||||||
},
|
},
|
||||||
"server_key": "Server Key",
|
"server_key": "Server Key",
|
||||||
"service_name": "Service Name",
|
"service_name": "Service Name",
|
||||||
"simnet": "SimNet",
|
|
||||||
"simnet_af_enabled": "Enable Anti-Fingerprint",
|
|
||||||
"simnet_af_fake_header_injection": "Fake Header Injection",
|
|
||||||
"simnet_af_handshake_polymorphism": "Handshake Polymorphism",
|
|
||||||
"simnet_af_magic_mode": "Anti-Fingerprint Magic Mode",
|
|
||||||
"simnet_af_path_mode": "Anti-Fingerprint Path Mode",
|
|
||||||
"simnet_af_path_prefix": "Anti-Fingerprint Path Prefix",
|
|
||||||
"simnet_af_path_prefix_placeholder": "Optional path prefix",
|
|
||||||
"simnet_af_path_suffix": "Anti-Fingerprint Path Suffix",
|
|
||||||
"simnet_af_path_suffix_placeholder": "Optional path suffix",
|
|
||||||
"simnet_af_response_jitter_ms": "Anti-Fingerprint Response Jitter",
|
|
||||||
"simnet_af_settings_jitter": "Settings Frame Jitter",
|
|
||||||
"simnet_client_max_concurrent_streams": "Client Concurrent Streams",
|
|
||||||
"simnet_client_max_streams_per_session": "Client Streams / Session",
|
|
||||||
"simnet_client_max_udp_sessions": "Client UDP Sessions",
|
|
||||||
"simnet_client_session_idle_timeout_secs": "Client Session Idle Timeout",
|
|
||||||
"simnet_egress_block_link_local": "Block Link-Local Egress",
|
|
||||||
"simnet_egress_block_loopback": "Block Loopback Egress",
|
|
||||||
"simnet_egress_block_metadata": "Block Metadata Egress",
|
|
||||||
"simnet_egress_block_private": "Block Private Egress",
|
|
||||||
"simnet_fallback_enabled": "Enable HTTPS Fallback Reverse Proxy",
|
|
||||||
"simnet_fallback_host_header": "Fallback Host Header",
|
|
||||||
"simnet_fallback_host_header_placeholder": "Optional, defaults to target host",
|
|
||||||
"simnet_fallback_target_host": "Fallback Target Host",
|
|
||||||
"simnet_fallback_target_host_placeholder": "e.g. www.example.com",
|
|
||||||
"simnet_fallback_target_port": "Fallback Target Port",
|
|
||||||
"simnet_fallback_target_scheme": "Fallback Target Scheme",
|
|
||||||
"simnet_fallback_tls_sni": "Fallback TLS SNI",
|
|
||||||
"simnet_fallback_tls_sni_placeholder": "Optional, defaults to target host",
|
|
||||||
"simnet_inbound_max_handler_tasks_per_session": "Handler Tasks / Session",
|
|
||||||
"simnet_inbound_max_streams_per_session": "Inbound Streams / Session",
|
|
||||||
"simnet_inbound_max_udp_streams_per_session": "Inbound UDP Streams / Session",
|
|
||||||
"simnet_initial_window_size": "H2 Initial Window",
|
|
||||||
"simnet_max_concurrent_streams": "H2 Concurrent Streams",
|
|
||||||
"simnet_max_frame_size": "H2 Max Frame Size",
|
|
||||||
"simnet_recv_window": "Receive Window",
|
|
||||||
"simnet_reverse_enabled": "Enable Local Reverse Proxy",
|
|
||||||
"simnet_reverse_listen_addr": "Reverse Listen Address",
|
|
||||||
"simnet_reverse_listen_port": "Reverse Listen Port",
|
|
||||||
"simnet_reverse_target_host": "Reverse Target Host",
|
|
||||||
"simnet_reverse_target_port": "Reverse Target Port",
|
|
||||||
"simnet_send_window": "Send Window",
|
|
||||||
"simnet_stream_data_channel_capacity": "Stream Data Channel",
|
|
||||||
"simnet_stream_event_channel_capacity": "Stream Event Channel",
|
|
||||||
"simnet_target_dial_timeout_ms": "Target Dial Timeout",
|
|
||||||
"simnet_target_max_concurrent_dials": "Concurrent Target Dials",
|
|
||||||
"sorted_success": "Sorted successfully",
|
"sorted_success": "Sorted successfully",
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"subscribeId": "Subscribe ID",
|
"subscribeId": "Subscribe ID",
|
||||||
|
|||||||
@@ -94,8 +94,6 @@
|
|||||||
"title": "Registration Settings",
|
"title": "Registration Settings",
|
||||||
"trialConfig": "Trial Configuration",
|
"trialConfig": "Trial Configuration",
|
||||||
"trialConfigDescription": "Configure trial subscription, duration and time unit for new users upon registration",
|
"trialConfigDescription": "Configure trial subscription, duration and time unit for new users upon registration",
|
||||||
"giftTraffic": "Gift Traffic",
|
|
||||||
"giftTrafficDescription": "Gift traffic for new users (GB). 0 = duration mode (uses the trial duration above); >0 = quota mode: N GB with no time limit, trial duration is ignored",
|
|
||||||
"year": "Year(s)"
|
"year": "Year(s)"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"createSubscription": "Create Subscription",
|
"createSubscription": "Create Subscription",
|
||||||
"createSuccess": "Created successfully",
|
"createSuccess": "Created successfully",
|
||||||
"createUser": "Create User",
|
"createUser": "Create User",
|
||||||
"current": "Current",
|
|
||||||
"currentCommission": "Current Commission",
|
"currentCommission": "Current Commission",
|
||||||
"delete": "Delete",
|
"delete": "Delete",
|
||||||
"deleted": "Deleted",
|
"deleted": "Deleted",
|
||||||
@@ -118,8 +117,6 @@
|
|||||||
"permanent": "Permanent",
|
"permanent": "Permanent",
|
||||||
"prev": "Prev",
|
"prev": "Prev",
|
||||||
"pleaseEnterEmail": "Enter email",
|
"pleaseEnterEmail": "Enter email",
|
||||||
"hour": "Hour",
|
|
||||||
"day": "Day",
|
|
||||||
"referer": "Referer",
|
"referer": "Referer",
|
||||||
"refererId": "Referer ID",
|
"refererId": "Referer ID",
|
||||||
"refererIdPlaceholder": "Enter referer ID",
|
"refererIdPlaceholder": "Enter referer ID",
|
||||||
@@ -152,12 +149,7 @@
|
|||||||
"save": "Save",
|
"save": "Save",
|
||||||
"shortCode": "Short Code",
|
"shortCode": "Short Code",
|
||||||
"speedLimit": "Speed Limit",
|
"speedLimit": "Speed Limit",
|
||||||
"speedLimitMbps": "Target Speed Limit (Mbps)",
|
|
||||||
"startTime": "startTime",
|
"startTime": "startTime",
|
||||||
"statType": "Statistics Type",
|
|
||||||
"statTypeDay": "Day",
|
|
||||||
"statTypeHour": "Hour",
|
|
||||||
"statValue": "Time Value",
|
|
||||||
"status": "Status",
|
"status": "Status",
|
||||||
"statusActive": "Active",
|
"statusActive": "Active",
|
||||||
"statusDeducted": "Deducted",
|
"statusDeducted": "Deducted",
|
||||||
@@ -179,7 +171,6 @@
|
|||||||
"subscriptionInfo": "subscriptionInfo",
|
"subscriptionInfo": "subscriptionInfo",
|
||||||
"subscriptionList": "Subscription List",
|
"subscriptionList": "Subscription List",
|
||||||
"subscriptionLogs": "Subscription Logs",
|
"subscriptionLogs": "Subscription Logs",
|
||||||
"subscriptionDefault": "Subscription Default",
|
|
||||||
"subscriptionName": "subscriptionName",
|
"subscriptionName": "subscriptionName",
|
||||||
"subscriptionNotifications": "Subscription Notifications",
|
"subscriptionNotifications": "Subscription Notifications",
|
||||||
"telephone": "Phone",
|
"telephone": "Phone",
|
||||||
@@ -190,25 +181,17 @@
|
|||||||
"tradeNotifications": "Trade Notifications",
|
"tradeNotifications": "Trade Notifications",
|
||||||
"trafficDetails": "Traffic Details",
|
"trafficDetails": "Traffic Details",
|
||||||
"trafficLimit": "Traffic Limit",
|
"trafficLimit": "Traffic Limit",
|
||||||
"trafficLimitRule": "Rule",
|
|
||||||
"trafficLimitRules": "Traffic Limit Rules",
|
|
||||||
"trafficLimitRuleSummary": "{{traffic}} GB in {{value}} {{type}} -> {{speed}} Mbps",
|
|
||||||
"trafficStats": "Traffic Stats",
|
"trafficStats": "Traffic Stats",
|
||||||
"trafficUsage": "Traffic Threshold (GB)",
|
"trafficUsage": "Traffic Usage",
|
||||||
"throttled": "Throttled",
|
|
||||||
"remainingTraffic": "Remaining Traffic",
|
"remainingTraffic": "Remaining Traffic",
|
||||||
"unlimited": "unlimited",
|
"unlimited": "unlimited",
|
||||||
"unverified": "Unverified",
|
"unverified": "Unverified",
|
||||||
"update": "Update",
|
"update": "Update",
|
||||||
"updateSuccess": "Updated successfully",
|
"updateSuccess": "Updated successfully",
|
||||||
"userOverride": "User Override",
|
|
||||||
"effectiveSpeed": "Effective Speed",
|
|
||||||
"groupUpdated": "Group updated successfully",
|
"groupUpdated": "Group updated successfully",
|
||||||
"upload": "Upload",
|
"upload": "Upload",
|
||||||
"uploadTraffic": "Upload Traffic",
|
"uploadTraffic": "Upload Traffic",
|
||||||
"userStatus": "User Status",
|
"userStatus": "User Status",
|
||||||
"subscribed": "Subscribed",
|
|
||||||
"unsubscribed": "No active subscription",
|
|
||||||
"userAgent": "User Agent",
|
"userAgent": "User Agent",
|
||||||
"userEmail": "Email",
|
"userEmail": "Email",
|
||||||
"userEmailPlaceholder": "Enter email",
|
"userEmailPlaceholder": "Enter email",
|
||||||
|
|||||||
@@ -49,8 +49,8 @@
|
|||||||
"encryption_rtt": "RTT",
|
"encryption_rtt": "RTT",
|
||||||
"encryption_server_padding": "服务器填充",
|
"encryption_server_padding": "服务器填充",
|
||||||
"encryption_ticket": "票据时间",
|
"encryption_ticket": "票据时间",
|
||||||
"expireTime": "过期时间",
|
|
||||||
"expired": "已过期",
|
"expired": "已过期",
|
||||||
|
"expireTime": "过期时间",
|
||||||
"extra": "额外",
|
"extra": "额外",
|
||||||
"flow": "流控",
|
"flow": "流控",
|
||||||
"generate_quantum_resistant_key": "生成量子抗性密钥",
|
"generate_quantum_resistant_key": "生成量子抗性密钥",
|
||||||
@@ -141,52 +141,6 @@
|
|||||||
},
|
},
|
||||||
"server_key": "服务器密钥",
|
"server_key": "服务器密钥",
|
||||||
"service_name": "服务名称",
|
"service_name": "服务名称",
|
||||||
"simnet": "SimNet",
|
|
||||||
"simnet_af_enabled": "启用反指纹",
|
|
||||||
"simnet_af_fake_header_injection": "注入伪装 Header",
|
|
||||||
"simnet_af_handshake_polymorphism": "握手多态",
|
|
||||||
"simnet_af_magic_mode": "反指纹 Magic 模式",
|
|
||||||
"simnet_af_path_mode": "反指纹路径模式",
|
|
||||||
"simnet_af_path_prefix": "反指纹路径前缀",
|
|
||||||
"simnet_af_path_prefix_placeholder": "可选路径前缀",
|
|
||||||
"simnet_af_path_suffix": "反指纹路径后缀",
|
|
||||||
"simnet_af_path_suffix_placeholder": "可选路径后缀",
|
|
||||||
"simnet_af_response_jitter_ms": "反指纹响应抖动",
|
|
||||||
"simnet_af_settings_jitter": "Settings 帧抖动",
|
|
||||||
"simnet_client_max_concurrent_streams": "客户端并发流上限",
|
|
||||||
"simnet_client_max_streams_per_session": "客户端每会话流上限",
|
|
||||||
"simnet_client_max_udp_sessions": "客户端 UDP 会话上限",
|
|
||||||
"simnet_client_session_idle_timeout_secs": "客户端会话空闲超时",
|
|
||||||
"simnet_egress_block_link_local": "阻止出口访问链路本地地址",
|
|
||||||
"simnet_egress_block_loopback": "阻止出口访问 Loopback",
|
|
||||||
"simnet_egress_block_metadata": "阻止出口访问元数据地址",
|
|
||||||
"simnet_egress_block_private": "阻止出口访问私网",
|
|
||||||
"simnet_fallback_enabled": "启用 HTTPS Fallback 反向代理",
|
|
||||||
"simnet_fallback_host_header": "Fallback Host Header",
|
|
||||||
"simnet_fallback_host_header_placeholder": "可选,默认使用目标主机",
|
|
||||||
"simnet_fallback_target_host": "Fallback 目标主机",
|
|
||||||
"simnet_fallback_target_host_placeholder": "例如 www.example.com",
|
|
||||||
"simnet_fallback_target_port": "Fallback 目标端口",
|
|
||||||
"simnet_fallback_target_scheme": "Fallback 目标协议",
|
|
||||||
"simnet_fallback_tls_sni": "Fallback TLS SNI",
|
|
||||||
"simnet_fallback_tls_sni_placeholder": "可选,默认使用目标主机",
|
|
||||||
"simnet_inbound_max_handler_tasks_per_session": "每会话处理任务上限",
|
|
||||||
"simnet_inbound_max_streams_per_session": "每会话入站流上限",
|
|
||||||
"simnet_inbound_max_udp_streams_per_session": "每会话入站 UDP 流上限",
|
|
||||||
"simnet_initial_window_size": "H2 初始窗口",
|
|
||||||
"simnet_max_concurrent_streams": "H2 并发流上限",
|
|
||||||
"simnet_max_frame_size": "H2 最大帧大小",
|
|
||||||
"simnet_recv_window": "接收窗口",
|
|
||||||
"simnet_reverse_enabled": "启用本地反向代理",
|
|
||||||
"simnet_reverse_listen_addr": "反代监听地址",
|
|
||||||
"simnet_reverse_listen_port": "反代监听端口",
|
|
||||||
"simnet_reverse_target_host": "反代目标主机",
|
|
||||||
"simnet_reverse_target_port": "反代目标端口",
|
|
||||||
"simnet_send_window": "发送窗口",
|
|
||||||
"simnet_stream_data_channel_capacity": "流数据队列容量",
|
|
||||||
"simnet_stream_event_channel_capacity": "流事件队列容量",
|
|
||||||
"simnet_target_dial_timeout_ms": "目标拨号超时",
|
|
||||||
"simnet_target_max_concurrent_dials": "并发目标拨号上限",
|
|
||||||
"sorted_success": "排序成功",
|
"sorted_success": "排序成功",
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"subscribeId": "订阅 ID",
|
"subscribeId": "订阅 ID",
|
||||||
|
|||||||
@@ -94,8 +94,6 @@
|
|||||||
"title": "注册设置",
|
"title": "注册设置",
|
||||||
"trialConfig": "试用配置",
|
"trialConfig": "试用配置",
|
||||||
"trialConfigDescription": "配置新用户注册时的试用订阅、时长和时间单位",
|
"trialConfigDescription": "配置新用户注册时的试用订阅、时长和时间单位",
|
||||||
"giftTraffic": "赠送流量",
|
|
||||||
"giftTrafficDescription": "新用户赠送流量(GB)。0 = 时长模式(按上方试用时长配置);大于 0 = 流量模式:赠送 N GB、不限时长、用完为止(此时忽略试用时长配置)",
|
|
||||||
"year": "年"
|
"year": "年"
|
||||||
},
|
},
|
||||||
"site": {
|
"site": {
|
||||||
|
|||||||
@@ -28,7 +28,6 @@
|
|||||||
"createSubscription": "创建订阅",
|
"createSubscription": "创建订阅",
|
||||||
"createSuccess": "创建成功",
|
"createSuccess": "创建成功",
|
||||||
"createUser": "创建用户",
|
"createUser": "创建用户",
|
||||||
"current": "当前",
|
|
||||||
"currentCommission": "当前佣金",
|
"currentCommission": "当前佣金",
|
||||||
"delete": "删除",
|
"delete": "删除",
|
||||||
"deleted": "已删除",
|
"deleted": "已删除",
|
||||||
@@ -119,8 +118,6 @@
|
|||||||
"permanent": "永久",
|
"permanent": "永久",
|
||||||
"prev": "上一页",
|
"prev": "上一页",
|
||||||
"pleaseEnterEmail": "输入邮箱",
|
"pleaseEnterEmail": "输入邮箱",
|
||||||
"hour": "小时",
|
|
||||||
"day": "天",
|
|
||||||
"referer": "推荐人",
|
"referer": "推荐人",
|
||||||
"refererId": "推荐人 ID",
|
"refererId": "推荐人 ID",
|
||||||
"refererIdPlaceholder": "输入推荐人 ID",
|
"refererIdPlaceholder": "输入推荐人 ID",
|
||||||
@@ -153,12 +150,7 @@
|
|||||||
"save": "保存",
|
"save": "保存",
|
||||||
"shortCode": "短码",
|
"shortCode": "短码",
|
||||||
"speedLimit": "速度限制",
|
"speedLimit": "速度限制",
|
||||||
"speedLimitMbps": "目标限速 (Mbps)",
|
|
||||||
"startTime": "开始时间",
|
"startTime": "开始时间",
|
||||||
"statType": "统计周期",
|
|
||||||
"statTypeDay": "天",
|
|
||||||
"statTypeHour": "小时",
|
|
||||||
"statValue": "周期值",
|
|
||||||
"status": "状态",
|
"status": "状态",
|
||||||
"statusActive": "活跃",
|
"statusActive": "活跃",
|
||||||
"statusDeducted": "已扣除",
|
"statusDeducted": "已扣除",
|
||||||
@@ -180,7 +172,6 @@
|
|||||||
"subscriptionInfo": "订阅信息",
|
"subscriptionInfo": "订阅信息",
|
||||||
"subscriptionList": "订阅列表",
|
"subscriptionList": "订阅列表",
|
||||||
"subscriptionLogs": "订阅日志",
|
"subscriptionLogs": "订阅日志",
|
||||||
"subscriptionDefault": "套餐默认",
|
|
||||||
"subscriptionName": "订阅名称",
|
"subscriptionName": "订阅名称",
|
||||||
"subscriptionNotifications": "订阅通知",
|
"subscriptionNotifications": "订阅通知",
|
||||||
"telephone": "电话",
|
"telephone": "电话",
|
||||||
@@ -191,25 +182,17 @@
|
|||||||
"tradeNotifications": "交易通知",
|
"tradeNotifications": "交易通知",
|
||||||
"trafficDetails": "流量详情",
|
"trafficDetails": "流量详情",
|
||||||
"trafficLimit": "流量限制",
|
"trafficLimit": "流量限制",
|
||||||
"trafficLimitRule": "规则",
|
|
||||||
"trafficLimitRules": "流量限速规则",
|
|
||||||
"trafficLimitRuleSummary": "{{value}}{{type}}内达到 {{traffic}} GB 后限速到 {{speed}} Mbps",
|
|
||||||
"trafficStats": "流量统计",
|
"trafficStats": "流量统计",
|
||||||
"trafficUsage": "触发流量 (GB)",
|
"trafficUsage": "流量使用",
|
||||||
"throttled": "已触发限速",
|
|
||||||
"remainingTraffic": "剩余流量",
|
"remainingTraffic": "剩余流量",
|
||||||
"unlimited": "无限制",
|
"unlimited": "无限制",
|
||||||
"unverified": "未验证",
|
"unverified": "未验证",
|
||||||
"update": "更新",
|
"update": "更新",
|
||||||
"updateSuccess": "更新成功",
|
"updateSuccess": "更新成功",
|
||||||
"userOverride": "用户覆盖",
|
|
||||||
"effectiveSpeed": "当前生效",
|
|
||||||
"groupUpdated": "分组更新成功",
|
"groupUpdated": "分组更新成功",
|
||||||
"upload": "上传",
|
"upload": "上传",
|
||||||
"uploadTraffic": "上传流量",
|
"uploadTraffic": "上传流量",
|
||||||
"userStatus": "用户状态",
|
"userStatus": "用户状态",
|
||||||
"subscribed": "有有效订阅",
|
|
||||||
"unsubscribed": "无有效订阅",
|
|
||||||
"userAgent": "用户代理",
|
"userAgent": "用户代理",
|
||||||
"userEmail": "邮箱",
|
"userEmail": "邮箱",
|
||||||
"userEmailPlaceholder": "输入邮箱",
|
"userEmailPlaceholder": "输入邮箱",
|
||||||
|
|||||||
@@ -18,8 +18,10 @@ import { Separator } from "@workspace/ui/components/separator";
|
|||||||
import { Tabs, TabsList, TabsTrigger } from "@workspace/ui/components/tabs";
|
import { Tabs, TabsList, TabsTrigger } from "@workspace/ui/components/tabs";
|
||||||
import Empty from "@workspace/ui/composed/empty";
|
import Empty from "@workspace/ui/composed/empty";
|
||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import { queryServerTotalData } from "@workspace/ui/services/admin/console";
|
import {
|
||||||
import { getWithdrawalList } from "@workspace/ui/services/admin/withdrawal";
|
queryServerTotalData,
|
||||||
|
queryTicketWaitReply,
|
||||||
|
} from "@workspace/ui/services/admin/console";
|
||||||
import { formatBytes } from "@workspace/ui/utils/formatting";
|
import { formatBytes } from "@workspace/ui/utils/formatting";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -39,15 +41,11 @@ import { UserStatisticsCard } from "./user-statistics-card";
|
|||||||
export default function Statistics() {
|
export default function Statistics() {
|
||||||
const { t } = useTranslation("dashboard");
|
const { t } = useTranslation("dashboard");
|
||||||
|
|
||||||
const { data: PendingWithdrawalTotal } = useQuery({
|
const { data: TicketTotal } = useQuery({
|
||||||
queryKey: ["queryPendingWithdrawalTotal"],
|
queryKey: ["queryTicketWaitReply"],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const { data } = await getWithdrawalList({
|
const { data } = await queryTicketWaitReply();
|
||||||
page: 1,
|
return data.data?.count;
|
||||||
size: 1,
|
|
||||||
status: 0,
|
|
||||||
});
|
|
||||||
return data.data?.total || 0;
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const { data: ServerTotal } = useQuery({
|
const { data: ServerTotal } = useQuery({
|
||||||
@@ -244,7 +242,7 @@ export default function Statistics() {
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: t("withdrawalManagement", "提现管理"),
|
title: t("withdrawalManagement", "提现管理"),
|
||||||
value: PendingWithdrawalTotal || 0,
|
value: TicketTotal || 0,
|
||||||
subtitle: t("pending", "Pending"),
|
subtitle: t("pending", "Pending"),
|
||||||
icon: "uil:clipboard-notes",
|
icon: "uil:clipboard-notes",
|
||||||
href: "/dashboard/withdrawal",
|
href: "/dashboard/withdrawal",
|
||||||
|
|||||||
@@ -50,8 +50,7 @@ export type ProtocolName =
|
|||||||
| "naive"
|
| "naive"
|
||||||
| "http"
|
| "http"
|
||||||
| "socks"
|
| "socks"
|
||||||
| "mieru"
|
| "mieru";
|
||||||
| "simnet";
|
|
||||||
|
|
||||||
const buildSchema = (t: TFunction) =>
|
const buildSchema = (t: TFunction) =>
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -93,8 +93,8 @@ function toFormValues(rule?: API.PromoRule): RuleFormValues {
|
|||||||
window_hours: rule?.params?.window_hours,
|
window_hours: rule?.params?.window_hours,
|
||||||
priority: rule?.priority ?? 0,
|
priority: rule?.priority ?? 0,
|
||||||
enabled: rule?.enabled ?? false,
|
enabled: rule?.enabled ?? false,
|
||||||
start_time: rule?.start_time ? rule.start_time * 1000 : undefined,
|
start_time: rule?.start_time,
|
||||||
end_time: rule?.end_time ? rule.end_time * 1000 : undefined,
|
end_time: rule?.end_time,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -112,8 +112,8 @@ function toRequest(values: RuleFormValues): API.CreatePromoRuleRequest {
|
|||||||
params,
|
params,
|
||||||
priority: values.priority,
|
priority: values.priority,
|
||||||
enabled: values.enabled,
|
enabled: values.enabled,
|
||||||
start_time: values.start_time ? Math.floor(values.start_time / 1000) : undefined,
|
start_time: values.start_time || undefined,
|
||||||
end_time: values.end_time ? Math.floor(values.end_time / 1000) : undefined,
|
end_time: values.end_time || undefined,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ export const protocols = [
|
|||||||
"naive",
|
"naive",
|
||||||
"http",
|
"http",
|
||||||
"mieru",
|
"mieru",
|
||||||
"simnet",
|
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
// Global label map for display; fallback to raw value if missing
|
// Global label map for display; fallback to raw value if missing
|
||||||
@@ -70,7 +69,6 @@ export const SECURITY = {
|
|||||||
anytls: ["none", "tls", "reality"] as const,
|
anytls: ["none", "tls", "reality"] as const,
|
||||||
naive: ["none", "tls"] as const,
|
naive: ["none", "tls"] as const,
|
||||||
http: ["none", "tls"] as const,
|
http: ["none", "tls"] as const,
|
||||||
simnet: ["none", "tls"] as const,
|
|
||||||
} as const;
|
} as const;
|
||||||
|
|
||||||
export const FLOWS = {
|
export const FLOWS = {
|
||||||
@@ -101,12 +99,6 @@ export const FINGERPRINTS = [
|
|||||||
|
|
||||||
export const CERT_MODES = ["none", "http", "dns", "self"] as const;
|
export const CERT_MODES = ["none", "http", "dns", "self"] as const;
|
||||||
|
|
||||||
// Simnet enums (first version keeps carrier/af modes locked to supported values).
|
|
||||||
export const SIMNET_CARRIERS = ["h2", "ws", "raw"] as const;
|
|
||||||
export const SIMNET_AF_PATH_MODES = ["api", "random"] as const;
|
|
||||||
export const SIMNET_AF_MAGIC_MODES = ["derived", "random"] as const;
|
|
||||||
export const SIMNET_FALLBACK_SCHEMES = ["https", "http"] as const;
|
|
||||||
|
|
||||||
export const multiplexLevels = ["none", "low", "middle", "high"] as const;
|
export const multiplexLevels = ["none", "low", "middle", "high"] as const;
|
||||||
|
|
||||||
export function getLabel(value: string): string {
|
export function getLabel(value: string): string {
|
||||||
|
|||||||
@@ -171,66 +171,6 @@ export function getProtocolDefaultConfig(proto: ProtocolType) {
|
|||||||
multiplex: "none",
|
multiplex: "none",
|
||||||
transport: "tcp",
|
transport: "tcp",
|
||||||
} as any;
|
} as any;
|
||||||
case "simnet":
|
|
||||||
return {
|
|
||||||
type: "simnet",
|
|
||||||
enable: false,
|
|
||||||
port: 443,
|
|
||||||
simnet_psk: null,
|
|
||||||
simnet_key_id: null,
|
|
||||||
simnet_ticket_id: null,
|
|
||||||
simnet_path: null,
|
|
||||||
simnet_carrier: "h2",
|
|
||||||
security: "tls",
|
|
||||||
sni: null,
|
|
||||||
allow_insecure: false,
|
|
||||||
cert_mode: "none",
|
|
||||||
cert_dns_provider: null,
|
|
||||||
cert_dns_env: null,
|
|
||||||
simnet_af_enabled: false,
|
|
||||||
simnet_af_path_mode: "api",
|
|
||||||
simnet_af_path_prefix: null,
|
|
||||||
simnet_af_path_suffix: null,
|
|
||||||
simnet_af_magic_mode: "derived",
|
|
||||||
simnet_af_response_jitter_ms: 50,
|
|
||||||
simnet_af_handshake_polymorphism: false,
|
|
||||||
simnet_af_settings_jitter: false,
|
|
||||||
simnet_af_fake_header_injection: false,
|
|
||||||
simnet_reverse_enabled: false,
|
|
||||||
simnet_reverse_listen_addr: "127.0.0.1",
|
|
||||||
simnet_reverse_listen_port: null,
|
|
||||||
simnet_reverse_target_host: "127.0.0.1",
|
|
||||||
simnet_reverse_target_port: null,
|
|
||||||
simnet_fallback_enabled: false,
|
|
||||||
simnet_fallback_target_scheme: "https",
|
|
||||||
simnet_fallback_target_host: null,
|
|
||||||
simnet_fallback_target_port: null,
|
|
||||||
simnet_fallback_host_header: null,
|
|
||||||
simnet_fallback_tls_sni: null,
|
|
||||||
simnet_inbound_max_streams_per_session: 128,
|
|
||||||
simnet_inbound_max_udp_streams_per_session: 64,
|
|
||||||
simnet_inbound_max_handler_tasks_per_session: 128,
|
|
||||||
simnet_stream_event_channel_capacity: 256,
|
|
||||||
simnet_stream_data_channel_capacity: 128,
|
|
||||||
simnet_target_dial_timeout_ms: 12_000,
|
|
||||||
simnet_target_max_concurrent_dials: 256,
|
|
||||||
simnet_egress_block_loopback: false,
|
|
||||||
simnet_egress_block_private: false,
|
|
||||||
simnet_egress_block_link_local: false,
|
|
||||||
simnet_egress_block_metadata: false,
|
|
||||||
simnet_send_window: 4_194_304,
|
|
||||||
simnet_recv_window: 4_194_304,
|
|
||||||
simnet_max_concurrent_streams: 100,
|
|
||||||
simnet_initial_window_size: 65_535,
|
|
||||||
simnet_max_frame_size: 16_384,
|
|
||||||
simnet_client_max_concurrent_streams: 32,
|
|
||||||
simnet_client_max_streams_per_session: 512,
|
|
||||||
simnet_client_session_idle_timeout_secs: 90,
|
|
||||||
simnet_client_max_udp_sessions: 64,
|
|
||||||
up_mbps: null,
|
|
||||||
down_mbps: null,
|
|
||||||
ratio: 1,
|
|
||||||
} as any;
|
|
||||||
case "anytls":
|
case "anytls":
|
||||||
return {
|
return {
|
||||||
type: "anytls",
|
type: "anytls",
|
||||||
|
|||||||
@@ -7,10 +7,6 @@ import {
|
|||||||
FLOWS,
|
FLOWS,
|
||||||
multiplexLevels,
|
multiplexLevels,
|
||||||
SECURITY,
|
SECURITY,
|
||||||
SIMNET_AF_MAGIC_MODES,
|
|
||||||
SIMNET_AF_PATH_MODES,
|
|
||||||
SIMNET_CARRIERS,
|
|
||||||
SIMNET_FALLBACK_SCHEMES,
|
|
||||||
SS_CIPHERS,
|
SS_CIPHERS,
|
||||||
TRANSPORTS,
|
TRANSPORTS,
|
||||||
TUIC_CONGESTION,
|
TUIC_CONGESTION,
|
||||||
@@ -22,7 +18,6 @@ const nullableString = z.string().nullish();
|
|||||||
const nullableBool = z.boolean().nullish();
|
const nullableBool = z.boolean().nullish();
|
||||||
const nullablePort = z.number().int().min(0).max(65_535).nullish();
|
const nullablePort = z.number().int().min(0).max(65_535).nullish();
|
||||||
const nullableRatio = z.number().min(0).nullish();
|
const nullableRatio = z.number().min(0).nullish();
|
||||||
const nullableNonNegativeInt = z.number().int().min(0).nullish();
|
|
||||||
|
|
||||||
const ss = z.object({
|
const ss = z.object({
|
||||||
ratio: nullableRatio,
|
ratio: nullableRatio,
|
||||||
@@ -212,66 +207,6 @@ const mieru = z.object({
|
|||||||
transport: z.enum(TRANSPORTS.mieru).nullish(),
|
transport: z.enum(TRANSPORTS.mieru).nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
const simnet = z.object({
|
|
||||||
ratio: nullableRatio,
|
|
||||||
type: z.literal("simnet"),
|
|
||||||
enable: nullableBool,
|
|
||||||
port: nullablePort,
|
|
||||||
simnet_psk: nullableString,
|
|
||||||
simnet_key_id: z.number().int().nullish(),
|
|
||||||
simnet_ticket_id: nullableString,
|
|
||||||
simnet_path: nullableString,
|
|
||||||
simnet_carrier: z.enum(SIMNET_CARRIERS).nullish(),
|
|
||||||
security: z.enum(["none", "tls"] as const).nullish(),
|
|
||||||
sni: nullableString,
|
|
||||||
allow_insecure: nullableBool,
|
|
||||||
cert_mode: z.enum(CERT_MODES).nullish(),
|
|
||||||
cert_dns_provider: nullableString,
|
|
||||||
cert_dns_env: nullableString,
|
|
||||||
simnet_af_enabled: nullableBool,
|
|
||||||
simnet_af_path_mode: z.enum(SIMNET_AF_PATH_MODES).nullish(),
|
|
||||||
simnet_af_path_prefix: nullableString,
|
|
||||||
simnet_af_path_suffix: nullableString,
|
|
||||||
simnet_af_magic_mode: z.enum(SIMNET_AF_MAGIC_MODES).nullish(),
|
|
||||||
simnet_af_response_jitter_ms: z.number().int().min(0).nullish(),
|
|
||||||
simnet_af_handshake_polymorphism: nullableBool,
|
|
||||||
simnet_af_settings_jitter: nullableBool,
|
|
||||||
simnet_af_fake_header_injection: nullableBool,
|
|
||||||
simnet_reverse_enabled: nullableBool,
|
|
||||||
simnet_reverse_listen_addr: nullableString,
|
|
||||||
simnet_reverse_listen_port: nullablePort,
|
|
||||||
simnet_reverse_target_host: nullableString,
|
|
||||||
simnet_reverse_target_port: nullablePort,
|
|
||||||
simnet_fallback_enabled: nullableBool,
|
|
||||||
simnet_fallback_target_scheme: z.enum(SIMNET_FALLBACK_SCHEMES).nullish(),
|
|
||||||
simnet_fallback_target_host: nullableString,
|
|
||||||
simnet_fallback_target_port: nullablePort,
|
|
||||||
simnet_fallback_host_header: nullableString,
|
|
||||||
simnet_fallback_tls_sni: nullableString,
|
|
||||||
simnet_inbound_max_streams_per_session: nullableNonNegativeInt,
|
|
||||||
simnet_inbound_max_udp_streams_per_session: nullableNonNegativeInt,
|
|
||||||
simnet_inbound_max_handler_tasks_per_session: nullableNonNegativeInt,
|
|
||||||
simnet_stream_event_channel_capacity: nullableNonNegativeInt,
|
|
||||||
simnet_stream_data_channel_capacity: nullableNonNegativeInt,
|
|
||||||
simnet_target_dial_timeout_ms: nullableNonNegativeInt,
|
|
||||||
simnet_target_max_concurrent_dials: nullableNonNegativeInt,
|
|
||||||
simnet_egress_block_loopback: nullableBool,
|
|
||||||
simnet_egress_block_private: nullableBool,
|
|
||||||
simnet_egress_block_link_local: nullableBool,
|
|
||||||
simnet_egress_block_metadata: nullableBool,
|
|
||||||
simnet_send_window: nullableNonNegativeInt,
|
|
||||||
simnet_recv_window: nullableNonNegativeInt,
|
|
||||||
simnet_max_concurrent_streams: nullableNonNegativeInt,
|
|
||||||
simnet_initial_window_size: nullableNonNegativeInt,
|
|
||||||
simnet_max_frame_size: nullableNonNegativeInt,
|
|
||||||
simnet_client_max_concurrent_streams: nullableNonNegativeInt,
|
|
||||||
simnet_client_max_streams_per_session: nullableNonNegativeInt,
|
|
||||||
simnet_client_session_idle_timeout_secs: nullableNonNegativeInt,
|
|
||||||
simnet_client_max_udp_sessions: nullableNonNegativeInt,
|
|
||||||
up_mbps: z.number().nullish(),
|
|
||||||
down_mbps: z.number().nullish(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const protocolApiScheme = z.discriminatedUnion("type", [
|
export const protocolApiScheme = z.discriminatedUnion("type", [
|
||||||
ss,
|
ss,
|
||||||
vmess,
|
vmess,
|
||||||
@@ -284,7 +219,6 @@ export const protocolApiScheme = z.discriminatedUnion("type", [
|
|||||||
naive,
|
naive,
|
||||||
http,
|
http,
|
||||||
mieru,
|
mieru,
|
||||||
simnet,
|
|
||||||
]);
|
]);
|
||||||
|
|
||||||
export const formSchema = z.object({
|
export const formSchema = z.object({
|
||||||
|
|||||||
@@ -32,8 +32,7 @@ export type FieldConfig = {
|
|||||||
| "security"
|
| "security"
|
||||||
| "reality"
|
| "reality"
|
||||||
| "obfs"
|
| "obfs"
|
||||||
| "encryption"
|
| "encryption";
|
||||||
| "simnet";
|
|
||||||
gridSpan?: 1 | 2;
|
gridSpan?: 1 | 2;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -15,10 +15,6 @@ import {
|
|||||||
FLOWS,
|
FLOWS,
|
||||||
multiplexLevels,
|
multiplexLevels,
|
||||||
SECURITY,
|
SECURITY,
|
||||||
SIMNET_AF_MAGIC_MODES,
|
|
||||||
SIMNET_AF_PATH_MODES,
|
|
||||||
SIMNET_CARRIERS,
|
|
||||||
SIMNET_FALLBACK_SCHEMES,
|
|
||||||
SS_CIPHERS,
|
SS_CIPHERS,
|
||||||
TRANSPORTS,
|
TRANSPORTS,
|
||||||
TUIC_CONGESTION,
|
TUIC_CONGESTION,
|
||||||
@@ -1225,499 +1221,6 @@ export function useProtocolFields() {
|
|||||||
condition: (p) => p.security === "reality",
|
condition: (p) => p.security === "reality",
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
simnet: [
|
|
||||||
{
|
|
||||||
name: "ratio",
|
|
||||||
type: "number",
|
|
||||||
label: t("traffic_ratio", "Ratio"),
|
|
||||||
min: 0,
|
|
||||||
step: 0.01,
|
|
||||||
defaultValue: 1,
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "port",
|
|
||||||
type: "number",
|
|
||||||
label: t("port", "Port"),
|
|
||||||
min: 1,
|
|
||||||
max: 65_535,
|
|
||||||
placeholder: "443",
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_psk",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_psk", "SimNet PSK"),
|
|
||||||
placeholder: t(
|
|
||||||
"simnet_psk_placeholder",
|
|
||||||
"Server pre-shared key (shared across all users)"
|
|
||||||
),
|
|
||||||
generate: {
|
|
||||||
function: () => generatePassword(32),
|
|
||||||
},
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_ticket_id",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_ticket_id", "SimNet Ticket ID"),
|
|
||||||
placeholder: t("simnet_ticket_id_placeholder", "Optional ticket id"),
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_path",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_path", "SimNet Path"),
|
|
||||||
placeholder: t("simnet_path_placeholder", "e.g. /simnet"),
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_carrier",
|
|
||||||
type: "select",
|
|
||||||
label: t("simnet_carrier", "Carrier"),
|
|
||||||
options: SIMNET_CARRIERS,
|
|
||||||
defaultValue: "h2",
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "up_mbps",
|
|
||||||
type: "number",
|
|
||||||
label: t("up_mbps", "Upload Bandwidth"),
|
|
||||||
min: 1,
|
|
||||||
placeholder: t(
|
|
||||||
"bandwidth_placeholder",
|
|
||||||
"Enter bandwidth, leave empty for BBR"
|
|
||||||
),
|
|
||||||
suffix: "Mbps",
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "down_mbps",
|
|
||||||
type: "number",
|
|
||||||
label: t("down_mbps", "Download Bandwidth"),
|
|
||||||
min: 1,
|
|
||||||
placeholder: t(
|
|
||||||
"bandwidth_placeholder",
|
|
||||||
"Enter bandwidth, leave empty for BBR"
|
|
||||||
),
|
|
||||||
suffix: "Mbps",
|
|
||||||
group: "basic",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "security",
|
|
||||||
type: "select",
|
|
||||||
label: t("security", "Security"),
|
|
||||||
options: ["none", "tls"],
|
|
||||||
defaultValue: "tls",
|
|
||||||
group: "security",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "sni",
|
|
||||||
type: "input",
|
|
||||||
label: t("security_sni", "SNI"),
|
|
||||||
placeholder: t("sni_placeholder", "e.g. edge.example.com"),
|
|
||||||
group: "security",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "allow_insecure",
|
|
||||||
type: "switch",
|
|
||||||
label: t("security_allow_insecure", "Allow Insecure"),
|
|
||||||
group: "security",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cert_mode",
|
|
||||||
type: "select",
|
|
||||||
label: t("cert_mode", "Certificate Mode"),
|
|
||||||
options: CERT_MODES,
|
|
||||||
defaultValue: "none",
|
|
||||||
group: "security",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cert_dns_provider",
|
|
||||||
type: "input",
|
|
||||||
label: t("cert_dns_provider", "DNS Provider"),
|
|
||||||
placeholder: "e.g. cloudflare, aliyun",
|
|
||||||
group: "security",
|
|
||||||
condition: (p) => p.cert_mode === "dns",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "cert_dns_env",
|
|
||||||
type: "textarea",
|
|
||||||
label: t("cert_dns_env", "DNS Environment Variables"),
|
|
||||||
placeholder:
|
|
||||||
"CF_DNS_API_TOKEN=1234567890abcdefghijklmnopqrstuvwxyz\nALI_ACCESS_KEY_ID=your_access_key_id\nALI_ACCESS_KEY_SECRET=your_access_key_secret",
|
|
||||||
group: "security",
|
|
||||||
condition: (p) => p.cert_mode === "dns",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_enabled",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_af_enabled", "Enable Anti-Fingerprint"),
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_path_mode",
|
|
||||||
type: "select",
|
|
||||||
label: t("simnet_af_path_mode", "AF Path Mode"),
|
|
||||||
options: SIMNET_AF_PATH_MODES,
|
|
||||||
defaultValue: "api",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_path_prefix",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_af_path_prefix", "AF Path Prefix"),
|
|
||||||
placeholder: t(
|
|
||||||
"simnet_af_path_prefix_placeholder",
|
|
||||||
"Optional path prefix"
|
|
||||||
),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_path_suffix",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_af_path_suffix", "AF Path Suffix"),
|
|
||||||
placeholder: t(
|
|
||||||
"simnet_af_path_suffix_placeholder",
|
|
||||||
"Optional path suffix"
|
|
||||||
),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_magic_mode",
|
|
||||||
type: "select",
|
|
||||||
label: t("simnet_af_magic_mode", "AF Magic Mode"),
|
|
||||||
options: SIMNET_AF_MAGIC_MODES,
|
|
||||||
defaultValue: "derived",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_response_jitter_ms",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_af_response_jitter_ms", "AF Response Jitter"),
|
|
||||||
min: 0,
|
|
||||||
suffix: "ms",
|
|
||||||
defaultValue: 50,
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_handshake_polymorphism",
|
|
||||||
type: "switch",
|
|
||||||
label: t(
|
|
||||||
"simnet_af_handshake_polymorphism",
|
|
||||||
"Handshake Polymorphism"
|
|
||||||
),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_settings_jitter",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_af_settings_jitter", "Settings Jitter"),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_af_fake_header_injection",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_af_fake_header_injection", "Fake Header Injection"),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_af_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_reverse_enabled",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_reverse_enabled", "Enable Local Reverse Proxy"),
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_reverse_listen_addr",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_reverse_listen_addr", "Reverse Listen Address"),
|
|
||||||
placeholder: "127.0.0.1",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_reverse_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_reverse_listen_port",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_reverse_listen_port", "Reverse Listen Port"),
|
|
||||||
min: 1,
|
|
||||||
max: 65_535,
|
|
||||||
placeholder: "18080",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_reverse_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_reverse_target_host",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_reverse_target_host", "Reverse Target Host"),
|
|
||||||
placeholder: "127.0.0.1",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_reverse_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_reverse_target_port",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_reverse_target_port", "Reverse Target Port"),
|
|
||||||
min: 1,
|
|
||||||
max: 65_535,
|
|
||||||
placeholder: "8080",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_reverse_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_fallback_enabled",
|
|
||||||
type: "switch",
|
|
||||||
label: t(
|
|
||||||
"simnet_fallback_enabled",
|
|
||||||
"Enable HTTPS Fallback Reverse Proxy"
|
|
||||||
),
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_fallback_target_scheme",
|
|
||||||
type: "select",
|
|
||||||
label: t("simnet_fallback_target_scheme", "Fallback Target Scheme"),
|
|
||||||
options: SIMNET_FALLBACK_SCHEMES,
|
|
||||||
defaultValue: "https",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_fallback_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_fallback_target_host",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_fallback_target_host", "Fallback Target Host"),
|
|
||||||
placeholder: t(
|
|
||||||
"simnet_fallback_target_host_placeholder",
|
|
||||||
"e.g. www.example.com"
|
|
||||||
),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_fallback_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_fallback_target_port",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_fallback_target_port", "Fallback Target Port"),
|
|
||||||
min: 1,
|
|
||||||
max: 65_535,
|
|
||||||
placeholder: "443 / 80",
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_fallback_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_fallback_host_header",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_fallback_host_header", "Fallback Host Header"),
|
|
||||||
placeholder: t(
|
|
||||||
"simnet_fallback_host_header_placeholder",
|
|
||||||
"Optional, defaults to target host"
|
|
||||||
),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_fallback_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_fallback_tls_sni",
|
|
||||||
type: "input",
|
|
||||||
label: t("simnet_fallback_tls_sni", "Fallback TLS SNI"),
|
|
||||||
placeholder: t(
|
|
||||||
"simnet_fallback_tls_sni_placeholder",
|
|
||||||
"Optional, defaults to target host"
|
|
||||||
),
|
|
||||||
group: "simnet",
|
|
||||||
condition: (p) => !!p.simnet_fallback_enabled,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_inbound_max_streams_per_session",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_inbound_max_streams_per_session",
|
|
||||||
"Inbound Streams / Session"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 128,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_inbound_max_udp_streams_per_session",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_inbound_max_udp_streams_per_session",
|
|
||||||
"Inbound UDP Streams / Session"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 64,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_inbound_max_handler_tasks_per_session",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_inbound_max_handler_tasks_per_session",
|
|
||||||
"Handler Tasks / Session"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 128,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_stream_event_channel_capacity",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_stream_event_channel_capacity",
|
|
||||||
"Stream Event Channel"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 256,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_stream_data_channel_capacity",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_stream_data_channel_capacity",
|
|
||||||
"Stream Data Channel"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 128,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_target_dial_timeout_ms",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_target_dial_timeout_ms", "Target Dial Timeout"),
|
|
||||||
min: 1,
|
|
||||||
suffix: "ms",
|
|
||||||
defaultValue: 12_000,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_target_max_concurrent_dials",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_target_max_concurrent_dials",
|
|
||||||
"Concurrent Target Dials"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 256,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_send_window",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_send_window", "Send Window"),
|
|
||||||
min: 1,
|
|
||||||
suffix: "bytes",
|
|
||||||
defaultValue: 4_194_304,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_recv_window",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_recv_window", "Receive Window"),
|
|
||||||
min: 1,
|
|
||||||
suffix: "bytes",
|
|
||||||
defaultValue: 4_194_304,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_max_concurrent_streams",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_max_concurrent_streams", "H2 Concurrent Streams"),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 100,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_initial_window_size",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_initial_window_size", "H2 Initial Window"),
|
|
||||||
min: 1,
|
|
||||||
suffix: "bytes",
|
|
||||||
defaultValue: 65_535,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_max_frame_size",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_max_frame_size", "H2 Max Frame Size"),
|
|
||||||
min: 1,
|
|
||||||
suffix: "bytes",
|
|
||||||
defaultValue: 16_384,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_client_max_concurrent_streams",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_client_max_concurrent_streams",
|
|
||||||
"Client Concurrent Streams"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 32,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_client_max_streams_per_session",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_client_max_streams_per_session",
|
|
||||||
"Client Streams / Session"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 512,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_client_session_idle_timeout_secs",
|
|
||||||
type: "number",
|
|
||||||
label: t(
|
|
||||||
"simnet_client_session_idle_timeout_secs",
|
|
||||||
"Client Session Idle Timeout"
|
|
||||||
),
|
|
||||||
min: 1,
|
|
||||||
suffix: "s",
|
|
||||||
defaultValue: 90,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_client_max_udp_sessions",
|
|
||||||
type: "number",
|
|
||||||
label: t("simnet_client_max_udp_sessions", "Client UDP Sessions"),
|
|
||||||
min: 1,
|
|
||||||
defaultValue: 64,
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_egress_block_loopback",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_egress_block_loopback", "Block Loopback Egress"),
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_egress_block_private",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_egress_block_private", "Block Private Egress"),
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_egress_block_link_local",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_egress_block_link_local", "Block Link-Local Egress"),
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "simnet_egress_block_metadata",
|
|
||||||
type: "switch",
|
|
||||||
label: t("simnet_egress_block_metadata", "Block Metadata Egress"),
|
|
||||||
group: "simnet",
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}),
|
}),
|
||||||
[t]
|
[t]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -696,15 +696,6 @@ export default function ServerForm(props: {
|
|||||||
i,
|
i,
|
||||||
current
|
current
|
||||||
)}
|
)}
|
||||||
{renderGroupCard(
|
|
||||||
t("simnet", "SimNet"),
|
|
||||||
fields,
|
|
||||||
"simnet",
|
|
||||||
control,
|
|
||||||
form,
|
|
||||||
i,
|
|
||||||
current
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
</AccordionContent>
|
</AccordionContent>
|
||||||
</AccordionItem>
|
</AccordionItem>
|
||||||
|
|||||||
@@ -41,7 +41,6 @@ const registerSchema = z.object({
|
|||||||
trial_subscribe: z.number().optional(),
|
trial_subscribe: z.number().optional(),
|
||||||
trial_time: z.number().optional(),
|
trial_time: z.number().optional(),
|
||||||
trial_time_unit: z.string().optional(),
|
trial_time_unit: z.string().optional(),
|
||||||
gift_traffic: z.number().optional(),
|
|
||||||
enable_trial_email_whitelist: z.boolean().optional(),
|
enable_trial_email_whitelist: z.boolean().optional(),
|
||||||
trial_email_domain_whitelist: z.string().optional(),
|
trial_email_domain_whitelist: z.string().optional(),
|
||||||
enable_ip_register_limit: z.boolean().optional(),
|
enable_ip_register_limit: z.boolean().optional(),
|
||||||
@@ -76,7 +75,6 @@ export default function RegisterConfig() {
|
|||||||
trial_subscribe: undefined,
|
trial_subscribe: undefined,
|
||||||
trial_time: 0,
|
trial_time: 0,
|
||||||
trial_time_unit: "day",
|
trial_time_unit: "day",
|
||||||
gift_traffic: 0,
|
|
||||||
enable_trial_email_whitelist: false,
|
enable_trial_email_whitelist: false,
|
||||||
trial_email_domain_whitelist: "",
|
trial_email_domain_whitelist: "",
|
||||||
enable_ip_register_limit: false,
|
enable_ip_register_limit: false,
|
||||||
@@ -451,40 +449,6 @@ export default function RegisterConfig() {
|
|||||||
)}
|
)}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormField
|
|
||||||
control={form.control}
|
|
||||||
name="gift_traffic"
|
|
||||||
render={({ field }) => (
|
|
||||||
<FormItem>
|
|
||||||
<FormLabel>
|
|
||||||
{t("register.giftTraffic", "Gift Traffic")}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
|
||||||
<EnhancedInput
|
|
||||||
min={0}
|
|
||||||
onValueBlur={(value) =>
|
|
||||||
field.onChange(Number(value))
|
|
||||||
}
|
|
||||||
placeholder={t(
|
|
||||||
"register.inputPlaceholder",
|
|
||||||
"Please enter"
|
|
||||||
)}
|
|
||||||
suffix="GB"
|
|
||||||
type="number"
|
|
||||||
value={field.value}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
|
||||||
<FormDescription>
|
|
||||||
{t(
|
|
||||||
"register.giftTrafficDescription",
|
|
||||||
"Gift traffic for new users (GB). 0 = duration mode (uses the trial duration above); >0 = quota mode: N GB with no time limit, trial duration is ignored"
|
|
||||||
)}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
control={form.control}
|
control={form.control}
|
||||||
name="enable_trial_email_whitelist"
|
name="enable_trial_email_whitelist"
|
||||||
|
|||||||
@@ -66,6 +66,7 @@ import { formatDate } from "@/utils/common";
|
|||||||
import FamilyManagement from "./family";
|
import FamilyManagement from "./family";
|
||||||
import { RemarkForm } from "./remark-form";
|
import { RemarkForm } from "./remark-form";
|
||||||
import { buildUserBasicInfoPayload } from "./user-basic-info-payload";
|
import { buildUserBasicInfoPayload } from "./user-basic-info-payload";
|
||||||
|
import { UserDetail } from "./user-detail";
|
||||||
import UserForm from "./user-form";
|
import UserForm from "./user-form";
|
||||||
import { UserIdentifier } from "./user-identifier";
|
import { UserIdentifier } from "./user-identifier";
|
||||||
import { UserInviteStatsSheet } from "./user-invite-stats-sheet";
|
import { UserInviteStatsSheet } from "./user-invite-stats-sheet";
|
||||||
@@ -335,31 +336,23 @@ export default function User() {
|
|||||||
id: "referer_id",
|
id: "referer_id",
|
||||||
accessorKey: "referer_id",
|
accessorKey: "referer_id",
|
||||||
header: t("referer", "Referer"),
|
header: t("referer", "Referer"),
|
||||||
cell: ({ row }) => (
|
cell: ({ row }) => <UserDetail id={row.original.referer_id} />,
|
||||||
<RefererProfileTrigger
|
|
||||||
onUpdated={() => ref.current?.refresh()}
|
|
||||||
userId={row.original.referer_id}
|
|
||||||
/>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
id: "member_status",
|
id: "last_login_time",
|
||||||
accessorKey: "member_status",
|
accessorKey: "last_login_time",
|
||||||
header: t("userStatus", "用户状态"),
|
header: t("userStatus", "用户状态"),
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
// member_status 由后端基于当前有效订阅计算:有有效订阅时为套餐名,否则为空
|
const lastLoginTime = (
|
||||||
const memberStatus = (
|
|
||||||
row.original as API.User & {
|
row.original as API.User & {
|
||||||
member_status?: string;
|
last_login_time?: number;
|
||||||
}
|
}
|
||||||
).member_status;
|
).last_login_time;
|
||||||
const hasActiveSubscription = Boolean(memberStatus);
|
const isOnline = Boolean(lastLoginTime);
|
||||||
const dotClassName = hasActiveSubscription
|
const dotClassName = isOnline ? "bg-green-500" : "bg-gray-400";
|
||||||
? "bg-green-500"
|
const statusLabel = isOnline
|
||||||
: "bg-gray-400";
|
? t("active", "Active")
|
||||||
const tooltipLabel = hasActiveSubscription
|
: t("inactive", "Inactive");
|
||||||
? memberStatus
|
|
||||||
: t("unsubscribed", "No active subscription");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<TooltipProvider>
|
<TooltipProvider>
|
||||||
@@ -370,10 +363,18 @@ export default function User() {
|
|||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
className={`h-2.5 w-2.5 rounded-full ${dotClassName}`}
|
className={`h-2.5 w-2.5 rounded-full ${dotClassName}`}
|
||||||
/>
|
/>
|
||||||
<span className="sr-only">{tooltipLabel}</span>
|
<span className="sr-only">
|
||||||
|
{isOnline
|
||||||
|
? `${statusLabel}: ${formatDate(lastLoginTime)}`
|
||||||
|
: statusLabel}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</TooltipTrigger>
|
</TooltipTrigger>
|
||||||
<TooltipContent>{tooltipLabel}</TooltipContent>
|
<TooltipContent>
|
||||||
|
{isOnline
|
||||||
|
? formatDate(lastLoginTime)
|
||||||
|
: t("neverLoggedIn", "Never logged in")}
|
||||||
|
</TooltipContent>
|
||||||
</Tooltip>
|
</Tooltip>
|
||||||
</TooltipProvider>
|
</TooltipProvider>
|
||||||
);
|
);
|
||||||
@@ -438,11 +439,9 @@ export default function User() {
|
|||||||
function ProfileSheet({
|
function ProfileSheet({
|
||||||
userId,
|
userId,
|
||||||
onUpdated,
|
onUpdated,
|
||||||
trigger,
|
|
||||||
}: {
|
}: {
|
||||||
userId: number;
|
userId: number;
|
||||||
onUpdated?: () => void;
|
onUpdated?: () => void;
|
||||||
trigger?: React.ReactNode;
|
|
||||||
}) {
|
}) {
|
||||||
const { t } = useTranslation("user");
|
const { t } = useTranslation("user");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
@@ -463,7 +462,7 @@ function ProfileSheet({
|
|||||||
return (
|
return (
|
||||||
<Sheet onOpenChange={setOpen} open={open}>
|
<Sheet onOpenChange={setOpen} open={open}>
|
||||||
<SheetTrigger asChild>
|
<SheetTrigger asChild>
|
||||||
{trigger || <Button variant="default">{t("edit", "Edit")}</Button>}
|
<Button variant="default">{t("edit", "Edit")}</Button>
|
||||||
</SheetTrigger>
|
</SheetTrigger>
|
||||||
<SheetContent
|
<SheetContent
|
||||||
className="w-[700px] max-w-full md:max-w-screen-lg"
|
className="w-[700px] max-w-full md:max-w-screen-lg"
|
||||||
@@ -527,42 +526,6 @@ 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 }) {
|
function InviteStatsMenuItem({ userId }: { userId: number }) {
|
||||||
const { t } = useTranslation("user");
|
const { t } = useTranslation("user");
|
||||||
const [open, setOpen] = useState(false);
|
const [open, setOpen] = useState(false);
|
||||||
|
|||||||
@@ -54,10 +54,6 @@ function toUserSubscribePayload(values: SubscriptionFormValues) {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
function resolvePlanSpeed(row: API.UserSubscribe) {
|
|
||||||
return row.plan_speed_limit ?? row.subscribe?.speed_limit ?? 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default function UserSubscription({ userId }: { userId: number }) {
|
export default function UserSubscription({ userId }: { userId: number }) {
|
||||||
const { t } = useTranslation("user");
|
const { t } = useTranslation("user");
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
@@ -316,22 +312,18 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
{
|
{
|
||||||
accessorKey: "speed_limit",
|
accessorKey: "speed_limit",
|
||||||
header: t("speedLimit", "Speed Limit"),
|
header: t("speedLimit", "Speed Limit"),
|
||||||
meta: {
|
|
||||||
className: "min-w-[220px] whitespace-normal",
|
|
||||||
},
|
|
||||||
cell: ({ row }) => {
|
cell: ({ row }) => {
|
||||||
const userSpeed = row.original.speed_limit ?? 0;
|
const userSpeed = row.original.speed_limit ?? 0;
|
||||||
const effectiveSpeed = row.original.effective_speed ?? 0;
|
const effectiveSpeed = row.original.effective_speed ?? 0;
|
||||||
const planSpeed = resolvePlanSpeed(row.original);
|
const planSpeed = row.original.subscribe?.speed_limit ?? 0;
|
||||||
const speed = effectiveSpeed || userSpeed || planSpeed;
|
const speed = effectiveSpeed || userSpeed || planSpeed;
|
||||||
const source = row.original.is_throttled
|
const source =
|
||||||
? t("effectiveSpeed")
|
userSpeed > 0
|
||||||
: userSpeed > 0
|
? t("userOverride", "User Override")
|
||||||
? t("userOverride")
|
: t("subscriptionDefault", "Subscription Default");
|
||||||
: t("subscriptionDefault");
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="flex min-w-[200px] flex-col gap-1 whitespace-normal">
|
<div className="flex flex-col gap-1">
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<Display type="trafficSpeed" value={speed} />
|
<Display type="trafficSpeed" value={speed} />
|
||||||
{row.original.is_throttled && (
|
{row.original.is_throttled && (
|
||||||
@@ -340,18 +332,9 @@ export default function UserSubscription({ userId }: { userId: number }) {
|
|||||||
</Badge>
|
</Badge>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid grid-cols-[auto_1fr] gap-x-2 gap-y-0.5 text-muted-foreground text-xs leading-5">
|
<span className="text-muted-foreground text-xs">
|
||||||
<span>{t("current")}</span>
|
{source}
|
||||||
<span>{source}</span>
|
</span>
|
||||||
<span>{t("userOverride")}</span>
|
|
||||||
<span>
|
|
||||||
<Display type="trafficSpeed" value={userSpeed} />
|
|
||||||
</span>
|
|
||||||
<span>{t("subscriptionDefault")}</span>
|
|
||||||
<span>
|
|
||||||
<Display type="trafficSpeed" value={planSpeed} />
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -87,12 +87,11 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
|
|||||||
|
|
||||||
if (detail.status !== 1) return null;
|
if (detail.status !== 1) return null;
|
||||||
|
|
||||||
const baseSpeed =
|
const baseSpeed = detail.subscribe?.speed_limit ?? 0;
|
||||||
detail.plan_speed_limit ?? detail.subscribe?.speed_limit ?? 0;
|
|
||||||
const userSpeed = detail.speed_limit ?? 0;
|
const userSpeed = detail.speed_limit ?? 0;
|
||||||
const effectiveSpeed = detail.effective_speed ?? 0;
|
const effectiveSpeed = detail.effective_speed ?? 0;
|
||||||
const isThrottled = detail.is_throttled;
|
const isThrottled = detail.is_throttled;
|
||||||
const rules = detail.user_traffic_limit || detail.traffic_limit || [];
|
const rules = detail.traffic_limit || [];
|
||||||
const hasLimitConfig =
|
const hasLimitConfig =
|
||||||
baseSpeed > 0 || userSpeed > 0 || effectiveSpeed > 0 || rules.length > 0;
|
baseSpeed > 0 || userSpeed > 0 || effectiveSpeed > 0 || rules.length > 0;
|
||||||
|
|
||||||
@@ -121,13 +120,13 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
|
|||||||
<div className="flex flex-wrap items-center gap-2 font-medium">
|
<div className="flex flex-wrap items-center gap-2 font-medium">
|
||||||
{isThrottled ? (
|
{isThrottled ? (
|
||||||
<>
|
<>
|
||||||
<span>{t("throttled")}</span>
|
<span>{t("throttled", "Speed Throttled")}</span>
|
||||||
<Badge className="text-xs" variant="destructive">
|
<Badge className="text-xs" variant="destructive">
|
||||||
{effectiveSpeed} Mbps
|
{effectiveSpeed} Mbps
|
||||||
</Badge>
|
</Badge>
|
||||||
{baseSpeed > 0 && (
|
{baseSpeed > 0 && (
|
||||||
<span className="text-muted-foreground text-xs line-through">
|
<span className="text-muted-foreground text-xs line-through">
|
||||||
{t("subscriptionDefault")}:{" "}
|
{t("subscriptionDefault", "Subscription Default")}:{" "}
|
||||||
{baseSpeed} Mbps
|
{baseSpeed} Mbps
|
||||||
</span>
|
</span>
|
||||||
)}
|
)}
|
||||||
@@ -138,22 +137,24 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
|
|||||||
{t("speedLimit", "Speed Limit")}
|
{t("speedLimit", "Speed Limit")}
|
||||||
</span>
|
</span>
|
||||||
<Badge className="text-xs" variant="secondary">
|
<Badge className="text-xs" variant="secondary">
|
||||||
{effectiveSpeed > 0 ? `${effectiveSpeed} Mbps` : t("unlimited")}
|
{effectiveSpeed > 0
|
||||||
|
? `${effectiveSpeed} Mbps`
|
||||||
|
: t("unlimited", "Unlimited")}
|
||||||
</Badge>
|
</Badge>
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<div className="grid gap-2 sm:grid-cols-3">
|
<div className="grid gap-2 sm:grid-cols-3">
|
||||||
<SpeedMetric
|
<SpeedMetric
|
||||||
label={t("subscriptionDefault")}
|
label={t("subscriptionDefault", "Subscription Default")}
|
||||||
value={baseSpeed}
|
value={baseSpeed}
|
||||||
/>
|
/>
|
||||||
<SpeedMetric
|
<SpeedMetric
|
||||||
label={t("userOverride")}
|
label={t("userOverride", "User Override")}
|
||||||
value={userSpeed}
|
value={userSpeed}
|
||||||
/>
|
/>
|
||||||
<SpeedMetric
|
<SpeedMetric
|
||||||
label={t("effectiveSpeed")}
|
label={t("effectiveSpeed", "Effective Speed")}
|
||||||
value={effectiveSpeed}
|
value={effectiveSpeed}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
@@ -171,7 +172,7 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
|
|||||||
{rules.length > 0 && (
|
{rules.length > 0 && (
|
||||||
<div className="space-y-2">
|
<div className="space-y-2">
|
||||||
<p className="font-medium text-foreground text-xs">
|
<p className="font-medium text-foreground text-xs">
|
||||||
{t("trafficLimitRules")}
|
{t("trafficLimitRules", "Traffic Limit Rules")}
|
||||||
</p>
|
</p>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
{rules.map((rule, index) => (
|
{rules.map((rule, index) => (
|
||||||
@@ -186,8 +187,8 @@ function SpeedLimitCard({ subscriptionId }: { subscriptionId: number }) {
|
|||||||
value: rule.stat_value,
|
value: rule.stat_value,
|
||||||
type:
|
type:
|
||||||
rule.stat_type === "hour"
|
rule.stat_type === "hour"
|
||||||
? t("hour")
|
? t("hour", "Hour")
|
||||||
: t("day"),
|
: t("day", "Day"),
|
||||||
speed: rule.speed_limit,
|
speed: rule.speed_limit,
|
||||||
})}
|
})}
|
||||||
</div>
|
</div>
|
||||||
@@ -207,7 +208,7 @@ function SpeedMetric({ label, value }: { label: string; value: number }) {
|
|||||||
<div className="rounded-md border bg-background/60 p-2">
|
<div className="rounded-md border bg-background/60 p-2">
|
||||||
<div className="text-muted-foreground text-xs">{label}</div>
|
<div className="text-muted-foreground text-xs">{label}</div>
|
||||||
<div className="font-medium text-foreground">
|
<div className="font-medium text-foreground">
|
||||||
{value > 0 ? `${value} Mbps` : t("unlimited")}
|
{value > 0 ? `${value} Mbps` : t("unlimited", "Unlimited")}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,11 +1,5 @@
|
|||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { Button } from "@workspace/ui/components/button";
|
import { Button } from "@workspace/ui/components/button";
|
||||||
import {
|
|
||||||
Card,
|
|
||||||
CardContent,
|
|
||||||
CardHeader,
|
|
||||||
CardTitle,
|
|
||||||
} from "@workspace/ui/components/card";
|
|
||||||
import {
|
import {
|
||||||
Form,
|
Form,
|
||||||
FormControl,
|
FormControl,
|
||||||
@@ -26,11 +20,10 @@ import {
|
|||||||
} from "@workspace/ui/components/sheet";
|
} from "@workspace/ui/components/sheet";
|
||||||
import { Combobox } from "@workspace/ui/composed/combobox";
|
import { Combobox } from "@workspace/ui/composed/combobox";
|
||||||
import { DatePicker } from "@workspace/ui/composed/date-picker";
|
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 { EnhancedInput } from "@workspace/ui/composed/enhanced-input";
|
||||||
import { Icon } from "@workspace/ui/composed/icon";
|
import { Icon } from "@workspace/ui/composed/icon";
|
||||||
import { cn } from "@workspace/ui/lib/utils";
|
|
||||||
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
|
import { unitConversion } from "@workspace/ui/utils/unit-conversions";
|
||||||
import { CircleMinusIcon, CirclePlusIcon } from "lucide-react";
|
|
||||||
import { type KeyboardEvent, type ReactNode, useState } from "react";
|
import { type KeyboardEvent, type ReactNode, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { useTranslation } from "react-i18next";
|
import { useTranslation } from "react-i18next";
|
||||||
@@ -47,12 +40,6 @@ interface Props {
|
|||||||
|
|
||||||
type SubscriptionTrafficLimitRule = API.UserSubscribeTrafficLimitRule;
|
type SubscriptionTrafficLimitRule = API.UserSubscribeTrafficLimitRule;
|
||||||
|
|
||||||
function getUserTrafficLimitRules(
|
|
||||||
data?: Pick<API.UserSubscribe, "traffic_limit" | "user_traffic_limit">
|
|
||||||
) {
|
|
||||||
return data?.user_traffic_limit || data?.traffic_limit || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
const defaultLimitRule: SubscriptionTrafficLimitRule = {
|
const defaultLimitRule: SubscriptionTrafficLimitRule = {
|
||||||
stat_type: "day",
|
stat_type: "day",
|
||||||
stat_value: 1,
|
stat_value: 1,
|
||||||
@@ -94,199 +81,6 @@ const normalizeLimitRule = (
|
|||||||
speed_limit: Math.max(0, Number(item.speed_limit) || 0),
|
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({
|
export function SubscriptionForm({
|
||||||
trigger,
|
trigger,
|
||||||
title,
|
title,
|
||||||
@@ -304,7 +98,7 @@ export function SubscriptionForm({
|
|||||||
traffic: initialData?.traffic || 0,
|
traffic: initialData?.traffic || 0,
|
||||||
speed_limit: initialData?.speed_limit || 0,
|
speed_limit: initialData?.speed_limit || 0,
|
||||||
device_limit: initialData?.device_limit || 0,
|
device_limit: initialData?.device_limit || 0,
|
||||||
traffic_limit: getUserTrafficLimitRules(initialData),
|
traffic_limit: initialData?.traffic_limit || [],
|
||||||
upload: initialData?.upload || 0,
|
upload: initialData?.upload || 0,
|
||||||
download: initialData?.download || 0,
|
download: initialData?.download || 0,
|
||||||
expired_at: initialData?.expire_time || 0,
|
expired_at: initialData?.expire_time || 0,
|
||||||
@@ -517,11 +311,98 @@ export function SubscriptionForm({
|
|||||||
{t("trafficLimitRules", "Traffic Limit Rules")}
|
{t("trafficLimitRules", "Traffic Limit Rules")}
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<TrafficLimitRuleEditor
|
<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)
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
onChange={(items) => {
|
onChange={(items) => {
|
||||||
field.onChange(items.map(normalizeLimitRule));
|
field.onChange(items.map(normalizeLimitRule));
|
||||||
}}
|
}}
|
||||||
value={field.value || []}
|
value={
|
||||||
|
field.value && field.value.length > 0
|
||||||
|
? field.value
|
||||||
|
: [defaultLimitRule]
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormDescription>
|
<FormDescription>
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ const WITHDRAWAL_METHOD_MAP: Record<number, string> = {
|
|||||||
0: "其他",
|
0: "其他",
|
||||||
1: "支付宝",
|
1: "支付宝",
|
||||||
2: "微信",
|
2: "微信",
|
||||||
3: "USDT(TRC20)",
|
3: "银行卡",
|
||||||
};
|
};
|
||||||
|
|
||||||
const rejectSchema = z.object({
|
const rejectSchema = z.object({
|
||||||
@@ -143,7 +143,7 @@ export default function WithdrawalPage() {
|
|||||||
const methodOptions = [
|
const methodOptions = [
|
||||||
{ value: "1", label: "支付宝" },
|
{ value: "1", label: "支付宝" },
|
||||||
{ value: "2", label: "微信" },
|
{ value: "2", label: "微信" },
|
||||||
{ value: "3", label: "USDT(TRC20)" },
|
{ value: "3", label: "银行卡" },
|
||||||
{ value: "0", label: "其他" },
|
{ value: "0", label: "其他" },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
|||||||
@@ -63,7 +63,7 @@ import { useGlobalStore } from "@/stores/global";
|
|||||||
const WITHDRAWAL_METHODS = [
|
const WITHDRAWAL_METHODS = [
|
||||||
{ value: 1, label: "支付宝" },
|
{ value: 1, label: "支付宝" },
|
||||||
{ value: 2, label: "微信" },
|
{ value: 2, label: "微信" },
|
||||||
{ value: 3, label: "USDT(TRC20)" },
|
{ value: 3, label: "银行卡" },
|
||||||
{ value: 0, label: "其他" },
|
{ value: 0, label: "其他" },
|
||||||
] as const;
|
] as const;
|
||||||
|
|
||||||
@@ -111,21 +111,13 @@ function QrCodeView({ url }: { url: string }) {
|
|||||||
<img
|
<img
|
||||||
alt="收款码"
|
alt="收款码"
|
||||||
className="h-16 w-16 cursor-zoom-in object-cover"
|
className="h-16 w-16 cursor-zoom-in object-cover"
|
||||||
height={64}
|
|
||||||
src={url}
|
src={url}
|
||||||
width={64}
|
|
||||||
/>
|
/>
|
||||||
</button>
|
</button>
|
||||||
{zoomOpen && (
|
{zoomOpen && (
|
||||||
<Dialog onOpenChange={setZoomOpen} open={zoomOpen}>
|
<Dialog onOpenChange={setZoomOpen} open={zoomOpen}>
|
||||||
<DialogContent className="max-w-sm">
|
<DialogContent className="max-w-sm">
|
||||||
<img
|
<img alt="收款码" className="w-full" src={url} />
|
||||||
alt="收款码"
|
|
||||||
className="w-full"
|
|
||||||
height={320}
|
|
||||||
src={url}
|
|
||||||
width={320}
|
|
||||||
/>
|
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
)}
|
)}
|
||||||
@@ -161,16 +153,18 @@ const withdrawalFormSchema = z
|
|||||||
if (!data.account) {
|
if (!data.account) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
message: "USDT(TRC20) 地址为必填",
|
message: "银行卡收款账号为必填",
|
||||||
|
path: ["account"],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} else if (data.method === 0) {
|
||||||
|
if (!data.account && !data.content) {
|
||||||
|
ctx.addIssue({
|
||||||
|
code: z.ZodIssueCode.custom,
|
||||||
|
message: "收款账号和备注至少填一项",
|
||||||
path: ["account"],
|
path: ["account"],
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
} else if (data.method === 0 && !(data.account || data.content)) {
|
|
||||||
ctx.addIssue({
|
|
||||||
code: z.ZodIssueCode.custom,
|
|
||||||
message: "收款账号和备注至少填一项",
|
|
||||||
path: ["account"],
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -427,7 +421,9 @@ export default function Affiliate() {
|
|||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>
|
||||||
收款码
|
收款码
|
||||||
<span className="ml-1 text-destructive">*</span>
|
<span className="ml-1 text-destructive">
|
||||||
|
*
|
||||||
|
</span>
|
||||||
</FormLabel>
|
</FormLabel>
|
||||||
{watchedQrCodeUrl ? (
|
{watchedQrCodeUrl ? (
|
||||||
<div className="relative inline-block">
|
<div className="relative inline-block">
|
||||||
@@ -450,9 +446,7 @@ export default function Affiliate() {
|
|||||||
returnType="file"
|
returnType="file"
|
||||||
>
|
>
|
||||||
<div className="flex h-24 w-24 flex-col items-center justify-center rounded border-2 border-dashed text-muted-foreground text-xs">
|
<div className="flex h-24 w-24 flex-col items-center justify-center rounded border-2 border-dashed text-muted-foreground text-xs">
|
||||||
{qrUploading
|
{qrUploading ? "上传中..." : "点击上传\n收款码"}
|
||||||
? "上传中..."
|
|
||||||
: "点击上传\n收款码"}
|
|
||||||
</div>
|
</div>
|
||||||
</UploadImage>
|
</UploadImage>
|
||||||
)}
|
)}
|
||||||
@@ -480,7 +474,7 @@ export default function Affiliate() {
|
|||||||
<Input
|
<Input
|
||||||
placeholder={
|
placeholder={
|
||||||
watchedMethod === 3
|
watchedMethod === 3
|
||||||
? "请输入 USDT(TRC20) 收款地址"
|
? "请输入银行卡号或账户信息"
|
||||||
: "请输入收款账号(与备注至少填一项)"
|
: "请输入收款账号(与备注至少填一项)"
|
||||||
}
|
}
|
||||||
{...field}
|
{...field}
|
||||||
@@ -525,7 +519,9 @@ export default function Affiliate() {
|
|||||||
</Button>
|
</Button>
|
||||||
<Button
|
<Button
|
||||||
disabled={
|
disabled={
|
||||||
submitting || qrUploading || availableCommission <= 0
|
submitting ||
|
||||||
|
qrUploading ||
|
||||||
|
availableCommission <= 0
|
||||||
}
|
}
|
||||||
form="withdrawal-form"
|
form="withdrawal-form"
|
||||||
type="submit"
|
type="submit"
|
||||||
|
|||||||
@@ -313,8 +313,7 @@ export function ProTable<
|
|||||||
<TableHead
|
<TableHead
|
||||||
className={cn(
|
className={cn(
|
||||||
"!z-auto",
|
"!z-auto",
|
||||||
getTableHeaderClass(header.column.id),
|
getTableHeaderClass(header.column.id)
|
||||||
getColumnClassName(header)
|
|
||||||
)}
|
)}
|
||||||
key={header.id}
|
key={header.id}
|
||||||
>
|
>
|
||||||
@@ -363,10 +362,7 @@ export function ProTable<
|
|||||||
.filter((cell) => cell.column.id !== "sortable")
|
.filter((cell) => cell.column.id !== "sortable")
|
||||||
.map((cell) => (
|
.map((cell) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
className={cn(
|
className={getTableCellClass(cell.column.id)}
|
||||||
getTableCellClass(cell.column.id),
|
|
||||||
getColumnClassName(cell)
|
|
||||||
)}
|
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
@@ -385,10 +381,7 @@ export function ProTable<
|
|||||||
>
|
>
|
||||||
{row.getVisibleCells().map((cell) => (
|
{row.getVisibleCells().map((cell) => (
|
||||||
<TableCell
|
<TableCell
|
||||||
className={cn(
|
className={getTableCellClass(cell.column.id)}
|
||||||
getTableCellClass(cell.column.id),
|
|
||||||
getColumnClassName(cell)
|
|
||||||
)}
|
|
||||||
key={cell.id}
|
key={cell.id}
|
||||||
>
|
>
|
||||||
{flexRender(
|
{flexRender(
|
||||||
@@ -483,17 +476,3 @@ function getTableCellClass(columnId: string) {
|
|||||||
}
|
}
|
||||||
return "truncate";
|
return "truncate";
|
||||||
}
|
}
|
||||||
|
|
||||||
function getColumnClassName(
|
|
||||||
columnLike: {
|
|
||||||
column: {
|
|
||||||
columnDef: {
|
|
||||||
meta?: {
|
|
||||||
className?: string;
|
|
||||||
};
|
|
||||||
};
|
|
||||||
};
|
|
||||||
}
|
|
||||||
) {
|
|
||||||
return columnLike.column.columnDef.meta?.className || "";
|
|
||||||
}
|
|
||||||
|
|||||||
-8
@@ -1938,7 +1938,6 @@ declare namespace API {
|
|||||||
trial_subscribe: number;
|
trial_subscribe: number;
|
||||||
trial_time: number;
|
trial_time: number;
|
||||||
trial_time_unit: string;
|
trial_time_unit: string;
|
||||||
gift_traffic: number;
|
|
||||||
enable_trial_email_whitelist: boolean;
|
enable_trial_email_whitelist: boolean;
|
||||||
trial_email_domain_whitelist: string;
|
trial_email_domain_whitelist: string;
|
||||||
enable_ip_register_limit: boolean;
|
enable_ip_register_limit: boolean;
|
||||||
@@ -2599,9 +2598,6 @@ declare namespace API {
|
|||||||
user_devices: UserDevice[];
|
user_devices: UserDevice[];
|
||||||
remark: string;
|
remark: string;
|
||||||
rules: string[];
|
rules: string[];
|
||||||
/** 当前有效订阅的套餐名(由后端计算),无有效订阅时为空 */
|
|
||||||
member_status?: string;
|
|
||||||
last_login_time?: number;
|
|
||||||
created_at: number;
|
created_at: number;
|
||||||
updated_at: number;
|
updated_at: number;
|
||||||
deleted_at?: number;
|
deleted_at?: number;
|
||||||
@@ -2668,13 +2664,11 @@ declare namespace API {
|
|||||||
download: number;
|
download: number;
|
||||||
upload: number;
|
upload: number;
|
||||||
speed_limit?: number;
|
speed_limit?: number;
|
||||||
plan_speed_limit?: number;
|
|
||||||
device_limit?: number;
|
device_limit?: number;
|
||||||
effective_speed?: number;
|
effective_speed?: number;
|
||||||
is_throttled?: boolean;
|
is_throttled?: boolean;
|
||||||
throttle_rule?: string;
|
throttle_rule?: string;
|
||||||
traffic_limit?: UserSubscribeTrafficLimitRule[];
|
traffic_limit?: UserSubscribeTrafficLimitRule[];
|
||||||
user_traffic_limit?: UserSubscribeTrafficLimitRule[];
|
|
||||||
token: string;
|
token: string;
|
||||||
status: number;
|
status: number;
|
||||||
short: string;
|
short: string;
|
||||||
@@ -2705,10 +2699,8 @@ declare namespace API {
|
|||||||
download: number;
|
download: number;
|
||||||
upload: number;
|
upload: number;
|
||||||
speed_limit?: number;
|
speed_limit?: number;
|
||||||
plan_speed_limit?: number;
|
|
||||||
device_limit?: number;
|
device_limit?: number;
|
||||||
traffic_limit?: UserSubscribeTrafficLimitRule[];
|
traffic_limit?: UserSubscribeTrafficLimitRule[];
|
||||||
user_traffic_limit?: UserSubscribeTrafficLimitRule[];
|
|
||||||
token: string;
|
token: string;
|
||||||
status: number;
|
status: number;
|
||||||
effective_speed: number;
|
effective_speed: number;
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
// @ts-expect-error
|
||||||
/* eslint-disable */
|
/* eslint-disable */
|
||||||
import request from "@workspace/ui/lib/request";
|
import request from "@workspace/ui/lib/request";
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user