AssetManager.UniApp/utils/api.ts
claw_bot fa2fa98985 feat: TypeScript 迁移完成
- 新增 tsconfig.json 配置
- 新增 types/ 目录(7个类型定义文件,与后端 DTO 对齐)
- 迁移 vite.config.js → vite.config.ts
- 迁移 main.js → main.ts
- 迁移 utils/api.js → utils/api.ts(泛型化请求封装)
- 迁移 utils/currency.js → utils/currency.ts
- 迁移 6 个 Vue 页面组件(添加 lang="ts" 和类型注解)
- 新增 TYPESCRIPT_MIGRATION.md 迁移计划文档
- 更新 todo.md 进度

收益:完整类型提示、编译时错误检查、重构安全性提升
2026-03-24 05:53:29 +00:00

327 lines
8.8 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* API工具类 - 统一封装后端API请求
*/
import type {
ApiResponse,
TotalAssetsResponse,
GetPortfoliosResponse,
PortfolioDetailResponse,
CreatePortfolioRequest,
CreatePortfolioResponse,
UpdatePortfolioRequest,
GetTransactionsResponse,
CreateTransactionRequest,
CreateTransactionResponse,
NavHistoryResponse,
BackfillNavResponse,
StrategyListResponse,
StrategyDetailResponse,
CreateStrategyRequest,
UpdateStrategyRequest,
StrategyResponse,
DeleteStrategyResponse,
StrategySignalResponse,
UserInfoResponse,
UserStatsResponse,
UpdateUserRequest,
UpdateUserResponse,
} from '@/types';
// API基础URL
const BASE_URL: string = import.meta.env.VITE_API_BASE_URL || 'https://localhost:7040/';
// 请求超时时间
const TIMEOUT = 10000;
// 登录锁,防止并发登录
let loginLock: Promise<any> | null = null;
// 全局loading计数
let loadingCount = 0;
const showLoading = (): void => {
if (loadingCount === 0) {
uni.showLoading({ title: '加载中...', mask: true });
}
loadingCount++;
};
const hideLoading = (): void => {
loadingCount--;
if (loadingCount <= 0) {
loadingCount = 0;
uni.hideLoading();
}
};
/**
* 获取微信登录码
*/
const getWechatCode = (): Promise<string> => {
return new Promise((resolve, reject) => {
uni.login({
provider: 'weixin',
success: (res) => {
if (res.code) {
resolve(res.code);
} else {
reject(new Error(`微信登录码获取失败: ${res.errMsg}`));
}
},
fail: (err) => {
reject(new Error(`微信登录失败: ${err.errMsg}`));
}
});
});
};
/**
* 执行微信登录
*/
const doWechatLogin = async (): Promise<ApiResponse<{ token: string; userId: string }>> => {
const code = await getWechatCode();
const res = await new Promise<any>((resolve, reject) => {
uni.request({
url: `${BASE_URL}api/auth/wechat-login`,
method: 'POST',
data: { code },
header: { 'Content-Type': 'application/json' },
timeout: TIMEOUT,
success: resolve,
fail: reject
});
});
if (res.statusCode === 200 && res.data?.code === 200 && res.data?.data?.token) {
uni.setStorageSync('token', res.data.data.token);
return res.data;
} else {
throw new Error(res.data?.message || '微信登录失败');
}
};
/**
* 带重试机制的请求方法
*/
const requestWithRetry = async <T = any>(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data: Record<string, any> = {},
headers: Record<string, string> = {},
retryCount: number = 0
): Promise<T> => {
try {
let token = uni.getStorageSync('token') as string | undefined;
// 如果没有token先执行微信登录
if (!token) {
if (loginLock) {
await loginLock;
token = uni.getStorageSync('token');
} else {
loginLock = doWechatLogin();
await loginLock;
loginLock = null;
token = uni.getStorageSync('token');
}
}
// 处理URL拼接
let fullUrl: string;
if (BASE_URL.endsWith('/') && url.startsWith('/')) {
fullUrl = BASE_URL + url.substring(1);
} else if (!BASE_URL.endsWith('/') && !url.startsWith('/')) {
fullUrl = BASE_URL + '/' + url;
} else {
fullUrl = BASE_URL + url;
}
showLoading();
const res = await new Promise<any>((resolve, reject) => {
uni.request({
url: fullUrl,
method,
data,
header: {
'Content-Type': 'application/json',
'Authorization': token ? `Bearer ${token}` : '',
...headers
},
timeout: TIMEOUT,
success: resolve,
fail: reject
});
});
hideLoading();
if (res.statusCode === 200) {
return res.data as T;
} else if (res.statusCode === 401) {
uni.removeStorageSync('token');
if (retryCount < 3) {
if (loginLock) {
await loginLock;
} else {
loginLock = doWechatLogin();
await loginLock;
loginLock = null;
}
return requestWithRetry<T>(url, method, data, headers, retryCount + 1);
} else {
uni.showToast({ title: '系统异常,请稍后重试', icon: 'none' });
throw new Error('登录已过期,重试次数超限');
}
} else {
throw new Error(`请求失败: ${res.statusCode}`);
}
} catch (error: any) {
hideLoading();
if (retryCount < 3 && error.message?.includes('网络请求失败')) {
return requestWithRetry<T>(url, method, data, headers, retryCount + 1);
}
if (retryCount >= 2) {
uni.showToast({ title: '系统异常,请稍后重试', icon: 'none', duration: 2000 });
}
throw error;
}
};
/**
* 基础请求方法
*/
const request = <T = any>(
url: string,
method: 'GET' | 'POST' | 'PUT' | 'DELETE' = 'GET',
data: Record<string, any> = {},
headers: Record<string, string> = {}
): Promise<T> => {
return requestWithRetry<T>(url, method, data, headers);
};
/**
* GET请求
*/
export const get = <T = any>(
url: string,
params: Record<string, any> = {},
headers: Record<string, string> = {}
): Promise<T> => {
const queryString = Object.keys(params)
.map(key => `${key}=${encodeURIComponent(params[key])}`)
.join('&');
const fullUrl = queryString ? `${url}?${queryString}` : url;
return request<T>(fullUrl, 'GET', {}, headers);
};
/**
* POST请求
*/
export const post = <T = any>(
url: string,
data: Record<string, any> = {},
headers: Record<string, string> = {}
): Promise<T> => {
return request<T>(url, 'POST', data, headers);
};
/**
* PUT请求
*/
export const put = <T = any>(
url: string,
data: Record<string, any> = {},
headers: Record<string, string> = {}
): Promise<T> => {
return request<T>(url, 'PUT', data, headers);
};
/**
* DELETE请求
*/
export const del = <T = any>(
url: string,
headers: Record<string, string> = {}
): Promise<T> => {
return request<T>(url, 'DELETE', {}, headers);
};
/**
* API接口封装
*/
export const api = {
assets: {
getAssetData: (): Promise<ApiResponse<TotalAssetsResponse>> =>
get('/api/v1/portfolio/assets'),
getHoldings: (): Promise<ApiResponse<GetPortfoliosResponse>> =>
get('/api/v1/portfolio'),
getPortfolio: (id: string | number): Promise<ApiResponse<PortfolioDetailResponse>> =>
get(`/api/v1/portfolio/${id}`),
getTransactions: (params: { portfolioId?: string; limit: number; offset: number }): Promise<ApiResponse<GetTransactionsResponse>> =>
get('/api/v1/portfolio/transactions', params),
createTransaction: (data: CreateTransactionRequest): Promise<ApiResponse<CreateTransactionResponse>> =>
post('/api/v1/portfolio/transactions', data),
createPortfolio: (data: CreatePortfolioRequest): Promise<ApiResponse<CreatePortfolioResponse>> =>
post('/api/v1/portfolio', data),
updatePortfolio: (id: string | number, data: UpdatePortfolioRequest): Promise<ApiResponse<void>> =>
put(`/api/v1/portfolio/${id}`, data),
getPortfolioSignal: (id: string | number): Promise<ApiResponse<StrategySignalResponse>> =>
get(`/api/v1/portfolio/${id}/signal`),
getNavHistory: (id: string | number, params?: { startDate?: string; endDate?: string; interval?: string }): Promise<ApiResponse<NavHistoryResponse>> =>
get(`/api/v1/portfolio/${id}/nav-history`, params),
backfillNavHistory: (id: string | number, force: boolean = false): Promise<ApiResponse<BackfillNavResponse>> =>
post(`/api/v1/portfolio/${id}/nav-history/backfill`, { force }),
},
strategies: {
getStrategies: (): Promise<ApiResponse<StrategyListResponse>> =>
get('/api/v1/strategy/strategies'),
getStrategy: (id: string | number): Promise<ApiResponse<StrategyDetailResponse>> =>
get(`/api/v1/strategy/${id}`),
createStrategy: (data: CreateStrategyRequest): Promise<ApiResponse<StrategyResponse>> =>
post('/api/v1/strategy', data),
updateStrategy: (id: string | number, data: UpdateStrategyRequest): Promise<ApiResponse<StrategyResponse>> =>
put(`/api/v1/strategy/${id}`, data),
deleteStrategy: (id: string | number): Promise<ApiResponse<DeleteStrategyResponse>> =>
del(`/api/v1/strategy/${id}`),
},
user: {
getUserInfo: (): Promise<ApiResponse<UserInfoResponse>> =>
get('/api/v1/user/info'),
getUserStats: (): Promise<ApiResponse<UserStatsResponse>> =>
get('/api/v1/user/stats'),
updateUserInfo: (data: UpdateUserRequest): Promise<ApiResponse<UpdateUserResponse>> =>
put('/api/v1/user/info', data),
},
ticker: {
search: (keyword: string, limit: number = 20): Promise<ApiResponse<{ items: { symbol: string; name: string }[] }>> =>
get('/api/v1/ticker/search', { keyword, limit }),
}
};
export default api;