更新:登录功能
This commit is contained in:
7
api/clients/main.ts
Normal file
7
api/clients/main.ts
Normal file
@@ -0,0 +1,7 @@
|
||||
// api/clients/main.ts
|
||||
import { createRequestClient } from '../request';
|
||||
import { SERVICE_MAP } from '../config';
|
||||
|
||||
export const mainClient = createRequestClient({
|
||||
baseURL: SERVICE_MAP.MAIN,
|
||||
});
|
||||
11
api/clients/payment.ts
Normal file
11
api/clients/payment.ts
Normal file
@@ -0,0 +1,11 @@
|
||||
// api/clients/payment.ts
|
||||
/**
|
||||
* 支付专用接口实例
|
||||
*/
|
||||
import { createRequestClient } from '../request';
|
||||
import { SERVICE_MAP } from '../config';
|
||||
|
||||
export const paymentClient = createRequestClient({
|
||||
baseURL: SERVICE_MAP.PAYMENT,
|
||||
});
|
||||
|
||||
28
api/config.ts
Normal file
28
api/config.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
// api/config.ts
|
||||
export const ENV = process.env.NODE_ENV || 'development';
|
||||
|
||||
/**
|
||||
* 开发/生产的 base url 组织成 SERVICE_MAP。
|
||||
* 根据实际域名替换下面的地址。
|
||||
*/
|
||||
const BASE_URL_MAP = {
|
||||
development: {
|
||||
MAIN: 'http://192.168.110.100:9300/pb/',
|
||||
// PAYMENT: 'https://dev-pay.example.com', // 暂时用不到
|
||||
// CDN: 'https://cdn-dev.example.com', // 暂时用不到
|
||||
},
|
||||
production: {
|
||||
MAIN: 'https://global.nuttyreading.com/',
|
||||
// PAYMENT: 'https://pay.example.com', // 暂时用不到
|
||||
// CDN: 'https://cdn.example.com', // 暂时用不到
|
||||
},
|
||||
} as const;
|
||||
|
||||
export const APP_INFO = {
|
||||
TYPE: 'abroad', // APP 名称
|
||||
VERSION_CODE: '1.0.0', // APP 版本号,可能升级的时候会用,这里需要再确定?
|
||||
}
|
||||
|
||||
export const REQUEST_TIMEOUT = 15000;
|
||||
|
||||
export const SERVICE_MAP = (BASE_URL_MAP as any)[ENV] ?? BASE_URL_MAP.development;
|
||||
32
api/interceptors/request.ts
Normal file
32
api/interceptors/request.ts
Normal file
@@ -0,0 +1,32 @@
|
||||
// 片段示例 - requestInterceptor 更稳健的写法
|
||||
import type { IRequestOptions } from '../types'
|
||||
import { useUserStore } from '@/stores/user'
|
||||
import { APP_INFO } from '@/api/config'
|
||||
|
||||
export function requestInterceptor(options: IRequestOptions): IRequestOptions {
|
||||
const headers = { ...(options.headers || {}) }
|
||||
|
||||
// 更明确地调用 useUserStore
|
||||
let token = ''
|
||||
try {
|
||||
const userStore = typeof useUserStore === 'function' ? useUserStore() : null
|
||||
token = userStore?.token || uni.getStorageSync('token') || ''
|
||||
} catch (e) {
|
||||
token = uni.getStorageSync('token') || ''
|
||||
}
|
||||
|
||||
if (token) headers.token = token
|
||||
|
||||
// Content-Type:只有在没有 files 时才默认为 application/json
|
||||
if (!options.files && !headers['Content-Type']) {
|
||||
headers['Content-Type'] = 'application/json;charset=UTF-8'
|
||||
}
|
||||
|
||||
headers['appType'] = APP_INFO.TYPE
|
||||
headers['version_code'] = APP_INFO.VERSION_CODE || '1.0.0'
|
||||
|
||||
return {
|
||||
...options,
|
||||
header: headers,
|
||||
}
|
||||
}
|
||||
78
api/interceptors/response.ts
Normal file
78
api/interceptors/response.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
// api/interceptors/response.ts
|
||||
import type { IApiResponse } from '../types';
|
||||
|
||||
/**
|
||||
* 响应拦截器:严格兼容原项目返回约定
|
||||
*
|
||||
* 原项目要点回顾:
|
||||
* - 当 response.statusCode == 200 且 (body.success === true || body.code == 0) 时判定为成功;
|
||||
* - 部分错误码("401", 1000,1001,1100,402 等)表示需要重新登录/清除状态并跳转;
|
||||
* - 错误时会返回 Promise.reject({ statusCode: 0, errMsg: ... , data: body })
|
||||
*
|
||||
*/
|
||||
|
||||
function handleAuthExpired() {
|
||||
// 清空本地登录信息(保持与原项目一致)
|
||||
try {
|
||||
uni.removeStorageSync('userInfo');
|
||||
} catch (e) {}
|
||||
// 跳转 login,与原项目保持一致的路径
|
||||
// 在小程序/APP/H5 情况下原项目分别做了适配,简单通用处理如下:
|
||||
uni.showToast({ title: '登录失效,请重新登录', icon: 'none' });
|
||||
setTimeout(() => {
|
||||
uni.navigateTo({ url: '/pages/login/login' });
|
||||
}, 600);
|
||||
}
|
||||
|
||||
export function responseInterceptor(res: UniApp.RequestSuccessCallbackResult) {
|
||||
// 先处理非 200 的 http 状态
|
||||
if (res.statusCode && res.statusCode !== 200) {
|
||||
const msg = `网络错误(${res.statusCode})`;
|
||||
uni.showToast({ title: msg, icon: 'none' });
|
||||
return Promise.reject({ statusCode: res.statusCode, errMsg: msg, response: res });
|
||||
}
|
||||
|
||||
// 可能为字符串,尝试解析(原项目也做了类似处理)
|
||||
let httpData: IApiResponse | string = res.data as any;
|
||||
if (typeof httpData === 'string') {
|
||||
try {
|
||||
httpData = JSON.parse(httpData);
|
||||
} catch (e) {
|
||||
// 无法解析仍然返回原始 body
|
||||
}
|
||||
}
|
||||
|
||||
// 规范化 message 字段
|
||||
const message = (httpData as any).msg || (httpData as any).message || (httpData as any).errMsg || '';
|
||||
|
||||
// 成功判断:与原项目一致的条件
|
||||
const successFlag = (httpData as any).success === true || (httpData as any).code === 0;
|
||||
|
||||
if (successFlag) {
|
||||
// 返回原始 httpData(与原项目 dataFactory 返回 Promise.resolve(httpData) 保持一致)
|
||||
// 但大多数调用者更关心 data 字段,这里返回整个 httpData,调用者可取 .data
|
||||
return Promise.resolve(httpData);
|
||||
}
|
||||
|
||||
// 登录失效或需要强制登录的一些 code(与原项目一致)
|
||||
const code = (httpData as any).code;
|
||||
if (code === '401' || code === 401) {
|
||||
// 触发登出流程
|
||||
handleAuthExpired();
|
||||
return Promise.reject({ statusCode: 0, errMsg: '登录失效', data: httpData });
|
||||
}
|
||||
|
||||
// 原项目还将 1000,1001,1100,402 等视作需要强制登录
|
||||
if (code === '1000' || code === '1001' || code === 1000 || code === 1001 || code === 1100 || code === '402' || code === 402) {
|
||||
handleAuthExpired();
|
||||
return Promise.reject({ statusCode: 0, errMsg: message || '请登录', data: httpData });
|
||||
}
|
||||
|
||||
// 其他后端业务错误:toast 并 reject
|
||||
const errMsg = message || '请求异常';
|
||||
if (errMsg) {
|
||||
uni.showToast({ title: errMsg, icon: 'none' });
|
||||
}
|
||||
|
||||
return Promise.reject({ statusCode: 0, errMsg, data: httpData });
|
||||
}
|
||||
49
api/modules/auth.ts
Normal file
49
api/modules/auth.ts
Normal file
@@ -0,0 +1,49 @@
|
||||
// api/modules/auth.ts
|
||||
import { mainClient } from '@/api/clients/main'
|
||||
import type { IApiResponse } from '@/api/types'
|
||||
import type { IUserInfo, ILoginResponse } from '@/types/user'
|
||||
|
||||
/**
|
||||
* 验证码登录/注册
|
||||
* @param tel 邮箱地址
|
||||
* @param code 验证码
|
||||
*/
|
||||
export async function loginWithCode(tel: string, code: string) {
|
||||
const res = await mainClient.request<IApiResponse<ILoginResponse>>({
|
||||
url: 'book/user/registerOrLogin',
|
||||
method: 'GET',
|
||||
data: { tel, code }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 密码登录
|
||||
* @param phone 邮箱地址
|
||||
* @param password 密码
|
||||
*/
|
||||
export async function loginWithPassword(phone: string, password: string) {
|
||||
const res = await mainClient.request<IApiResponse<ILoginResponse>>({
|
||||
url: 'book/user/login',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: { phone, password }
|
||||
})
|
||||
return res
|
||||
}
|
||||
|
||||
/**
|
||||
* 重置密码
|
||||
* @param phone 邮箱地址
|
||||
* @param code 验证码
|
||||
* @param password 新密码
|
||||
*/
|
||||
export async function resetPassword(phone: string, code: string, password: string) {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'book/user/setPassword',
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
|
||||
data: { phone, code, password }
|
||||
})
|
||||
return res.data
|
||||
}
|
||||
31
api/modules/common.ts
Normal file
31
api/modules/common.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
// api/modules/common.ts
|
||||
import { mainClient } from '@/api/clients/main'
|
||||
import type { IApiResponse } from '@/api/types'
|
||||
import type { IAgreement } from '@/types/user'
|
||||
|
||||
export const commonApi = {
|
||||
/**
|
||||
* 发送邮箱验证码
|
||||
* @param email 邮箱地址
|
||||
*/
|
||||
sendMailCaptcha: async (email: string) => {
|
||||
const res = await mainClient.request<IApiResponse>({
|
||||
url: 'common/user/getMailCaptcha',
|
||||
method: 'GET',
|
||||
data: { email }
|
||||
})
|
||||
return res.data
|
||||
},
|
||||
/**
|
||||
* 获取协议内容
|
||||
* @param id 协议 ID (111: 用户协议, 112: 隐私政策)
|
||||
*/
|
||||
getAgreement: async (id: number) => {
|
||||
const res = await mainClient.request<IApiResponse<IAgreement>>({
|
||||
url: 'sys/agreement/getAgreement',
|
||||
method: 'POST',
|
||||
data: { id }
|
||||
})
|
||||
return res.agreement
|
||||
}
|
||||
}
|
||||
45
api/request.ts
Normal file
45
api/request.ts
Normal file
@@ -0,0 +1,45 @@
|
||||
// api/request.ts
|
||||
import { requestInterceptor } from './interceptors/request';
|
||||
import { responseInterceptor } from './interceptors/response';
|
||||
import type { IRequestOptions, ICreateClientConfig } from './types';
|
||||
import { REQUEST_TIMEOUT } from './config';
|
||||
|
||||
export function createRequestClient(cfg: ICreateClientConfig) {
|
||||
const baseURL = cfg.baseURL;
|
||||
const timeout = cfg.timeout ?? REQUEST_TIMEOUT;
|
||||
|
||||
async function request<T = any>(options: IRequestOptions): Promise<T> {
|
||||
// 组装 final options
|
||||
const final: UniApp.RequestOptions = {
|
||||
...options,
|
||||
url: (options.url || '').startsWith('http') ? options.url : `${baseURL}${options.url}`,
|
||||
timeout,
|
||||
header: options.header || {},
|
||||
};
|
||||
|
||||
// run request interceptor to mutate headers, etc.
|
||||
const intercepted = requestInterceptor(final as IRequestOptions);
|
||||
|
||||
return new Promise((resolve, reject) => {
|
||||
uni.request({
|
||||
...intercepted,
|
||||
success(res: any) {
|
||||
// delegate to response interceptor
|
||||
responseInterceptor(res)
|
||||
.then((r) => {
|
||||
resolve(r as any);
|
||||
})
|
||||
.catch((err) => {
|
||||
reject(err);
|
||||
});
|
||||
},
|
||||
fail(err: any) {
|
||||
uni.showToast({ title: '网络连接失败', icon: 'none' });
|
||||
reject(err);
|
||||
},
|
||||
} as any);
|
||||
});
|
||||
}
|
||||
|
||||
return { request };
|
||||
}
|
||||
25
api/types.d.ts
vendored
Normal file
25
api/types.d.ts
vendored
Normal file
@@ -0,0 +1,25 @@
|
||||
// api/types.d.ts
|
||||
export interface IApiResponse<T = any> {
|
||||
/** 有些后端使用 code 字段,有些使用 success 字段 */
|
||||
code?: number | string;
|
||||
success?: boolean;
|
||||
data?: T;
|
||||
msg?: string;
|
||||
message?: string;
|
||||
errMsg?: string;
|
||||
[k: string]: any;
|
||||
}
|
||||
|
||||
/**
|
||||
* createRequestClient 返回的 request 函数签名
|
||||
*/
|
||||
export interface IRequestOptions extends UniApp.RequestOptions {
|
||||
// 允许扩展额外字段(例如 FILE 上传专用等)
|
||||
maxSize?: number;
|
||||
files?: Array<{ name: string; uri: string; fileType?: string }>;
|
||||
}
|
||||
|
||||
export interface ICreateClientConfig {
|
||||
baseURL: string;
|
||||
timeout?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user