feat: 添加手机号登录和短信验证码功能,更新 API 地址,重构用户状态管理

This commit is contained in:
Mikuisnotavailable
2026-03-31 02:24:20 +08:00
parent c9a70a139c
commit 9b49550a6a
10 changed files with 864 additions and 83 deletions

696
API_GUIDE.md Normal file
View File

@@ -0,0 +1,696 @@
# 云印享 - 前端 API 使用指南
本文档面向前端开发者,详细说明如何调用云印享后端 API 接口。后端基于 gRPC 构建,前端通过 HTTP RESTful 风格接口进行通信。
## 目录
- [基础配置](#基础配置)
- [认证服务 (Auth)](#认证服务-auth)
- [文件管理服务 (File)](#文件管理服务-file)
- [订单服务 (Order)](#订单服务-order)
- [打印服务 (Print)](#打印服务-print)
- [支付服务 (Pay)](#支付服务-pay)
- [价目表服务 (Price)](#价目表服务-price)
- [错误处理](#错误处理)
- [注意事项](#注意事项)
---
## 基础配置
### API 基础地址
API 地址配置在 [`src/config/api.ts`](src/config/api.ts:1) 中:
```typescript
const DEV_API_URL = "http://localhost:8080";
const PROD_API_URL = "http://localhost:8080";
export const API_BASE_URL =
process.env.NODE_ENV === "development" ? DEV_API_URL : PROD_API_URL;
```
### 请求封装
项目已封装统一的请求工具 [`src/utils/request.ts`](src/utils/request.ts:1),自动处理:
- Token 注入Bearer 认证)
- 401 未授权处理(自动登出)
- 错误捕获
```typescript
// 使用示例
import { request } from "../utils/request";
const response = await request({
url: "/auth/userinfo",
method: "GET",
});
```
### 统一响应格式
所有 API 返回遵循统一格式:
```typescript
interface ApiResponse<T = any> {
code: number; // 状态码0 表示成功
message: string; // 响应消息
data: T; // 实际数据
}
```
---
## 认证服务 (Auth)
**基础路径**: `/auth`
### 1. 微信登录
获取用户访问令牌。
| 项目 | 说明 |
| ------------ | ------------------------------------------------------------------- |
| **接口地址** | `POST /auth/login` |
| **请求参数** | `{ code: string }` - 微信登录凭证 code |
| **返回数据** | `{ accessToken: string; refreshToken: string; userInfo: UserInfo }` |
**代码示例**
```typescript
import { authApi } from "./api/auth";
// 调用登录接口
const response = await authApi.login(wxCode);
// response.accessToken // 访问令牌
// response.refreshToken // 刷新令牌
// response.userInfo // 用户信息
```
**响应示例**
```json
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "dGhpcyBpcyBhIHJlZnJl...",
"userInfo": {
"id": 1,
"openid": "oXXXX-xxxxx",
"nickname": "用户昵称",
"avatar": "https://xxx.com/avatar.jpg",
"balance": 10.5,
"createdAt": "2024-01-01T00:00:00Z"
}
}
```
### 2. 刷新 Token
使用刷新令牌获取新的访问令牌。
| 项目 | 说明 |
| ------------ | -------------------------- |
| **接口地址** | `POST /auth/refresh` |
| **请求参数** | `{ refreshToken: string }` |
| **返回数据** | `{ accessToken: string }` |
**代码示例**
```typescript
const { accessToken } = await authApi.refreshToken(refreshToken);
```
### 3. 获取用户信息
获取当前登录用户的详细信息。
| 项目 | 说明 |
| ------------ | --------------------------------- |
| **接口地址** | `GET /auth/userinfo` |
| **请求参数** | 无 |
| **返回数据** | [`UserInfo`](src/types/user.ts:1) |
**代码示例**
```typescript
const userInfo = await authApi.getUserInfo();
console.log(userInfo.nickname, userInfo.balance);
```
### 4. 更新用户信息
更新用户昵称和头像。
| 项目 | 说明 |
| ------------ | ---------------------------------------- |
| **接口地址** | `POST /auth/update` |
| **请求参数** | `{ nickname?: string; avatar?: string }` |
| **返回数据** | [`UserInfo`](src/types/user.ts:1) |
**代码示例**
```typescript
const updatedUser = await authApi.updateUser({
nickname: "新昵称",
avatar: "https://new-avatar.jpg",
});
```
### 5. 手机号登录
使用手机号和短信验证码登录。
| 项目 | 说明 |
| ------------ | ------------------------------------------------------------------- |
| **接口地址** | `POST /auth/phone/login` |
| **请求参数** | `{ phone: string; code: string }` |
| **返回数据** | `{ accessToken: string; refreshToken: string; userInfo: UserInfo }` |
**代码示例**
```typescript
import { authApi } from "./api/auth";
// 调用手机号登录接口
const response = await authApi.phoneLogin({
phone: "13800138000",
code: "123456",
});
```
### 6. 发送短信验证码
发送短信验证码到指定手机号。
| 项目 | 说明 |
| ------------ | --------------------- |
| **接口地址** | `POST /auth/sms/code` |
| **请求参数** | `{ phone: string }` |
| **返回数据** | `void` |
**代码示例**
```typescript
import { authApi } from "./api/auth";
// 发送短信验证码
await authApi.sendSmsCode({ phone: "13800138000" });
```
---
## 文件管理服务 (File)
**基础路径**: `/file`
### 1. 上传文件
支持分片上传,使用 Taro 原生上传能力。
| 项目 | 说明 |
| ------------ | ------------------------------------- |
| **接口地址** | `POST /file/upload` |
| **请求参数** | `filePath: string` - 本地文件路径 |
| **返回数据** | `{ fileId: number; fileUrl: string }` |
**代码示例**
```typescript
import { fileApi } from "./api/file";
// 上传文件
const result = await fileApi.upload(tempFilePath);
console.log(result.fileId, result.fileUrl);
```
### 2. 获取文件列表
分页获取用户上传的文件列表。
| 项目 | 说明 |
| ------------ | ----------------------------------------------------------------- |
| **接口地址** | `GET /file/list` |
| **请求参数** | `{ page?: number; pageSize?: number }` - 默认 page=1, pageSize=20 |
| **返回数据** | `{ files: FileInfo[]; total: number }` |
**代码示例**
```typescript
const { files, total } = await fileApi.getFileList(1, 20);
```
### 3. 获取文件详情
| 项目 | 说明 |
| ------------ | --------------------------------- |
| **接口地址** | `GET /file/:fileId` |
| **请求参数** | `fileId: number` |
| **返回数据** | [`FileInfo`](src/types/file.ts:1) |
**代码示例**
```typescript
const fileInfo = await fileApi.getFile(123);
```
### 4. 删除文件
| 项目 | 说明 |
| ------------ | ---------------------- |
| **接口地址** | `DELETE /file/:fileId` |
| **请求参数** | `fileId: number` |
| **返回数据** | `void` |
**代码示例**
```typescript
await fileApi.deleteFile(123);
```
### 5. 添加水印
| 项目 | 说明 |
| ------------ | ----------------------------------------- |
| **接口地址** | `POST /file/:fileId/watermark` |
| **请求参数** | `fileId: number`, `{ watermark: string }` |
| **返回数据** | `{ watermarkUrl: string }` |
**代码示例**
```typescript
const { watermarkUrl } = await fileApi.addWatermark(123, "仅供打印使用");
```
---
## 订单服务 (Order)
**基础路径**: `/order`
### 1. 创建订单
| 项目 | 说明 |
| ------------ | --------------------------------------------- |
| **接口地址** | `POST /order/create` |
| **请求参数** | [`CreateOrderRequest`](src/types/order.ts:22) |
| **返回数据** | [`Order`](src/types/order.ts:4) |
**请求参数结构**
```typescript
{
fileId: number; // 文件ID
printerId: number; // 打印机ID
printType: "A4" | "A3" | "照片"; // 打印类型
colorType: "黑白" | "彩色"; // 颜色类型
copies: number; // 份数
}
```
**代码示例**
```typescript
import { orderApi } from "./api/order";
const order = await orderApi.createOrder({
fileId: 123,
printerId: 1,
printType: "A4",
colorType: "黑白",
copies: 2,
});
```
### 2. 获取订单详情
| 项目 | 说明 |
| ------------ | ------------------------------- |
| **接口地址** | `GET /order/:orderId` |
| **请求参数** | `orderId: number` |
| **返回数据** | [`Order`](src/types/order.ts:4) |
**代码示例**
```typescript
const order = await orderApi.getOrder(456);
```
### 3. 获取订单列表
| 项目 | 说明 |
| ------------ | -------------------------------------- |
| **接口地址** | `GET /order/list` |
| **请求参数** | `{ page?: number; pageSize?: number }` |
| **返回数据** | `{ orders: Order[]; total: number }` |
**代码示例**
```typescript
const { orders, total } = await orderApi.getOrderList(1, 10);
```
### 4. 更新订单状态
| 项目 | 说明 |
| ------------ | --------------------------------------- |
| **接口地址** | `PUT /order/:orderId/status` |
| **请求参数** | `orderId: number`, `{ status: number }` |
| **返回数据** | [`Order`](src/types/order.ts:4) |
**订单状态枚举**
| 值 | 状态 |
|----|------|
| 0 | 待支付 |
| 1 | 已支付 |
| 2 | 打印中 |
| 3 | 已完成 |
| 4 | 已取消 |
**代码示例**
```typescript
await orderApi.updateOrderStatus(456, 1); // 标记为已支付
```
### 5. 取消订单
| 项目 | 说明 |
| ------------ | ------------------------------- |
| **接口地址** | `PUT /order/:orderId/cancel` |
| **请求参数** | `orderId: number` |
| **返回数据** | [`Order`](src/types/order.ts:4) |
**代码示例**
```typescript
await orderApi.cancelOrder(456);
```
---
## 打印服务 (Print)
**基础路径**: `/print`
### 1. 获取打印机列表
| 项目 | 说明 |
| ------------ | ------------------------------------- |
| **接口地址** | `GET /print/printers` |
| **请求参数** | 无 |
| **返回数据** | [`Printer[]`](src/types/printer.ts:3) |
**代码示例**
```typescript
import { printerApi } from "./api/printer";
const printers = await printerApi.getPrinters();
```
### 2. 获取打印机状态
| 项目 | 说明 |
| ------------ | -------------------------------------- |
| **接口地址** | `GET /print/printer/:printerId/status` |
| **请求参数** | `printerId: number` |
| **返回数据** | [`Printer`](src/types/printer.ts:3) |
**打印机状态枚举**
| 值 | 状态 |
|----|------|
| 0 | 空闲 |
| 1 | 打印中 |
| 2 | 离线 |
**代码示例**
```typescript
const printer = await printerApi.getPrinterStatus(1);
```
### 3. 注册打印机
| 项目 | 说明 |
| ------------ | ------------------------------------ |
| **接口地址** | `POST /print/printer/register` |
| **请求参数** | `{ name: string; location: string }` |
| **返回数据** | [`Printer`](src/types/printer.ts:3) |
**代码示例**
```typescript
const printer = await printerApi.registerPrinter("打印机1", "一楼大厅");
```
### 4. 提交打印任务
| 项目 | 说明 |
| ------------ | ---------------------------------------- |
| **接口地址** | `POST /print/job/submit` |
| **请求参数** | `{ orderId: number; printerId: number }` |
| **返回数据** | `{ jobId: number }` |
**代码示例**
```typescript
const { jobId } = await printerApi.submitPrintJob(456, 1);
```
### 5. 取消打印任务
| 项目 | 说明 |
| ------------ | ------------------------------ |
| **接口地址** | `PUT /print/job/:jobId/cancel` |
| **请求参数** | `jobId: number` |
| **返回数据** | `void` |
**代码示例**
```typescript
await printerApi.cancelPrintJob(789);
```
---
## 支付服务 (Pay)
**基础路径**: `/pay`
### 1. 创建微信支付订单
| 项目 | 说明 |
| ------------ | ---------------------------------------------------------------------------------------------- |
| **接口地址** | `POST /pay/create` |
| **请求参数** | `{ orderId: number }` |
| **返回数据** | `{ prepayId: string; paySign: string; timeStamp: string; nonceStr: string; signType: string }` |
**代码示例**
```typescript
import { payApi } from "./api/pay";
const payParams = await payApi.createWxPayOrder(456);
// 使用返回参数调起微信支付
```
### 2. 查询支付状态
| 项目 | 说明 |
| ------------ | ------------------------- |
| **接口地址** | `GET /pay/query/:orderId` |
| **请求参数** | `orderId: number` |
| **返回数据** | `{ status: number }` |
**支付状态枚举**
| 值 | 状态 |
|----|------|
| 0 | 待支付 |
| 1 | 支付中 |
| 2 | 成功 |
| 3 | 失败 |
**代码示例**
```typescript
const { status } = await payApi.queryPayStatus(456);
```
### 3. 余额支付
| 项目 | 说明 |
| ------------ | ---------------------- |
| **接口地址** | `POST /pay/balance` |
| **请求参数** | `{ orderId: number }` |
| **返回数据** | `{ success: boolean }` |
**代码示例**
```typescript
const { success } = await payApi.payByBalance(456);
```
---
## 价目表服务 (Price)
**基础路径**: `/price`
### 1. 获取价目表
| 项目 | 说明 |
| ------------ | ------------------------------------- |
| **接口地址** | `GET /price/list` |
| **请求参数** | 无 |
| **返回数据** | [`PriceItem[]`](src/types/price.ts:3) |
**代码示例**
```typescript
import { priceApi } from "./api/price";
const priceList = await priceApi.getPriceList();
```
**价目表示例**
```json
[
{ "id": 1, "name": "A4黑白打印", "type": "A4", "price": 0.1, "unit": "页" },
{ "id": 2, "name": "A4彩色打印", "type": "A4", "price": 0.3, "unit": "页" },
{ "id": 3, "name": "A3黑白打印", "type": "A3", "price": 0.2, "unit": "页" },
{ "id": 4, "name": "A3彩色打印", "type": "A3", "price": 0.6, "unit": "页" },
{ "id": 5, "name": "照片打印", "type": "照片", "price": 2.0, "unit": "张" }
]
```
---
## 错误处理
### HTTP 状态码
| 状态码 | 说明 | 处理建议 |
| ------ | -------------- | ---------------------------- |
| 200 | 请求成功 | 正常处理响应数据 |
| 400 | 请求参数错误 | 检查请求参数格式 |
| 401 | 未授权 | Token 过期或无效,需重新登录 |
| 403 | 禁止访问 | 权限不足 |
| 404 | 资源不存在 | 检查请求的资源 ID |
| 500 | 服务器内部错误 | 联系后端排查 |
### 统一错误处理
[`request.ts`](src/utils/request.ts:1) 已封装错误处理逻辑:
```typescript
try {
const result = await orderApi.createOrder(orderData);
} catch (error) {
if (error.message === "Unauthorized") {
// 处理未授权,跳转登录
Taro.redirectTo({ url: "/pages/login/index" });
} else {
// 显示错误提示
Taro.showToast({ title: error.message, icon: "none" });
}
}
```
---
## 注意事项
### 1. 认证机制
- 所有需要用户身份的接口都需要在请求头中携带 `Authorization: Bearer <token>`
- Token 由 [`request.ts`](src/utils/request.ts:16) 自动注入,无需手动处理
- Token 过期时,服务器返回 401前端自动触发登出
### 2. 文件上传
- 使用 Taro 原生 `uploadFile` API支持断点续传
- 上传成功后返回 `fileId``fileUrl`,用于后续订单创建
### 3. 分页查询
- 文件列表和订单列表接口支持分页
- 默认 `page=1`, `pageSize=20`
- 响应中包含 `total` 字段表示总记录数
### 4. 状态枚举
- 订单状态、支付状态、打印机状态均使用数字枚举
- 类型定义文件(如 [`order.ts`](src/types/order.ts:1))中提供了状态映射表,便于显示
### 5. 数据格式
- 所有时间字段使用 ISO 8601 格式字符串(如 `"2024-01-01T12:00:00Z"`
- 价格字段使用数字类型,单位为元(如 `0.10` 表示 0.1 元)
### 6. 开发环境配置
- 开发环境 API 地址:`http://localhost:8080`
- 确保后端服务已启动且端口正确
- 如需修改地址,编辑 [`src/config/api.ts`](src/config/api.ts:1)
### 7. 类型安全
- 所有 API 接口都有完整的 TypeScript 类型定义
- 导入对应的类型文件(如 `import type { Order } from '../types'`
- 使用类型提示可以减少运行时错误
---
## 快速开始示例
以下是一个完整的打印流程示例:
```typescript
import { authApi } from "./api/auth";
import { fileApi } from "./api/file";
import { orderApi } from "./api/order";
import { printerApi } from "./api/printer";
import { payApi } from "./api/pay";
// 1. 登录
const { accessToken, userInfo } = await authApi.login(wxCode);
// 2. 上传文件
const { fileId } = await fileApi.upload(tempFilePath);
// 3. 获取打印机列表
const printers = await printerApi.getPrinters();
const availablePrinter = printers.find((p) => p.status === 0);
// 4. 创建订单
const order = await orderApi.createOrder({
fileId,
printerId: availablePrinter.id,
printType: "A4",
colorType: "黑白",
copies: 2,
});
// 5. 发起支付
const payParams = await payApi.createWxPayOrder(order.id);
// 调起微信支付...
// 6. 查询支付状态
const { status } = await payApi.queryPayStatus(order.id);
if (status === 2) {
// 支付成功,提交打印任务
await printerApi.submitPrintJob(order.id, availablePrinter.id);
}
```
---
## 更新日志
- 2024-01-01: 初始版本,基于后端 README 和前端现有实现整理
---
**文档维护**: 前端开发团队
**最后更新**: 2024-01-01

View File

@@ -1,7 +1,17 @@
import { request } from '../utils/request';
import type { UserInfo, LoginResponse, UpdateUserRequest } from '../types';
export interface PhoneLoginRequest {
phone: string;
code: string;
}
export interface SmsCodeRequest {
phone: string;
}
export const authApi = {
// 微信登录
login: (code: string) => {
return request<LoginResponse>({
url: '/auth/login',
@@ -10,6 +20,24 @@ export const authApi = {
});
},
// 手机号登录
phoneLogin: (data: PhoneLoginRequest) => {
return request<LoginResponse>({
url: '/auth/phone/login',
method: 'POST',
data,
});
},
// 发送短信验证码
sendSmsCode: (data: SmsCodeRequest) => {
return request<void>({
url: '/auth/sms/code',
method: 'POST',
data,
});
},
refreshToken: (refreshToken: string) => {
return request<{ accessToken: string }>({
url: '/auth/refresh',

View File

@@ -1,5 +1,7 @@
import { Component } from 'react'
import type { PropsWithChildren } from 'react'
import { Provider } from 'react-redux'
import { store } from './store'
import './app.css'
@@ -12,7 +14,11 @@ class App extends Component<PropsWithChildren> {
componentDidHide() { }
render() {
return this.props.children
return (
<Provider store={store}>
{this.props.children}
</Provider>
)
}
}

View File

@@ -1,4 +1,5 @@
const DEV_API_URL = 'http://localhost:8080';
const PROD_API_URL = 'http://localhost:8080';
// 生产环境 API 地址,请根据实际部署环境修改
const PROD_API_URL = 'https://api.yourdomain.com';
export const API_BASE_URL = process.env.NODE_ENV === 'development' ? DEV_API_URL : PROD_API_URL;

View File

@@ -8,6 +8,7 @@ export const useAuth = () => {
const dispatch = useAppDispatch();
const { info, token, refreshToken, isLoggedIn, loading } = useAppSelector(state => state.user);
// 微信登录
const login = useCallback(async (code: string) => {
dispatch(setLoading(true));
try {
@@ -18,6 +19,7 @@ export const useAuth = () => {
refreshToken: res.refreshToken,
}));
storage.setToken(res.accessToken);
storage.setRefreshToken(res.refreshToken);
storage.setUserInfo(res.userInfo);
return res;
} catch (error) {
@@ -28,9 +30,42 @@ export const useAuth = () => {
}
}, [dispatch]);
// 手机号登录
const phoneLogin = useCallback(async (phone: string, code: string) => {
dispatch(setLoading(true));
try {
const res = await authApi.phoneLogin({ phone, code });
dispatch(setUser({
user: res.userInfo,
token: res.accessToken,
refreshToken: res.refreshToken,
}));
storage.setToken(res.accessToken);
storage.setRefreshToken(res.refreshToken);
storage.setUserInfo(res.userInfo);
return res;
} catch (error) {
console.error('Phone login failed:', error);
throw error;
} finally {
dispatch(setLoading(false));
}
}, [dispatch]);
// 发送短信验证码
const sendSmsCode = useCallback(async (phone: string) => {
try {
await authApi.sendSmsCode({ phone });
} catch (error) {
console.error('Send SMS code failed:', error);
throw error;
}
}, []);
const logout = useCallback(() => {
dispatch(logoutAction());
storage.removeToken();
storage.removeRefreshToken();
storage.removeUserInfo();
}, [dispatch]);
@@ -63,6 +98,8 @@ export const useAuth = () => {
isLoggedIn,
loading,
login,
phoneLogin,
sendSmsCode,
logout,
refreshToken: refreshAccessToken,
updateInfo,

View File

@@ -1,6 +1,7 @@
import React, { useState, useCallback, useMemo } from "react";
import { useState, useCallback, useMemo } from "react";
import { View, Text, Button, Input } from "@tarojs/components";
import Taro from '@tarojs/taro';
import { useAuth } from '../../hooks/useAuth';
import './index.css';
const Login = () => {
@@ -8,8 +9,9 @@ const Login = () => {
const [code, setCode] = useState('');
const [countdown, setCountdown] = useState(0);
const [loading, setLoading] = useState(false);
const [showNicknameModal, setShowNicknameModal] = useState(false);
const [nickname, setNickname] = useState('');
// 使用封装好的 useAuth hook
const { login, phoneLogin, sendSmsCode } = useAuth();
const handlePhoneInput = useCallback((e: any) => {
const value = e.detail.value;
@@ -29,7 +31,7 @@ const Login = () => {
return '';
}, [phone]);
const handleGetCode = useCallback(() => {
const handleGetCode = useCallback(async () => {
if (!phone) {
Taro.showToast({ title: '请输入手机号', icon: 'none' });
return;
@@ -38,18 +40,23 @@ const Login = () => {
Taro.showToast({ title: '请输入正确的手机号', icon: 'none' });
return;
}
setCountdown(60);
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
Taro.showToast({ title: '验证码已发送', icon: 'success' });
}, [phone, isPhoneValid]);
try {
await sendSmsCode(phone);
setCountdown(60);
const timer = setInterval(() => {
setCountdown(prev => {
if (prev <= 1) {
clearInterval(timer);
return 0;
}
return prev - 1;
});
}, 1000);
Taro.showToast({ title: '验证码已发送', icon: 'success' });
} catch (error) {
Taro.showToast({ title: '发送失败,请重试', icon: 'none' });
}
}, [phone, isPhoneValid, sendSmsCode]);
const handlePhoneLogin = useCallback(async () => {
if (!phone) {
@@ -66,59 +73,40 @@ const Login = () => {
}
setLoading(true);
try {
await new Promise(resolve => setTimeout(resolve, 1000));
setShowNicknameModal(true);
await phoneLogin(phone, code);
Taro.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
Taro.navigateBack();
}, 1500);
} catch (error) {
Taro.showToast({ title: '登录失败', icon: 'none' });
Taro.showToast({ title: '登录失败,请重试', icon: 'none' });
} finally {
setLoading(false);
}
}, [phone, isPhoneValid, code]);
}, [phone, isPhoneValid, code, phoneLogin]);
const handleCreateNickname = useCallback(() => {
if (!nickname.trim()) {
Taro.showToast({ title: '请输入用户名', icon: 'none' });
return;
}
const userInfo = {
nickname: nickname.trim(),
avatar: '',
balance: 0,
};
Taro.setStorageSync('userInfo', userInfo);
Taro.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
Taro.navigateBack();
}, 1500);
}, [nickname]);
// 移除 handleCreateNickname 函数,因为现在使用后端登录
const handleWechatLogin = useCallback(async () => {
setLoading(true);
try {
// 1. 获取微信登录 code
const loginResult = await Taro.login();
console.log('微信登录 code:', loginResult.code);
let avatarUrl = '';
let nickName = '微信用户';
// 2. 尝试获取用户信息(可选,用于展示)
try {
const userInfoRes = await Taro.getUserProfile({
await Taro.getUserProfile({
desc: '用于完善用户资料',
});
if (userInfoRes.userInfo) {
avatarUrl = userInfoRes.userInfo.avatarUrl;
nickName = userInfoRes.userInfo.nickName;
}
// 用户信息已通过 login() 从后端获取,这里只是请求授权
} catch (e) {
console.log('游客模式获取用户信息受限,使用默认信息');
}
const userInfo = {
nickname: nickName,
avatar: avatarUrl,
balance: 0,
};
Taro.setStorageSync('userInfo', userInfo);
// 3. 调用后端 API 进行登录
await login(loginResult.code);
Taro.showToast({ title: '登录成功', icon: 'success' });
setTimeout(() => {
Taro.navigateBack();
@@ -129,28 +117,7 @@ const Login = () => {
} finally {
setLoading(false);
}
}, []);
if (showNicknameModal) {
return (
<View className='login-page'>
<View className='nickname-modal'>
<Text className='modal-title'></Text>
<Text className='modal-subtitle'>便</Text>
<Input
className='nickname-input'
placeholder='请输入用户名'
maxlength={20}
value={nickname}
onInput={(e) => setNickname(e.detail.value)}
/>
<Button className='confirm-btn' onClick={handleCreateNickname}>
</Button>
</View>
</View>
);
}
}, [login]);
return (
<View className='login-page'>

View File

@@ -1,7 +1,8 @@
import React, { useEffect, useState } from "react";
import { useEffect, useState } from "react";
import { View, Text, Button } from "@tarojs/components";
import Taro from '@tarojs/taro';
import TabBar from '../../components/TabBar/TabBar';
import { useAuth } from '../../hooks/useAuth';
import './index.css';
interface UserInfo {
@@ -15,15 +16,18 @@ const UserIndex = () => {
const [userInfo, setUserInfo] = useState<UserInfo | null>(null);
const [tabIndex, setTabIndex] = useState(4); // 用户中心是第5个标签索引4
// 使用封装好的 useAuth hook
const { user, isLoggedIn: isAuthLoggedIn, logout } = useAuth();
useEffect(() => {
checkLoginStatus();
}, []);
const checkLoginStatus = () => {
const storedUser = Taro.getStorageSync('userInfo');
if (storedUser) {
// 使用 Redux store 中的登录状态
if (isAuthLoggedIn && user) {
setIsLoggedIn(true);
setUserInfo(storedUser);
setUserInfo(user);
}
};
@@ -52,8 +56,8 @@ const UserIndex = () => {
content: '确定要退出登录吗?',
success: (res) => {
if (res.confirm) {
Taro.removeStorageSync('userInfo');
Taro.removeStorageSync('token');
// 使用封装好的 logout 方法
logout();
setIsLoggedIn(false);
setUserInfo(null);
Taro.showToast({ title: '已退出登录', icon: 'success' });

View File

@@ -3,6 +3,16 @@ import { userReducer } from './slices/userSlice';
import { orderReducer } from './slices/orderSlice';
import { fileReducer } from './slices/fileSlice';
import { printerReducer } from './slices/printerSlice';
import { storage } from '../utils/storage';
// 从本地存储恢复用户状态
const restoredUserState = {
info: storage.getUserInfo() || null,
token: storage.getToken() || null,
refreshToken: storage.getRefreshToken() || null,
isLoggedIn: !!storage.getToken(),
loading: false,
};
export const store = configureStore({
reducer: {
@@ -11,6 +21,9 @@ export const store = configureStore({
file: fileReducer,
printer: printerReducer,
},
preloadedState: {
user: restoredUserState,
},
});
export type RootState = ReturnType<typeof store.getState>;

View File

@@ -10,6 +10,13 @@ interface RequestOptions {
header?: Record<string, string>;
}
// 统一响应格式
interface ApiResponse<T = any> {
code: number;
message: string;
data: T;
}
export const request = async <T>(options: RequestOptions): Promise<T> => {
const { url, method = 'GET', data, header = {} } = options;
@@ -19,7 +26,7 @@ export const request = async <T>(options: RequestOptions): Promise<T> => {
}
try {
const response = await Taro.request({
const response = await Taro.request<ApiResponse<T>>({
url: `${API_BASE_URL}${url}`,
method,
data,
@@ -30,8 +37,17 @@ export const request = async <T>(options: RequestOptions): Promise<T> => {
});
if (response.statusCode === 200) {
return response.data as T;
const apiResponse = response.data;
// 按照 API 文档规范检查 code 字段0 表示成功
if (apiResponse.code === 0) {
return apiResponse.data;
} else {
// 处理业务错误
console.error('API Error:', apiResponse.message || 'Request failed');
throw new Error(apiResponse.message || 'Request failed');
}
} else if (response.statusCode === 401) {
// 未授权,自动登出
store.dispatch(logout());
throw new Error('Unauthorized');
} else {
@@ -43,7 +59,7 @@ export const request = async <T>(options: RequestOptions): Promise<T> => {
}
};
export const uploadFile = <T>(filePath: string, onProgress?: (progress: number) => void): Promise<T> => {
export const uploadFile = <T>(filePath: string): Promise<T> => {
return new Promise((resolve, reject) => {
const token = store.getState().user.token;

View File

@@ -1,6 +1,7 @@
import Taro from '@tarojs/taro';
const USER_TOKEN_KEY = 'user_token';
const USER_REFRESH_TOKEN_KEY = 'user_refresh_token';
const USER_INFO_KEY = 'user_info';
export const storage = {
@@ -16,6 +17,18 @@ export const storage = {
Taro.removeStorageSync(USER_TOKEN_KEY);
},
setRefreshToken: (refreshToken: string) => {
Taro.setStorageSync(USER_REFRESH_TOKEN_KEY, refreshToken);
},
getRefreshToken: (): string => {
return Taro.getStorageSync(USER_REFRESH_TOKEN_KEY) || '';
},
removeRefreshToken: () => {
Taro.removeStorageSync(USER_REFRESH_TOKEN_KEY);
},
setUserInfo: (info: any) => {
Taro.setStorageSync(USER_INFO_KEY, info);
},