410 lines
11 KiB
JavaScript
410 lines
11 KiB
JavaScript
/**
|
||
* 前端 API 工具类
|
||
* 用于在渲染进程(Vue)中直接调用后端 API
|
||
*/
|
||
|
||
// API 基础 URL(从环境变量或配置中获取)
|
||
|
||
/**
|
||
* HTTP 请求工具
|
||
*/
|
||
class ApiClient {
|
||
constructor() {
|
||
|
||
this.token = this.getToken();
|
||
// 缓存 electronAPI 可用状态,避免每次检查
|
||
this._electronAPIChecked = false;
|
||
this._electronAPIAvailable = false;
|
||
}
|
||
|
||
/**
|
||
* 检查 window.electronAPI 是否可用(快速检查,不等待)
|
||
* @param {boolean} forceCheck 强制重新检查(默认 false)
|
||
* @returns {boolean} 是否可用
|
||
*/
|
||
isElectronAPIAvailable(forceCheck = false) {
|
||
// 如果已经检查过且不需要强制检查,直接返回缓存结果
|
||
if (this._electronAPIChecked && !forceCheck) {
|
||
return this._electronAPIAvailable;
|
||
}
|
||
|
||
// 快速检查
|
||
const available = !!(window.electronAPI && window.electronAPI.http);
|
||
|
||
// 缓存结果
|
||
this._electronAPIChecked = true;
|
||
this._electronAPIAvailable = available;
|
||
|
||
return available;
|
||
}
|
||
|
||
/**
|
||
* 等待 window.electronAPI 可用(仅在首次检查时等待)
|
||
* @param {number} maxWaitTime 最大等待时间(毫秒),默认500ms(减少等待时间)
|
||
* @param {number} checkInterval 检查间隔(毫秒),默认50ms(加快检查频率)
|
||
* @returns {Promise<boolean>} 是否可用
|
||
*/
|
||
async waitForElectronAPI(maxWaitTime = 500, checkInterval = 50) {
|
||
// 如果已经确认可用,直接返回
|
||
if (this.isElectronAPIAvailable()) {
|
||
return true;
|
||
}
|
||
|
||
// 如果已经检查过但不可用,直接返回 false(不重复等待)
|
||
if (this._electronAPIChecked && !this._electronAPIAvailable) {
|
||
return false;
|
||
}
|
||
|
||
// 首次检查时,短暂等待(preload 脚本是同步的,通常立即可用)
|
||
const startTime = Date.now();
|
||
return new Promise((resolve) => {
|
||
const check = () => {
|
||
if (window.electronAPI && window.electronAPI.http) {
|
||
this._electronAPIChecked = true;
|
||
this._electronAPIAvailable = true;
|
||
resolve(true);
|
||
return;
|
||
}
|
||
|
||
const elapsed = Date.now() - startTime;
|
||
if (elapsed >= maxWaitTime) {
|
||
// 超时后标记为不可用
|
||
this._electronAPIChecked = true;
|
||
this._electronAPIAvailable = false;
|
||
resolve(false);
|
||
return;
|
||
}
|
||
|
||
setTimeout(check, checkInterval);
|
||
};
|
||
check();
|
||
});
|
||
}
|
||
|
||
/**
|
||
* 使用 electronAPI.http 发送请求
|
||
* @param {string} method HTTP 方法
|
||
* @param {string} endpoint API 端点
|
||
* @param {Object} data 请求数据
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
/**
|
||
* 将响应式对象(Proxy)转换为普通对象,以便通过 IPC 传递
|
||
* @param {any} obj 要转换的对象
|
||
* @returns {any} 普通对象
|
||
*/
|
||
toPlainObject(obj) {
|
||
if (obj === null || obj === undefined) {
|
||
return obj;
|
||
}
|
||
|
||
// 如果是基本类型,直接返回
|
||
if (typeof obj !== 'object') {
|
||
return obj;
|
||
}
|
||
|
||
// 如果是 Date 对象,转换为 ISO 字符串
|
||
if (obj instanceof Date) {
|
||
return obj.toISOString();
|
||
}
|
||
|
||
// 如果是 RegExp 对象,转换为字符串
|
||
if (obj instanceof RegExp) {
|
||
return obj.toString();
|
||
}
|
||
|
||
// 尝试使用 JSON 序列化/反序列化(最可靠的方法)
|
||
try {
|
||
return JSON.parse(JSON.stringify(obj));
|
||
} catch (e) {
|
||
// 如果 JSON 序列化失败,使用递归方法
|
||
console.warn('[API] JSON 序列化失败,使用递归方法:', e);
|
||
|
||
// 如果是数组,递归处理每个元素
|
||
if (Array.isArray(obj)) {
|
||
return obj.map(item => this.toPlainObject(item));
|
||
}
|
||
|
||
// 如果是对象,递归处理每个属性
|
||
const plainObj = {};
|
||
for (const key in obj) {
|
||
if (Object.prototype.hasOwnProperty.call(obj, key)) {
|
||
try {
|
||
plainObj[key] = this.toPlainObject(obj[key]);
|
||
} catch (err) {
|
||
// 如果无法访问属性,跳过
|
||
console.warn(`[API] 无法访问属性 ${key}:`, err);
|
||
}
|
||
}
|
||
}
|
||
return plainObj;
|
||
}
|
||
}
|
||
|
||
async requestWithElectronAPI(method, endpoint, data = null) {
|
||
const fullEndpoint = endpoint.startsWith('/') ? endpoint : `/${endpoint}`;
|
||
|
||
// 将响应式对象转换为普通对象(避免 Proxy 无法序列化的问题)
|
||
const plainData = data ? this.toPlainObject(data) : null;
|
||
|
||
// 主进程不再缓存 token,每次请求都从浏览器端实时获取,这里不需要同步
|
||
|
||
try {
|
||
let result;
|
||
|
||
if (method === 'GET') {
|
||
result = await window.electronAPI.http.get(fullEndpoint, plainData || {});
|
||
} else {
|
||
result = await window.electronAPI.http.post(fullEndpoint, plainData || {});
|
||
}
|
||
// 输出详细的响应日志
|
||
console.log('requestWithElectronAPI result', method, fullEndpoint ,plainData, result);
|
||
|
||
|
||
return result;
|
||
} catch (error) {
|
||
// 输出详细的错误日志
|
||
console.error('========================================');
|
||
console.error(`[API错误] ${method}`);
|
||
console.error(`[错误信息]`, error.message || error);
|
||
console.error(`[错误堆栈]`, error.stack);
|
||
console.error('========================================');
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 设置认证 token
|
||
* @param {string} token
|
||
*/
|
||
setToken(token) {
|
||
this.token = token;
|
||
if (token) {
|
||
localStorage.setItem('api_token', token);
|
||
// 主进程不再缓存 token,每次请求都从浏览器端实时获取
|
||
} else {
|
||
localStorage.removeItem('api_token');
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 获取认证 token
|
||
* @returns {string|null}
|
||
*/
|
||
getToken() {
|
||
if (!this.token) {
|
||
this.token = localStorage.getItem('api_token');
|
||
}
|
||
return this.token;
|
||
}
|
||
|
||
/**
|
||
* 构建请求头
|
||
* @returns {Object}
|
||
*/
|
||
buildHeaders() {
|
||
const headers = {
|
||
'Content-Type': 'application/json'
|
||
};
|
||
|
||
const token = this.getToken();
|
||
if (token) {
|
||
headers['applet-token'] = `${token}`;
|
||
}
|
||
|
||
return headers;
|
||
}
|
||
|
||
/**
|
||
* 处理错误
|
||
* @param {Error|Response} error
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
async handleError(error) {
|
||
if (error.response) {
|
||
// 服务器返回了错误响应
|
||
const { status, data } = error.response;
|
||
return {
|
||
code: status,
|
||
message: data?.message || `请求失败: ${status}`,
|
||
data: null
|
||
};
|
||
} else if (error.request) {
|
||
// 请求已发出但没有收到响应
|
||
return {
|
||
code: -1,
|
||
message: '网络错误,请检查网络连接',
|
||
data: null
|
||
};
|
||
} else {
|
||
// 其他错误
|
||
return {
|
||
code: -1,
|
||
message: error.message || '未知错误',
|
||
data: null
|
||
};
|
||
}
|
||
}
|
||
|
||
/**
|
||
* 显示错误提示
|
||
* @param {string} message 错误消息
|
||
*/
|
||
showError(message) {
|
||
// 优先使用 electronAPI 的日志功能
|
||
if (window.electronAPI && typeof window.electronAPI.log === 'function') {
|
||
try {
|
||
window.electronAPI.log('error', `[API错误] ${message}`);
|
||
} catch (error) {
|
||
console.error('调用 electronAPI.log 失败:', error);
|
||
}
|
||
}
|
||
|
||
console.error('[API错误]', message);
|
||
|
||
}
|
||
|
||
/**
|
||
* 确保 electronAPI 可用并发送请求
|
||
* @param {string} method HTTP 方法
|
||
* @param {string} endpoint API 端点
|
||
* @param {Object} data 请求数据
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
async _ensureElectronAPIAndRequest(method, endpoint, data = null) {
|
||
// 检查 electronAPI 是否可用
|
||
if (!this.isElectronAPIAvailable()) {
|
||
// 首次检查时,短暂等待一次
|
||
const electronAPIAvailable = await this.waitForElectronAPI();
|
||
if (!electronAPIAvailable) {
|
||
const errorMsg = 'electronAPI 不可用,无法发送请求';
|
||
this.showError(errorMsg);
|
||
throw new Error(errorMsg);
|
||
}
|
||
}
|
||
|
||
// 发送请求
|
||
const result = await this.requestWithElectronAPI(method, endpoint, data);
|
||
|
||
// 检查返回结果,如果 code 不为 0,显示错误提示
|
||
if (result && result.code !== undefined && result.code !== 0) {
|
||
const errorMsg = result.message || '请求失败';
|
||
this.showError(errorMsg);
|
||
}
|
||
|
||
return result;
|
||
}
|
||
|
||
/**
|
||
* 发送 HTTP 请求
|
||
* @param {string} method HTTP 方法
|
||
* @param {string} endpoint API 端点
|
||
* @param {Object} data 请求数据
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
async request(method, endpoint, data = null) {
|
||
try {
|
||
const res = await this._ensureElectronAPIAndRequest(method, endpoint, data);
|
||
return res
|
||
} catch (error) {
|
||
const errorMsg = error.message || '请求失败';
|
||
this.showError(errorMsg);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* GET 请求
|
||
* @param {string} endpoint API 端点
|
||
* @param {Object} params 查询参数
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
async get(endpoint, params = {}) {
|
||
try {
|
||
return await this._ensureElectronAPIAndRequest('GET', endpoint, params);
|
||
} catch (error) {
|
||
const errorMsg = error.message || '请求失败';
|
||
this.showError(errorMsg);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
/**
|
||
* POST 请求
|
||
* @param {string} endpoint API 端点
|
||
* @param {Object} data 请求数据
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
async post(endpoint, data = {}) {
|
||
try {
|
||
const result = await this.request('POST', endpoint, data);
|
||
return result;
|
||
} catch (error) {
|
||
console.error(`[ApiClient] POST 请求失败:`, { error: error.message, endpoint, data });
|
||
const errorResult = await this.handleError(error);
|
||
// 如果 handleError 返回了错误结果,显示错误提示
|
||
if (errorResult && errorResult.code !== 0) {
|
||
this.showError(errorResult.message || '请求失败');
|
||
}
|
||
return errorResult;
|
||
}
|
||
}
|
||
|
||
}
|
||
|
||
// 创建默认实例
|
||
const apiClient = new ApiClient();
|
||
|
||
/**
|
||
* 用户登录
|
||
* @param {string} email 邮箱
|
||
* @param {string} password 密码
|
||
* @param {string} deviceId 设备ID(可选)
|
||
* @returns {Promise<Object>}
|
||
*/
|
||
export async function login(email, password, deviceId = null) {
|
||
const requestData = {
|
||
login_name: email, // 后端使用 login_name 字段
|
||
password
|
||
};
|
||
|
||
if (deviceId) {
|
||
requestData.device_id = deviceId;
|
||
}
|
||
|
||
const response = await apiClient.post('/user/login', requestData);
|
||
|
||
// 如果登录成功,保存 token
|
||
if (response.code === 0 && response.data && response.data.token) {
|
||
apiClient.setToken(response.data.token);
|
||
}
|
||
|
||
return response;
|
||
}
|
||
|
||
/**
|
||
* 设置 API token(用于其他地方设置的 token)
|
||
* @param {string} token
|
||
*/
|
||
export function setToken(token) {
|
||
apiClient.setToken(token);
|
||
}
|
||
|
||
/**
|
||
* 获取 API token
|
||
* @returns {string|null}
|
||
*/
|
||
export function getToken() {
|
||
return apiClient.getToken();
|
||
}
|
||
|
||
/**
|
||
* 清除 API token
|
||
*/
|
||
export function clearToken() {
|
||
apiClient.setToken(null);
|
||
}
|
||
|
||
// 导出默认实例,方便直接使用
|
||
export default apiClient;
|