diff --git a/API_GUIDE.md b/API_GUIDE.md new file mode 100644 index 0000000..1e2d661 --- /dev/null +++ b/API_GUIDE.md @@ -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 { + 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 由 [`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 diff --git a/src/api/auth.ts b/src/api/auth.ts index 9b4e94f..f02e6e6 100644 --- a/src/api/auth.ts +++ b/src/api/auth.ts @@ -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({ url: '/auth/login', @@ -10,6 +20,24 @@ export const authApi = { }); }, + // 手机号登录 + phoneLogin: (data: PhoneLoginRequest) => { + return request({ + url: '/auth/phone/login', + method: 'POST', + data, + }); + }, + + // 发送短信验证码 + sendSmsCode: (data: SmsCodeRequest) => { + return request({ + url: '/auth/sms/code', + method: 'POST', + data, + }); + }, + refreshToken: (refreshToken: string) => { return request<{ accessToken: string }>({ url: '/auth/refresh', diff --git a/src/app.ts b/src/app.tsx similarity index 61% rename from src/app.ts rename to src/app.tsx index 9dfdb7e..015d4a4 100644 --- a/src/app.ts +++ b/src/app.tsx @@ -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 { componentDidHide() { } render() { - return this.props.children + return ( + + {this.props.children} + + ) } } diff --git a/src/config/api.ts b/src/config/api.ts index efdb534..fb6c7b7 100644 --- a/src/config/api.ts +++ b/src/config/api.ts @@ -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; diff --git a/src/hooks/useAuth.ts b/src/hooks/useAuth.ts index efdff9b..86e8000 100644 --- a/src/hooks/useAuth.ts +++ b/src/hooks/useAuth.ts @@ -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, diff --git a/src/pages/login/index.tsx b/src/pages/login/index.tsx index d3823e2..469b9c9 100644 --- a/src/pages/login/index.tsx +++ b/src/pages/login/index.tsx @@ -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 ( - - - 创建用户名 - 请设置一个方便识别的用户名 - setNickname(e.detail.value)} - /> - - - - ); - } + }, [login]); return ( diff --git a/src/pages/user/index.tsx b/src/pages/user/index.tsx index ba343be..b20bbf8 100644 --- a/src/pages/user/index.tsx +++ b/src/pages/user/index.tsx @@ -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 { @@ -14,16 +15,19 @@ const UserIndex = () => { const [isLoggedIn, setIsLoggedIn] = useState(false); const [userInfo, setUserInfo] = useState(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' }); diff --git a/src/store/index.ts b/src/store/index.ts index 72dca52..c111487 100644 --- a/src/store/index.ts +++ b/src/store/index.ts @@ -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; diff --git a/src/utils/request.ts b/src/utils/request.ts index 8ecb961..b06405e 100644 --- a/src/utils/request.ts +++ b/src/utils/request.ts @@ -10,6 +10,13 @@ interface RequestOptions { header?: Record; } +// 统一响应格式 +interface ApiResponse { + code: number; + message: string; + data: T; +} + export const request = async (options: RequestOptions): Promise => { const { url, method = 'GET', data, header = {} } = options; @@ -19,7 +26,7 @@ export const request = async (options: RequestOptions): Promise => { } try { - const response = await Taro.request({ + const response = await Taro.request>({ url: `${API_BASE_URL}${url}`, method, data, @@ -30,8 +37,17 @@ export const request = async (options: RequestOptions): Promise => { }); 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 (options: RequestOptions): Promise => { } }; -export const uploadFile = (filePath: string, onProgress?: (progress: number) => void): Promise => { +export const uploadFile = (filePath: string): Promise => { return new Promise((resolve, reject) => { const token = store.getState().user.token; diff --git a/src/utils/storage.ts b/src/utils/storage.ts index c618708..8c7d80b 100644 --- a/src/utils/storage.ts +++ b/src/utils/storage.ts @@ -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); },