- 新增 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 进度 收益:完整类型提示、编译时错误检查、重构安全性提升
327 lines
8.8 KiB
TypeScript
327 lines
8.8 KiB
TypeScript
/**
|
||
* 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;
|