106 lines
4.3 KiB
TypeScript
106 lines
4.3 KiB
TypeScript
export type UploadProgress = {
|
|
loaded: number;
|
|
total: number;
|
|
};
|
|
|
|
export type AdminApiOptions = {
|
|
csrf?: string;
|
|
};
|
|
|
|
const exactMessages: Record<string, string> = {
|
|
"current password is invalid": "当前密码不正确",
|
|
"new password is required": "新密码不能为空",
|
|
"new password must be at least 8 characters": "新密码至少需要 8 位",
|
|
"new password cannot be admin": "新密码不能为 admin",
|
|
"new password must be different from current password": "新密码不能与当前密码相同",
|
|
"invalid password or captcha": "密码或验证码不正确",
|
|
"login required": "需要登录后继续操作",
|
|
"csrf token required": "页面安全令牌已失效,请刷新后重试",
|
|
"csrf token invalid": "页面安全令牌无效,请刷新后重试",
|
|
"code is required": "缺少反馈编号",
|
|
"revisionid is required": "请选择要恢复的历史版本",
|
|
"post required": "该操作需要使用 POST 请求",
|
|
"get required": "该操作需要使用 GET 请求",
|
|
"file is required": "请选择要上传的文件",
|
|
"invalid filename": "文件名不合法",
|
|
"path escape rejected": "文件路径不合法",
|
|
"check job not found": "未找到心跳检测任务",
|
|
"streaming is not supported": "当前运行环境不支持实时事件流",
|
|
};
|
|
|
|
const codeMessages: Record<string, string> = {
|
|
UNAUTHORIZED: "需要登录后继续操作",
|
|
LOGIN_FAILED: "登录失败,请检查密码和验证码",
|
|
PASSWORD_CHANGE_FAILED: "密码修改失败",
|
|
INVALID_PAYLOAD: "提交内容格式不正确",
|
|
DATABASE_TEST_FAILED: "数据库连接测试失败",
|
|
DATABASE_IMPORT_FAILED: "SQLite 导入远端库失败",
|
|
DATABASE_SYNC_FAILED: "远端库同步回本地失败",
|
|
LEGACY_SAVE_FAILED: "兼容 JSON 保存失败",
|
|
LEGACY_VALIDATE_FAILED: "兼容 JSON 校验失败",
|
|
LEGACY_RESTORE_FAILED: "兼容 JSON 恢复失败",
|
|
NOTICE_SAVE_FAILED: "版本日志保存失败",
|
|
NOTICE_VALIDATE_FAILED: "版本日志校验失败",
|
|
NOTICE_RESTORE_FAILED: "版本日志恢复失败",
|
|
PACKAGE_UPLOAD_FAILED: "发布包上传失败",
|
|
SOURCE_SAVE_FAILED: "接口源保存失败",
|
|
CHECK_FAILED: "接口健康检测失败",
|
|
SYNC_FAILED: "同步操作失败",
|
|
FORBIDDEN: "没有权限执行该操作",
|
|
METHOD_NOT_ALLOWED: "请求方法不正确",
|
|
};
|
|
|
|
export async function adminFetch<T>(target: string, init: RequestInit = {}, options: AdminApiOptions = {}): Promise<T> {
|
|
const headers = new Headers(init.headers);
|
|
if (!headers.has("Content-Type") && init.body && !(init.body instanceof FormData)) {
|
|
headers.set("Content-Type", "application/json");
|
|
}
|
|
if (options.csrf) headers.set("X-CSRF-Token", options.csrf);
|
|
const res = await fetch(target, { ...init, headers, credentials: "include" });
|
|
const data = await res.json().catch(() => ({}));
|
|
if (!res.ok || data.ok === false) {
|
|
throw new Error(toChineseError(data.message || data.error || `HTTP ${res.status}`));
|
|
}
|
|
return data as T;
|
|
}
|
|
|
|
export function uploadAdminFile<T>(target: string, form: FormData, options: AdminApiOptions, onProgress: (progress: UploadProgress) => void): Promise<T> {
|
|
return new Promise((resolve, reject) => {
|
|
const xhr = new XMLHttpRequest();
|
|
xhr.open("POST", target);
|
|
xhr.withCredentials = true;
|
|
if (options.csrf) xhr.setRequestHeader("X-CSRF-Token", options.csrf);
|
|
xhr.upload.onprogress = (event) => {
|
|
if (event.lengthComputable) onProgress({ loaded: event.loaded, total: event.total });
|
|
};
|
|
xhr.onload = () => {
|
|
const data = parseJSONSafe(xhr.responseText, {});
|
|
if (xhr.status < 200 || xhr.status >= 300 || data.ok === false) {
|
|
reject(new Error(toChineseError(data.message || data.error || `HTTP ${xhr.status}`)));
|
|
return;
|
|
}
|
|
resolve(data as T);
|
|
};
|
|
xhr.onerror = () => reject(new Error("网络异常,发布包上传失败"));
|
|
xhr.onabort = () => reject(new Error("发布包上传已取消"));
|
|
xhr.send(form);
|
|
});
|
|
}
|
|
|
|
export function toChineseError(value: string) {
|
|
const raw = String(value || "").trim();
|
|
const lower = raw.toLowerCase();
|
|
if (exactMessages[lower]) return exactMessages[lower];
|
|
if (codeMessages[raw]) return codeMessages[raw];
|
|
if (/^HTTP\s+\d+/.test(raw)) return `请求失败:${raw}`;
|
|
return raw || "操作失败";
|
|
}
|
|
|
|
function parseJSONSafe(value: string, fallback: any) {
|
|
try {
|
|
return JSON.parse(value || "{}");
|
|
} catch {
|
|
return fallback;
|
|
}
|
|
}
|