feat: 登录模块完成

This commit is contained in:
许永平
2025-07-04 15:49:22 +08:00
parent aad2d055b7
commit 8de943d4a4
22 changed files with 1661 additions and 257 deletions

82
nkebao/src/api/auth.ts Normal file
View File

@@ -0,0 +1,82 @@
import { request } from './request';
import type { ApiResponse } from '@/types/common';
// 登录响应数据类型
export interface LoginResponse {
token: string;
token_expired: string;
member: {
id: number;
username: string;
account: string;
avatar?: string;
s2_accountId: string;
};
}
// 验证码响应类型
export interface VerificationCodeResponse {
code: string;
expire_time: string;
}
// 认证相关API
export const authApi = {
// 账号密码登录
login: async (account: string, password: string) => {
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/login', {
account,
password,
typeId: 1 // 默认使用用户类型1
});
return response as unknown as ApiResponse<LoginResponse>;
},
// 验证码登录
loginWithCode: async (account: string, code: string) => {
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/login/code', {
account,
code,
typeId: 1
});
return response as unknown as ApiResponse<LoginResponse>;
},
// 发送验证码
sendVerificationCode: async (account: string) => {
const response = await request.post<ApiResponse<VerificationCodeResponse>>('/v1/auth/send-code', {
account,
type: 'login' // 登录验证码
});
return response as unknown as ApiResponse<VerificationCodeResponse>;
},
// 获取用户信息
getUserInfo: async () => {
const response = await request.get<ApiResponse<any>>('/v1/auth/info');
return response as unknown as ApiResponse<any>;
},
// 刷新Token
refreshToken: async () => {
const response = await request.post<ApiResponse<{ token: string; token_expired: string }>>('/v1/auth/refresh', {});
return response as unknown as ApiResponse<{ token: string; token_expired: string }>;
},
// 微信登录
wechatLogin: async (code: string) => {
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/wechat', {
code
});
return response as unknown as ApiResponse<LoginResponse>;
},
// Apple登录
appleLogin: async (identityToken: string, authorizationCode: string) => {
const response = await request.post<ApiResponse<LoginResponse>>('/v1/auth/apple', {
identity_token: identityToken,
authorization_code: authorizationCode
});
return response as unknown as ApiResponse<LoginResponse>;
},
};

View File

@@ -8,7 +8,6 @@ import type {
CreateDeviceParams,
UpdateDeviceParams,
DeviceStatus,
ServerDevice,
ServerDevicesResponse
} from '@/types/device';

13
nkebao/src/api/index.ts Normal file
View File

@@ -0,0 +1,13 @@
// 导出所有API相关的内容
export * from './auth';
export * from './utils';
export * from './interceptors';
export * from './request';
// 导出现有的API模块
export * from './devices';
export * from './scenarios';
export * from './wechat-accounts';
// 默认导出request实例
export { default as request } from './request';

View File

@@ -0,0 +1,152 @@
import { refreshAuthToken, isTokenExpiringSoon, clearToken } from './utils';
// Token过期处理
export const handleTokenExpired = () => {
if (typeof window !== 'undefined') {
// 清除本地存储
clearToken();
// 跳转到登录页面
setTimeout(() => {
window.location.href = '/login';
}, 0);
}
};
// 显示API错误但不会重定向
export const showApiError = (error: any, defaultMessage: string = '请求失败') => {
if (typeof window === 'undefined') return; // 服务端不处理
let errorMessage = defaultMessage;
// 尝试从各种可能的错误格式中获取消息
if (error) {
if (typeof error === 'string') {
errorMessage = error;
} else if (error instanceof Error) {
errorMessage = error.message || defaultMessage;
} else if (typeof error === 'object') {
// 尝试从API响应中获取错误消息
errorMessage = error.msg || error.message || error.error || defaultMessage;
}
}
// 显示错误消息
console.error('API错误:', errorMessage);
// 这里可以集成toast系统
// 由于toast context在组件层级这里暂时用console
// 实际项目中可以通过事件系统或其他方式集成
};
// 请求拦截器 - 检查token是否需要刷新
export const requestInterceptor = async (): Promise<boolean> => {
if (typeof window === 'undefined') {
return true;
}
// 检查token是否即将过期
if (isTokenExpiringSoon()) {
try {
console.log('Token即将过期尝试刷新...');
const success = await refreshAuthToken();
if (!success) {
console.log('Token刷新失败需要重新登录');
handleTokenExpired();
return false;
}
console.log('Token刷新成功');
} catch (error) {
console.error('Token刷新过程中出错:', error);
handleTokenExpired();
return false;
}
}
return true;
};
// 响应拦截器 - 处理常见错误
export const responseInterceptor = (response: any, result: any) => {
// 处理401未授权
if (response?.status === 401 || (result && result.code === 401)) {
handleTokenExpired();
throw new Error('登录已过期,请重新登录');
}
// 处理403禁止访问
if (response?.status === 403 || (result && result.code === 403)) {
throw new Error('没有权限访问此资源');
}
// 处理404未找到
if (response?.status === 404 || (result && result.code === 404)) {
throw new Error('请求的资源不存在');
}
// 处理500服务器错误
if (response?.status >= 500 || (result && result.code >= 500)) {
throw new Error('服务器内部错误,请稍后重试');
}
return result;
};
// 错误拦截器 - 统一错误处理
export const errorInterceptor = (error: any) => {
console.error('API请求错误:', error);
let errorMessage = '网络请求失败,请稍后重试';
if (error) {
if (typeof error === 'string') {
errorMessage = error;
} else if (error instanceof Error) {
errorMessage = error.message;
} else if (error.name === 'TypeError' && error.message.includes('fetch')) {
errorMessage = '网络连接失败,请检查网络设置';
} else if (error.name === 'AbortError') {
errorMessage = '请求已取消';
}
}
showApiError(error, errorMessage);
throw new Error(errorMessage);
};
// 网络状态监听
export const setupNetworkListener = () => {
if (typeof window === 'undefined') return;
const handleOnline = () => {
console.log('网络已连接');
// 可以在这里添加网络恢复后的处理逻辑
};
const handleOffline = () => {
console.log('网络已断开');
showApiError(null, '网络连接已断开,请检查网络设置');
};
window.addEventListener('online', handleOnline);
window.addEventListener('offline', handleOffline);
// 返回清理函数
return () => {
window.removeEventListener('online', handleOnline);
window.removeEventListener('offline', handleOffline);
};
};
// 初始化拦截器
export const initInterceptors = () => {
// 设置网络监听
const cleanupNetwork = setupNetworkListener();
// 返回清理函数
return () => {
if (cleanupNetwork) {
cleanupNetwork();
}
};
};

View File

@@ -1,8 +1,9 @@
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios';
import { requestInterceptor, responseInterceptor, errorInterceptor } from './interceptors';
// 创建axios实例
const request: AxiosInstance = axios.create({
baseURL: process.env.REACT_APP_API_BASE || 'http://localhost:3000/api',
baseURL: process.env.REACT_APP_API_BASE_URL || 'http://www.yishi.com',
timeout: 10000,
headers: {
'Content-Type': 'application/json',
@@ -11,12 +12,21 @@ const request: AxiosInstance = axios.create({
// 请求拦截器
request.interceptors.request.use(
(config) => {
// 可以在这里添加token等认证信息
async (config) => {
// 检查token是否需要刷新
if (config.headers.Authorization) {
const shouldContinue = await requestInterceptor();
if (!shouldContinue) {
throw new Error('请求被拦截,需要重新登录');
}
}
// 添加token到请求头
const token = localStorage.getItem('token');
if (token) {
config.headers.Authorization = `Bearer ${token}`;
}
return config;
},
(error) => {
@@ -27,12 +37,14 @@ request.interceptors.request.use(
// 响应拦截器
request.interceptors.response.use(
(response: AxiosResponse) => {
return response.data;
// 处理响应数据
const result = response.data;
const processedResult = responseInterceptor(response, result);
return processedResult;
},
(error) => {
// 统一错误处理
console.error('API请求错误:', error);
return Promise.reject(error);
return errorInterceptor(error);
}
);

View File

@@ -12,13 +12,6 @@ export interface SceneItem {
deleteTime: number | null;
}
// 服务器返回的场景列表响应类型
export interface ScenesResponse {
code: number;
message: string;
data: SceneItem[];
}
// 前端使用的场景数据类型
export interface Channel {
id: string;
@@ -51,7 +44,7 @@ export const fetchScenes = async (params: {
page?: number;
limit?: number;
keyword?: string;
} = {}): Promise<ScenesResponse> => {
} = {}): Promise<ApiResponse<SceneItem[]>> => {
const { page = 1, limit = 10, keyword = "" } = params;
const queryParams = new URLSearchParams();
@@ -63,7 +56,7 @@ export const fetchScenes = async (params: {
}
try {
return await get<ScenesResponse>(`/v1/plan/scenes?${queryParams.toString()}`);
return await get<ApiResponse<SceneItem[]>>(`/v1/plan/scenes?${queryParams.toString()}`);
} catch (error) {
console.error("Error fetching scenes:", error);
// 返回一个错误响应

100
nkebao/src/api/utils.ts Normal file
View File

@@ -0,0 +1,100 @@
import { authApi } from './auth';
// 设置token到localStorage
export const setToken = (token: string) => {
if (typeof window !== 'undefined') {
localStorage.setItem('token', token);
}
};
// 获取token
export const getToken = (): string | null => {
if (typeof window !== 'undefined') {
return localStorage.getItem('token');
}
return null;
};
// 清除token
export const clearToken = () => {
if (typeof window !== 'undefined') {
localStorage.removeItem('token');
localStorage.removeItem('userInfo');
localStorage.removeItem('token_expired');
localStorage.removeItem('s2_accountId');
}
};
// 验证token是否有效
export const validateToken = async (): Promise<boolean> => {
try {
const response = await authApi.getUserInfo();
return response.code === 200;
} catch (error) {
console.error('Token验证失败:', error);
return false;
}
};
// 刷新令牌
export const refreshAuthToken = async (): Promise<boolean> => {
if (typeof window === 'undefined') {
return false;
}
try {
const response = await authApi.refreshToken();
if (response.code === 200 && response.data?.token) {
setToken(response.data.token);
// 更新过期时间
if (response.data.token_expired) {
localStorage.setItem('token_expired', response.data.token_expired);
}
return true;
}
return false;
} catch (error) {
console.error('刷新Token失败:', error);
return false;
}
};
// 检查token是否即将过期
export const isTokenExpiringSoon = (): boolean => {
if (typeof window === 'undefined') {
return false;
}
const tokenExpired = localStorage.getItem('token_expired');
if (!tokenExpired) return true;
try {
const expiredTime = new Date(tokenExpired).getTime();
const currentTime = new Date().getTime();
// 提前10分钟认为即将过期
return currentTime >= (expiredTime - 10 * 60 * 1000);
} catch (error) {
console.error('解析token过期时间失败:', error);
return true;
}
};
// 检查token是否已过期
export const isTokenExpired = (): boolean => {
if (typeof window === 'undefined') {
return false;
}
const tokenExpired = localStorage.getItem('token_expired');
if (!tokenExpired) return true;
try {
const expiredTime = new Date(tokenExpired).getTime();
const currentTime = new Date().getTime();
// 提前5分钟认为过期给刷新留出时间
return currentTime >= (expiredTime - 5 * 60 * 1000);
} catch (error) {
console.error('解析token过期时间失败:', error);
return true;
}
};

View File

@@ -27,23 +27,6 @@ interface WechatAccountSummary {
}[];
}
interface WechatAccountSummaryResponse {
code: number;
message: string;
data: WechatAccountSummary;
}
interface ServerWechatAccountsResponse {
code: number;
message: string;
data: {
list: any[];
total: number;
page: number;
limit: number;
};
}
interface QueryWechatAccountParams {
page?: number;
limit?: number;
@@ -57,7 +40,12 @@ interface QueryWechatAccountParams {
* @param params 查询参数
* @returns 微信账号列表响应
*/
export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise<ServerWechatAccountsResponse> => {
export const fetchWechatAccountList = async (params: QueryWechatAccountParams = {}): Promise<ApiResponse<{
list: any[];
total: number;
page: number;
limit: number;
}>> => {
const queryParams = new URLSearchParams();
// 添加查询参数
@@ -68,15 +56,20 @@ export const fetchWechatAccountList = async (params: QueryWechatAccountParams =
if (params.order) queryParams.append('order', params.order);
// 发起API请求
return get<ServerWechatAccountsResponse>(`/v1/wechats?${queryParams.toString()}`);
return get<ApiResponse<{
list: any[];
total: number;
page: number;
limit: number;
}>>(`/v1/wechats?${queryParams.toString()}`);
};
/**
* 刷新微信账号状态
* @returns 刷新结果
*/
export const refreshWechatAccounts = async (): Promise<{ code: number; message: string; data: any }> => {
return put<{ code: number; message: string; data: any }>('/v1/wechats/refresh', {});
export const refreshWechatAccounts = async (): Promise<ApiResponse<any>> => {
return put<ApiResponse<any>>('/v1/wechats/refresh', {});
};
/**
@@ -85,8 +78,8 @@ export const refreshWechatAccounts = async (): Promise<{ code: number; message:
* @param targetId 目标微信账号ID
* @returns 转移结果
*/
export const transferWechatFriends = async (sourceId: string | number, targetId: string | number): Promise<{ code: number; message: string; data: any }> => {
return post<{ code: number; message: string; data: any }>('/v1/wechats/transfer-friends', {
export const transferWechatFriends = async (sourceId: string | number, targetId: string | number): Promise<ApiResponse<any>> => {
return post<ApiResponse<any>>('/v1/wechats/transfer-friends', {
source_id: sourceId,
target_id: targetId
});
@@ -175,9 +168,9 @@ export const fetchWechatFriends = async (wechatId: string, page: number = 1, pag
* @param id 微信账号ID
* @returns 微信账号概览信息
*/
export const fetchWechatAccountSummary = async (wechatIdid: string): Promise<WechatAccountSummaryResponse> => {
export const fetchWechatAccountSummary = async (wechatId: string): Promise<ApiResponse<WechatAccountSummary>> => {
try {
return get<WechatAccountSummaryResponse>(`/v1/wechats/${wechatIdid}/summary`);
return get<ApiResponse<WechatAccountSummary>>(`/v1/wechats/${wechatId}/summary`);
} catch (error) {
console.error("获取账号概览失败:", error);
throw error;
@@ -198,20 +191,13 @@ export interface WechatFriendDetail {
wechatId: string;
addDate: string;
tags: string[];
playDate: string;
memo: string;
source: string;
}
interface WechatFriendDetailResponse {
code: number;
message: string;
data: WechatFriendDetail;
}
export const fetchWechatFriendDetail = async (wechatId: string): Promise<WechatFriendDetailResponse> => {
export const fetchWechatFriendDetail = async (wechatId: string): Promise<ApiResponse<WechatFriendDetail>> => {
try {
return get<WechatFriendDetailResponse>(`/v1/wechats/${wechatId}`);
return get<ApiResponse<WechatFriendDetail>>(`/v1/wechats/${wechatId}/friend-detail`);
} catch (error) {
console.error("获取好友详情失败:", error);
throw error;