更新:登录功能

This commit is contained in:
2025-11-04 12:37:04 +08:00
commit a21fb92916
897 changed files with 51500 additions and 0 deletions

7
api/clients/main.ts Normal file
View 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
View 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
View 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;

View 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,
}
}

View 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
View 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
View 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
View 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
View 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;
}