18 文件 ~35 处 UI 硬编码中文→t()/i18n.t() 调用: - agent-utils.ts timeAgo() 秒前/分钟前/小时前 - utils.ts formatDistanceToNow() 分钟前/小时前/天前 - request.ts 响应解析失败错误消息 - ActiveSessionsPanel/HistorySessionsPanel RDP 在线/历史会话标签 - MetricsExplorer 清除/已选/最近1小时 - HostMonitor 当前主机 subtitle - AIAnalysisResult 证据链 (新增 useTranslation hook) - capacity-predict.ts 容量预测摘要模板 - ApprovalResourcePage 阿里云/腾讯云/华为云 - ApprovalPermissionPage 堡垒机 - site-monitor/helpers.ts 频率/时间/证书天数格式化 - site-monitor/analytics.ts 证书剩余/检查失败/延迟波动事件 - middleware-monitor MwTargetChartsPanel/MwTrendChart/helpers.ts - snmp-monitor SnmpMetricTrendChart/SnmpDeviceChartsPanel - host-monitor/utils.ts 图表点数标签 + PortPolicyDialog 系统进程说明 新增 34 翻译键 (common.duration.secAgo/hourAgo/dayAgo + common.selected/ responseParseError/rdpActiveSessions/rdpHistorySessions/rdpSessionCount/ evidenceChain/last1Hour/currentHost + monitor.lbl 10键 + monitor.siteMonitor 8键 + approval.provider 3键 + approval.permType 1键 + monitor.portPolicyDialog.systemProcessDesc + aiops.capacity.summary) zh/en 7813→7847 对齐; tsc -b=0, npm build✓ (17.31s), ESLint=0/43/any=0, go build=0, 17/17 测试通过
194 lines
6.1 KiB
TypeScript
194 lines
6.1 KiB
TypeScript
import { useUserStore } from '@/store';
|
||
import { getToken, clearToken } from './storage';
|
||
import i18n from '@/i18n';
|
||
|
||
const API_PREFIX = '/api/v1';
|
||
|
||
// 后端统一响应格式
|
||
export interface ApiResponse<T = unknown> {
|
||
code: number;
|
||
message: string;
|
||
data: T;
|
||
}
|
||
|
||
// 列表响应格式
|
||
export interface ListResponse<T = unknown> {
|
||
list: T[];
|
||
total: number;
|
||
}
|
||
|
||
type Json = unknown;
|
||
|
||
/** 统一 API 根路径,避免 get/post 与 download/upload 行为不一致 // Logan.Li */
|
||
export function getApiBase(): string {
|
||
const envBase = import.meta.env.VITE_API_BASE_URL;
|
||
if (envBase) {
|
||
const normalized = String(envBase).replace(/\/$/, '');
|
||
return normalized.endsWith(API_PREFIX) ? normalized : `${normalized}${API_PREFIX}`;
|
||
}
|
||
return API_PREFIX;
|
||
}
|
||
|
||
/** 对外展示的 API 根路径(curl/CI 文档),无环境变量时拼接当前浏览器域名 // Logan.Li */
|
||
export function getPublicApiBase(): string {
|
||
const base = getApiBase();
|
||
if (/^https?:\/\//i.test(base)) return base;
|
||
return `${window.location.origin}${base}`;
|
||
}
|
||
|
||
/** 拼接 REST API 完整路径(含 /api/v1 前缀) // Logan.Li */
|
||
export function buildApiUrl(path: string, params?: URLSearchParams): string {
|
||
const p = path.startsWith('/') ? path : `/${path}`;
|
||
let url = `${getApiBase()}${p}`;
|
||
const qs = params?.toString();
|
||
if (qs) url += `?${qs}`;
|
||
return url;
|
||
}
|
||
|
||
function resolveApiUrl(path: string): string {
|
||
return buildApiUrl(path);
|
||
}
|
||
|
||
/** 获取 WebSocket 根路径(不含 /api/v1) // Logan.Li */
|
||
export function getWsBase(): string {
|
||
const envBase = import.meta.env.VITE_API_BASE_URL;
|
||
if (envBase) {
|
||
const origin = String(envBase).replace(/\/$/, '').replace(/\/api\/v1$/, '');
|
||
return origin.replace(/^http/, 'ws');
|
||
}
|
||
const proto = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
|
||
return `${proto}//${window.location.host}`;
|
||
}
|
||
|
||
export function getAuthToken(): string {
|
||
return getToken();
|
||
}
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
async function request<T = any>(
|
||
method: string,
|
||
url: string,
|
||
body?: Json,
|
||
params?: Record<string, string | number | boolean | undefined>,
|
||
): Promise<T> {
|
||
let fullUrl = resolveApiUrl(url);
|
||
if (params) {
|
||
const searchParams = new URLSearchParams();
|
||
Object.entries(params).forEach(([key, value]) => {
|
||
if (value !== undefined && value !== null && value !== '') {
|
||
searchParams.append(key, String(value));
|
||
}
|
||
});
|
||
const qs = searchParams.toString();
|
||
if (qs) fullUrl += '?' + qs;
|
||
}
|
||
|
||
const token = getAuthToken();
|
||
const headers: Record<string, string> = {
|
||
'Content-Type': 'application/json',
|
||
};
|
||
if (token) {
|
||
headers.Authorization = 'Bearer ' + token;
|
||
}
|
||
|
||
const response = await fetch(fullUrl, {
|
||
method,
|
||
headers,
|
||
body: body ? JSON.stringify(body) : undefined,
|
||
});
|
||
|
||
if (response.status === 401) {
|
||
clearToken();
|
||
useUserStore.getState().logout();
|
||
window.location.href = '/login';
|
||
throw new Error(i18n.t('common.request.tokenExpired'));
|
||
}
|
||
|
||
let json: ApiResponse<T>;
|
||
try {
|
||
json = await response.json();
|
||
} catch {
|
||
throw new Error(`HTTP ${response.status}: ${i18n.t('common.responseParseError')}`);
|
||
}
|
||
|
||
if (!response.ok || (json.code && json.code !== 200)) {
|
||
const msg = json.message || `HTTP ${response.status}`;
|
||
throw new Error(msg);
|
||
}
|
||
|
||
return (json.data !== undefined ? json.data : json) as T;
|
||
}
|
||
|
||
export const http = {
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
get: <T = any>(url: string, params?: Record<string, string | number | boolean | undefined>) =>
|
||
request<T>('GET', url, undefined, params),
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
post: <T = any>(url: string, body?: Json) => request<T>('POST', url, body),
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
put: <T = any>(url: string, body?: Json) => request<T>('PUT', url, body),
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
delete: <T = any>(url: string, body?: Json) => request<T>('DELETE', url, body),
|
||
|
||
getRaw: async (url: string): Promise<string> => {
|
||
const resp = await fetch(resolveApiUrl(url), {
|
||
headers: { Authorization: 'Bearer ' + getAuthToken() },
|
||
});
|
||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||
return resp.text();
|
||
},
|
||
|
||
putRaw: async (url: string, body: string, contentType: string): Promise<unknown> => {
|
||
const resp = await fetch(resolveApiUrl(url), {
|
||
method: 'PUT',
|
||
headers: {
|
||
Authorization: 'Bearer ' + getAuthToken(),
|
||
'Content-Type': contentType,
|
||
},
|
||
body,
|
||
});
|
||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||
return resp.json();
|
||
},
|
||
|
||
download: async (url: string, filename?: string): Promise<void> => {
|
||
const resp = await fetch(resolveApiUrl(url), {
|
||
headers: { Authorization: 'Bearer ' + getAuthToken() },
|
||
});
|
||
if (!resp.ok) {
|
||
const ct = resp.headers.get('Content-Type') || '';
|
||
if (ct.includes('json')) {
|
||
try {
|
||
const json = await resp.json() as { message?: string; Message?: string };
|
||
throw new Error(json.message || json.Message || `HTTP ${resp.status}`);
|
||
} catch (e) {
|
||
if (e instanceof Error && !e.message.startsWith('HTTP ')) throw e;
|
||
}
|
||
}
|
||
throw new Error(`HTTP ${resp.status}`);
|
||
}
|
||
const blob = await resp.blob();
|
||
const a = document.createElement('a');
|
||
a.href = URL.createObjectURL(blob);
|
||
const disp = resp.headers.get('Content-Disposition') || '';
|
||
const m = disp.match(/filename="?(.+?)"?$/);
|
||
a.download = filename || (m ? m[1] : 'download');
|
||
document.body.appendChild(a);
|
||
a.click();
|
||
document.body.removeChild(a);
|
||
URL.revokeObjectURL(a.href);
|
||
},
|
||
|
||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||
upload: async <T = any>(url: string, formData: FormData): Promise<T> => {
|
||
const resp = await fetch(resolveApiUrl(url), {
|
||
method: 'POST',
|
||
headers: { Authorization: 'Bearer ' + getAuthToken() },
|
||
body: formData,
|
||
});
|
||
if (!resp.ok) throw new Error('HTTP ' + resp.status);
|
||
return resp.json();
|
||
},
|
||
};
|
||
|
||
export default http; |