merge: 同步 upstream/main 新功能到定制版本
Build and Release / Build (push) Has been cancelled
Issue Close Require / issue-close-require (push) Has been cancelled

- 分流规则 + 用户流量统计
- 验证码功能
- 节点分组管理 UI
- 重构用户分组组件
This commit is contained in:
2026-03-19 03:16:59 -07:00
89 changed files with 7036 additions and 383 deletions
+6 -5
View File
@@ -53,17 +53,18 @@ export function DatePicker({
)}
variant="outline"
>
{value ? intlFormat(value) : <span>{placeholder}</span>}
<span className="truncate">{value ? intlFormat(value) : <span>{placeholder}</span>}</span>
<div className="flex items-center gap-2">
{value && (
<button
className="flex items-center"
<span
className="flex items-center cursor-pointer"
onClick={handleClear}
onMouseDown={handleClear}
type="button"
role="button"
tabIndex={0}
>
<X className="size-4 opacity-50 hover:opacity-100" />
</button>
</span>
)}
<CalendarIcon className="size-4" />
</div>
+17
View File
@@ -0,0 +1,17 @@
// @ts-nocheck
/* eslint-disable */
import request from "@workspace/ui/lib/request";
/** Generate captcha POST /v1/auth/admin/captcha/generate */
export async function adminGenerateCaptcha(options?: { [key: string]: any }) {
return request<API.Response & { data?: API.GenerateCaptchaResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/auth/admin/captcha/generate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
...(options || {}),
}
);
}
+394
View File
@@ -0,0 +1,394 @@
// @ts-nocheck
/* eslint-disable */
import request from "@workspace/ui/lib/request";
/** Get user group list GET /v1/admin/group/user/list */
export async function getUserGroupList(
params: API.GetUserGroupListRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.GetUserGroupListResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/user/list`,
{
method: "GET",
params: {
...params,
},
...(options || {}),
}
);
}
/** Create user group POST /v1/admin/group/user */
export async function createUserGroup(
body: API.CreateUserGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/user`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Update user group PUT /v1/admin/group/user */
export async function updateUserGroup(
body: API.UpdateUserGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/user`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Delete user group DELETE /v1/admin/group/user */
export async function deleteUserGroup(
body: API.DeleteUserGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/user`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Get node group list GET /v1/admin/group/node/list */
export async function getNodeGroupList(
params: API.GetNodeGroupListRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.GetNodeGroupListResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/node/list`,
{
method: "GET",
params: {
...params,
},
...(options || {}),
}
);
}
/** Create node group POST /v1/admin/group/node */
export async function createNodeGroup(
body: API.CreateNodeGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/node`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Update node group PUT /v1/admin/group/node */
export async function updateNodeGroup(
body: API.UpdateNodeGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/node`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Delete node group DELETE /v1/admin/group/node */
export async function deleteNodeGroup(
body: API.DeleteNodeGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/node`,
{
method: "DELETE",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Get subscribe mapping list GET /v1/admin/group/subscribe/mapping */
export async function getSubscribeMapping(
params: API.GetSubscribeMappingRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.GetSubscribeMappingResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/subscribe/mapping`,
{
method: "GET",
params: {
...params,
},
...(options || {}),
}
);
}
/** Update subscribe mapping PUT /v1/admin/group/subscribe/mapping */
export async function updateSubscribeMapping(
body: API.UpdateSubscribeMappingRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/subscribe/mapping`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Get subscribe group mapping GET /v1/admin/group/subscribe/mapping */
export async function getSubscribeGroupMapping(
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.GetSubscribeGroupMappingResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/subscribe/mapping`,
{
method: "GET",
...(options || {}),
}
);
}
/** Get group config GET /v1/admin/group/config */
export async function getGroupConfig(
params?: API.GetGroupConfigRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.GetGroupConfigResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/config`,
{
method: "GET",
params: params || {},
...(options || {}),
}
);
}
/** Update group config PUT /v1/admin/group/config */
export async function updateGroupConfig(
body: API.UpdateGroupConfigRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/config`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Get recalculation status GET /v1/admin/group/recalculation/status */
export async function getRecalculationStatus(options?: { [key: string]: any }) {
return request<API.Response & { data?: API.RecalculationState }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/recalculation/status`,
{
method: "GET",
...(options || {}),
}
);
}
/** Recalculate groups POST /v1/admin/group/recalculate */
export async function recalculateGroup(
body: API.RecalculateGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/recalculate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Get group history GET /v1/admin/group/history */
export async function getGroupHistory(
params: API.GetGroupHistoryRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.GetGroupHistoryResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/history`,
{
method: "GET",
params: {
...params,
},
...(options || {}),
}
);
}
/** Get group history detail GET /v1/admin/group/history/detail */
export async function getGroupHistoryDetail(
params: { id: number },
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.GetGroupHistoryDetailResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/history/detail`,
{
method: "GET",
params: {
id: params.id,
},
...(options || {}),
}
);
}
/** Export group result GET /v1/admin/group/export */
export async function exportGroupResult(
params?: API.ExportGroupResultRequest,
options?: { [key: string]: any }
) {
return request<Blob>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/export`,
{
method: "GET",
params: params || {},
responseType: 'blob',
...(options || {}),
}
);
}
/** Preview user nodes GET /v1/admin/group/preview */
export async function previewUserNodes(
params: API.PreviewUserNodesRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.PreviewUserNodesResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/preview`,
{
method: "GET",
params: {
user_id: params.user_id,
},
...(options || {}),
}
);
}
/** Migrate users to another group POST /v1/admin/group/migrate */
export async function migrateUsersToGroup(
body: API.MigrateUsersRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: API.MigrateUsersResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/migrate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Update user user group PUT /v1/admin/user/user_group */
export async function updateUserUserGroup(
body: API.UpdateUserUserGroupRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/user/user_group`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Reset all groups POST /v1/admin/group/reset */
export async function resetGroups(
body: API.ResetGroupsRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/reset`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
/** Bind node group to user groups POST /v1/admin/group/user/bind-node-groups */
export async function bindNodeGroups(
body: API.BindNodeGroupsRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: any }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/admin/group/user/bind-node-groups`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
data: body,
...(options || {}),
}
);
}
+4
View File
@@ -5,10 +5,12 @@
import * as ads from "./ads";
import * as announcement from "./announcement";
import * as application from "./application";
import * as auth from "./auth";
import * as authMethod from "./authMethod";
import * as console from "./console";
import * as coupon from "./coupon";
import * as document from "./document";
import * as group from "./group";
import * as log from "./log";
import * as marketing from "./marketing";
import * as order from "./order";
@@ -24,10 +26,12 @@ export default {
ads,
announcement,
application,
auth,
authMethod,
console,
coupon,
document,
group,
log,
marketing,
order,
+291 -5
View File
@@ -103,6 +103,7 @@ declare namespace API {
longitude: string;
created_at: number;
download: number;
port: number;
};
type AuthConfig = {
@@ -455,7 +456,7 @@ declare namespace API {
};
type DeleteUserSubscribeRequest = {
user_subscribe_id: number;
user_subscribe_id: string;
};
type DeviceAuthticateConfig = {
@@ -623,12 +624,14 @@ declare namespace API {
page: number;
size: number;
search?: string;
node_group_id?: number;
};
type FilterNodeListRequest = {
page: number;
size: number;
search?: string;
node_group_id?: number;
};
type FilterNodeListResponse = {
@@ -1068,6 +1071,7 @@ declare namespace API {
size: number;
language?: string;
search?: string;
node_group_id?: number;
};
type GetSubscribeListRequest = {
@@ -1075,6 +1079,7 @@ declare namespace API {
size: number;
language?: string;
search?: string;
node_group_id?: number;
};
type GetSubscribeListResponse = {
@@ -1131,6 +1136,7 @@ declare namespace API {
unscoped?: boolean;
subscribe_id?: number;
user_subscribe_id?: number;
user_group_id?: number;
};
type GetUserListRequest = {
@@ -1362,6 +1368,8 @@ declare namespace API {
protocol: string;
enabled: boolean;
sort?: number;
node_group_id?: number;
node_group_ids?: number[];
created_at: number;
updated_at: number;
};
@@ -1842,11 +1850,11 @@ declare namespace API {
};
type ResetUserSubscribeTokenRequest = {
user_subscribe_id: number;
user_subscribe_id: any;
};
type ResetUserSubscribeTrafficRequest = {
user_subscribe_id: number;
user_subscribe_id: string;
};
type Response = {
@@ -2027,6 +2035,8 @@ declare namespace API {
quota: number;
nodes: number[];
node_tags: string[];
node_group_ids?: number[];
node_group_id?: number;
show: boolean;
sell: boolean;
sort: number;
@@ -2092,6 +2102,8 @@ declare namespace API {
quota?: number;
nodes?: number[];
node_tags?: string[];
node_group_ids?: number[];
node_group_id?: number;
show?: boolean;
sell?: boolean;
sort?: number;
@@ -2165,7 +2177,7 @@ declare namespace API {
};
type ToggleUserSubscribeStatusRequest = {
user_subscribe_id: number;
user_subscribe_id: any;
};
type TosConfig = {
@@ -2394,7 +2406,7 @@ declare namespace API {
};
type UpdateUserSubscribeRequest = {
user_subscribe_id: number;
user_subscribe_id: string;
subscribe_id: number;
traffic: number;
expired_at: number;
@@ -2419,6 +2431,8 @@ declare namespace API {
enable_login_notify: boolean;
enable_subscribe_notify: boolean;
enable_trade_notify: boolean;
user_group_id: string;
group_locked: boolean;
auth_methods: UserAuthMethod[];
user_devices: UserDevice[];
rules: string[];
@@ -2614,6 +2628,19 @@ declare namespace API {
updated_at: number;
};
// ===== Group Management Types =====
type UserGroup = {
id: number;
name: string;
description: string;
sort: number;
node_group_id?: number | null;
for_calculation?: boolean;
created_at: number;
updated_at: number;
};
type GetFamilyDetailParams = {
id: number;
};
@@ -2697,6 +2724,23 @@ declare namespace API {
updated_at: number;
};
type NodeGroup = {
id: number;
name: string;
description: string;
sort: number;
for_calculation: boolean;
is_expired_group: boolean;
expired_days_limit: number;
max_traffic_gb_expired?: number;
speed_limit: number;
min_traffic_gb?: number;
max_traffic_gb?: number;
node_count?: number;
created_at: number;
updated_at: number;
};
type RedemptionRecord = {
id: number;
redemption_code_id: number;
@@ -2762,4 +2806,246 @@ declare namespace API {
total: number;
list: RedemptionRecord[];
};
type Subscribe = {
id: number;
name: string;
unit_price: number;
unit_time: number;
show?: boolean;
sell?: boolean;
sort: number;
created_at: number;
updated_at: number;
};
type SubscribeGroupMapping = {
subscribe_id: number;
user_group_id: string;
created_at: number;
updated_at: number;
};
type SubscribeGroupMappingInfo = {
id: number;
subscribe_id: number;
user_group_id: number;
subscribe?: Subscribe;
user_group?: UserGroup;
created_at: number;
updated_at: number;
};
type GroupHistory = {
id: number;
group_mode: string;
trigger_type: string;
total_users: number;
success_count: number;
failed_count: number;
start_time?: number;
end_time?: number;
operator?: string;
error_log?: string;
created_at: number;
};
type GroupHistoryDetailItem = {
id: number;
history_id: number;
user_group_id: string;
node_group_id: string;
user_count: number;
node_count: number;
user_data?: string;
created_at: number;
};
type GroupHistoryDetail = {
id: number;
group_mode: string;
trigger_type: string;
total_users: number;
success_count: number;
failed_count: number;
start_time?: number;
end_time?: number;
operator?: string;
error_log?: string;
created_at: number;
config_snapshot?: {
group_details?: GroupHistoryDetailItem[];
config?: Record<string, unknown>;
};
};
type RecalculationState = {
state: string;
progress: number;
total: number;
};
// ===== Group Request/Response Types =====
type GetUserGroupListRequest = {
page: number;
size: number;
group_id?: string;
};
type GetUserGroupListResponse = {
total: number;
list: UserGroup[];
};
type CreateUserGroupRequest = {
name: string;
description?: string;
sort?: number;
node_group_id?: number | null;
for_calculation?: boolean | null;
};
type UpdateUserGroupRequest = {
id: number;
name?: string;
description?: string;
sort?: number;
node_group_id?: number | null;
for_calculation?: boolean | null;
};
type DeleteUserGroupRequest = {
id: number;
};
type BindNodeGroupsRequest = {
user_group_ids: number[];
node_group_id?: number | null;
};
type GetNodeGroupListRequest = {
page: number;
size: number;
group_id?: string;
};
type GetNodeGroupListResponse = {
total: number;
list: NodeGroup[];
};
type CreateNodeGroupRequest = {
name: string;
description?: string;
sort?: number;
for_calculation?: boolean;
is_expired_group?: boolean;
expired_days_limit?: number;
max_traffic_gb_expired?: number;
speed_limit?: number;
min_traffic_gb?: number;
max_traffic_gb?: number;
};
type UpdateNodeGroupRequest = {
id: number;
name?: string;
description?: string;
sort?: number;
for_calculation?: boolean;
is_expired_group?: boolean;
expired_days_limit?: number;
max_traffic_gb_expired?: number;
speed_limit?: number;
min_traffic_gb?: number;
max_traffic_gb?: number;
};
type DeleteNodeGroupRequest = {
id: number;
};
type GetSubscribeMappingRequest = {
page: number;
size: number;
subscribe_id?: number;
user_group_id?: number;
};
type GetSubscribeMappingResponse = {
total: number;
list: SubscribeGroupMappingInfo[];
};
type UpdateSubscribeMappingRequest = {
subscribe_id: number;
user_group_id: number;
};
type GetGroupConfigResponse = {
enabled: boolean;
mode: string;
};
type UpdateGroupConfigRequest = {
enabled?: boolean;
mode?: string;
};
type RecalculateGroupRequest = {
mode: string;
};
type GetGroupHistoryRequest = {
page: number;
size: number;
group_mode?: string;
trigger_type?: string;
};
type GetGroupHistoryResponse = {
total: number;
list: GroupHistory[];
};
type GetGroupHistoryDetailRequest = {
id: number;
};
type GetGroupHistoryDetailResponse = GroupHistoryDetail;
type ExportGroupResultRequest = {
history_id?: number;
};
type MigrateUsersRequest = {
from_user_group_id: number;
to_user_group_id: number;
include_locked?: boolean;
};
type MigrateUsersResponse = {
success_count: number;
failed_count: number;
};
type ResetGroupsRequest = {
confirm: boolean;
};
type PreviewUserNodesRequest = {
user_id: number;
};
type PreviewUserNodesResponse = {
user_id: number;
node_groups: Array<{
id: number;
name: string;
nodes: Array<{
id: number;
name: string;
address: string;
port: number;
}>;
}>;
};
}
+14
View File
@@ -163,3 +163,17 @@ export async function telephoneResetPassword(
}
);
}
/** Generate captcha POST /v1/auth/captcha/generate */
export async function generateCaptcha(options?: { [key: string]: any }) {
return request<API.Response & { data?: API.GenerateCaptchaResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/auth/captcha/generate`,
{
method: "POST",
headers: {
"Content-Type": "application/json",
},
...(options || {}),
}
);
}
+5
View File
@@ -1112,9 +1112,14 @@ declare namespace API {
type VeifyConfig = {
turnstile_site_key: string;
captcha_type: string;
enable_login_verify: boolean;
enable_register_verify: boolean;
enable_reset_password_verify: boolean;
enable_user_login_captcha: boolean;
enable_user_register_captcha: boolean;
enable_user_reset_password_captcha: boolean;
enable_admin_login_captcha: boolean;
};
type VerifyCodeConfig = {
+2
View File
@@ -9,6 +9,7 @@ import * as payment from "./payment";
import * as portal from "./portal";
import * as subscribe from "./subscribe";
import * as ticket from "./ticket";
import * as traffic from "./traffic";
import * as user from "./user";
export default {
announcement,
@@ -18,5 +19,6 @@ export default {
portal,
subscribe,
ticket,
traffic,
user,
};
+37
View File
@@ -0,0 +1,37 @@
import request from "@workspace/ui/lib/request";
export interface GetUserTrafficStatsRequest {
user_subscribe_id: string; // 保持字符串,避免精度问题
days: 7 | 30;
}
export interface DailyTrafficStats {
date: string;
upload: number;
download: number;
total: number;
}
export interface GetUserTrafficStatsResponse {
list: DailyTrafficStats[];
total_upload: number;
total_download: number;
total_traffic: number;
}
/** Get User Traffic Statistics GET /v1/public/user/traffic_stats */
export async function getUserTrafficStats(
params: GetUserTrafficStatsRequest,
options?: { [key: string]: any }
) {
return request<API.Response & { data?: GetUserTrafficStatsResponse }>(
`${import.meta.env.VITE_API_PREFIX || ""}/v1/public/user/traffic_stats`,
{
method: "GET",
params: {
...params,
},
...(options || {}),
}
);
}
+1
View File
@@ -1185,6 +1185,7 @@ declare namespace API {
type UserSubscribe = {
id: number;
id_str: string;
user_id: number;
order_id: number;
subscribe_id: number;