mirror of
https://github.com/Rain-kl/OpenFlare.git
synced 2026-10-10 17:26:38 +08:00
prettier
This commit is contained in:
@@ -47,7 +47,10 @@ export async function readApiEnvelope<T>(
|
||||
|
||||
const envelope = body as ApiEnvelope<T>;
|
||||
if (!res.ok) {
|
||||
throw new ApiEnvelopeError(envelope.error_msg || fallbackMessage, res.status);
|
||||
throw new ApiEnvelopeError(
|
||||
envelope.error_msg || fallbackMessage,
|
||||
res.status,
|
||||
);
|
||||
}
|
||||
if (envelope.error_msg) {
|
||||
throw new ApiEnvelopeError(envelope.error_msg, res.status);
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
import packageJson from "../package.json"
|
||||
import packageJson from '../package.json';
|
||||
|
||||
const rawVersion = process.env.NEXT_PUBLIC_APP_VERSION || packageJson.version
|
||||
const rawVersion = process.env.NEXT_PUBLIC_APP_VERSION || packageJson.version;
|
||||
|
||||
export const APP_VERSION = rawVersion.startsWith('v') ? rawVersion.slice(1) : rawVersion
|
||||
export const APP_BUILD_DATE = process.env.NEXT_PUBLIC_APP_BUILD_DATE || ""
|
||||
export const APP_VERSION = rawVersion.startsWith('v')
|
||||
? rawVersion.slice(1)
|
||||
: rawVersion;
|
||||
export const APP_BUILD_DATE = process.env.NEXT_PUBLIC_APP_BUILD_DATE || '';
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type {LucideIcon} from 'lucide-react';
|
||||
import type { LucideIcon } from 'lucide-react';
|
||||
import {
|
||||
FileText,
|
||||
Gauge,
|
||||
@@ -32,16 +32,16 @@ export interface OpenFlareNavGroup {
|
||||
}
|
||||
|
||||
export type OpenFlareSidebarNavEntry =
|
||||
| ({kind: 'item'} & OpenFlareNavItem)
|
||||
| ({kind: 'group'} & OpenFlareNavGroup);
|
||||
| ({ kind: 'item' } & OpenFlareNavItem)
|
||||
| ({ kind: 'group' } & OpenFlareNavGroup);
|
||||
|
||||
/** 安全性折叠组 */
|
||||
export const openflareSecurityNavGroup: OpenFlareNavGroup = {
|
||||
title: '安全性',
|
||||
icon: ShieldCheck,
|
||||
items: [
|
||||
{title: 'WAF', url: '/waf'},
|
||||
{title: 'IP 组', url: '/ip-groups'},
|
||||
{ title: 'WAF', url: '/waf' },
|
||||
{ title: 'IP 组', url: '/ip-groups' },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -50,10 +50,10 @@ export const openflareWebsiteNavGroup: OpenFlareNavGroup = {
|
||||
title: '网站管理',
|
||||
icon: Globe,
|
||||
items: [
|
||||
{title: '域名列表', url: '/websites', childUrls: ['/websites/detail']},
|
||||
{title: 'TLS证书', url: '/certificates'},
|
||||
{title: 'DNS账号', url: '/dns-accounts'},
|
||||
{title: '源站地址', url: '/origins', childUrls: ['/origins/detail']},
|
||||
{ title: '域名列表', url: '/websites', childUrls: ['/websites/detail'] },
|
||||
{ title: 'TLS证书', url: '/certificates' },
|
||||
{ title: 'DNS账号', url: '/dns-accounts' },
|
||||
{ title: '源站地址', url: '/origins', childUrls: ['/origins/detail'] },
|
||||
],
|
||||
};
|
||||
|
||||
@@ -62,34 +62,63 @@ export const openflareWebsiteNavGroup: OpenFlareNavGroup = {
|
||||
* 调整菜单顺序或折叠组位置时,只需修改此数组。
|
||||
*/
|
||||
export const openflareSidebarNav: OpenFlareSidebarNavEntry[] = [
|
||||
{kind: 'item', title: '数据看板', url: '/', icon: LayoutDashboard},
|
||||
{kind: 'item', title: '节点管理', url: '/nodes', icon: Server, childUrls: ['/nodes/detail']},
|
||||
{kind: 'item', title: '规则管理', url: '/proxy-routes', icon: Route, childUrls: ['/proxy-routes/detail']},
|
||||
{kind: 'group', ...openflareWebsiteNavGroup},
|
||||
{kind: 'group', ...openflareSecurityNavGroup},
|
||||
{kind: 'item', title: 'Pages', url: '/pages', icon: FileText, childUrls: ['/pages/detail']},
|
||||
{kind: 'item', title: '版本发布', url: '/config-versions', icon: GitBranch},
|
||||
{kind: 'item', title: '访问日志', url: '/access-logs', icon: ScrollText},
|
||||
{kind: 'item', title: '性能调优', url: '/performance', icon: Gauge},
|
||||
{ kind: 'item', title: '数据看板', url: '/', icon: LayoutDashboard },
|
||||
{
|
||||
kind: 'item',
|
||||
title: '节点管理',
|
||||
url: '/nodes',
|
||||
icon: Server,
|
||||
childUrls: ['/nodes/detail'],
|
||||
},
|
||||
{
|
||||
kind: 'item',
|
||||
title: '规则管理',
|
||||
url: '/proxy-routes',
|
||||
icon: Route,
|
||||
childUrls: ['/proxy-routes/detail'],
|
||||
},
|
||||
{ kind: 'group', ...openflareWebsiteNavGroup },
|
||||
{ kind: 'group', ...openflareSecurityNavGroup },
|
||||
{
|
||||
kind: 'item',
|
||||
title: 'Pages',
|
||||
url: '/pages',
|
||||
icon: FileText,
|
||||
childUrls: ['/pages/detail'],
|
||||
},
|
||||
{ kind: 'item', title: '版本发布', url: '/config-versions', icon: GitBranch },
|
||||
{ kind: 'item', title: '访问日志', url: '/access-logs', icon: ScrollText },
|
||||
{ kind: 'item', title: '性能调优', url: '/performance', icon: Gauge },
|
||||
];
|
||||
|
||||
/** 扁平菜单项(供路由判断等逻辑复用) */
|
||||
export const openflareNavItems: OpenFlareNavItem[] = openflareSidebarNav
|
||||
.filter((entry): entry is {kind: 'item'} & OpenFlareNavItem => entry.kind === 'item')
|
||||
.filter(
|
||||
(entry): entry is { kind: 'item' } & OpenFlareNavItem =>
|
||||
entry.kind === 'item',
|
||||
)
|
||||
.map((entry) => {
|
||||
const {kind, ...item} = entry;
|
||||
const { kind, ...item } = entry;
|
||||
void kind;
|
||||
return item;
|
||||
});
|
||||
|
||||
/** 网站模块页内二级导航 */
|
||||
export const openflareWebsiteSubNav = [
|
||||
{title: '网站列表', url: '/websites'},
|
||||
{title: '证书', url: '/certificates'},
|
||||
{title: 'DNS 账号', url: '/dns-accounts'},
|
||||
{ title: '网站列表', url: '/websites' },
|
||||
{ title: '证书', url: '/certificates' },
|
||||
{ title: 'DNS 账号', url: '/dns-accounts' },
|
||||
] as const;
|
||||
|
||||
const nonConsoleRoutePrefixes = ['/admin', '/settings', '/files', '/home', '/login', '/register', '/docs'];
|
||||
const nonConsoleRoutePrefixes = [
|
||||
'/admin',
|
||||
'/settings',
|
||||
'/files',
|
||||
'/home',
|
||||
'/login',
|
||||
'/register',
|
||||
'/docs',
|
||||
];
|
||||
|
||||
export function matchesNavPath(
|
||||
pathname: string,
|
||||
@@ -109,13 +138,22 @@ export function matchesNavPath(
|
||||
);
|
||||
}
|
||||
|
||||
export function isNavGroupActive(pathname: string, group: OpenFlareNavGroup): boolean {
|
||||
return group.items.some((item) => matchesNavPath(pathname, item.url, item.childUrls));
|
||||
export function isNavGroupActive(
|
||||
pathname: string,
|
||||
group: OpenFlareNavGroup,
|
||||
): boolean {
|
||||
return group.items.some((item) =>
|
||||
matchesNavPath(pathname, item.url, item.childUrls),
|
||||
);
|
||||
}
|
||||
|
||||
/** 判断当前路径是否属于 OpenFlare 业务控制台 */
|
||||
export function isOpenFlareConsoleRoute(pathname: string): boolean {
|
||||
if (nonConsoleRoutePrefixes.some((prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`))) {
|
||||
if (
|
||||
nonConsoleRoutePrefixes.some(
|
||||
(prefix) => pathname === prefix || pathname.startsWith(`${prefix}/`),
|
||||
)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import type {AuthSource, AuthSourceRequest, ToggleAuthSourceRequest} from './types';
|
||||
import type {AuthSource, AuthSourceRequest, ToggleAuthSourceRequest,} from './types';
|
||||
|
||||
export class AdminAuthSourceService extends BaseService {
|
||||
protected static readonly basePath = '/api/v1/admin';
|
||||
@@ -8,19 +8,27 @@ export class AdminAuthSourceService extends BaseService {
|
||||
return this.get<AuthSource[]>('/auth-sources');
|
||||
}
|
||||
|
||||
static async createAuthSource(request: AuthSourceRequest): Promise<AuthSource> {
|
||||
static async createAuthSource(
|
||||
request: AuthSourceRequest,
|
||||
): Promise<AuthSource> {
|
||||
return this.post<AuthSource>('/auth-sources', request);
|
||||
}
|
||||
|
||||
static async updateAuthSource(id: string, request: AuthSourceRequest): Promise<AuthSource> {
|
||||
static async updateAuthSource(
|
||||
id: string,
|
||||
request: AuthSourceRequest,
|
||||
): Promise<AuthSource> {
|
||||
return this.put<AuthSource>(`/auth-sources/${id}`, request);
|
||||
}
|
||||
|
||||
static async toggleAuthSource(id: string, request: ToggleAuthSourceRequest): Promise<void> {
|
||||
static async toggleAuthSource(
|
||||
id: string,
|
||||
request: ToggleAuthSourceRequest,
|
||||
): Promise<void> {
|
||||
return this.put<void>(`/auth-sources/${id}/toggle`, request);
|
||||
}
|
||||
|
||||
static async deleteAuthSource(id: string): Promise<void> {
|
||||
return this.delete<void>(`/auth-sources/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ export class AdminCacheService extends BaseService {
|
||||
static async clearCache(): Promise<void> {
|
||||
return this.post<void>('/cache/clear');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import { BaseService } from '@/lib/services/core';
|
||||
|
||||
export class AdminLogService extends BaseService {
|
||||
protected static readonly basePath = '/api/v1/admin';
|
||||
|
||||
static async getLogs(cursor: number = 0, limit: number = 200): Promise<{
|
||||
static async getLogs(
|
||||
cursor: number = 0,
|
||||
limit: number = 200,
|
||||
): Promise<{
|
||||
lines: Array<{ index: number; data: string }>;
|
||||
has_more: boolean;
|
||||
next_cursor: number;
|
||||
@@ -50,4 +53,4 @@ export class AdminLogService extends BaseService {
|
||||
}> {
|
||||
return this.get('/logs/analytics');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import type {AppUpdateStatus, SystemStatus} from './types';
|
||||
import { BaseService } from '@/lib/services/core';
|
||||
import type { AppUpdateStatus, SystemStatus } from './types';
|
||||
|
||||
export class AdminStatusService extends BaseService {
|
||||
protected static readonly basePath = '/api/v1/admin';
|
||||
@@ -15,4 +15,4 @@ export class AdminStatusService extends BaseService {
|
||||
static async applyUpdate(): Promise<void> {
|
||||
return this.post<void>('/update/apply');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,14 +1,22 @@
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import type {CreateSystemConfigRequest, SystemConfig, UpdateSystemConfigRequest,} from './types';
|
||||
import { BaseService } from '@/lib/services/core';
|
||||
import type {
|
||||
CreateSystemConfigRequest,
|
||||
SystemConfig,
|
||||
UpdateSystemConfigRequest,
|
||||
} from './types';
|
||||
|
||||
export class AdminSystemConfigService extends BaseService {
|
||||
protected static readonly basePath = '/api/v1/admin';
|
||||
|
||||
static async createSystemConfig(request: CreateSystemConfigRequest): Promise<void> {
|
||||
static async createSystemConfig(
|
||||
request: CreateSystemConfigRequest,
|
||||
): Promise<void> {
|
||||
return this.post<void>('/system-configs', request);
|
||||
}
|
||||
|
||||
static async listSystemConfigs(type?: 'system' | 'business'): Promise<SystemConfig[]> {
|
||||
static async listSystemConfigs(
|
||||
type?: 'system' | 'business',
|
||||
): Promise<SystemConfig[]> {
|
||||
const query = type ? `?type=${type}` : '';
|
||||
return this.get<SystemConfig[]>(`/system-configs${query}`);
|
||||
}
|
||||
@@ -17,7 +25,10 @@ export class AdminSystemConfigService extends BaseService {
|
||||
return this.get<SystemConfig>(`/system-configs/${key}`);
|
||||
}
|
||||
|
||||
static async updateSystemConfig(key: string, request: UpdateSystemConfigRequest): Promise<void> {
|
||||
static async updateSystemConfig(
|
||||
key: string,
|
||||
request: UpdateSystemConfigRequest,
|
||||
): Promise<void> {
|
||||
return this.put<void>(`/system-configs/${key}`, request);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import { BaseService } from '@/lib/services/core';
|
||||
import type {
|
||||
CreateScheduleRequest,
|
||||
DispatchTaskRequest,
|
||||
@@ -61,15 +61,20 @@ export class AdminTaskService extends BaseService {
|
||||
return this.get<Schedule[]>('/tasks/schedules');
|
||||
}
|
||||
|
||||
static async createSchedule(request: CreateScheduleRequest): Promise<Schedule> {
|
||||
static async createSchedule(
|
||||
request: CreateScheduleRequest,
|
||||
): Promise<Schedule> {
|
||||
return this.post<Schedule>('/tasks/schedules', request);
|
||||
}
|
||||
|
||||
static async updateSchedule(id: string, request: UpdateScheduleRequest): Promise<Schedule> {
|
||||
static async updateSchedule(
|
||||
id: string,
|
||||
request: UpdateScheduleRequest,
|
||||
): Promise<Schedule> {
|
||||
return this.put<Schedule>(`/tasks/schedules/${id}`, request);
|
||||
}
|
||||
|
||||
static async deleteSchedule(id: string): Promise<void> {
|
||||
return this.delete<void>(`/tasks/schedules/${id}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,9 @@
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import type {CreateTemplateRequest, Template, UpdateTemplateRequest} from './types';
|
||||
import { BaseService } from '@/lib/services/core';
|
||||
import type {
|
||||
CreateTemplateRequest,
|
||||
Template,
|
||||
UpdateTemplateRequest,
|
||||
} from './types';
|
||||
|
||||
export class AdminTemplateService extends BaseService {
|
||||
protected static readonly basePath = '/api/v1/admin';
|
||||
@@ -12,15 +16,20 @@ export class AdminTemplateService extends BaseService {
|
||||
return this.get<Template>(`/templates/${key}`);
|
||||
}
|
||||
|
||||
static async createTemplate(request: CreateTemplateRequest): Promise<Template> {
|
||||
static async createTemplate(
|
||||
request: CreateTemplateRequest,
|
||||
): Promise<Template> {
|
||||
return this.post<Template>('/templates', request);
|
||||
}
|
||||
|
||||
static async updateTemplate(key: string, request: UpdateTemplateRequest): Promise<Template> {
|
||||
static async updateTemplate(
|
||||
key: string,
|
||||
request: UpdateTemplateRequest,
|
||||
): Promise<Template> {
|
||||
return this.put<Template>(`/templates/${key}`, request);
|
||||
}
|
||||
|
||||
static async deleteTemplate(key: string): Promise<void> {
|
||||
return this.delete<void>(`/templates/${key}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
|
||||
|
||||
/**
|
||||
* 系统配置信息
|
||||
*/
|
||||
@@ -178,7 +176,8 @@ export interface DispatchTaskRequest {
|
||||
payload?: string;
|
||||
}
|
||||
|
||||
export type TaskExecutionStatus = 'pending' | 'running' | 'succeeded' | 'failed';
|
||||
export type TaskExecutionStatus =
|
||||
'pending' | 'running' | 'succeeded' | 'failed';
|
||||
|
||||
/**
|
||||
* 任务执行记录
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import { BaseService } from '@/lib/services/core';
|
||||
import type {
|
||||
AdminUser,
|
||||
CreateUserRequest,
|
||||
@@ -11,15 +11,23 @@ import type {
|
||||
export class AdminUserService extends BaseService {
|
||||
protected static readonly basePath = '/api/v1/admin';
|
||||
|
||||
static async listUsers(request: ListUsersRequest): Promise<ListUsersResponse> {
|
||||
return this.get<ListUsersResponse>('/users', request as unknown as Record<string, unknown>);
|
||||
static async listUsers(
|
||||
request: ListUsersRequest,
|
||||
): Promise<ListUsersResponse> {
|
||||
return this.get<ListUsersResponse>(
|
||||
'/users',
|
||||
request as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
static async getUser(id: string): Promise<AdminUser> {
|
||||
return this.get<AdminUser>(`/users/${id}`);
|
||||
}
|
||||
|
||||
static async updateUserStatus(id: string, request: UpdateUserStatusRequest): Promise<void> {
|
||||
static async updateUserStatus(
|
||||
id: string,
|
||||
request: UpdateUserStatusRequest,
|
||||
): Promise<void> {
|
||||
return this.put<void>(`/users/${id}/status`, request);
|
||||
}
|
||||
|
||||
@@ -31,7 +39,10 @@ export class AdminUserService extends BaseService {
|
||||
return this.delete<void>(`/users/${id}`);
|
||||
}
|
||||
|
||||
static async updateUser(id: string, request: UpdateUserRequest): Promise<void> {
|
||||
static async updateUser(
|
||||
id: string,
|
||||
request: UpdateUserRequest,
|
||||
): Promise<void> {
|
||||
return this.put<void>(`/users/${id}`, request);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,8 +40,13 @@ export class AuthService extends BaseService {
|
||||
return this.get<AuthSource[]>('/oauth/sources');
|
||||
}
|
||||
|
||||
static async getAuthorizeUrl(source: string, purpose: 'login' | 'bind' = 'login'): Promise<OAuthAuthorizeResponse> {
|
||||
return this.get<OAuthAuthorizeResponse>(`/oauth/${encodeURIComponent(source)}/authorize?purpose=${purpose}`);
|
||||
static async getAuthorizeUrl(
|
||||
source: string,
|
||||
purpose: 'login' | 'bind' = 'login',
|
||||
): Promise<OAuthAuthorizeResponse> {
|
||||
return this.get<OAuthAuthorizeResponse>(
|
||||
`/oauth/${encodeURIComponent(source)}/authorize?purpose=${purpose}`,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -64,7 +69,9 @@ export class AuthService extends BaseService {
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
static async handleCallback(request: OAuthCallbackRequest): Promise<OAuthCallbackResult> {
|
||||
static async handleCallback(
|
||||
request: OAuthCallbackRequest,
|
||||
): Promise<OAuthCallbackResult> {
|
||||
return this.post<OAuthCallbackResult>('/oauth/callback', request);
|
||||
}
|
||||
|
||||
@@ -105,16 +112,44 @@ export class AuthService extends BaseService {
|
||||
await this.get<void>('/oauth/logout');
|
||||
}
|
||||
|
||||
static async login(request: LoginRequest, headers?: Record<string, string>): Promise<User> {
|
||||
return this.post<User>('/user/login', request, headers ? ({ headers } as unknown as InternalAxiosRequestConfig) : undefined);
|
||||
static async login(
|
||||
request: LoginRequest,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<User> {
|
||||
return this.post<User>(
|
||||
'/user/login',
|
||||
request,
|
||||
headers
|
||||
? ({ headers } as unknown as InternalAxiosRequestConfig)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
static async register(request: RegisterRequest, headers?: Record<string, string>): Promise<User> {
|
||||
return this.post<User>('/user/register', request, headers ? ({ headers } as unknown as InternalAxiosRequestConfig) : undefined);
|
||||
static async register(
|
||||
request: RegisterRequest,
|
||||
headers?: Record<string, string>,
|
||||
): Promise<User> {
|
||||
return this.post<User>(
|
||||
'/user/register',
|
||||
request,
|
||||
headers
|
||||
? ({ headers } as unknown as InternalAxiosRequestConfig)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
static async sendEmailCode(email: string, scene: 'register' | 'login', headers?: Record<string, string>): Promise<void> {
|
||||
return this.post<void>('/user/send-email-code', { email, scene }, headers ? ({ headers } as unknown as InternalAxiosRequestConfig) : undefined);
|
||||
static async sendEmailCode(
|
||||
email: string,
|
||||
scene: 'register' | 'login',
|
||||
headers?: Record<string, string>,
|
||||
): Promise<void> {
|
||||
return this.post<void>(
|
||||
'/user/send-email-code',
|
||||
{ email, scene },
|
||||
headers
|
||||
? ({ headers } as unknown as InternalAxiosRequestConfig)
|
||||
: undefined,
|
||||
);
|
||||
}
|
||||
|
||||
static async changePassword(request: ChangePasswordRequest): Promise<void> {
|
||||
@@ -130,6 +165,8 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
|
||||
static async deleteExternalAccountBinding(id: string): Promise<void> {
|
||||
return this.post<void>(`/oauth/external-accounts/${encodeURIComponent(id)}/delete`);
|
||||
return this.post<void>(
|
||||
`/oauth/external-accounts/${encodeURIComponent(id)}/delete`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 认证服务模块
|
||||
*
|
||||
*
|
||||
* @description
|
||||
* 提供 OAuth 认证相关的功能,包括:
|
||||
* - 获取登录 URL
|
||||
@@ -8,18 +8,18 @@
|
||||
* - 获取用户信息
|
||||
* - 用户登出
|
||||
* - 发起登录流程
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { AuthService } from '@/lib/services';
|
||||
*
|
||||
*
|
||||
* // 发起登录
|
||||
* await AuthService.initiateLogin();
|
||||
*
|
||||
*
|
||||
* // 获取用户信息
|
||||
* const user = await AuthService.getUserInfo();
|
||||
* console.log('当前用户:', user.username);
|
||||
*
|
||||
*
|
||||
* // 登出
|
||||
* await AuthService.logout();
|
||||
* ```
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
/**
|
||||
* 配置服务模块
|
||||
*
|
||||
*
|
||||
* @description
|
||||
* 提供系统公共配置相关的功能,包括:
|
||||
* - 获取公共配置(争议时间窗口等)
|
||||
* - 获取用户平台等级配置(公开接口)
|
||||
*
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* import { ConfigService } from '@/lib/services';
|
||||
*
|
||||
*
|
||||
* // 获取公共配置
|
||||
* const config = await ConfigService.getPublicConfig();
|
||||
* console.log('争议时间窗口:', config.dispute_time_window_hours, '小时');
|
||||
*
|
||||
*
|
||||
* // 获取用户平台等级配置
|
||||
* const payConfigs = await ConfigService.getUserPayConfigs();
|
||||
* console.log('平台等级列表:', payConfigs);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import axios, {AxiosError, AxiosResponse, CancelTokenSource, InternalAxiosRequestConfig} from 'axios';
|
||||
import axios, {AxiosError, AxiosResponse, CancelTokenSource, InternalAxiosRequestConfig,} from 'axios';
|
||||
import {toast} from 'sonner';
|
||||
import {apiConfig, getApiBaseUrl} from './config';
|
||||
import {
|
||||
@@ -50,20 +50,22 @@ const cancelTokens = new Map<string, CancelTokenSource>();
|
||||
*/
|
||||
const pendingRequests = new Map<string, Promise<AxiosResponse<ApiResponse>>>();
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 生成请求的唯一键
|
||||
* 包含方法、URL 和请求数据的哈希,确保不同参数的请求不会被误取消
|
||||
*/
|
||||
function getRequestKey(config: { method?: string; url?: string; data?: unknown }): string {
|
||||
const baseKey = `${ config.method?.toUpperCase() }_${ config.url }`;
|
||||
function getRequestKey(config: {
|
||||
method?: string;
|
||||
url?: string;
|
||||
data?: unknown;
|
||||
}): string {
|
||||
const baseKey = `${config.method?.toUpperCase()}_${config.url}`;
|
||||
|
||||
/* 序列化加入键中 */
|
||||
if (config.data) {
|
||||
try {
|
||||
const dataHash = JSON.stringify(config.data);
|
||||
return `${ baseKey }_${ dataHash }`;
|
||||
return `${baseKey}_${dataHash}`;
|
||||
} catch {
|
||||
// 失败使用基础键
|
||||
return baseKey;
|
||||
@@ -111,7 +113,7 @@ function initiateLogin(currentPath: string): Promise<never> {
|
||||
window.location.href = loginUrl.toString();
|
||||
}
|
||||
|
||||
return new Promise<never>(() => { });
|
||||
return new Promise<never>(() => {});
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -140,7 +142,9 @@ apiClient.interceptors.response.use(
|
||||
|
||||
/* 请求被取消时静默处理 */
|
||||
if (axios.isCancel(error)) {
|
||||
const cancelError = new Error(error.message || '请求已被取消') as Error & { __CANCEL__?: boolean };
|
||||
const cancelError = new Error(
|
||||
error.message || '请求已被取消',
|
||||
) as Error & { __CANCEL__?: boolean };
|
||||
cancelError.__CANCEL__ = true;
|
||||
return Promise.reject(cancelError);
|
||||
}
|
||||
@@ -153,7 +157,11 @@ apiClient.interceptors.response.use(
|
||||
/* 403 权限不足错误 */
|
||||
if (error.response?.status === 403) {
|
||||
return Promise.reject(
|
||||
new ForbiddenError(error.response.data?.error_msg || '权限不足,请过盾后重试', error.response.data?.error_code, error.response.data?.details),
|
||||
new ForbiddenError(
|
||||
error.response.data?.error_msg || '权限不足,请过盾后重试',
|
||||
error.response.data?.error_code,
|
||||
error.response.data?.details,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -177,17 +185,16 @@ apiClient.interceptors.response.use(
|
||||
/* 429 速率限制错误 */
|
||||
if (error.response?.status === 429) {
|
||||
const retryAfter = error.response.headers?.['retry-after'];
|
||||
const message = error.response.data?.error_msg ||
|
||||
`请求过于频繁,请 ${ retryAfter || '稍后' } 秒后重试`;
|
||||
const message =
|
||||
error.response.data?.error_msg ||
|
||||
`请求过于频繁,请 ${retryAfter || '稍后'} 秒后重试`;
|
||||
|
||||
toast.error('请求频率限制', {
|
||||
description: message,
|
||||
id: 'rate-limit-error',
|
||||
});
|
||||
|
||||
return Promise.reject(
|
||||
new ApiErrorBase(message, 'RATE_LIMITED', 429),
|
||||
);
|
||||
return Promise.reject(new ApiErrorBase(message, 'RATE_LIMITED', 429));
|
||||
}
|
||||
|
||||
/* 5xx 服务器错误 */
|
||||
@@ -231,9 +238,7 @@ apiClient.interceptors.response.use(
|
||||
}
|
||||
|
||||
/* 兜底错误 */
|
||||
return Promise.reject(
|
||||
new ApiErrorBase(error.message || '网络请求失败'),
|
||||
);
|
||||
return Promise.reject(new ApiErrorBase(error.message || '网络请求失败'));
|
||||
},
|
||||
);
|
||||
|
||||
@@ -243,7 +248,7 @@ apiClient.interceptors.response.use(
|
||||
* @param url - 请求 URL
|
||||
*/
|
||||
export function cancelRequest(method: string, url: string): void {
|
||||
const requestKey = `${ method.toUpperCase() }_${ url }`;
|
||||
const requestKey = `${method.toUpperCase()}_${url}`;
|
||||
const source = cancelTokens.get(requestKey);
|
||||
if (source) {
|
||||
source.cancel('请求已被手动取消');
|
||||
@@ -268,18 +273,29 @@ export function cancelAllRequests(): void {
|
||||
*/
|
||||
function createRequestMethod(
|
||||
method: 'get' | 'post' | 'put' | 'patch' | 'delete',
|
||||
hasBody: boolean
|
||||
hasBody: boolean,
|
||||
) {
|
||||
if (hasBody) {
|
||||
return <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => {
|
||||
const requestKey = getRequestKey({ method: method.toUpperCase(), url, data });
|
||||
return <T = ApiResponse>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) => {
|
||||
const requestKey = getRequestKey({
|
||||
method: method.toUpperCase(),
|
||||
url,
|
||||
data,
|
||||
});
|
||||
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
return pendingRequests.get(requestKey) as Promise<AxiosResponse<T>>;
|
||||
}
|
||||
|
||||
const promise = apiClient[method]<T>(url, data, config);
|
||||
pendingRequests.set(requestKey, promise as Promise<AxiosResponse<ApiResponse>>);
|
||||
pendingRequests.set(
|
||||
requestKey,
|
||||
promise as Promise<AxiosResponse<ApiResponse>>,
|
||||
);
|
||||
|
||||
promise.then(
|
||||
() => pendingRequests.delete(requestKey),
|
||||
@@ -290,15 +306,25 @@ function createRequestMethod(
|
||||
};
|
||||
}
|
||||
|
||||
return <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => {
|
||||
const requestKey = getRequestKey({ method: method.toUpperCase(), url, data: config?.params });
|
||||
return <T = ApiResponse>(
|
||||
url: string,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) => {
|
||||
const requestKey = getRequestKey({
|
||||
method: method.toUpperCase(),
|
||||
url,
|
||||
data: config?.params,
|
||||
});
|
||||
|
||||
if (pendingRequests.has(requestKey)) {
|
||||
return pendingRequests.get(requestKey) as Promise<AxiosResponse<T>>;
|
||||
}
|
||||
|
||||
const promise = apiClient[method]<T>(url, config);
|
||||
pendingRequests.set(requestKey, promise as Promise<AxiosResponse<ApiResponse>>);
|
||||
pendingRequests.set(
|
||||
requestKey,
|
||||
promise as Promise<AxiosResponse<ApiResponse>>,
|
||||
);
|
||||
|
||||
promise.then(
|
||||
() => pendingRequests.delete(requestKey),
|
||||
@@ -314,11 +340,29 @@ function createRequestMethod(
|
||||
* 在原有 axios 实例基础上添加请求缓存功能
|
||||
*/
|
||||
const wrappedApiClient = {
|
||||
get: createRequestMethod('get', false) as <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
|
||||
post: createRequestMethod('post', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
|
||||
put: createRequestMethod('put', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
|
||||
patch: createRequestMethod('patch', true) as <T = ApiResponse>(url: string, data?: unknown, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
|
||||
delete: createRequestMethod('delete', false) as <T = ApiResponse>(url: string, config?: InternalAxiosRequestConfig) => Promise<AxiosResponse<T>>,
|
||||
get: createRequestMethod('get', false) as <T = ApiResponse>(
|
||||
url: string,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) => Promise<AxiosResponse<T>>,
|
||||
post: createRequestMethod('post', true) as <T = ApiResponse>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) => Promise<AxiosResponse<T>>,
|
||||
put: createRequestMethod('put', true) as <T = ApiResponse>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) => Promise<AxiosResponse<T>>,
|
||||
patch: createRequestMethod('patch', true) as <T = ApiResponse>(
|
||||
url: string,
|
||||
data?: unknown,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) => Promise<AxiosResponse<T>>,
|
||||
delete: createRequestMethod('delete', false) as <T = ApiResponse>(
|
||||
url: string,
|
||||
config?: InternalAxiosRequestConfig,
|
||||
) => Promise<AxiosResponse<T>>,
|
||||
};
|
||||
|
||||
export default wrappedApiClient;
|
||||
|
||||
@@ -30,7 +30,7 @@ export class BaseService {
|
||||
* @returns 完整路径
|
||||
*/
|
||||
protected static getFullPath(path: string): string {
|
||||
return `${ this.basePath }${ path }`;
|
||||
return `${this.basePath}${path}`;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,14 +154,10 @@ export class BaseService {
|
||||
* @remarks
|
||||
* 仅用于不遵循标准响应格式的特殊端点(如 /api.php)
|
||||
*/
|
||||
protected static async rawGet<T>(
|
||||
url: string,
|
||||
params?: unknown,
|
||||
): Promise<T> {
|
||||
const response = await apiClient.get<T>(
|
||||
url,
|
||||
{ params } as InternalAxiosRequestConfig,
|
||||
);
|
||||
protected static async rawGet<T>(url: string, params?: unknown): Promise<T> {
|
||||
const response = await apiClient.get<T>(url, {
|
||||
params,
|
||||
} as InternalAxiosRequestConfig);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
@@ -185,4 +181,3 @@ export class BaseService {
|
||||
return response.data;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,11 @@
|
||||
*/
|
||||
export function getApiBaseUrl(): string {
|
||||
if (typeof window === 'undefined') {
|
||||
return process.env.WAVELET_BACKEND_URL || process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || 'http://localhost:3000';
|
||||
return (
|
||||
process.env.WAVELET_BACKEND_URL ||
|
||||
process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL ||
|
||||
'http://localhost:3000'
|
||||
);
|
||||
}
|
||||
return process.env.NEXT_PUBLIC_WAVELET_BACKEND_URL || '';
|
||||
}
|
||||
@@ -29,4 +33,3 @@ export const apiConfig = {
|
||||
/** 携带凭证 */
|
||||
withCredentials: true,
|
||||
} as const;
|
||||
|
||||
|
||||
@@ -101,5 +101,12 @@ export class ValidationError extends ApiErrorBase {
|
||||
* @returns 是否为取消错误
|
||||
*/
|
||||
export function isCancelError(error: unknown): boolean {
|
||||
return error !== null && typeof error === 'object' && ('__CANCEL__' in error && (error as { __CANCEL__?: boolean }).__CANCEL__ === true || ('message' in error && (error as { message?: string }).message === '请求已被取消'));
|
||||
return (
|
||||
error !== null &&
|
||||
typeof error === 'object' &&
|
||||
(('__CANCEL__' in error &&
|
||||
(error as { __CANCEL__?: boolean }).__CANCEL__ === true) ||
|
||||
('message' in error &&
|
||||
(error as { message?: string }).message === '请求已被取消'))
|
||||
);
|
||||
}
|
||||
|
||||
@@ -3,7 +3,11 @@
|
||||
* 提供 API 请求的基础设施
|
||||
*/
|
||||
|
||||
export { default as apiClient, cancelRequest, cancelAllRequests } from './api-client';
|
||||
export {
|
||||
default as apiClient,
|
||||
cancelRequest,
|
||||
cancelAllRequests,
|
||||
} from './api-client';
|
||||
export { BaseService } from './base.service';
|
||||
export { apiConfig } from './config';
|
||||
export {
|
||||
@@ -24,4 +28,3 @@ export type {
|
||||
PaginationResponse,
|
||||
RequestConfig,
|
||||
} from './types';
|
||||
|
||||
|
||||
@@ -61,4 +61,3 @@ export interface RequestConfig {
|
||||
/** 是否携带凭证 */
|
||||
withCredentials?: boolean;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import type {InternalAxiosRequestConfig} from 'axios';
|
||||
|
||||
import apiClient from '@/lib/services/core/api-client';
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import type {DBOverview, ExecuteSQLResponse, TableDataResponse} from './types';
|
||||
import type {DBOverview, ExecuteSQLResponse, TableDataResponse,} from './types';
|
||||
|
||||
/**
|
||||
* 数据库管理服务类
|
||||
@@ -32,7 +32,10 @@ export class DbManageService extends BaseService {
|
||||
page: number;
|
||||
pageSize: number;
|
||||
}): Promise<TableDataResponse> {
|
||||
return this.get<TableDataResponse>('/table-data', params as unknown as Record<string, unknown>);
|
||||
return this.get<TableDataResponse>(
|
||||
'/table-data',
|
||||
params as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -51,7 +54,8 @@ export class DbManageService extends BaseService {
|
||||
responseType: 'blob',
|
||||
} as InternalAxiosRequestConfig);
|
||||
|
||||
const disposition = response.headers['content-disposition'] as string | undefined;
|
||||
const disposition = response.headers['content-disposition'] as
|
||||
string | undefined;
|
||||
let filename = 'openflare_export';
|
||||
if (disposition) {
|
||||
const match = disposition.match(/filename="?([^";]+)"?/);
|
||||
|
||||
@@ -1,2 +1,6 @@
|
||||
export { DbManageService } from './db-manage.service';
|
||||
export type { DBOverview, TableDataResponse, ExecuteSQLResponse } from './types';
|
||||
export type {
|
||||
DBOverview,
|
||||
TableDataResponse,
|
||||
ExecuteSQLResponse,
|
||||
} from './types';
|
||||
|
||||
@@ -23,7 +23,7 @@ export interface TableDataResponse {
|
||||
* 执行自定义 SQL 的响应
|
||||
*/
|
||||
export interface ExecuteSQLResponse {
|
||||
type: "select" | "exec";
|
||||
type: 'select' | 'exec';
|
||||
columns?: string[];
|
||||
results?: Record<string, unknown>[];
|
||||
affected_rows: number;
|
||||
|
||||
@@ -20,12 +20,12 @@ import {
|
||||
AdminTemplateService,
|
||||
AdminUserService,
|
||||
} from './admin';
|
||||
import {AuthService} from './auth';
|
||||
import {ConfigService} from './config';
|
||||
import {DbManageService} from './db-manage';
|
||||
import {PushService} from './push';
|
||||
import {AdminUploadService, UploadService} from './upload';
|
||||
import {UserService} from './user';
|
||||
import { AuthService } from './auth';
|
||||
import { ConfigService } from './config';
|
||||
import { DbManageService } from './db-manage';
|
||||
import { PushService } from './push';
|
||||
import { AdminUploadService, UploadService } from './upload';
|
||||
import { UserService } from './user';
|
||||
import {
|
||||
AccessLogService,
|
||||
ApplyLogService,
|
||||
@@ -115,7 +115,15 @@ export type {
|
||||
// ==================== 业务服务导出 ====================
|
||||
|
||||
export { AuthService } from './auth';
|
||||
export type { User, OAuthLoginUrlResponse, OAuthCallbackRequest, AuthSource, ExternalAccountBinding, ChangePasswordRequest, UpdateProfileRequest } from './auth';
|
||||
export type {
|
||||
User,
|
||||
OAuthLoginUrlResponse,
|
||||
OAuthCallbackRequest,
|
||||
AuthSource,
|
||||
ExternalAccountBinding,
|
||||
ChangePasswordRequest,
|
||||
UpdateProfileRequest,
|
||||
} from './auth';
|
||||
|
||||
export { ConfigService } from './config';
|
||||
export type { PublicConfigResponse } from './config';
|
||||
@@ -170,14 +178,37 @@ export type {
|
||||
export { UserService } from './user';
|
||||
export type { AccessToken, CreateTokenResponse } from './user';
|
||||
|
||||
export { UploadService, AdminUploadService, formatFileSize, getFileUrl } from './upload';
|
||||
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse, ImageQuality } from './upload';
|
||||
export {
|
||||
UploadService,
|
||||
AdminUploadService,
|
||||
formatFileSize,
|
||||
getFileUrl,
|
||||
} from './upload';
|
||||
export type {
|
||||
UploadImageResponse,
|
||||
Upload,
|
||||
ListUploadsResponse,
|
||||
FileStatsResponse,
|
||||
ImageQuality,
|
||||
} from './upload';
|
||||
|
||||
export { DbManageService } from './db-manage';
|
||||
export type { DBOverview, TableDataResponse, ExecuteSQLResponse } from './db-manage';
|
||||
export type {
|
||||
DBOverview,
|
||||
TableDataResponse,
|
||||
ExecuteSQLResponse,
|
||||
} from './db-manage';
|
||||
|
||||
export { PushService } from './push';
|
||||
export type { PushEvent, PushHistory, PushChannelConfig, ListPushHistoriesRequest, ListPushHistoriesResponse, UpdatePushEventRequest, TestPushRequest } from './push';
|
||||
export type {
|
||||
PushEvent,
|
||||
PushHistory,
|
||||
PushChannelConfig,
|
||||
ListPushHistoriesRequest,
|
||||
ListPushHistoriesResponse,
|
||||
UpdatePushEventRequest,
|
||||
TestPushRequest,
|
||||
} from './push';
|
||||
|
||||
// ==================== OpenFlare 业务服务导出 ====================
|
||||
|
||||
|
||||
@@ -16,12 +16,12 @@ import type {
|
||||
|
||||
function buildSearchParams(filters: object): Record<string, unknown> {
|
||||
const params: Record<string, unknown> = {};
|
||||
Object.entries(filters as Record<string, string | number | undefined>).forEach(
|
||||
([key, value]) => {
|
||||
if (value === undefined || value === null || value === '') return;
|
||||
params[key] = value;
|
||||
},
|
||||
);
|
||||
Object.entries(
|
||||
filters as Record<string, string | number | undefined>,
|
||||
).forEach(([key, value]) => {
|
||||
if (value === undefined || value === null || value === '') return;
|
||||
params[key] = value;
|
||||
});
|
||||
return params;
|
||||
}
|
||||
|
||||
@@ -35,10 +35,7 @@ export class AccessLogService extends OpenFlareBaseService {
|
||||
static listFolds(
|
||||
filters: FoldedAccessLogFilters,
|
||||
): Promise<FoldedAccessLogList> {
|
||||
return this.get<FoldedAccessLogList>(
|
||||
'/folds',
|
||||
buildSearchParams(filters),
|
||||
);
|
||||
return this.get<FoldedAccessLogList>('/folds', buildSearchParams(filters));
|
||||
}
|
||||
|
||||
static listFoldIPs(
|
||||
@@ -73,4 +70,4 @@ export class AccessLogService extends OpenFlareBaseService {
|
||||
): Promise<AccessLogCleanupResult> {
|
||||
return this.post<AccessLogCleanupResult>('/cleanup', payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,5 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import type {
|
||||
ApplyLogCleanupPayload,
|
||||
ApplyLogCleanupResult,
|
||||
ApplyLogList,
|
||||
ApplyLogListQuery,
|
||||
} from './types';
|
||||
import type {ApplyLogCleanupPayload, ApplyLogCleanupResult, ApplyLogList, ApplyLogListQuery,} from './types';
|
||||
|
||||
export class ApplyLogService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/apply-logs';
|
||||
@@ -26,7 +21,9 @@ export class ApplyLogService extends OpenFlareBaseService {
|
||||
return this.get<ApplyLogList>('/', params);
|
||||
}
|
||||
|
||||
static cleanup(payload: ApplyLogCleanupPayload): Promise<ApplyLogCleanupResult> {
|
||||
static cleanup(
|
||||
payload: ApplyLogCleanupPayload,
|
||||
): Promise<ApplyLogCleanupResult> {
|
||||
return this.post<ApplyLogCleanupResult>('/cleanup', payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,4 @@ import {BaseService} from '@/lib/services/core';
|
||||
|
||||
export class OpenFlareBaseService extends BaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,8 @@ import type {
|
||||
} from './types';
|
||||
|
||||
export class ConfigVersionService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/config-versions';
|
||||
protected static override readonly basePath: string =
|
||||
'/api/v1/d/config-versions';
|
||||
|
||||
static list(): Promise<ConfigVersionSummary[]> {
|
||||
return this.get<ConfigVersionSummary[]>('/');
|
||||
@@ -52,4 +53,4 @@ export class ConfigVersionService extends OpenFlareBaseService {
|
||||
): Promise<ConfigVersionCleanupResult> {
|
||||
return this.post<ConfigVersionCleanupResult>('/cleanup', payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -161,9 +161,7 @@ function normalizeDiskIOTrendPoints(
|
||||
|
||||
function normalizeDashboardNodes(
|
||||
items:
|
||||
| Array<DashboardNodeHealth | CompactDashboardNodeHealth>
|
||||
| null
|
||||
| undefined,
|
||||
Array<DashboardNodeHealth | CompactDashboardNodeHealth> | null | undefined,
|
||||
): DashboardNodeHealth[] {
|
||||
return arrayOrEmpty(items).map((item) =>
|
||||
isCompactDashboardNode(item)
|
||||
|
||||
@@ -2,7 +2,8 @@ import {OpenFlareBaseService} from './base.service';
|
||||
import type {DnsAccountItem, DnsAccountMutationPayload} from './types';
|
||||
|
||||
export class DnsAccountService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/dns-accounts';
|
||||
protected static override readonly basePath: string =
|
||||
'/api/v1/d/dns-accounts';
|
||||
|
||||
static async list(): Promise<DnsAccountItem[]> {
|
||||
return this.get<DnsAccountItem[]>('/');
|
||||
@@ -24,4 +25,4 @@ export class DnsAccountService extends OpenFlareBaseService {
|
||||
static async deleteById(id: number): Promise<void> {
|
||||
return this.post<void>(`/${id}/delete`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -126,21 +126,21 @@ export type {
|
||||
TlsCertificateMutationPayload,
|
||||
} from './types';
|
||||
|
||||
import {AccessLogService} from './access-log.service';
|
||||
import {ApplyLogService} from './apply-log.service';
|
||||
import {ConfigVersionService} from './config-version.service';
|
||||
import {DashboardService} from './dashboard.service';
|
||||
import {DnsAccountService} from './dns-account.service';
|
||||
import {NodeService} from './node.service';
|
||||
import {OptionService} from './option.service';
|
||||
import {StatusService} from './status.service';
|
||||
import {UptimeKumaService} from './uptimekuma.service';
|
||||
import {OriginService} from './origin.service';
|
||||
import {PagesService} from './pages.service';
|
||||
import {ProxyRouteService} from './proxy-route.service';
|
||||
import {TlsCertificateService} from './tls-certificate.service';
|
||||
import {WafService} from './waf.service';
|
||||
import {ZoneDomainService, ZoneService} from './zone.service';
|
||||
import { AccessLogService } from './access-log.service';
|
||||
import { ApplyLogService } from './apply-log.service';
|
||||
import { ConfigVersionService } from './config-version.service';
|
||||
import { DashboardService } from './dashboard.service';
|
||||
import { DnsAccountService } from './dns-account.service';
|
||||
import { NodeService } from './node.service';
|
||||
import { OptionService } from './option.service';
|
||||
import { StatusService } from './status.service';
|
||||
import { UptimeKumaService } from './uptimekuma.service';
|
||||
import { OriginService } from './origin.service';
|
||||
import { PagesService } from './pages.service';
|
||||
import { ProxyRouteService } from './proxy-route.service';
|
||||
import { TlsCertificateService } from './tls-certificate.service';
|
||||
import { WafService } from './waf.service';
|
||||
import { ZoneDomainService, ZoneService } from './zone.service';
|
||||
|
||||
export const openflareServices = {
|
||||
node: NodeService,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type {
|
||||
NodeAgentReleaseInfo,
|
||||
NodeAgentUpdatePayload,
|
||||
@@ -20,7 +20,10 @@ export class NodeService extends OpenFlareBaseService {
|
||||
return this.post<NodeItem>('/', payload);
|
||||
}
|
||||
|
||||
static async updateNode(id: number, payload: NodeMutationPayload): Promise<NodeItem> {
|
||||
static async updateNode(
|
||||
id: number,
|
||||
payload: NodeMutationPayload,
|
||||
): Promise<NodeItem> {
|
||||
return this.post<NodeItem>(`/${id}/update`, payload);
|
||||
}
|
||||
|
||||
@@ -75,4 +78,4 @@ export class NodeService extends OpenFlareBaseService {
|
||||
`/${id}/observability/cleanup`,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type {
|
||||
DatabaseCleanupPayload,
|
||||
DatabaseCleanupResult,
|
||||
@@ -32,4 +32,4 @@ export class OptionService extends OpenFlareBaseService {
|
||||
): Promise<DatabaseCleanupResult> {
|
||||
return this.post<DatabaseCleanupResult>('/database/cleanup', payload);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import type {OriginDetail, OriginItem, OriginMutationPayload} from './types';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type { OriginDetail, OriginItem, OriginMutationPayload } from './types';
|
||||
|
||||
export class OriginService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/origins';
|
||||
@@ -26,4 +26,4 @@ export class OriginService extends OpenFlareBaseService {
|
||||
static deleteById(id: number): Promise<void> {
|
||||
return this.post<void>(`/${id}/delete`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type {AxiosProgressEvent, InternalAxiosRequestConfig} from 'axios';
|
||||
import type { AxiosProgressEvent, InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
import apiClient from '@/lib/services/core/api-client';
|
||||
import {apiConfig} from '@/lib/services/core/config';
|
||||
import type {ApiResponse} from '@/lib/services/core';
|
||||
import { apiConfig } from '@/lib/services/core/config';
|
||||
import type { ApiResponse } from '@/lib/services/core';
|
||||
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type {
|
||||
PagesDeployment,
|
||||
PagesDeploymentFile,
|
||||
@@ -80,9 +80,7 @@ export class PagesService extends OpenFlareBaseService {
|
||||
projectId: number,
|
||||
deploymentId: number,
|
||||
): Promise<void> {
|
||||
return this.post<void>(
|
||||
`/${projectId}/deployments/${deploymentId}/delete`,
|
||||
);
|
||||
return this.post<void>(`/${projectId}/deployments/${deploymentId}/delete`);
|
||||
}
|
||||
|
||||
private static async postFormData<T>(
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import type {ProxyRouteItem, ProxyRouteMutationPayload} from './types';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type { ProxyRouteItem, ProxyRouteMutationPayload } from './types';
|
||||
|
||||
export class ProxyRouteService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/proxy-routes';
|
||||
protected static override readonly basePath: string =
|
||||
'/api/v1/d/proxy-routes';
|
||||
|
||||
static async list(): Promise<ProxyRouteItem[]> {
|
||||
return this.get<ProxyRouteItem[]>('/');
|
||||
@@ -12,7 +13,9 @@ export class ProxyRouteService extends OpenFlareBaseService {
|
||||
return this.get<ProxyRouteItem>(`/${id}`);
|
||||
}
|
||||
|
||||
static async create(payload: ProxyRouteMutationPayload): Promise<ProxyRouteItem> {
|
||||
static async create(
|
||||
payload: ProxyRouteMutationPayload,
|
||||
): Promise<ProxyRouteItem> {
|
||||
return this.post<ProxyRouteItem>('/', payload);
|
||||
}
|
||||
|
||||
@@ -26,4 +29,4 @@ export class ProxyRouteService extends OpenFlareBaseService {
|
||||
static async deleteById(id: number): Promise<void> {
|
||||
return this.post<void>(`/${id}/delete`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import type {OpenFlarePublicStatus} from './types';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type { OpenFlarePublicStatus } from './types';
|
||||
|
||||
export class StatusService extends OpenFlareBaseService {
|
||||
static getPublicStatus(): Promise<OpenFlarePublicStatus> {
|
||||
return this.get<OpenFlarePublicStatus>('/status');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type {
|
||||
AcmeAccountItem,
|
||||
TlsCertificateApplyPayload,
|
||||
@@ -10,7 +10,8 @@ import type {
|
||||
} from './types';
|
||||
|
||||
class AcmeAccountApi extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/acme-accounts';
|
||||
protected static override readonly basePath: string =
|
||||
'/api/v1/d/acme-accounts';
|
||||
|
||||
static getDefault(): Promise<AcmeAccountItem> {
|
||||
return this.get<AcmeAccountItem>('/default');
|
||||
@@ -18,7 +19,8 @@ class AcmeAccountApi extends OpenFlareBaseService {
|
||||
}
|
||||
|
||||
export class TlsCertificateService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/tls-certificates';
|
||||
protected static override readonly basePath: string =
|
||||
'/api/v1/d/tls-certificates';
|
||||
|
||||
static async list(): Promise<TlsCertificateItem[]> {
|
||||
return this.get<TlsCertificateItem[]>('/');
|
||||
@@ -88,4 +90,4 @@ export class TlsCertificateService extends OpenFlareBaseService {
|
||||
static getDefaultAcmeAccount(): Promise<AcmeAccountItem> {
|
||||
return AcmeAccountApi.getDefault();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type {XYPosition} from '@xyflow/react';
|
||||
import type { XYPosition } from '@xyflow/react';
|
||||
|
||||
export type ReleaseChannel = 'stable' | 'preview';
|
||||
|
||||
@@ -201,12 +201,7 @@ export interface NodeObservability {
|
||||
}
|
||||
|
||||
export type ProxyRouteConfigSection =
|
||||
| 'domains'
|
||||
| 'limits'
|
||||
| 'proxy'
|
||||
| 'cache'
|
||||
| 'waf'
|
||||
| 'auth';
|
||||
'domains' | 'limits' | 'proxy' | 'cache' | 'waf' | 'auth';
|
||||
|
||||
export interface ProxyRouteCustomHeader {
|
||||
key: string;
|
||||
@@ -666,9 +661,7 @@ export interface GeoIPLookupResult {
|
||||
}
|
||||
|
||||
export type DatabaseCleanupTarget =
|
||||
| 'node_access_logs'
|
||||
| 'node_metric_snapshots'
|
||||
| 'node_request_reports';
|
||||
'node_access_logs' | 'node_metric_snapshots' | 'node_request_reports';
|
||||
|
||||
export interface DatabaseCleanupPayload {
|
||||
target: DatabaseCleanupTarget;
|
||||
@@ -715,12 +708,37 @@ export interface BlockNodeConfig {
|
||||
}
|
||||
|
||||
export type WAFRuleNode =
|
||||
| {id: string; type: 'start'; position: XYPosition; config: Record<string, never>}
|
||||
| {id: string; type: 'ip_match'; position: XYPosition; config: IPMatchConfig}
|
||||
| {id: string; type: 'geo_match'; position: XYPosition; config: GeoMatchConfig}
|
||||
| {id: string; type: 'pow'; position: XYPosition; config: PoWNodeConfig}
|
||||
| {id: string; type: 'allow'; position: XYPosition; config: Record<string, never>}
|
||||
| {id: string; type: 'block'; position: XYPosition; config: BlockNodeConfig};
|
||||
| {
|
||||
id: string;
|
||||
type: 'start';
|
||||
position: XYPosition;
|
||||
config: Record<string, never>;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: 'ip_match';
|
||||
position: XYPosition;
|
||||
config: IPMatchConfig;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: 'geo_match';
|
||||
position: XYPosition;
|
||||
config: GeoMatchConfig;
|
||||
}
|
||||
| { id: string; type: 'pow'; position: XYPosition; config: PoWNodeConfig }
|
||||
| {
|
||||
id: string;
|
||||
type: 'allow';
|
||||
position: XYPosition;
|
||||
config: Record<string, never>;
|
||||
}
|
||||
| {
|
||||
id: string;
|
||||
type: 'block';
|
||||
position: XYPosition;
|
||||
config: BlockNodeConfig;
|
||||
};
|
||||
|
||||
export interface WAFRuleEdge {
|
||||
id: string;
|
||||
@@ -1077,10 +1095,6 @@ export interface ZoneStats {
|
||||
series: ZoneStatsPoint[];
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export interface TlsCertificateItem {
|
||||
id: number;
|
||||
name: string;
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
|
||||
export class UptimeKumaService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath: string = '/api/v1/d/uptimekuma';
|
||||
@@ -6,4 +6,4 @@ export class UptimeKumaService extends OpenFlareBaseService {
|
||||
static sync(): Promise<void> {
|
||||
return this.post<void>('/sync');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {OpenFlareBaseService} from './base.service';
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type {
|
||||
WAFIPGroup,
|
||||
WAFIPGroupAutoTestPayload,
|
||||
@@ -70,7 +70,10 @@ export class WafService extends OpenFlareBaseService {
|
||||
return this.post<WAFIPGroup>('/ip-groups', payload);
|
||||
}
|
||||
|
||||
static async updateIPGroup(id: number, payload: WAFIPGroupPayload): Promise<WAFIPGroup> {
|
||||
static async updateIPGroup(
|
||||
id: number,
|
||||
payload: WAFIPGroupPayload,
|
||||
): Promise<WAFIPGroup> {
|
||||
return this.post<WAFIPGroup>(`/ip-groups/${id}/update`, payload);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {OpenFlareBaseService} from './base.service'
|
||||
import { OpenFlareBaseService } from './base.service';
|
||||
import type {
|
||||
ZoneDomainItem,
|
||||
ZoneDomainMutationPayload,
|
||||
@@ -7,27 +7,56 @@ import type {
|
||||
ZoneOverview,
|
||||
ZoneStats,
|
||||
ZoneStatsRange,
|
||||
} from './types'
|
||||
} from './types';
|
||||
|
||||
export const zoneQueryKey = ['openflare', 'zones'] as const
|
||||
export const zoneQueryKey = ['openflare', 'zones'] as const;
|
||||
|
||||
export class ZoneService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath = '/api/v1/d/zones'
|
||||
protected static override readonly basePath = '/api/v1/d/zones';
|
||||
|
||||
static list(): Promise<ZoneItem[]> { return this.get<ZoneItem[]>('/') }
|
||||
static getOverview(id: number): Promise<ZoneOverview> { return this.get<ZoneOverview>(`/${id}/overview`) }
|
||||
static getStats(id: number, range: ZoneStatsRange = '24h'): Promise<ZoneStats> {
|
||||
return this.get<ZoneStats>(`/${id}/stats`, {range})
|
||||
static list(): Promise<ZoneItem[]> {
|
||||
return this.get<ZoneItem[]>('/');
|
||||
}
|
||||
static getOverview(id: number): Promise<ZoneOverview> {
|
||||
return this.get<ZoneOverview>(`/${id}/overview`);
|
||||
}
|
||||
static getStats(
|
||||
id: number,
|
||||
range: ZoneStatsRange = '24h',
|
||||
): Promise<ZoneStats> {
|
||||
return this.get<ZoneStats>(`/${id}/stats`, { range });
|
||||
}
|
||||
static create(payload: ZoneMutationPayload): Promise<ZoneItem> {
|
||||
return this.post<ZoneItem>('/', payload);
|
||||
}
|
||||
static update(id: number, payload: ZoneMutationPayload): Promise<ZoneItem> {
|
||||
return this.post<ZoneItem>(`/${id}/update`, payload);
|
||||
}
|
||||
static deleteById(id: number): Promise<void> {
|
||||
return this.post<void>(`/${id}/delete`);
|
||||
}
|
||||
static create(payload: ZoneMutationPayload): Promise<ZoneItem> { return this.post<ZoneItem>('/', payload) }
|
||||
static update(id: number, payload: ZoneMutationPayload): Promise<ZoneItem> { return this.post<ZoneItem>(`/${id}/update`, payload) }
|
||||
static deleteById(id: number): Promise<void> { return this.post<void>(`/${id}/delete`) }
|
||||
}
|
||||
|
||||
export class ZoneDomainService extends OpenFlareBaseService {
|
||||
protected static override readonly basePath = '/api/v1/d/zones'
|
||||
protected static override readonly basePath = '/api/v1/d/zones';
|
||||
|
||||
static create(zoneId: number, payload: ZoneDomainMutationPayload): Promise<ZoneDomainItem> { return this.post<ZoneDomainItem>(`/${zoneId}/domains`, payload) }
|
||||
static update(zoneId: number, id: number, payload: ZoneDomainMutationPayload): Promise<ZoneDomainItem> { return this.post<ZoneDomainItem>(`/${zoneId}/domains/${id}/update`, payload) }
|
||||
static deleteById(zoneId: number, id: number): Promise<void> { return this.post<void>(`/${zoneId}/domains/${id}/delete`) }
|
||||
static create(
|
||||
zoneId: number,
|
||||
payload: ZoneDomainMutationPayload,
|
||||
): Promise<ZoneDomainItem> {
|
||||
return this.post<ZoneDomainItem>(`/${zoneId}/domains`, payload);
|
||||
}
|
||||
static update(
|
||||
zoneId: number,
|
||||
id: number,
|
||||
payload: ZoneDomainMutationPayload,
|
||||
): Promise<ZoneDomainItem> {
|
||||
return this.post<ZoneDomainItem>(
|
||||
`/${zoneId}/domains/${id}/update`,
|
||||
payload,
|
||||
);
|
||||
}
|
||||
static deleteById(zoneId: number, id: number): Promise<void> {
|
||||
return this.post<void>(`/${zoneId}/domains/${id}/delete`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
// Copyright 2026 Arctel.net
|
||||
// SPDX-License-Identifier: Apache-2.0
|
||||
|
||||
import {BaseService} from '@/lib/services/core';
|
||||
import { BaseService } from '@/lib/services/core';
|
||||
import type {
|
||||
ChannelDefinition,
|
||||
CreateChannelRequest,
|
||||
@@ -14,7 +14,7 @@ import type {
|
||||
TestChannelRequest,
|
||||
TestPushRequest,
|
||||
UpdateChannelRequest,
|
||||
UpdatePushEventRequest
|
||||
UpdatePushEventRequest,
|
||||
} from './types';
|
||||
|
||||
/**
|
||||
@@ -41,14 +41,23 @@ export class PushService extends BaseService {
|
||||
* 创建通知事件配置
|
||||
*/
|
||||
static async createEvent(data: CreatePushEventRequest): Promise<PushEvent> {
|
||||
return this.post<PushEvent>('/events', data as unknown as Record<string, unknown>);
|
||||
return this.post<PushEvent>(
|
||||
'/events',
|
||||
data as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定通知事件配置
|
||||
*/
|
||||
static async updateEvent(id: number, data: UpdatePushEventRequest): Promise<void> {
|
||||
return this.put<void>(`/events/${id}`, data as unknown as Record<string, unknown>);
|
||||
static async updateEvent(
|
||||
id: number,
|
||||
data: UpdatePushEventRequest,
|
||||
): Promise<void> {
|
||||
return this.put<void>(
|
||||
`/events/${id}`,
|
||||
data as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -68,8 +77,13 @@ export class PushService extends BaseService {
|
||||
/**
|
||||
* 分页查询通知推送历史
|
||||
*/
|
||||
static async listHistories(params: ListPushHistoriesRequest): Promise<ListPushHistoriesResponse> {
|
||||
return this.get<ListPushHistoriesResponse>('/histories', params as unknown as Record<string, unknown>);
|
||||
static async listHistories(
|
||||
params: ListPushHistoriesRequest,
|
||||
): Promise<ListPushHistoriesResponse> {
|
||||
return this.get<ListPushHistoriesResponse>(
|
||||
'/histories',
|
||||
params as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -90,14 +104,23 @@ export class PushService extends BaseService {
|
||||
* 创建新的消息通道
|
||||
*/
|
||||
static async createChannel(data: CreateChannelRequest): Promise<PushChannel> {
|
||||
return this.post<PushChannel>('/channels', data as unknown as Record<string, unknown>);
|
||||
return this.post<PushChannel>(
|
||||
'/channels',
|
||||
data as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新指定消息通道
|
||||
*/
|
||||
static async updateChannel(id: number, data: UpdateChannelRequest): Promise<PushChannel> {
|
||||
return this.put<PushChannel>(`/channels/${id}`, data as unknown as Record<string, unknown>);
|
||||
static async updateChannel(
|
||||
id: number,
|
||||
data: UpdateChannelRequest,
|
||||
): Promise<PushChannel> {
|
||||
return this.put<PushChannel>(
|
||||
`/channels/${id}`,
|
||||
data as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -111,7 +134,10 @@ export class PushService extends BaseService {
|
||||
* 测试消息通道连通性
|
||||
*/
|
||||
static async testChannel(data: TestChannelRequest): Promise<void> {
|
||||
return this.post<void>('/channels/test', data as unknown as Record<string, unknown>);
|
||||
return this.post<void>(
|
||||
'/channels/test',
|
||||
data as unknown as Record<string, unknown>,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -135,4 +135,3 @@ export interface ChannelDefinition {
|
||||
description: string;
|
||||
fields: ChannelFieldDef[];
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,10 @@ export class AdminUploadService extends BaseService {
|
||||
type?: string,
|
||||
extension?: string,
|
||||
): Promise<ListUploadsResponse> {
|
||||
const params: Record<string, string | number> = { page, page_size: pageSize };
|
||||
const params: Record<string, string | number> = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
};
|
||||
if (keyword) params.keyword = keyword;
|
||||
if (type) params.type = type;
|
||||
if (extension) params.extension = extension;
|
||||
@@ -37,4 +40,4 @@ export class AdminUploadService extends BaseService {
|
||||
responseType: 'blob',
|
||||
} as InternalAxiosRequestConfig);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,4 +2,9 @@ export { UploadService } from './upload.service';
|
||||
export { AdminUploadService } from './admin-upload.service';
|
||||
export { formatFileSize, getFileUrl } from './utils';
|
||||
export type { ImageQuality } from './utils';
|
||||
export type { UploadImageResponse, Upload, ListUploadsResponse, FileStatsResponse } from './types';
|
||||
export type {
|
||||
UploadImageResponse,
|
||||
Upload,
|
||||
ListUploadsResponse,
|
||||
FileStatsResponse,
|
||||
} from './types';
|
||||
|
||||
@@ -2,34 +2,34 @@
|
||||
* 文件上传元数据
|
||||
*/
|
||||
export interface UploadMetadata {
|
||||
width?: number
|
||||
height?: number
|
||||
duration?: number
|
||||
original_mime?: string
|
||||
user_agent?: string
|
||||
client_ip?: string
|
||||
bucket?: string
|
||||
extra?: Record<string, unknown>
|
||||
width?: number;
|
||||
height?: number;
|
||||
duration?: number;
|
||||
original_mime?: string;
|
||||
user_agent?: string;
|
||||
client_ip?: string;
|
||||
bucket?: string;
|
||||
extra?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
/**
|
||||
* 上传记录
|
||||
*/
|
||||
export interface Upload {
|
||||
id: string
|
||||
user_id: string
|
||||
file_name: string
|
||||
file_path: string
|
||||
file_size: number
|
||||
mime_type: string
|
||||
extension: string
|
||||
hash: string
|
||||
type: string
|
||||
status: string
|
||||
access_mode: number
|
||||
metadata: UploadMetadata
|
||||
created_at: string
|
||||
updated_at: string
|
||||
id: string;
|
||||
user_id: string;
|
||||
file_name: string;
|
||||
file_path: string;
|
||||
file_size: number;
|
||||
mime_type: string;
|
||||
extension: string;
|
||||
hash: string;
|
||||
type: string;
|
||||
status: string;
|
||||
access_mode: number;
|
||||
metadata: UploadMetadata;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -37,55 +37,55 @@ export interface Upload {
|
||||
*/
|
||||
export interface UploadImageResponse {
|
||||
/** 上传记录 ID */
|
||||
id: string
|
||||
id: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件列表查询参数
|
||||
*/
|
||||
export interface ListUploadsQuery {
|
||||
page?: number
|
||||
page_size?: number
|
||||
type?: string
|
||||
extension?: string
|
||||
keyword?: string
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
type?: string;
|
||||
extension?: string;
|
||||
keyword?: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件列表分页响应
|
||||
*/
|
||||
export interface ListUploadsResponse {
|
||||
total: number
|
||||
page: number
|
||||
page_size: number
|
||||
items: Upload[]
|
||||
total: number;
|
||||
page: number;
|
||||
page_size: number;
|
||||
items: Upload[];
|
||||
}
|
||||
|
||||
/**
|
||||
* 近 7 天新增文件趋势项
|
||||
*/
|
||||
export interface TrendItem {
|
||||
date: string
|
||||
count: number
|
||||
size: number
|
||||
date: string;
|
||||
count: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件分类/类型统计项
|
||||
*/
|
||||
export interface DistributionItem {
|
||||
name: string
|
||||
count: number
|
||||
size: number
|
||||
name: string;
|
||||
count: number;
|
||||
size: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 文件统计信息响应
|
||||
*/
|
||||
export interface FileStatsResponse {
|
||||
total_count: number
|
||||
total_size: number
|
||||
trend: TrendItem[]
|
||||
categories: DistributionItem[]
|
||||
types: DistributionItem[]
|
||||
}
|
||||
total_count: number;
|
||||
total_size: number;
|
||||
trend: TrendItem[];
|
||||
categories: DistributionItem[];
|
||||
types: DistributionItem[];
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import type {InternalAxiosRequestConfig} from 'axios';
|
||||
import type { InternalAxiosRequestConfig } from 'axios';
|
||||
|
||||
import {apiConfig} from '../core/config';
|
||||
import {BaseService} from '../core/base.service';
|
||||
import type {ListUploadsResponse, Upload, UploadImageResponse} from './types';
|
||||
import { apiConfig } from '../core/config';
|
||||
import { BaseService } from '../core/base.service';
|
||||
import type { ListUploadsResponse, Upload, UploadImageResponse } from './types';
|
||||
|
||||
export class UploadService extends BaseService {
|
||||
protected static readonly basePath = '/api/v1/upload';
|
||||
@@ -50,7 +50,10 @@ export class UploadService extends BaseService {
|
||||
type?: string,
|
||||
extension?: string,
|
||||
): Promise<ListUploadsResponse> {
|
||||
const params: Record<string, string | number> = { page, page_size: pageSize };
|
||||
const params: Record<string, string | number> = {
|
||||
page,
|
||||
page_size: pageSize,
|
||||
};
|
||||
if (keyword) params.keyword = keyword;
|
||||
if (type) params.type = type;
|
||||
if (extension) params.extension = extension;
|
||||
@@ -61,7 +64,11 @@ export class UploadService extends BaseService {
|
||||
return this.delete<void>(`/${id}`);
|
||||
}
|
||||
|
||||
static async updateMyFile(id: string, fileName: string, accessMode?: number): Promise<Upload> {
|
||||
static async updateMyFile(
|
||||
id: string,
|
||||
fileName: string,
|
||||
accessMode?: number,
|
||||
): Promise<Upload> {
|
||||
return this.put<Upload>(`/${id}`, {
|
||||
file_name: fileName,
|
||||
access_mode: accessMode,
|
||||
@@ -77,4 +84,4 @@ export class UploadService extends BaseService {
|
||||
static getDownloadUrl(id: string): string {
|
||||
return `${this.basePath}/download/${id}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -15,4 +15,4 @@ export function formatFileSize(bytes: number): string {
|
||||
const sizes = ['B', 'KB', 'MB', 'GB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
/**
|
||||
* 用户服务模块
|
||||
*
|
||||
*
|
||||
* @description
|
||||
* 提供用户个人设置相关的功能
|
||||
*/
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import {BaseService} from '../core/base.service';
|
||||
import { BaseService } from '../core/base.service';
|
||||
|
||||
export interface AccessToken {
|
||||
id: number;
|
||||
@@ -34,8 +34,14 @@ export class UserService extends BaseService {
|
||||
* @param name - 令牌名称
|
||||
* @param isAdmin - 是否赋予管理员权限(默认 false)
|
||||
*/
|
||||
static async createAccessToken(name: string, isAdmin = false): Promise<CreateTokenResponse> {
|
||||
return this.post<CreateTokenResponse>('/access-tokens', { name, is_admin: isAdmin });
|
||||
static async createAccessToken(
|
||||
name: string,
|
||||
isAdmin = false,
|
||||
): Promise<CreateTokenResponse> {
|
||||
return this.post<CreateTokenResponse>('/access-tokens', {
|
||||
name,
|
||||
is_admin: isAdmin,
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
* - 不引入任何框架/UI 依赖,纯工具函数。
|
||||
*/
|
||||
|
||||
import type {TaskParam} from '@/lib/services/admin/types';
|
||||
import type { TaskParam } from '@/lib/services/admin/types';
|
||||
|
||||
/** buildTaskPayload 的返回结构 */
|
||||
export interface BuildPayloadResult {
|
||||
@@ -32,7 +32,10 @@ export interface BuildPayloadResult {
|
||||
function coerceParamValue(
|
||||
param: TaskParam,
|
||||
rawValue: string,
|
||||
): { value: string | number | boolean; ok: true } | { value: undefined; ok: true } | { error: string; ok: false } {
|
||||
):
|
||||
| { value: string | number | boolean; ok: true }
|
||||
| { value: undefined; ok: true }
|
||||
| { error: string; ok: false } {
|
||||
const trimmed = rawValue.trim();
|
||||
|
||||
switch (param.type) {
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import type {ThemeConfig} from "./types"
|
||||
import type {ThemeConfig} from './types';
|
||||
|
||||
/**
|
||||
* 主题系统配置
|
||||
* 定义主题文件的存储位置和 localStorage 键名
|
||||
*/
|
||||
export const THEME_CONFIG: ThemeConfig = {
|
||||
styleDir: "public/style",
|
||||
storageKey: "app-theme-id",
|
||||
}
|
||||
styleDir: 'public/style',
|
||||
storageKey: 'app-theme-id',
|
||||
};
|
||||
|
||||
@@ -1,70 +1,74 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import React, {createContext, useCallback, useContext, useEffect, useState} from "react"
|
||||
import {useTheme as useNextTheme} from "next-themes"
|
||||
import type {Theme} from "./types"
|
||||
import {getStoredThemeId, setStoredThemeId} from "./storage"
|
||||
import {getAvailableThemes} from "./parser"
|
||||
import React, {createContext, useCallback, useContext, useEffect, useState,} from 'react';
|
||||
import {useTheme as useNextTheme} from 'next-themes';
|
||||
import type {Theme} from './types';
|
||||
import {getStoredThemeId, setStoredThemeId} from './storage';
|
||||
import {getAvailableThemes} from './parser';
|
||||
|
||||
/**
|
||||
* 主题上下文类型定义
|
||||
*/
|
||||
interface ThemeContextValue {
|
||||
/** 所有可用主题列表 */
|
||||
themes: Theme[]
|
||||
themes: Theme[];
|
||||
/** 当前激活的主题对象 */
|
||||
currentTheme: Theme | null
|
||||
currentTheme: Theme | null;
|
||||
/** 当前激活的主题 ID */
|
||||
currentThemeId: string | null
|
||||
currentThemeId: string | null;
|
||||
/** 设置主题的方法 */
|
||||
setTheme: (themeId: string) => void
|
||||
setTheme: (themeId: string) => void;
|
||||
/** 是否正在加载主题 */
|
||||
isLoading: boolean
|
||||
isLoading: boolean;
|
||||
}
|
||||
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null)
|
||||
const ThemeContext = createContext<ThemeContextValue | null>(null);
|
||||
|
||||
/**
|
||||
* 自定义主题 Hook
|
||||
* 必须在 CustomThemeProvider 内部使用
|
||||
*/
|
||||
export function useCustomTheme() {
|
||||
const context = useContext(ThemeContext)
|
||||
const context = useContext(ThemeContext);
|
||||
if (!context) {
|
||||
throw new Error("useCustomTheme must be used within CustomThemeProvider")
|
||||
throw new Error('useCustomTheme must be used within CustomThemeProvider');
|
||||
}
|
||||
return context
|
||||
return context;
|
||||
}
|
||||
|
||||
/**
|
||||
* 自定义主题提供者
|
||||
* 管理用户选择的界面外观主题(配色方案),独立于明暗模式
|
||||
*/
|
||||
export function CustomThemeProvider({ children }: { children: React.ReactNode }) {
|
||||
const [themes, setThemes] = useState<Theme[]>([])
|
||||
const [currentThemeId, setCurrentThemeId] = useState<string | null>(null)
|
||||
const [isLoading, setIsLoading] = useState(true)
|
||||
const [mounted, setMounted] = useState(false)
|
||||
const { resolvedTheme } = useNextTheme()
|
||||
export function CustomThemeProvider({
|
||||
children,
|
||||
}: {
|
||||
children: React.ReactNode;
|
||||
}) {
|
||||
const [themes, setThemes] = useState<Theme[]>([]);
|
||||
const [currentThemeId, setCurrentThemeId] = useState<string | null>(null);
|
||||
const [isLoading, setIsLoading] = useState(true);
|
||||
const [mounted, setMounted] = useState(false);
|
||||
const { resolvedTheme } = useNextTheme();
|
||||
|
||||
/* 加载所有可用主题 */
|
||||
useEffect(() => {
|
||||
async function loadThemes() {
|
||||
const availableThemes = await getAvailableThemes()
|
||||
const storedId = getStoredThemeId()
|
||||
const availableThemes = await getAvailableThemes();
|
||||
const storedId = getStoredThemeId();
|
||||
|
||||
setThemes(availableThemes)
|
||||
setCurrentThemeId(storedId)
|
||||
setIsLoading(false)
|
||||
setThemes(availableThemes);
|
||||
setCurrentThemeId(storedId);
|
||||
setIsLoading(false);
|
||||
}
|
||||
|
||||
loadThemes()
|
||||
}, [])
|
||||
loadThemes();
|
||||
}, []);
|
||||
|
||||
/* 确保组件已挂载,避免水合不一致 */
|
||||
useEffect(() => {
|
||||
setMounted(true)
|
||||
}, [])
|
||||
setMounted(true);
|
||||
}, []);
|
||||
|
||||
/**
|
||||
* 将主题颜色应用到 DOM
|
||||
@@ -72,46 +76,49 @@ export function CustomThemeProvider({ children }: { children: React.ReactNode })
|
||||
* @param isDark - 是否为暗色模式
|
||||
*/
|
||||
const applyThemeColors = useCallback((theme: Theme, isDark: boolean) => {
|
||||
if (typeof window === "undefined") return
|
||||
if (typeof window === 'undefined') return;
|
||||
|
||||
const root = document.documentElement
|
||||
const colors = isDark ? theme.colors.dark : theme.colors.light
|
||||
const root = document.documentElement;
|
||||
const colors = isDark ? theme.colors.dark : theme.colors.light;
|
||||
|
||||
/* 应用所有 CSS 变量到根元素 */
|
||||
Object.entries(colors).forEach(([key, value]) => {
|
||||
root.style.setProperty(`--${ key }`, value)
|
||||
})
|
||||
}, [])
|
||||
root.style.setProperty(`--${key}`, value);
|
||||
});
|
||||
}, []);
|
||||
|
||||
/* 当主题 ID 或模式改变时,重新应用颜色 */
|
||||
useEffect(() => {
|
||||
if (!mounted || !currentThemeId || themes.length === 0) return
|
||||
if (!mounted || !currentThemeId || themes.length === 0) return;
|
||||
|
||||
const theme = themes.find(t => t.id === currentThemeId)
|
||||
if (!theme) return
|
||||
const theme = themes.find((t) => t.id === currentThemeId);
|
||||
if (!theme) return;
|
||||
|
||||
/* 使用 next-themes 的 resolvedTheme,它会自动处理系统偏好 */
|
||||
const isDark = resolvedTheme === "dark"
|
||||
applyThemeColors(theme, isDark)
|
||||
}, [currentThemeId, resolvedTheme, themes, mounted, applyThemeColors])
|
||||
const isDark = resolvedTheme === 'dark';
|
||||
applyThemeColors(theme, isDark);
|
||||
}, [currentThemeId, resolvedTheme, themes, mounted, applyThemeColors]);
|
||||
|
||||
/**
|
||||
* 设置主题
|
||||
* @param themeId - 要设置的主题 ID
|
||||
*/
|
||||
const setTheme = useCallback((themeId: string) => {
|
||||
const theme = themes.find(t => t.id === themeId)
|
||||
if (!theme) return
|
||||
const setTheme = useCallback(
|
||||
(themeId: string) => {
|
||||
const theme = themes.find((t) => t.id === themeId);
|
||||
if (!theme) return;
|
||||
|
||||
setCurrentThemeId(themeId)
|
||||
setStoredThemeId(themeId)
|
||||
setCurrentThemeId(themeId);
|
||||
setStoredThemeId(themeId);
|
||||
|
||||
/* 根据当前解析的主题模式计算是否为暗色 */
|
||||
const isDark = resolvedTheme === "dark"
|
||||
applyThemeColors(theme, isDark)
|
||||
}, [themes, resolvedTheme, applyThemeColors])
|
||||
/* 根据当前解析的主题模式计算是否为暗色 */
|
||||
const isDark = resolvedTheme === 'dark';
|
||||
applyThemeColors(theme, isDark);
|
||||
},
|
||||
[themes, resolvedTheme, applyThemeColors],
|
||||
);
|
||||
|
||||
const currentTheme = themes.find(t => t.id === currentThemeId) || null
|
||||
const currentTheme = themes.find((t) => t.id === currentThemeId) || null;
|
||||
|
||||
return (
|
||||
<ThemeContext.Provider
|
||||
@@ -120,10 +127,10 @@ export function CustomThemeProvider({ children }: { children: React.ReactNode })
|
||||
currentTheme,
|
||||
currentThemeId,
|
||||
setTheme,
|
||||
isLoading
|
||||
isLoading,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</ThemeContext.Provider>
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,16 +4,20 @@
|
||||
*/
|
||||
|
||||
/* 类型定义 */
|
||||
export type { Theme, ThemeColors, ThemeConfig } from "./types"
|
||||
export type { Theme, ThemeColors, ThemeConfig } from './types';
|
||||
|
||||
/* 配置 */
|
||||
export { THEME_CONFIG } from "./config"
|
||||
export { THEME_CONFIG } from './config';
|
||||
|
||||
/* 服务端工具 */
|
||||
export { getAvailableThemes } from "./parser"
|
||||
export { getAvailableThemes } from './parser';
|
||||
|
||||
/* 客户端工具 */
|
||||
export { getStoredThemeId, setStoredThemeId, removeStoredThemeId } from "./storage"
|
||||
export {
|
||||
getStoredThemeId,
|
||||
setStoredThemeId,
|
||||
removeStoredThemeId,
|
||||
} from './storage';
|
||||
|
||||
/* React 上下文和 Hooks */
|
||||
export { CustomThemeProvider, useCustomTheme } from "./context"
|
||||
export { CustomThemeProvider, useCustomTheme } from './context';
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import type {Theme} from "./types"
|
||||
import themesData from "./themes.json"
|
||||
import type { Theme } from './types';
|
||||
import themesData from './themes.json';
|
||||
|
||||
/**
|
||||
* 获取所有可用主题
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
"use client"
|
||||
'use client';
|
||||
|
||||
import {THEME_CONFIG} from "./config"
|
||||
import { THEME_CONFIG } from './config';
|
||||
|
||||
/**
|
||||
* 客户端主题存储工具
|
||||
@@ -11,22 +11,22 @@ import {THEME_CONFIG} from "./config"
|
||||
* 获取存储的主题 ID
|
||||
*/
|
||||
export function getStoredThemeId(): string | null {
|
||||
if (typeof window === "undefined") return null
|
||||
return localStorage.getItem(THEME_CONFIG.storageKey)
|
||||
if (typeof window === 'undefined') return null;
|
||||
return localStorage.getItem(THEME_CONFIG.storageKey);
|
||||
}
|
||||
|
||||
/**
|
||||
* 保存主题 ID 到本地存储
|
||||
*/
|
||||
export function setStoredThemeId(themeId: string): void {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.setItem(THEME_CONFIG.storageKey, themeId)
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.setItem(THEME_CONFIG.storageKey, themeId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 移除存储的主题 ID
|
||||
*/
|
||||
export function removeStoredThemeId(): void {
|
||||
if (typeof window === "undefined") return
|
||||
localStorage.removeItem(THEME_CONFIG.storageKey)
|
||||
if (typeof window === 'undefined') return;
|
||||
localStorage.removeItem(THEME_CONFIG.storageKey);
|
||||
}
|
||||
|
||||
@@ -4559,4 +4559,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
]
|
||||
|
||||
@@ -7,8 +7,8 @@
|
||||
* 包含明亮和暗色两种模式的 CSS 变量映射
|
||||
*/
|
||||
export interface ThemeColors {
|
||||
light: Record<string, string>
|
||||
dark: Record<string, string>
|
||||
light: Record<string, string>;
|
||||
dark: Record<string, string>;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -16,11 +16,11 @@ export interface ThemeColors {
|
||||
*/
|
||||
export interface Theme {
|
||||
/** 主题唯一标识符(CSS 文件名) */
|
||||
id: string
|
||||
id: string;
|
||||
/** 主题显示名称 */
|
||||
name: string
|
||||
name: string;
|
||||
/** 主题颜色配置 */
|
||||
colors: ThemeColors
|
||||
colors: ThemeColors;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,7 @@ export interface Theme {
|
||||
*/
|
||||
export interface ThemeConfig {
|
||||
/** CSS 样式文件所在目录 */
|
||||
styleDir: string
|
||||
styleDir: string;
|
||||
/** 主题 ID 在 localStorage 中的存储键名 */
|
||||
storageKey: string
|
||||
storageKey: string;
|
||||
}
|
||||
|
||||
+77
-64
@@ -1,13 +1,13 @@
|
||||
import {type ClassValue, clsx} from "clsx"
|
||||
import {twMerge} from "tailwind-merge"
|
||||
import { type ClassValue, clsx } from 'clsx';
|
||||
import { twMerge } from 'tailwind-merge';
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
return twMerge(clsx(inputs));
|
||||
}
|
||||
|
||||
export function formatDateTime(dateStr: string | Date) {
|
||||
try {
|
||||
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr
|
||||
const date = typeof dateStr === 'string' ? new Date(dateStr) : dateStr;
|
||||
return new Intl.DateTimeFormat('zh-CN', {
|
||||
timeZone: 'Asia/Shanghai',
|
||||
year: 'numeric',
|
||||
@@ -15,10 +15,10 @@ export function formatDateTime(dateStr: string | Date) {
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
hour12: false
|
||||
}).format(date)
|
||||
hour12: false,
|
||||
}).format(date);
|
||||
} catch {
|
||||
return String(dateStr)
|
||||
return String(dateStr);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -28,18 +28,21 @@ export function formatDateTime(dateStr: string | Date) {
|
||||
* @returns 格式化后的日期字符串,如 "2024-01-15T00:00:00+08:00"
|
||||
*/
|
||||
export function formatLocalDate(date: Date): string {
|
||||
const year = date.getFullYear()
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
||||
const day = String(date.getDate()).padStart(2, '0')
|
||||
const hours = String(date.getHours()).padStart(2, '0')
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0')
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0')
|
||||
return `${ year }-${ month }-${ day }T${ hours }:${ minutes }:${ seconds }+08:00`
|
||||
const year = date.getFullYear();
|
||||
const month = String(date.getMonth() + 1).padStart(2, '0');
|
||||
const day = String(date.getDate()).padStart(2, '0');
|
||||
const hours = String(date.getHours()).padStart(2, '0');
|
||||
const minutes = String(date.getMinutes()).padStart(2, '0');
|
||||
const seconds = String(date.getSeconds()).padStart(2, '0');
|
||||
return `${year}-${month}-${day}T${hours}:${minutes}:${seconds}+08:00`;
|
||||
}
|
||||
|
||||
type Base64Buffer = {
|
||||
from: (input: string, encoding: 'utf-8') => { toString: (encoding: 'base64') => string }
|
||||
}
|
||||
from: (
|
||||
input: string,
|
||||
encoding: 'utf-8',
|
||||
) => { toString: (encoding: 'base64') => string };
|
||||
};
|
||||
|
||||
/**
|
||||
* Base64 编码
|
||||
@@ -48,15 +51,17 @@ type Base64Buffer = {
|
||||
*/
|
||||
export function encodeBase64(value: string): string {
|
||||
if (typeof globalThis.btoa === 'function') {
|
||||
return globalThis.btoa(value)
|
||||
return globalThis.btoa(value);
|
||||
}
|
||||
|
||||
const bufferConstructor = (globalThis as typeof globalThis & { Buffer?: Base64Buffer }).Buffer
|
||||
const bufferConstructor = (
|
||||
globalThis as typeof globalThis & { Buffer?: Base64Buffer }
|
||||
).Buffer;
|
||||
if (bufferConstructor) {
|
||||
return bufferConstructor.from(value, 'utf-8').toString('base64')
|
||||
return bufferConstructor.from(value, 'utf-8').toString('base64');
|
||||
}
|
||||
|
||||
throw new Error('当前环境不支持 Base64 编码')
|
||||
throw new Error('当前环境不支持 Base64 编码');
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -65,80 +70,88 @@ export function encodeBase64(value: string): string {
|
||||
* @returns 缓存键字符串
|
||||
*/
|
||||
export function generateTransactionCacheKey(params: {
|
||||
types?: string[]
|
||||
statuses?: string[]
|
||||
payee_transfer_status?: string
|
||||
client_id?: string
|
||||
page?: number
|
||||
page_size?: number
|
||||
startTime?: string
|
||||
endTime?: string
|
||||
id?: string
|
||||
order_name?: string
|
||||
payer_username?: string
|
||||
payee_username?: string
|
||||
types?: string[];
|
||||
statuses?: string[];
|
||||
payee_transfer_status?: string;
|
||||
client_id?: string;
|
||||
page?: number;
|
||||
page_size?: number;
|
||||
startTime?: string;
|
||||
endTime?: string;
|
||||
id?: string;
|
||||
order_name?: string;
|
||||
payer_username?: string;
|
||||
payee_username?: string;
|
||||
}): string {
|
||||
const typesKey = params.types?.length ? params.types.sort().join(',') : 'all'
|
||||
const statusesKey = params.statuses?.length ? params.statuses.sort().join(',') : 'all'
|
||||
const transferStatusKey = params.payee_transfer_status || 'all'
|
||||
const clientIdKey = params.client_id || 'all'
|
||||
const startTimeKey = params.startTime || 'no-start'
|
||||
const endTimeKey = params.endTime || 'no-end'
|
||||
const idKey = params.id || 'no-id'
|
||||
const orderNameKey = params.order_name || 'no-name'
|
||||
const payerKey = params.payer_username || 'no-payer'
|
||||
const payeeKey = params.payee_username || 'no-payee'
|
||||
const typesKey = params.types?.length ? params.types.sort().join(',') : 'all';
|
||||
const statusesKey = params.statuses?.length
|
||||
? params.statuses.sort().join(',')
|
||||
: 'all';
|
||||
const transferStatusKey = params.payee_transfer_status || 'all';
|
||||
const clientIdKey = params.client_id || 'all';
|
||||
const startTimeKey = params.startTime || 'no-start';
|
||||
const endTimeKey = params.endTime || 'no-end';
|
||||
const idKey = params.id || 'no-id';
|
||||
const orderNameKey = params.order_name || 'no-name';
|
||||
const payerKey = params.payer_username || 'no-payer';
|
||||
const payeeKey = params.payee_username || 'no-payee';
|
||||
|
||||
return `${ typesKey }_${ statusesKey }_${ transferStatusKey }_${ clientIdKey }_${ params.page }_${ params.page_size }_${ startTimeKey }_${ endTimeKey }_${ idKey }_${ orderNameKey }_${ payerKey }_${ payeeKey }`
|
||||
return `${typesKey}_${statusesKey}_${transferStatusKey}_${clientIdKey}_${params.page}_${params.page_size}_${startTimeKey}_${endTimeKey}_${idKey}_${orderNameKey}_${payerKey}_${payeeKey}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* 验证并净化重定向目标 URL,防止 Open Redirect 和 XSS 攻击。
|
||||
* 只允许以单个斜杠 "/" 开头的相对路径,拒绝 "//"、协议、反斜杠、控制字符等编码变体。
|
||||
*/
|
||||
export function safeRedirectTarget(url: string | null | undefined, fallback = "/"): string {
|
||||
if (!url) return fallback
|
||||
export function safeRedirectTarget(
|
||||
url: string | null | undefined,
|
||||
fallback = '/',
|
||||
): string {
|
||||
if (!url) return fallback;
|
||||
|
||||
// 1. 拒绝包含控制字符、空白字符或反斜杠的 URL
|
||||
if (/[\u0000-\u001F\u007F-\u009F\s\\]/.test(url)) {
|
||||
return fallback
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 2. 必须以单个 '/' 开头,且不能以 '//' 开头
|
||||
if (!url.startsWith("/") || url.startsWith("//")) {
|
||||
return fallback
|
||||
if (!url.startsWith('/') || url.startsWith('//')) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 3. 递归 URL 解码并检测潜在的绕过载荷
|
||||
try {
|
||||
let decoded = url
|
||||
let prev = ""
|
||||
let attempts = 0
|
||||
while (decoded !== prev && decoded.includes("%") && attempts < 3) {
|
||||
prev = decoded
|
||||
decoded = decodeURIComponent(decoded)
|
||||
attempts++
|
||||
let decoded = url;
|
||||
let prev = '';
|
||||
let attempts = 0;
|
||||
while (decoded !== prev && decoded.includes('%') && attempts < 3) {
|
||||
prev = decoded;
|
||||
decoded = decodeURIComponent(decoded);
|
||||
attempts++;
|
||||
}
|
||||
|
||||
// 检查解码后的内容是否包含控制字符、空白字符或反斜杠
|
||||
if (/[\u0000-\u001F\u007F-\u009F\s\\]/.test(decoded)) {
|
||||
return fallback
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 检查解码后的内容是否以单个 '/' 开头,且不能以 '//' 开头
|
||||
if (!decoded.startsWith("/") || decoded.startsWith("//")) {
|
||||
return fallback
|
||||
if (!decoded.startsWith('/') || decoded.startsWith('//')) {
|
||||
return fallback;
|
||||
}
|
||||
|
||||
// 提取路径部分(即问号 ? 或井号 # 之前的内容),确保其中不含冒号(防止 scheme)、双斜杠或反斜杠
|
||||
const pathPart = decoded.split(/[?#]/)[0]
|
||||
if (pathPart.includes(":") || pathPart.includes("//") || pathPart.includes("\\")) {
|
||||
return fallback
|
||||
const pathPart = decoded.split(/[?#]/)[0];
|
||||
if (
|
||||
pathPart.includes(':') ||
|
||||
pathPart.includes('//') ||
|
||||
pathPart.includes('\\')
|
||||
) {
|
||||
return fallback;
|
||||
}
|
||||
} catch {
|
||||
return fallback
|
||||
return fallback;
|
||||
}
|
||||
|
||||
return url
|
||||
return url;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
import {isCancelError} from '@/lib/services/core'
|
||||
import {toast} from 'sonner'
|
||||
import {isCancelError} from '@/lib/services/core';
|
||||
import {toast} from 'sonner';
|
||||
|
||||
/**
|
||||
* 错误处理选项
|
||||
*/
|
||||
interface HandleContextErrorOptions {
|
||||
/** 是否显示 toast 提示 */
|
||||
showToast?: boolean
|
||||
showToast?: boolean;
|
||||
/** 是否在控制台记录错误 */
|
||||
logError?: boolean
|
||||
logError?: boolean;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -35,27 +35,28 @@ interface HandleContextErrorOptions {
|
||||
export function handleContextError(
|
||||
error: unknown,
|
||||
defaultMessage: string,
|
||||
options: HandleContextErrorOptions = {}
|
||||
options: HandleContextErrorOptions = {},
|
||||
): Error {
|
||||
const { showToast = false, logError = true } = options
|
||||
const { showToast = false, logError = true } = options;
|
||||
|
||||
// 取消的请求不算错误
|
||||
if (isCancelError(error)) {
|
||||
return new Error('请求已取消')
|
||||
return new Error('请求已取消');
|
||||
}
|
||||
|
||||
const errorMessage = error instanceof Error ? error.message : defaultMessage
|
||||
const errorObject = error instanceof Error ? error : new Error(defaultMessage)
|
||||
const errorMessage = error instanceof Error ? error.message : defaultMessage;
|
||||
const errorObject =
|
||||
error instanceof Error ? error : new Error(defaultMessage);
|
||||
|
||||
if (logError) {
|
||||
console.error(defaultMessage, error)
|
||||
console.error(defaultMessage, error);
|
||||
}
|
||||
|
||||
if (showToast) {
|
||||
toast.error(defaultMessage, {
|
||||
description: errorMessage
|
||||
})
|
||||
description: errorMessage,
|
||||
});
|
||||
}
|
||||
|
||||
return errorObject
|
||||
return errorObject;
|
||||
}
|
||||
|
||||
@@ -25,7 +25,7 @@ export function formatBytes(
|
||||
value?: number | null,
|
||||
options: FormatBytesOptions = {},
|
||||
) {
|
||||
const {emptyText = '—', zeroText = '0 B'} = options;
|
||||
const { emptyText = '—', zeroText = '0 B' } = options;
|
||||
|
||||
if (value === undefined || value === null || Number.isNaN(value)) {
|
||||
return emptyText;
|
||||
@@ -80,4 +80,4 @@ export function calculateNiceAxisMax(values: number[]) {
|
||||
const niceStep = steps.find((step) => normalized <= step) ?? 10;
|
||||
|
||||
return niceStep * magnitude;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import PinyinMatch from 'pinyin-match'
|
||||
import PinyinMatch from 'pinyin-match';
|
||||
|
||||
type MatchResult = [number, number] | false;
|
||||
type MatchFunction = (input: string, keys: string) => MatchResult;
|
||||
@@ -30,14 +30,14 @@ const match = ((): MatchFunction | null => {
|
||||
})();
|
||||
|
||||
export interface SearchItem {
|
||||
id: string
|
||||
title: string
|
||||
description: string
|
||||
url: string
|
||||
category: 'page' | 'feature' | 'setting' | 'admin'
|
||||
keywords: string[]
|
||||
icon?: string
|
||||
matchRange?: [number, number]
|
||||
id: string;
|
||||
title: string;
|
||||
description: string;
|
||||
url: string;
|
||||
category: 'page' | 'feature' | 'setting' | 'admin';
|
||||
keywords: string[];
|
||||
icon?: string;
|
||||
matchRange?: [number, number];
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -72,7 +72,15 @@ export const searchData: SearchItem[] = [
|
||||
description: '管理边缘节点、中继节点与内网穿透通道',
|
||||
url: '/nodes',
|
||||
category: 'page',
|
||||
keywords: ['node', '节点', '边缘节点', '中继', '内网穿透', 'tunnel', '服务器'],
|
||||
keywords: [
|
||||
'node',
|
||||
'节点',
|
||||
'边缘节点',
|
||||
'中继',
|
||||
'内网穿透',
|
||||
'tunnel',
|
||||
'服务器',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'console-proxy-routes',
|
||||
@@ -104,7 +112,15 @@ export const searchData: SearchItem[] = [
|
||||
description: '配置 DNS 服务商 API 凭证以自动申请证书及管理解析',
|
||||
url: '/dns-accounts',
|
||||
category: 'page',
|
||||
keywords: ['dns', 'dns account', '账号', '域名解析', 'cloudflare', 'aliyun', 'tencent'],
|
||||
keywords: [
|
||||
'dns',
|
||||
'dns account',
|
||||
'账号',
|
||||
'域名解析',
|
||||
'cloudflare',
|
||||
'aliyun',
|
||||
'tencent',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'console-origins',
|
||||
@@ -160,7 +176,15 @@ export const searchData: SearchItem[] = [
|
||||
description: '查看节点配置下发、同步与生效的历史记录',
|
||||
url: '/apply-logs',
|
||||
category: 'page',
|
||||
keywords: ['apply', 'log', 'logs', '应用记录', '配置下发', '同步', '部署历史'],
|
||||
keywords: [
|
||||
'apply',
|
||||
'log',
|
||||
'logs',
|
||||
'应用记录',
|
||||
'配置下发',
|
||||
'同步',
|
||||
'部署历史',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'console-performance',
|
||||
@@ -202,7 +226,15 @@ export const searchData: SearchItem[] = [
|
||||
description: '管理系统登录注册与认证源配置 (管理员专属)',
|
||||
url: '/admin/settings',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '系统设置', '安全', 'security', 'oidc', 'login'],
|
||||
keywords: [
|
||||
'admin',
|
||||
'管理员',
|
||||
'系统设置',
|
||||
'安全',
|
||||
'security',
|
||||
'oidc',
|
||||
'login',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin-system',
|
||||
@@ -226,7 +258,15 @@ export const searchData: SearchItem[] = [
|
||||
description: '下发与排查后台异步定时任务 (管理员专属)',
|
||||
url: '/admin/tasks',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '任务', '异步', 'tasks', 'scheduler', 'worker'],
|
||||
keywords: [
|
||||
'admin',
|
||||
'管理员',
|
||||
'任务',
|
||||
'异步',
|
||||
'tasks',
|
||||
'scheduler',
|
||||
'worker',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin-files',
|
||||
@@ -239,7 +279,8 @@ export const searchData: SearchItem[] = [
|
||||
{
|
||||
id: 'admin-database',
|
||||
title: '数据管理',
|
||||
description: '监控数据库表大小、分页浏览物理表内容并支持交互式 SQL (管理员专属)',
|
||||
description:
|
||||
'监控数据库表大小、分页浏览物理表内容并支持交互式 SQL (管理员专属)',
|
||||
url: '/admin/database',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '数据库', 'database', 'sql', 'query', 'gorm'],
|
||||
@@ -250,7 +291,16 @@ export const searchData: SearchItem[] = [
|
||||
description: '配置与下发邮件、Lark 和 Telegram 渠道通知推送 (管理员专属)',
|
||||
url: '/admin/push',
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '推送', '通知', 'push', 'mail', 'telegram', 'lark'],
|
||||
keywords: [
|
||||
'admin',
|
||||
'管理员',
|
||||
'推送',
|
||||
'通知',
|
||||
'push',
|
||||
'mail',
|
||||
'telegram',
|
||||
'lark',
|
||||
],
|
||||
},
|
||||
{
|
||||
id: 'admin-logs',
|
||||
@@ -260,7 +310,7 @@ export const searchData: SearchItem[] = [
|
||||
category: 'admin',
|
||||
keywords: ['admin', '管理员', '日志', 'logs', 'system log', 'terminal'],
|
||||
},
|
||||
]
|
||||
];
|
||||
|
||||
/**
|
||||
* 搜索功能
|
||||
@@ -268,47 +318,61 @@ export const searchData: SearchItem[] = [
|
||||
* @param isAdmin 是否为管理员
|
||||
* @returns 匹配的搜索结果
|
||||
*/
|
||||
export function searchItems(query: string, isAdmin: boolean = false): SearchItem[] {
|
||||
const trimmedQuery = query.trim()
|
||||
|
||||
export function searchItems(
|
||||
query: string,
|
||||
isAdmin: boolean = false,
|
||||
): SearchItem[] {
|
||||
const trimmedQuery = query.trim();
|
||||
|
||||
// 非管理员不能搜索 admin 类别项
|
||||
const filteredData = isAdmin
|
||||
? searchData
|
||||
: searchData.filter(item => item.category !== 'admin')
|
||||
const filteredData = isAdmin
|
||||
? searchData
|
||||
: searchData.filter((item) => item.category !== 'admin');
|
||||
|
||||
if (!trimmedQuery) {
|
||||
return filteredData
|
||||
return filteredData;
|
||||
}
|
||||
|
||||
return filteredData.map(item => {
|
||||
// 优先匹配标题
|
||||
const titleMatch = typeof match === 'function' ? match(item.title, trimmedQuery) : null
|
||||
if (titleMatch) {
|
||||
return { ...item, matchRange: titleMatch as [number, number] }
|
||||
}
|
||||
return filteredData
|
||||
.map((item) => {
|
||||
// 优先匹配标题
|
||||
const titleMatch =
|
||||
typeof match === 'function' ? match(item.title, trimmedQuery) : null;
|
||||
if (titleMatch) {
|
||||
return { ...item, matchRange: titleMatch as [number, number] };
|
||||
}
|
||||
|
||||
// 匹配描述
|
||||
if (typeof match === 'function' && match(item.description, trimmedQuery)) {
|
||||
return item
|
||||
}
|
||||
// 匹配描述
|
||||
if (
|
||||
typeof match === 'function' &&
|
||||
match(item.description, trimmedQuery)
|
||||
) {
|
||||
return item;
|
||||
}
|
||||
|
||||
// 匹配关键词
|
||||
if (item.keywords.some(keyword => typeof match === 'function' && match(keyword, trimmedQuery))) {
|
||||
return item
|
||||
}
|
||||
// 匹配关键词
|
||||
if (
|
||||
item.keywords.some(
|
||||
(keyword) =>
|
||||
typeof match === 'function' && match(keyword, trimmedQuery),
|
||||
)
|
||||
) {
|
||||
return item;
|
||||
}
|
||||
|
||||
return null
|
||||
}).filter((item): item is SearchItem => item !== null)
|
||||
return null;
|
||||
})
|
||||
.filter((item): item is SearchItem => item !== null)
|
||||
.sort((a, b) => {
|
||||
// 标题匹配优先
|
||||
if (a.matchRange && !b.matchRange) return -1
|
||||
if (!a.matchRange && b.matchRange) return 1
|
||||
|
||||
if (a.matchRange && !b.matchRange) return -1;
|
||||
if (!a.matchRange && b.matchRange) return 1;
|
||||
|
||||
// 如果都是标题匹配,按匹配位置排序
|
||||
if (a.matchRange && b.matchRange) {
|
||||
return a.matchRange[0] - b.matchRange[0]
|
||||
return a.matchRange[0] - b.matchRange[0];
|
||||
}
|
||||
|
||||
return 0
|
||||
})
|
||||
|
||||
return 0;
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user