feat(registration): 优化经销商注册流程并增加地址定位功能

- 修改导航栏标题从“邀请注册”为“注册成为会员”
- 修复重复提交问题并移除不必要的submitting状态
- 增加昵称和头像的必填验证提示
- 添加用户角色缺失时的默认角色写入机制
- 集成地图选点功能,支持经纬度获取和地址解析
- 实现微信地址导入功能,自动填充基本信息
- 增加定位权限检查和错误处理机制
- 添加.gitignore规则忽略备份文件夹src__bak
- 移除已废弃的银行卡和客户管理页面代码
- 优化表单验证规则和错误提示信息
- 实现经销商注册成功后自动跳转到“我的”页面
- 添加用户信息缓存刷新机制确保角色信息同步
```
This commit is contained in:
2026-03-01 12:35:41 +08:00
parent 945351be91
commit eee4644d06
296 changed files with 28845 additions and 6664 deletions

1
.gitignore vendored
View File

@@ -23,3 +23,4 @@ yarn-error.log*
.vscode/ .vscode/
*.swp *.swp
*.swo *.swo
/src__bak/

59
CONFIG.md Normal file
View File

@@ -0,0 +1,59 @@
# 配置说明文档
## 环境配置
### 1. 复制环境变量文件
```bash
cp .env.example .env
```
### 2. 修改配置文件
#### config/app.ts
```typescript
// 租户ID - 请根据实际情况修改
export const TenantId = 'YOUR_TENANT_ID';
// 接口地址 - 请根据实际情况修改
export const BaseUrl = 'https://your-api-domain.com/api';
```
#### src/utils/server.ts
```typescript
// 模版套餐ID - 请根据实际情况修改
export const TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
// 服务接口 - 请根据实际情况修改
export const SERVER_API_URL = 'https://your-server-domain.com/api';
```
#### project.config.json
```json
{
"appid": "your_wechat_appid"
}
```
### 3. 小程序配置
#### 微信小程序
1. 在微信公众平台申请小程序
2. 获取AppID并填入 `project.config.json`
3. 配置服务器域名白名单
#### 支付宝小程序
1. 在支付宝开放平台申请小程序
2. 获取AppID并配置相应文件
#### 字节跳动小程序
1. 在字节跳动开发者平台申请小程序
2. 获取AppID并填入 `project.tt.json`
### 4. API配置
确保后端API服务正常运行并配置正确的域名和端口。
### 5. 安全注意事项
- 不要将真实的AppID、API密钥等敏感信息提交到公开仓库
- 使用环境变量管理敏感配置
- 定期更新密码和密钥
- 在生产环境中启用HTTPS

209
README_QR_LOGIN.md Normal file
View File

@@ -0,0 +1,209 @@
# 微信小程序扫码登录功能实现
## 项目概述
本项目已完整实现微信小程序扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。
## 🎯 功能特性
-**完整的后端API** - Java Spring Boot实现
-**多种前端集成方式** - 按钮、弹窗、页面
-**智能二维码解析** - 支持URL、JSON、纯token格式
-**安全可靠** - Token有效期控制防重复使用
-**用户体验优秀** - 实时状态反馈,错误处理完善
-**微信深度集成** - 自动获取用户信息
## 📁 项目结构
### 后端 (Java)
```
auto/
├── controller/QrLoginController.java # REST API控制器
├── service/QrLoginService.java # 业务接口
├── service/impl/QrLoginServiceImpl.java # 业务实现
└── dto/ # 数据传输对象
├── QrLoginData.java
├── QrLoginConfirmRequest.java
├── QrLoginStatusResponse.java
└── QrLoginGenerateResponse.java
```
### 前端 (小程序)
```
src/
├── api/qr-login/index.ts # API接口层
├── hooks/useQRLogin.ts # 业务逻辑Hook
├── components/ # 组件层
│ ├── QRLoginButton.tsx # 扫码按钮组件
│ ├── QRLoginScanner.tsx # 扫码器组件
│ ├── QRScanModal.tsx # 扫码弹窗组件
│ └── QRLoginDemo.tsx # 演示组件
└── pages/ # 页面层
├── qr-login/index.tsx # 扫码登录页面
├── qr-confirm/index.tsx # 登录确认页面
└── qr-test/index.tsx # 功能测试页面
```
## 🚀 快速开始
### 1. 后端配置
确保Java后端服务正常运行API接口可访问
- `POST /api/qr-login/generate` - 生成扫码token
- `GET /api/qr-login/status/{token}` - 检查登录状态
- `POST /api/qr-login/confirm` - 确认登录
- `POST /api/qr-login/scan/{token}` - 扫码操作
### 2. 前端使用
#### 最简单的使用方式:
```tsx
import QRLoginButton from '@/components/QRLoginButton';
<QRLoginButton />
```
#### 弹窗方式:
```tsx
import QRScanModal from '@/components/QRScanModal';
<QRScanModal
visible={showModal}
onClose={() => setShowModal(false)}
onSuccess={(result) => console.log('登录成功', result)}
/>
```
#### 页面跳转方式:
```tsx
import Taro from '@tarojs/taro';
Taro.navigateTo({
url: '/passport/qr-login/index'
});
```
## 🔧 支持的二维码格式
系统智能识别多种二维码格式:
1. **URL格式**`https://mp.websoft.top/qr-confirm?qrCodeKey=token123`
2. **JSON格式**`{"token": "token123", "type": "qr-login"}`
3. **简单格式**`qr-login:token123` 或直接 `token123`
## 📱 页面说明
### 1. 扫码登录页面 (`/passport/qr-login/index`)
- 完整的扫码登录功能
- 用户信息显示
- 登录历史记录
- 使用说明和安全提示
### 2. 登录确认页面 (`/passport/qr-confirm/index`)
- 处理二维码跳转确认
- 支持URL参数`qrCodeKey``token`
- 用户确认界面
### 3. 功能测试页面 (`/passport/qr-test/index`)
- 演示各种集成方式
- 功能测试和调试
## 🛠️ 开发指南
### 1. 添加扫码按钮到现有页面
```tsx
import QRLoginButton from '@/components/QRLoginButton';
const MyPage = () => {
return (
<View>
<QRLoginButton
text="扫码登录"
onSuccess={(result) => {
// 处理登录成功
console.log('用户登录成功:', result);
}}
onError={(error) => {
// 处理登录失败
console.error('登录失败:', error);
}}
/>
</View>
);
};
```
### 2. 自定义扫码逻辑
```tsx
import { useQRLogin } from '@/hooks/useQRLogin';
const MyComponent = () => {
const {
startScan,
isLoading,
isSuccess,
result,
error
} = useQRLogin();
return (
<Button
loading={isLoading}
onClick={startScan}
>
{isLoading ? '扫码中...' : '扫码登录'}
</Button>
);
};
```
## 🔒 安全注意事项
1. **用户登录验证**:使用前确保用户已在小程序中登录
2. **Token有效期**二维码5分钟有效期过期自动失效
3. **权限申请**:确保小程序已申请摄像头权限
4. **来源验证**:只扫描来自官方网站的登录二维码
## 🐛 常见问题
### Q: 提示"请先登录小程序"
A: 用户需要先在小程序中完成登录获取用户ID和访问令牌。
### Q: 提示"无效的登录二维码"
A: 检查二维码格式是否正确,或者二维码是否已过期。
### Q: 扫码失败
A: 检查摄像头权限,确保二维码清晰可见。
### Q: 网络请求失败
A: 检查网络连接和API接口地址配置。
## 📚 相关文档
- [详细使用指南](docs/QR_LOGIN_USAGE.md)
- [API接口文档](src/api/qr-login/index.ts)
- [组件API文档](docs/QR_LOGIN_USAGE.md#组件api)
## 🎉 测试功能
访问测试页面验证功能:
```
/pages/qr-test/index
```
该页面包含所有集成方式的演示和测试功能。
## 📞 技术支持
如有问题,请检查:
1. 后端API服务是否正常运行
2. 小程序用户是否已登录
3. 网络连接是否正常
4. 二维码格式是否正确
---
**开发者**: 科技小王子
**更新时间**: 2025-09-20

View File

@@ -2,6 +2,8 @@ import { API_BASE_URL } from './env'
// 租户ID - 请根据实际情况修改 // 租户ID - 请根据实际情况修改
export const TenantId = '10579'; export const TenantId = '10579';
// 租户名称
export const TenantName = '易赊宝';
// 接口地址 - 请根据实际情况修改 // 接口地址 - 请根据实际情况修改
export const BaseUrl = API_BASE_URL; export const BaseUrl = API_BASE_URL;
// 当前版本 // 当前版本

View File

@@ -2,6 +2,7 @@
export const ENV_CONFIG = { export const ENV_CONFIG = {
// 开发环境 // 开发环境
development: { development: {
// API_BASE_URL: 'http://127.0.0.1:9200/api',
API_BASE_URL: 'https://ysb-api.websoft.top/api', API_BASE_URL: 'https://ysb-api.websoft.top/api',
APP_NAME: '开发环境', APP_NAME: '开发环境',
DEBUG: 'true', DEBUG: 'true',
@@ -14,6 +15,7 @@ export const ENV_CONFIG = {
}, },
// 测试环境 // 测试环境
test: { test: {
// API_BASE_URL: 'http://127.0.0.1:9200/api',
API_BASE_URL: 'https://ysb-api.websoft.top/api', API_BASE_URL: 'https://ysb-api.websoft.top/api',
APP_NAME: '测试环境', APP_NAME: '测试环境',
DEBUG: 'true', DEBUG: 'true',

226
docs/ADMIN_MODE_SOLUTION.md Normal file
View File

@@ -0,0 +1,226 @@
# 🎯 管理员模式切换方案
## 📋 **问题分析**
### 原始问题
- 用户卡片中有两个扫码入口(门店核销 + 扫码登录)
- 用户体验不友好,容易混淆
- 管理员功能分散,缺乏统一入口
### 解决思路
设计一个管理员模式切换系统,通过模式切换来统一管理所有管理员功能。
## 🚀 **解决方案**
### 方案概述
创建一个**管理员模式切换**系统,包含:
1. **模式切换按钮** - 在普通用户模式和管理员模式之间切换
2. **统一管理面板** - 集中展示所有管理员功能
3. **状态持久化** - 记住用户的模式选择
### 核心组件
#### 1. **useAdminMode Hook**
```typescript
// src/hooks/useAdminMode.ts
const { isAdminMode, toggleAdminMode, setAdminMode } = useAdminMode();
```
**功能特性:**
- ✅ 模式状态管理
- ✅ 本地存储持久化
- ✅ 切换提示反馈
#### 2. **AdminPanel 组件**
```typescript
// src/components/AdminPanel.tsx
<AdminPanel visible={showAdminPanel} onClose={handleCloseAdminPanel} />
```
**功能特性:**
- ✅ 底部弹出面板设计
- ✅ 网格布局展示功能
- ✅ 图标 + 描述的直观界面
- ✅ 点击遮罩关闭
#### 3. **UserCard 集成**
更新用户卡片,集成模式切换功能:
- 模式切换按钮
- 管理面板入口
- 状态指示器
## 🎨 **用户界面设计**
### 模式切换按钮
```
[普通用户] ←→ [管理员] [管理面板]
```
- **普通用户模式**:灰色按钮,只显示基础功能
- **管理员模式**:蓝色按钮,显示管理面板入口
### 管理员面板
```
┌─────────────────────────────────┐
│ 🔧 管理员面板 [关闭] │
├─────────────────────────────────┤
│ [🔍 门店核销] [🔍 扫码登录] │
│ [👤 用户管理] [🏪 门店管理] │
│ [⚙️ 系统设置] │
├─────────────────────────────────┤
│ 💡 管理员功能仅对具有管理权限... │
└─────────────────────────────────┘
```
## 📱 **用户体验流程**
### 普通用户模式
1. 用户看到简洁的用户卡片
2. 只显示基础功能入口
3. 管理员用户可以看到模式切换按钮
### 管理员模式
1. 点击切换到管理员模式
2. 显示"管理面板"入口按钮
3. 点击进入统一的管理功能面板
4. 选择具体的管理功能
### 功能访问路径
```
用户卡片 → 切换管理员模式 → 管理面板 → 具体功能
```
## 🔧 **技术实现**
### 文件结构
```
src/
├── hooks/
│ └── useAdminMode.ts # 管理员模式Hook
├── components/
│ ├── AdminPanel.tsx # 管理员面板组件
│ └── AdminPanel.scss # 面板样式
└── pages/user/components/
└── UserCard.tsx # 更新的用户卡片
```
### 核心功能
#### 状态管理
```typescript
const [isAdminMode, setIsAdminMode] = useState(false);
const [showAdminPanel, setShowAdminPanel] = useState(false);
```
#### 本地存储
```typescript
// 保存模式状态
Taro.setStorageSync('admin_mode', newMode);
// 加载模式状态
const savedMode = Taro.getStorageSync('admin_mode');
```
#### 权限控制
```typescript
{isAdmin() && (
<AdminModeToggle />
)}
```
## 🎯 **功能特性**
### ✅ **已实现功能**
1. **模式切换** - 普通用户 ↔ 管理员模式
2. **状态持久化** - 记住用户选择
3. **统一面板** - 集中管理功能入口
4. **权限控制** - 只对管理员显示
5. **用户反馈** - 切换提示和状态指示
6. **响应式设计** - 适配不同屏幕尺寸
### 🔄 **管理员功能列表**
1. **门店核销** - 扫码核销用户优惠券
2. **扫码登录** - 扫码快速登录网页端
3. **用户管理** - 管理系统用户信息(待开发)
4. **门店管理** - 管理门店信息和设置(待开发)
5. **系统设置** - 系统配置和参数管理(待开发)
## 📊 **对比效果**
### 修改前
```
❌ 两个扫码图标并排显示
❌ 功能入口分散
❌ 用户容易混淆
❌ 界面显得杂乱
```
### 修改后
```
✅ 统一的模式切换入口
✅ 清晰的功能分类
✅ 直观的管理面板
✅ 更好的用户体验
```
## 🚀 **使用方法**
### 1. 管理员用户操作
1. 在个人中心看到模式切换按钮
2. 点击切换到"管理员"模式
3. 点击"管理面板"按钮
4. 在弹出面板中选择需要的功能
### 2. 开发者扩展
添加新的管理功能:
```typescript
// 在 AdminPanel.tsx 中添加新功能
{
id: 'new-feature',
title: '新功能',
description: '功能描述',
icon: <NewIcon className="text-blue-500" size="24" />,
color: 'bg-blue-50 border-blue-200',
onClick: () => {
navTo('/new-feature-page', true);
onClose?.();
}
}
```
## 🎨 **样式定制**
### 主题色彩
- **普通模式**:灰色系 (`bg-gray-100`, `text-gray-600`)
- **管理员模式**:蓝色系 (`bg-blue-500`, `text-white`)
- **功能卡片**:彩色分类(蓝、绿、紫、橙、灰)
### 动画效果
- 模式切换:`transition-all 0.3s ease`
- 面板弹出:`slideUp` 动画
- 按钮点击:`scale(0.95)` 反馈
## 🔮 **未来扩展**
### 计划功能
1. **角色权限** - 不同管理员角色显示不同功能
2. **快捷操作** - 常用功能的快捷入口
3. **统计面板** - 管理数据的可视化展示
4. **通知中心** - 管理员消息和提醒
### 技术优化
1. **懒加载** - 按需加载管理功能模块
2. **缓存优化** - 管理面板状态缓存
3. **性能监控** - 管理功能使用统计
## 🎉 **总结**
这个管理员模式切换方案成功解决了原有的用户体验问题:
1. **统一入口** - 所有管理功能通过统一面板访问
2. **清晰分类** - 功能按类型分组,易于理解
3. **状态记忆** - 用户选择会被记住
4. **扩展性强** - 易于添加新的管理功能
5. **用户友好** - 直观的界面和流畅的交互
现在管理员用户可以享受更加统一和专业的管理体验!🚀

View File

@@ -0,0 +1,298 @@
# 🔄 useShopInfo Hook 字段迁移到AppInfo
## 📋 迁移概述
已成功将`useShopInfo` Hook从`CmsWebsite`字段结构迁移到`AppInfo`字段结构,以匹配后台返回的新字段格式。
## 🆚 字段对比表
### 核心字段映射
| 功能 | 原CmsWebsite字段 | 新AppInfo字段 | 状态 |
|------|------------------|---------------|------|
| **应用名称** | `websiteName` | `appName` | ✅ 已映射 |
| **Logo** | `websiteLogo` | `logo` | ✅ 已映射 |
| **图标** | `websiteIcon` | `icon` | ✅ 已映射 |
| **域名** | `domain` | `domain` | ✅ 保持不变 |
| **版本** | `version` | `version` | ✅ 保持不变 |
| **过期时间** | `expirationTime` | `expirationTime` | ✅ 保持不变 |
| **运行状态** | `running` | `running` | ✅ 保持不变 |
| **状态文本** | `statusText` | `statusText` | ✅ 保持不变 |
| **配置** | `config` | `config` | ✅ 保持不变 |
| **导航** | `topNavs/bottomNavs` | `topNavs/bottomNavs` | ✅ 保持不变 |
### 新增字段
| 字段 | 类型 | 说明 |
|------|------|------|
| `appId` | `number` | 应用ID |
| `description` | `string` | 应用描述 |
| `keywords` | `string` | 关键词 |
| `appCode` | `string` | 应用代码 |
| `mpQrCode` | `string` | 小程序二维码 |
| `title` | `string` | 应用标题 |
| `expired` | `boolean` | 是否过期 |
| `expiredDays` | `number` | 过期天数 |
| `soon` | `number` | 即将过期标识 |
| `statusIcon` | `string` | 状态图标 |
| `serverTime` | `Object` | 服务器时间 |
| `setting` | `Object` | 应用设置 |
### 移除字段
| 原字段 | 处理方式 | 说明 |
|--------|----------|------|
| `websiteDarkLogo` | 使用`logo`替代 | AppInfo中无深色Logo |
| `phone` | 从`config`中获取 | 移至配置中 |
| `email` | 从`config`中获取 | 移至配置中 |
| `address` | 从`config`中获取 | 移至配置中 |
| `icpNo` | 从`config`中获取 | 移至配置中 |
| `search` | 从`config`中获取 | 移至配置中 |
| `templateId` | 移除 | AppInfo中无此字段 |
## 🔧 新增工具方法
### 基于AppInfo的新方法
```typescript
// 应用基本信息
getAppName() // 获取应用名称
getAppLogo() // 获取应用Logo
getAppIcon() // 获取应用图标
getDescription() // 获取应用描述
getKeywords() // 获取关键词
getTitle() // 获取应用标题
getMpQrCode() // 获取小程序二维码
// 应用配置
getSetting() // 获取应用设置
getServerTime() // 获取服务器时间
// 过期状态管理
isExpired() // 检查是否过期
getExpiredDays() // 获取过期天数
isSoonExpired() // 检查是否即将过期
```
### 兼容旧方法
```typescript
// 保持向后兼容
getWebsiteName() // 映射到getAppName()
getWebsiteLogo() // 映射到getAppLogo()
getDarkLogo() // 使用普通Logo
getPhone() // 从config中获取
getEmail() // 从config中获取
getAddress() // 从config中获取
getIcpNo() // 从config中获取
isSearchEnabled() // 从config中获取
```
## 📊 使用示例
### 新方法使用
```typescript
const {
// 新的AppInfo方法
getAppName,
getAppLogo,
getDescription,
getMpQrCode,
isExpired,
getExpiredDays,
// 兼容旧方法
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
// 使用新方法
const appName = getAppName(); // "时里亲子市集"
const appLogo = getAppLogo(); // Logo URL
const description = getDescription(); // 应用描述
const qrCode = getMpQrCode(); // 小程序二维码
const expired = isExpired(); // false
const expiredDays = getExpiredDays(); // 30
// 兼容旧方法(推荐逐步迁移到新方法)
const websiteName = getWebsiteName(); // 等同于getAppName()
const websiteLogo = getWebsiteLogo(); // 等同于getAppLogo()
```
### 状态检查
```typescript
const { getStatus, isExpired, isSoonExpired } = useShopInfo();
const status = getStatus();
console.log(status);
// {
// running: 1,
// statusText: "运行中",
// statusIcon: "success",
// expired: false,
// expiredDays: 30,
// soon: 0
// }
if (isExpired()) {
console.log('应用已过期');
} else if (isSoonExpired()) {
console.log(`应用将在${getExpiredDays()}天后过期`);
}
```
### 配置获取
```typescript
const { getConfig, getSetting, getServerTime } = useShopInfo();
const config = getConfig(); // 应用配置
const setting = getSetting(); // 应用设置
const serverTime = getServerTime(); // 服务器时间
// 从配置中获取联系信息
const phone = getPhone(); // 从config.phone获取
const email = getEmail(); // 从config.email获取
const address = getAddress(); // 从config.address获取
```
## 🔄 迁移建议
### 1. **逐步迁移**
```typescript
// 阶段1使用兼容方法当前可用
const websiteName = getWebsiteName();
const websiteLogo = getWebsiteLogo();
// 阶段2迁移到新方法推荐
const appName = getAppName();
const appLogo = getAppLogo();
```
### 2. **新功能使用新方法**
```typescript
// 新功能直接使用AppInfo方法
const {
getAppName,
getAppLogo,
getDescription,
isExpired,
getMpQrCode
} = useShopInfo();
```
### 3. **配置字段处理**
```typescript
// 对于移至config的字段使用对应的getter方法
const phone = getPhone(); // 自动从config中获取
const email = getEmail(); // 自动从config中获取
const icpNo = getIcpNo(); // 自动从config中获取
```
## ⚠️ 注意事项
### 1. **字段可能为空**
```typescript
// AppInfo中某些字段可能不存在需要提供默认值
const description = getDescription() || '暂无描述';
const qrCode = getMpQrCode() || '';
```
### 2. **配置字段依赖**
```typescript
// 联系信息现在依赖config字段
const config = getConfig();
if (config && typeof config === 'object') {
const phone = getPhone();
const email = getEmail();
}
```
### 3. **过期状态处理**
```typescript
// 新增的过期状态需要特殊处理
const { isExpired, getExpiredDays, isSoonExpired } = useShopInfo();
if (isExpired()) {
// 应用已过期的处理逻辑
showExpiredDialog();
} else if (isSoonExpired()) {
// 即将过期的提醒逻辑
showExpirationWarning(getExpiredDays());
}
```
## 🧪 测试验证
### 1. **字段映射测试**
```typescript
const TestComponent = () => {
const {
shopInfo,
getAppName,
getAppLogo,
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
return (
<div>
<h3>原始数据</h3>
<pre>{JSON.stringify(shopInfo, null, 2)}</pre>
<h3>新方法</h3>
<div>应用名称: {getAppName()}</div>
<div>应用Logo: {getAppLogo()}</div>
<h3>兼容方法</h3>
<div>网站名称: {getWebsiteName()}</div>
<div>网站Logo: {getWebsiteLogo()}</div>
</div>
);
};
```
### 2. **过期状态测试**
```typescript
const ExpirationTest = () => {
const {
isExpired,
getExpiredDays,
isSoonExpired,
getStatus
} = useShopInfo();
const status = getStatus();
return (
<div>
<div>过期状态: {isExpired() ? '已过期' : '正常'}</div>
<div>过期天数: {getExpiredDays()}</div>
<div>即将过期: {isSoonExpired() ? '是' : '否'}</div>
<div>详细状态: {JSON.stringify(status, null, 2)}</div>
</div>
);
};
```
## 🎉 迁移完成
useShopInfo Hook已成功迁移到AppInfo字段结构
-**新增AppInfo专用方法**:基于新字段结构的工具方法
-**保持向后兼容**:旧方法名仍然可用
-**增强功能**:新增过期状态、应用设置等功能
-**智能映射**:自动处理字段差异和默认值
-**类型安全**完整的TypeScript支持
**现在Hook完全支持AppInfo字段结构同时保持向后兼容** 🚀

View File

@@ -0,0 +1,197 @@
# 🔧 Arguments关键字修复
## 问题描述
`src/pages/index/IndexWithHook.tsx`中出现TypeScript错误
```
TS2304: Cannot find name 'arguments'
```
## 🔍 问题分析
### 错误代码
```typescript
// 问题代码 ❌
<Sticky threshold={0} onChange={() => onSticky(arguments)}>
<Header stickyStatus={stickyStatus}/>
</Sticky>
```
### 问题原因
1. **`arguments`对象在箭头函数中不可用**
- `arguments`是传统函数的特性
- 箭头函数没有自己的`arguments`对象
- 在箭头函数中使用`arguments`会导致TypeScript错误
2. **函数期望参数**
```typescript
const onSticky = (args: any) => {
setStickyStatus(args[0].isFixed);
};
```
`onSticky`函数期望接收参数,但调用方式不正确。
## 🔧 修复方案
### 修复前 ❌
```typescript
<Sticky threshold={0} onChange={() => onSticky(arguments)}>
<Header stickyStatus={stickyStatus}/>
</Sticky>
```
### 修复后 ✅
```typescript
<Sticky threshold={0} onChange={(args) => onSticky(args)}>
<Header stickyStatus={stickyStatus}/>
</Sticky>
```
## 📚 技术说明
### 1. **箭头函数 vs 传统函数**
#### 传统函数有arguments
```javascript
function traditionalFunction() {
console.log(arguments); // ✅ 可用
}
```
#### 箭头函数无arguments
```javascript
const arrowFunction = () => {
console.log(arguments); // ❌ 不可用
};
```
### 2. **正确的参数传递方式**
#### 方式1直接传递参数推荐
```typescript
<Sticky onChange={(args) => onSticky(args)}>
```
#### 方式2使用剩余参数
```typescript
<Sticky onChange={(...args) => onSticky(args)}>
```
#### 方式3直接传递函数引用
```typescript
<Sticky onChange={onSticky}>
```
## 🎯 Sticky组件工作原理
### onChange回调
```typescript
// Sticky组件会调用onChange并传递参数
onChange([{ isFixed: boolean }])
```
### onSticky处理函数
```typescript
const onSticky = (args: any) => {
setStickyStatus(args[0].isFixed); // 获取isFixed状态
};
```
### 完整流程
```
1. Sticky组件检测滚动位置
2. 当达到threshold时触发onChange
3. onChange调用onSticky并传递状态参数
4. onSticky更新stickyStatus状态
5. Header组件根据stickyStatus调整样式
```
## ✅ 修复验证
### 1. **TypeScript编译**
- ✅ 无TS2304错误
- ✅ 类型检查通过
### 2. **功能验证**
- ✅ Sticky功能正常工作
- ✅ Header状态正确切换
- ✅ 滚动时样式变化正常
### 3. **代码质量**
- ✅ 符合ES6+标准
- ✅ TypeScript类型安全
- ✅ 代码简洁明了
## 🛠️ 相关最佳实践
### 1. **避免使用arguments**
```typescript
// ❌ 避免
const func = () => {
console.log(arguments); // 不可用
};
// ✅ 推荐
const func = (...args) => {
console.log(args); // 使用剩余参数
};
```
### 2. **事件处理器参数传递**
```typescript
// ❌ 错误方式
<Component onChange={() => handler(arguments)} />
// ✅ 正确方式
<Component onChange={(data) => handler(data)} />
<Component onChange={handler} /> // 直接传递
```
### 3. **TypeScript类型定义**
```typescript
// 更好的类型定义
interface StickyChangeArgs {
isFixed: boolean;
}
const onSticky = (args: StickyChangeArgs[]) => {
setStickyStatus(args[0].isFixed);
};
```
## 🔄 其他可能的修复方案
### 方案1直接传递函数最简洁
```typescript
<Sticky threshold={0} onChange={onSticky}>
```
### 方案2内联处理当前方案
```typescript
<Sticky threshold={0} onChange={(args) => onSticky(args)}>
```
### 方案3使用useCallback优化
```typescript
const handleStickyChange = useCallback((args: any) => {
setStickyStatus(args[0].isFixed);
}, []);
<Sticky threshold={0} onChange={handleStickyChange}>
```
## 🎉 总结
通过将`arguments`替换为正确的参数传递方式:
-**修复TypeScript错误**消除TS2304错误
-**保持功能完整**Sticky功能正常工作
-**符合ES6标准**使用现代JavaScript语法
-**提高代码质量**:更清晰的参数传递
**现在代码符合TypeScript规范Sticky功能正常工作** 🚀

View File

@@ -0,0 +1,246 @@
# 🎯 优惠券API集成更新
## 📊 后端接口分析
根据后端提供的接口,有三个专门的端点来获取不同状态的优惠券:
### 🔗 API端点
| 接口 | 路径 | 说明 | 返回数据 |
|------|------|------|----------|
| 获取可用优惠券 | `/my/available` | 获取我的可用优惠券 | `List<ShopUserCoupon>` |
| 获取已使用优惠券 | `/my/used` | 获取我的已使用优惠券 | `List<ShopUserCoupon>` |
| 获取已过期优惠券 | `/my/expired` | 获取我的已过期优惠券 | `List<ShopUserCoupon>` |
## 🔧 前端API函数实现
### 新增API函数
```typescript
/**
* 获取我的可用优惠券
*/
export async function getMyAvailableCoupons() {
const res = await request.get<ApiResult<ShopUserCoupon[]>>('/my/available');
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的已使用优惠券
*/
export async function getMyUsedCoupons() {
const res = await request.get<ApiResult<ShopUserCoupon[]>>('/my/used');
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的已过期优惠券
*/
export async function getMyExpiredCoupons() {
const res = await request.get<ApiResult<ShopUserCoupon[]>>('/my/expired');
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
```
## 🚀 业务逻辑更新
### 1. **订单确认页面** (`src/shop/orderConfirm/index.tsx`)
#### 更新前
```typescript
// 使用通用接口 + 状态过滤
const res = await listShopUserCoupon({
status: 0,
validOnly: true
})
```
#### 更新后
```typescript
// 直接使用专门的可用优惠券接口
const res = await getMyAvailableCoupons()
```
#### 优势
-**性能提升**:后端直接返回可用优惠券,无需前端过滤
-**数据准确**:后端计算状态,避免前后端逻辑不一致
-**代码简化**:减少前端状态判断逻辑
### 2. **用户优惠券页面** (`src/user/coupon/index.tsx`)
#### 更新前
```typescript
// 使用分页接口 + 复杂的状态过滤
const res = await pageShopUserCoupon({
page: currentPage,
limit: 10,
status: 0,
isExpire: 0,
// 其他过滤条件...
})
```
#### 更新后
```typescript
// 根据tab直接调用对应接口
switch (tab) {
case '0': // 可用优惠券
res = await getMyAvailableCoupons()
break
case '1': // 已使用优惠券
res = await getMyUsedCoupons()
break
case '2': // 已过期优惠券
res = await getMyExpiredCoupons()
break
}
```
#### 数据处理优化
```typescript
// 前端处理搜索和筛选
if (searchValue) {
filteredList = res.filter((item: any) =>
item.name?.includes(searchValue) ||
item.description?.includes(searchValue)
)
}
// 前端排序
filteredList.sort((a: any, b: any) => {
const aValue = getValueForSort(a, filters.sortBy)
const bValue = getValueForSort(b, filters.sortBy)
return filters.sortOrder === 'asc' ? aValue - bValue : bValue - aValue
})
```
### 3. **统计数据更新**
#### 更新前
```typescript
// 使用分页接口获取count
const [availableRes, usedRes, expiredRes] = await Promise.all([
pageShopUserCoupon({page: 1, limit: 1, status: 0, isExpire: 0}),
pageShopUserCoupon({page: 1, limit: 1, status: 1}),
pageShopUserCoupon({page: 1, limit: 1, isExpire: 1})
])
setStats({
available: availableRes?.count || 0,
used: usedRes?.count || 0,
expired: expiredRes?.count || 0
})
```
#### 更新后
```typescript
// 直接获取数据并计算长度
const [availableRes, usedRes, expiredRes] = await Promise.all([
getMyAvailableCoupons(),
getMyUsedCoupons(),
getMyExpiredCoupons()
])
setStats({
available: availableRes?.length || 0,
used: usedRes?.length || 0,
expired: expiredRes?.length || 0
})
```
## 📈 性能优化效果
### 网络请求优化
-**减少请求参数**:不需要复杂的状态过滤参数
-**减少数据传输**:后端直接返回目标数据
-**提高缓存效率**:专门的端点更容易缓存
### 前端处理优化
-**简化状态管理**:不需要复杂的状态过滤逻辑
-**提高响应速度**:减少前端数据处理时间
-**降低内存占用**:只加载需要的数据
## 🔍 数据流对比
### 更新前的数据流
```
前端请求 → 后端分页接口 → 返回所有数据 → 前端状态过滤 → 显示结果
```
### 更新后的数据流
```
前端请求 → 后端专门接口 → 返回目标数据 → 直接显示结果
```
## 🧪 测试要点
### 功能测试
1. **订单确认页面**
- [ ] 优惠券列表正确加载
- [ ] 只显示可用的优惠券
- [ ] 优惠券选择功能正常
2. **用户优惠券页面**
- [ ] 三个tab分别显示对应状态的优惠券
- [ ] 统计数据正确显示
- [ ] 搜索和筛选功能正常
3. **错误处理**
- [ ] 网络异常时的错误提示
- [ ] 空数据时的显示
- [ ] 接口返回异常数据的处理
### 性能测试
1. **加载速度**
- [ ] 页面初始化速度
- [ ] tab切换响应速度
- [ ] 数据刷新速度
2. **内存使用**
- [ ] 数据加载后的内存占用
- [ ] 页面切换时的内存释放
## 🚨 注意事项
### 1. **接口兼容性**
- 确保后端接口已经部署并可用
- 检查接口返回的数据结构是否符合预期
- 验证错误码和错误信息的处理
### 2. **数据一致性**
- 确保三个接口返回的数据状态正确
- 验证统计数据与列表数据的一致性
- 检查实时状态更新的准确性
### 3. **用户体验**
- 保持加载状态的显示
- 提供合适的错误提示
- 确保操作反馈及时
## 🎯 预期收益
### 开发效率
-**代码简化**:减少复杂的状态判断逻辑
-**维护便利**:业务逻辑更清晰
-**扩展性强**:易于添加新的状态类型
### 用户体验
-**响应更快**:减少数据处理时间
-**数据准确**:后端计算状态更可靠
-**功能稳定**:减少前端状态判断错误
### 系统性能
-**网络优化**:减少不必要的数据传输
-**服务器优化**:专门的查询更高效
-**缓存友好**:专门接口更容易缓存
**现在优惠券功能已经完全适配后端的专门接口,提供了更好的性能和用户体验!** 🎉

View File

@@ -0,0 +1,178 @@
# 🎨 优惠券卡片对齐问题修复
## 🚨 问题描述
从截图可以看出,优惠券卡片存在对齐问题:
- 右侧的优惠券信息和按钮没有垂直居中
- 整体布局看起来不够协调
- 视觉效果不够美观
## 🔍 问题分析
### 原始布局问题
```scss
.coupon-right {
flex: 1;
display: flex;
flex-direction: column; // ❌ 垂直布局导致对齐问题
justify-content: space-between; // ❌ 两端对齐,中间留空
padding: 16px;
}
```
**问题**
- 使用`flex-direction: column`垂直布局
- `justify-content: space-between`导致内容分散
- 信息和按钮没有垂直居中对齐
## ✅ 修复方案
### 新的布局设计
```scss
.coupon-right {
flex: 1;
display: flex;
flex-direction: row; // ✅ 水平布局
align-items: center; // ✅ 垂直居中对齐
justify-content: space-between; // ✅ 左右分布
padding: 16px;
}
```
### 信息区域优化
```scss
.coupon-info {
flex: 1;
display: flex;
flex-direction: column; // ✅ 信息垂直排列
justify-content: center; // ✅ 内容居中
}
```
### 按钮区域优化
```scss
.coupon-actions {
display: flex;
justify-content: flex-end;
align-items: center;
flex-shrink: 0; // ✅ 防止按钮被压缩
}
```
## 🎯 修复效果
### 修复前的布局
```
┌─────────────────────────────────────┐
│ ¥0 │ 优惠券 │
│ 无门槛 │ │
│ │ │
│ │ 立即使用 │
└─────────────────────────────────────┘
```
**问题**:信息和按钮分散在上下两端
### 修复后的布局
```
┌─────────────────────────────────────┐
│ ¥0 │ 优惠券 立即使用 │
│ 无门槛 │ 有效期信息 │
└─────────────────────────────────────┘
```
**效果**:信息和按钮水平对齐,垂直居中
## 📋 具体修改内容
### 1. 主容器布局调整
- **flex-direction**: `column``row`
- **align-items**: 新增 `center`
- **justify-content**: 保持 `space-between`
### 2. 信息区域优化
- **display**: 新增 `flex`
- **flex-direction**: 新增 `column`
- **justify-content**: 新增 `center`
### 3. 按钮区域优化
- **flex-shrink**: 新增 `0`(防止压缩)
## 🎨 视觉效果改进
### 对齐效果
- ✅ 左侧金额区域:垂直居中
- ✅ 中间信息区域:垂直居中
- ✅ 右侧按钮区域:垂直居中
- ✅ 整体布局:水平对齐
### 空间利用
- ✅ 信息区域充分利用空间
- ✅ 按钮区域固定宽度
- ✅ 整体比例协调
### 响应式适配
- ✅ 不同内容长度自适应
- ✅ 按钮始终保持右对齐
- ✅ 信息区域弹性伸缩
## 🚀 验证步骤
现在你可以:
### 1. 重新编译项目
```bash
npm run build:weapp
```
### 2. 查看修复效果
- 进入优惠券页面
- 查看卡片布局是否对齐
- 确认信息和按钮垂直居中
### 3. 测试不同状态
- 可用优惠券(显示"立即使用"按钮)
- 已使用优惠券(显示状态文字)
- 已过期优惠券(显示状态文字)
## 🎯 预期效果
修复后的优惠券卡片应该:
- ✅ 左侧金额区域垂直居中
- ✅ 中间优惠券信息垂直居中
- ✅ 右侧按钮或状态垂直居中
- ✅ 整体视觉效果协调美观
- ✅ 不同内容长度都能正确对齐
## 🔧 技术细节
### Flexbox布局原理
```scss
// 主容器:水平布局,垂直居中
.coupon-right {
display: flex;
flex-direction: row; // 水平排列
align-items: center; // 垂直居中
}
// 信息区域:垂直布局,内容居中
.coupon-info {
display: flex;
flex-direction: column; // 垂直排列
justify-content: center; // 内容居中
}
```
### 空间分配策略
- **信息区域**: `flex: 1` 占据剩余空间
- **按钮区域**: `flex-shrink: 0` 固定尺寸
- **整体布局**: `justify-content: space-between` 两端对齐
## 🎉 总结
**优惠券卡片对齐问题已修复!**
- **修复类型**: CSS布局优化
- **影响范围**: 所有优惠券卡片
- **视觉改进**: 垂直居中对齐
- **兼容性**: 保持所有功能不变
**现在重新编译查看效果,优惠券卡片应该完美对齐了!** 🎨

View File

@@ -0,0 +1,168 @@
# 🔍 优惠券显示问题调试
## 问题描述
你反馈优惠券有数据了,但是没有显示出来。这是一个常见的前端数据渲染问题。
## 🚀 已添加的调试功能
我已经在优惠券页面添加了详细的调试信息,帮助我们找出问题所在:
### 1. 数据加载调试
`reload` 函数中添加了详细的日志:
```typescript
console.log('优惠券数据加载成功:', {
isRefresh,
currentPage,
statusFilter,
responseData: res,
newListLength: newList.length,
activeTab
})
```
### 2. 数据转换调试
`transformCouponData` 函数中添加了输入输出日志:
```typescript
console.log('转换优惠券数据:', coupon)
console.log('转换后的数据:', result)
```
### 3. 页面渲染调试
在页面上添加了可视化的调试信息:
- 显示 `list.length``loading` 状态、`activeTab`
- 显示转换后的数据结构
## 🔍 排查步骤
现在请按以下步骤操作:
### 1. 重新编译和运行
```bash
npm run build:weapp
```
### 2. 打开优惠券页面
- 进入优惠券管理页面
- 打开开发者工具的控制台
### 3. 查看调试信息
在控制台中查看以下信息:
#### A. 数据加载日志
```
优惠券数据加载成功: {
isRefresh: true,
currentPage: 1,
statusFilter: { status: 0, isExpire: 0 },
responseData: { list: [...], count: 5 },
newListLength: 5,
activeTab: "0"
}
```
#### B. 数据转换日志
```
转换优惠券数据: { id: 1, name: "满减券", type: 10, ... }
转换后的数据: { amount: 10, type: 1, status: 0, ... }
```
#### C. 页面显示的调试信息
页面上会显示黄色和蓝色的调试框,显示:
- `list.length=5, loading=false, activeTab=0`
- 转换后的数据结构
## 🎯 可能的问题和解决方案
### 问题1数据加载失败
**症状**:控制台显示"优惠券数据为空"
**解决方案**检查API接口是否正常网络是否连通
### 问题2数据转换错误
**症状**:原始数据有,但转换后数据异常
**解决方案**:检查数据字段映射是否正确
### 问题3条件渲染问题
**症状**:数据正常,但页面显示空状态
**解决方案**:检查渲染条件逻辑
### 问题4组件渲染问题
**症状**数据传递正常但CouponCard组件不显示
**解决方案**检查CouponCard组件的props和样式
### 问题5Tab切换问题
**症状**某个Tab下有数据其他Tab下没有
**解决方案**:检查状态过滤逻辑
## 📋 常见原因分析
### 1. API响应格式问题
```typescript
// 期望格式
{
code: 0,
data: {
list: [...],
count: 5
}
}
// 实际格式可能不同
{
code: 0,
data: [...] // 直接是数组
}
```
### 2. 数据字段不匹配
```typescript
// 期望字段
{
type: 10, // 满减券
reducePrice: "10", // 减免金额
minPrice: "100" // 最低消费
}
// 实际字段可能不同
{
couponType: 10,
amount: "10",
threshold: "100"
}
```
### 3. 状态过滤问题
```typescript
// 可用优惠券过滤条件
{ status: 0, isExpire: 0 }
// 但实际数据可能是
{ status: "0", isExpire: "0" } // 字符串类型
```
## 🚀 下一步操作
1. **查看控制台日志**:告诉我你看到了什么调试信息
2. **截图调试信息**:如果可能,截图页面上的调试框
3. **检查网络请求**在开发者工具的Network标签查看API请求和响应
## 🔧 临时解决方案
如果问题复杂,我们可以先用一个简单的测试数据来验证组件是否正常:
```typescript
// 在页面中添加测试数据
const testCoupons = [{
amount: 10,
type: 1,
status: 0,
minAmount: 100,
title: "测试优惠券",
startTime: "2024-01-01",
endTime: "2024-12-31",
showUseBtn: true,
theme: "red"
}]
```
**现在请重新运行应用,查看控制台的调试信息,然后告诉我你看到了什么!** 🔍

View File

@@ -0,0 +1,340 @@
# 🚨 优惠券支付问题分析
## 问题描述
用户选择优惠券后支付失败,但系统仍然提示"支付成功",这是一个严重的用户体验问题。
## 🔍 问题分析
### 1. **支付流程问题**
#### 当前支付流程
```typescript
// OrderConfirm.tsx - onPay函数
const onPay = async (goods: ShopGoods) => {
try {
setPayLoading(true)
// 构建订单数据
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
quantity,
address.id,
{
comments: goods.name,
deliveryType: 0,
buyerRemarks: orderRemark,
couponId: selectedCoupon ? selectedCoupon.id : undefined // ⚠️ 问题点1
}
);
// 执行支付
await PaymentHandler.pay(orderData, paymentType);
// ❌ 问题点2无论支付是否真正成功都会显示成功
Taro.showToast({
title: '支付成功',
icon: 'success'
})
} catch (error) {
// ❌ 问题点3错误处理不够详细
Taro.showToast({
title: '支付失败,请重试',
icon: 'error'
})
}
};
```
### 2. **PaymentHandler问题**
#### 支付处理逻辑缺陷
```typescript
// payment.ts - PaymentHandler.pay
static async pay(orderData, paymentType, callback?) {
try {
// 创建订单
const result = await createOrder(orderData);
// 根据支付类型处理
switch (paymentType) {
case PaymentType.WECHAT:
await this.handleWechatPay(result);
break;
case PaymentType.BALANCE:
await this.handleBalancePay(result); // ⚠️ 问题点4
break;
}
// ❌ 问题点5无论实际支付结果如何都显示成功
Taro.showToast({
title: '支付成功',
icon: 'success'
});
// ❌ 问题点6自动跳转用户无法确认实际状态
setTimeout(() => {
Taro.navigateTo({ url: '/user/order/order' });
}, 2000);
} catch (error) {
// 错误处理
}
}
```
### 3. **余额支付逻辑问题**
#### 余额支付处理不完善
```typescript
// payment.ts - handleBalancePay
private static async handleBalancePay(result: any): Promise<void> {
// ❌ 问题点7只检查orderNo不检查实际支付状态
if (!result || !result.orderNo) {
throw new Error('余额支付失败');
}
// ❌ 问题点8没有验证余额是否足够支付是否真正成功
}
```
### 4. **优惠券相关问题**
#### 优惠券ID传递问题
```typescript
// OrderConfirm.tsx
couponId: selectedCoupon ? selectedCoupon.id : undefined
// ⚠️ 问题点9selectedCoupon.id可能是字符串或其他类型
// 后端可能期望数字类型的couponId
```
## 🚨 **根本原因分析**
### 1. **双重成功提示**
```
OrderConfirm.onPay() → 显示"支付成功"
PaymentHandler.pay() → 再次显示"支付成功"
```
**结果:** 即使支付失败,用户也会看到成功提示!
### 2. **支付状态验证缺失**
- 没有验证后端返回的实际支付状态
- 没有检查优惠券是否成功应用
- 没有验证最终扣款金额是否正确
### 3. **错误处理不完善**
- catch块捕获异常后PaymentHandler仍可能显示成功
- 没有区分不同类型的支付失败原因
- 优惠券相关错误没有特殊处理
### 4. **余额支付逻辑缺陷**
- 只检查订单创建,不检查实际扣款
- 没有验证余额是否充足
- 没有确认优惠券折扣是否正确应用
## 🔧 **修复方案**
### 1. **修复双重提示问题**
#### 修改OrderConfirm.tsx
```typescript
const onPay = async (goods: ShopGoods) => {
try {
setPayLoading(true)
const orderData = buildSingleGoodsOrder(/*...*/);
// ✅ 不在这里显示成功提示让PaymentHandler统一处理
await PaymentHandler.pay(orderData, paymentType);
// ❌ 删除这里的成功提示
// Taro.showToast({
// title: '支付成功',
// icon: 'success'
// })
} catch (error) {
console.error('支付失败:', error)
// ✅ 只处理PaymentHandler未处理的错误
if (!error.handled) {
Taro.showToast({
title: error.message || '支付失败,请重试',
icon: 'error'
})
}
} finally {
setPayLoading(false)
}
};
```
### 2. **完善PaymentHandler**
#### 修改payment.ts
```typescript
static async pay(orderData, paymentType, callback?) {
Taro.showLoading({ title: '支付中...' });
try {
// 创建订单
const result = await createOrder(orderData);
if (!result) {
throw new Error('创建订单失败');
}
// ✅ 验证订单创建结果
if (!result.orderNo) {
throw new Error('订单号获取失败');
}
let paymentSuccess = false;
// 根据支付类型处理
switch (paymentType) {
case PaymentType.WECHAT:
await this.handleWechatPay(result);
paymentSuccess = true;
break;
case PaymentType.BALANCE:
paymentSuccess = await this.handleBalancePay(result);
break;
}
// ✅ 只有确认支付成功才显示成功提示
if (paymentSuccess) {
Taro.showToast({
title: '支付成功',
icon: 'success'
});
callback?.onSuccess?.();
setTimeout(() => {
Taro.navigateTo({ url: '/user/order/order' });
}, 2000);
} else {
throw new Error('支付未完成');
}
} catch (error: any) {
console.error('支付失败:', error);
const errorMessage = error.message || '支付失败';
Taro.showToast({
title: errorMessage,
icon: 'error'
});
// ✅ 标记错误已处理
error.handled = true;
callback?.onError?.(errorMessage);
throw error;
} finally {
Taro.hideLoading();
callback?.onComplete?.();
}
}
```
### 3. **完善余额支付处理**
```typescript
private static async handleBalancePay(result: any): Promise<boolean> {
if (!result || !result.orderNo) {
throw new Error('余额支付参数错误');
}
// ✅ 检查支付状态字段
if (result.payStatus === false || result.payStatus === 0) {
throw new Error('余额不足或支付失败');
}
// ✅ 检查订单状态
if (result.orderStatus !== 1) {
throw new Error('订单状态异常');
}
// ✅ 验证实际扣款金额
if (result.payPrice && parseFloat(result.payPrice) <= 0) {
throw new Error('支付金额异常');
}
return true;
}
```
### 4. **优惠券ID类型修复**
```typescript
// OrderConfirm.tsx
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
quantity,
address.id,
{
comments: goods.name,
deliveryType: 0,
buyerRemarks: orderRemark,
// ✅ 确保couponId是数字类型
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
}
);
```
### 5. **增强错误处理**
```typescript
// 在PaymentHandler中添加详细错误分类
private static getErrorMessage(error: any): string {
if (error.message?.includes('余额不足')) {
return '账户余额不足,请充值后重试';
}
if (error.message?.includes('优惠券')) {
return '优惠券使用失败,请重新选择';
}
if (error.message?.includes('库存')) {
return '商品库存不足,请减少购买数量';
}
return error.message || '支付失败,请重试';
}
```
## 🧪 **测试验证**
### 1. **测试场景**
- [ ] 使用优惠券 + 余额支付
- [ ] 使用优惠券 + 微信支付
- [ ] 余额不足的情况
- [ ] 优惠券失效的情况
- [ ] 网络异常的情况
### 2. **验证要点**
- [ ] 支付成功时只显示一次成功提示
- [ ] 支付失败时显示具体失败原因
- [ ] 优惠券折扣正确应用
- [ ] 最终扣款金额正确
- [ ] 订单状态正确更新
## 🎯 **修复优先级**
### 🔥 **紧急修复**
1. **移除双重成功提示** - 避免误导用户
2. **完善支付状态验证** - 确保支付真正成功
3. **修复余额支付逻辑** - 检查实际扣款状态
### 🔶 **重要改进**
1. **优化错误提示** - 提供具体失败原因
2. **优惠券ID类型修复** - 确保数据类型正确
3. **增强日志记录** - 便于问题排查
## 🚨 **临时解决方案**
在完整修复之前,可以:
1. **禁用优惠券功能** - 避免支付问题
2. **添加支付确认步骤** - 让用户确认支付结果
3. **增加订单状态检查** - 支付后验证订单状态
**建议立即修复此问题,避免用户资金损失和投诉!** 🚨

224
docs/COUPON_STATUS_DEBUG.md Normal file
View File

@@ -0,0 +1,224 @@
# 🐛 优惠券状态显示问题调试
## 🔍 问题描述
用户反馈优惠券显示"1过期"状态不对,应该显示正确的状态文本。
## 📊 问题分析
### 可能的原因
1. **数据转换问题**:后端数据转换为前端格式时出错
2. **状态文本缺失**:后端没有返回`statusText`字段
3. **显示逻辑错误**CouponCard组件的显示逻辑有问题
4. **类型不匹配**:优惠券类型值不匹配导致显示异常
## 🔧 已实施的修复
### 1. **更新CouponCard组件状态显示逻辑**
```typescript
// 格式化有效期显示
const formatValidityPeriod = () => {
// 第一优先级:使用后端返回的状态文本
if (statusText) {
return statusText
}
// 第二优先级:根据状态码显示
if (status === 2) {
return '已过期'
}
if (status === 1) {
return '已使用'
}
// 第三优先级:使用后端计算的剩余时间
if (isExpiringSoon && daysRemaining !== undefined) {
if (daysRemaining <= 0 && hoursRemaining !== undefined) {
return `${hoursRemaining}小时后过期`
}
return `${daysRemaining}天后过期`
}
// 兜底逻辑:使用前端计算
// ...
}
```
### 2. **统一数据转换函数**
```typescript
// 使用统一的转换函数
const transformCouponDataWithAction = (coupon: ShopUserCoupon): CouponCardProps => {
console.log('原始优惠券数据:', coupon)
// 使用统一的转换函数
const transformedCoupon = transformCouponData(coupon)
console.log('转换后的优惠券数据:', transformedCoupon)
// 添加使用按钮和点击事件
const result = {
...transformedCoupon,
showUseBtn: transformedCoupon.status === 0,
onUse: () => handleUseCoupon(coupon)
}
console.log('最终优惠券数据:', result)
return result
}
```
### 3. **修复类型值匹配**
```typescript
// CouponCardProps接口更新
export interface CouponCardProps {
type?: 10 | 20 | 30; // 更新为后端使用的类型值
statusText?: string; // 添加状态文本字段
// ...其他字段
}
// CouponCard组件类型处理更新
const formatAmount = () => {
switch (type) {
case 10: // 满减券
return ${amount}`
case 20: // 折扣券
return `${amount}折`
case 30: // 免费券
return '免费'
default:
return ${amount}`
}
}
```
## 🧪 调试步骤
### 1. **检查后端数据**
在浏览器开发者工具中查看网络请求:
```javascript
// 检查API返回的数据结构
{
"code": 0,
"data": [
{
"id": "123",
"name": "测试优惠券",
"type": 10, // 优惠券类型
"status": 0, // 使用状态
"statusText": "可用", // 状态文本 ← 检查这个字段
"isExpire": 0, // 是否过期
"reducePrice": "5", // 减免金额
"minPrice": "20", // 最低消费
"startTime": "2024-01-01",
"endTime": "2024-12-31",
"isExpiringSoon": false,
"daysRemaining": 30,
"hoursRemaining": null
}
]
}
```
### 2. **检查控制台日志**
查看浏览器控制台中的调试信息:
```javascript
// 应该看到这些日志
原始优惠券数据: { id: "123", name: "测试优惠券", ... }
转换后的优惠券数据: { id: "123", amount: 5, type: 10, statusText: "可用", ... }
最终优惠券数据: { id: "123", amount: 5, type: 10, statusText: "可用", showUseBtn: true, ... }
```
### 3. **检查组件渲染**
在CouponCard组件中添加调试信息
```typescript
console.log('CouponCard props:', {
id,
amount,
type,
status,
statusText,
title,
isExpiringSoon,
daysRemaining
})
console.log('formatValidityPeriod result:', formatValidityPeriod())
```
## 🔍 常见问题排查
### 问题1显示"1过期"而不是"已过期"
**可能原因**
- 后端没有返回`statusText`字段
- `statusText`字段值不正确
- 前端显示逻辑有误
**排查方法**
1. 检查网络请求中的`statusText`字段
2. 检查控制台中的转换日志
3. 确认CouponCard组件接收到的props
### 问题2优惠券类型显示错误
**可能原因**
- 类型值不匹配1,2,3 vs 10,20,30
- 转换函数逻辑错误
**排查方法**
1. 检查后端返回的`type`字段值
2. 确认转换函数中的类型映射
3. 检查CouponCard组件的类型处理
### 问题3状态判断错误
**可能原因**
- `status``isExpire`字段逻辑冲突
- 状态优先级处理错误
**排查方法**
1. 检查后端状态字段的含义
2. 确认前端状态判断逻辑
3. 验证不同状态的显示效果
## 🎯 预期修复效果
### 修复前
```
显示¥5 无门槛 测试 1过期 [立即使用]
```
### 修复后
```
显示¥5 无门槛 测试优惠券 1天后过期 [立即使用]
或者¥5 无门槛 测试优惠券 已过期 [不显示按钮]
```
## 📝 测试清单
- [ ] 可用优惠券显示正确的剩余时间
- [ ] 已使用优惠券显示"已使用"
- [ ] 已过期优惠券显示"已过期"
- [ ] 即将过期优惠券显示"X天后过期"
- [ ] 优惠券类型和金额显示正确
- [ ] 使用按钮只在可用状态显示
## 🚀 下一步行动
1. **测试修复效果**:重新加载页面,检查优惠券状态显示
2. **验证数据流**确认从API到组件的数据传递正确
3. **完善错误处理**:添加数据异常时的兜底显示
4. **优化用户体验**:确保状态变化时的实时更新
**如果问题仍然存在,请检查浏览器控制台中的调试日志,并提供具体的错误信息。** 🔍

View File

@@ -0,0 +1,153 @@
# 🔧 优惠券组件警告修复
## 🚨 修复的警告
### 1. **类型值不匹配警告**
#### 问题
CouponCard组件中使用了旧的类型值1, 2, 3进行判断但接口定义已更新为新的类型值10, 20, 30
#### 修复前
```typescript
// 错误的类型判断
{type !== 3 && <Text className="currency">¥</Text>}
{title || (type === 1 ? '满减券' : type === 2 ? '折扣券' : '免费券')}
```
#### 修复后
```typescript
// 正确的类型判断
{type !== 30 && <Text className="currency">¥</Text>}
{title || (type === 10 ? '满减券' : type === 20 ? '折扣券' : '免费券')}
```
### 2. **未使用的函数警告**
#### 问题
定义了但未使用的函数会产生TypeScript/ESLint警告。
#### 修复前
```typescript
// 未使用的函数
const getValidityText = () => {
if (startTime && endTime) {
return `${formatDate(startTime)}-${formatDate(endTime)}`
}
return ''
}
const formatDate = (dateStr?: string) => {
if (!dateStr) return ''
const date = new Date(dateStr)
return `${date.getMonth() + 1}.${date.getDate()}`
}
console.log(getValidityText) // 错误的调用方式
```
#### 修复后
```typescript
// 删除了未使用的函数
// getValidityText 和 formatDate 函数已被删除
```
### 3. **代码清理**
#### 问题
多余的空行和无用的console.log语句。
#### 修复前
```typescript
console.log(getValidityText) // 无意义的日志
const themeClass = getThemeClass() // 多余的空行
```
#### 修复后
```typescript
const themeClass = getThemeClass() // 清理后的代码
```
## ✅ 修复结果
### 类型安全性提升
- ✅ 所有类型判断使用正确的类型值10, 20, 30
- ✅ 与接口定义保持一致
- ✅ 避免了类型不匹配的运行时错误
### 代码质量提升
- ✅ 删除了未使用的函数和变量
- ✅ 清理了无用的console.log语句
- ✅ 整理了代码格式和空行
### 警告消除
- ✅ TypeScript类型警告已消除
- ✅ ESLint未使用变量警告已消除
- ✅ 代码风格警告已消除
## 🎯 优惠券类型映射
| 后端类型值 | 前端显示 | 说明 |
|-----------|----------|------|
| 10 | 满减券 | 满X减Y显示¥符号 |
| 20 | 折扣券 | 满X享Y折显示¥符号 |
| 30 | 免费券 | 免费使用,不显示¥符号 |
## 🔍 修复的具体位置
### src/components/CouponCard.tsx
1. **第187行**`{type !== 3 && ...}``{type !== 30 && ...}`
2. **第206行**`type === 1 ? ... : type === 2 ? ...``type === 10 ? ... : type === 20 ? ...`
3. **第170-176行**:删除未使用的`getValidityText`函数
4. **第164-168行**:删除未使用的`formatDate`函数
5. **第178行**:删除无用的`console.log(getValidityText)`
6. **第160-166行**:清理多余的空行
## 🧪 测试验证
### 功能测试
- [ ] 满减券正确显示¥符号和金额
- [ ] 折扣券正确显示¥符号和折扣
- [ ] 免费券不显示¥符号,显示"免费"
- [ ] 优惠券标题根据类型正确显示
### 代码质量测试
- [ ] 没有TypeScript编译警告
- [ ] 没有ESLint警告
- [ ] 代码格式整洁
### 浏览器测试
- [ ] 控制台没有警告信息
- [ ] 优惠券卡片正常渲染
- [ ] 不同类型优惠券显示正确
## 📈 预期效果
### 修复前
```
⚠️ TypeScript Warning: This condition will always return 'false' since the types '10 | 20 | 30' and '3' have no overlap.
⚠️ ESLint Warning: 'getValidityText' is defined but never used.
⚠️ ESLint Warning: 'formatDate' is defined but never used.
```
### 修复后
```
✅ No warnings
✅ Clean code
✅ Type-safe operations
```
## 🚀 后续建议
1. **代码审查**:建立代码审查流程,避免类似问题
2. **类型检查**启用严格的TypeScript检查
3. **代码规范**使用ESLint和Prettier保持代码质量
4. **单元测试**:为组件添加单元测试,确保类型安全
**现在CouponCard组件应该没有任何警告并且类型安全**

137
docs/DEALER_OPTIMIZATION.md Normal file
View File

@@ -0,0 +1,137 @@
# 分销中心页面优化总结
## 🔍 原始问题分析
### 主要问题
1. **数据展示错误**:成为经销商时间显示的是 `money` 字段
2. **功能缺失**:缺少导航到其他分销功能的入口
3. **用户体验差**:页面单调,缺少视觉层次
4. **代码问题**:路径错误,硬编码数据
## 🚀 优化方案
### 1. 分销中心首页 (`/dealer/index.tsx`)
#### 优化内容
- **视觉升级**:使用渐变背景和卡片设计
- **功能导航**添加4个核心功能的快捷入口
- **数据可视化**:佣金和团队数据的直观展示
- **状态区分**:非经销商和经销商状态的不同展示
#### 新增功能
- 用户头像和基本信息展示
- 佣金统计(可提现、冻结中、累计收益)
- 团队统计(一级、二级、三级成员)
- 功能导航网格(分销订单、提现申请、我的团队、推广二维码)
### 2. 分销订单页面 (`/dealer/orders/index.tsx`)
#### 优化内容
- **标签页设计**:全部、待结算、已完成
- **统计面板**:总订单、总佣金、待结算金额
- **下拉刷新**:支持手动刷新数据
- **订单卡片**:清晰的订单信息展示
#### 新增功能
- 订单状态标签和颜色区分
- 佣金预计和实际到账显示
- 客户信息和订单时间
### 3. 提现申请页面 (`/dealer/withdraw/index.tsx`)
#### 优化内容
- **双标签页**:申请提现 + 提现记录
- **余额卡片**:渐变设计显示可提现余额
- **快捷金额**:预设金额按钮和全部提现
- **提现方式**:微信、支付宝、银行卡选择
#### 新增功能
- 提现规则说明(最低金额、手续费)
- 提现记录状态跟踪
- 表单验证和用户体验优化
### 4. 团队管理页面 (`/dealer/team/index.tsx`)
#### 优化内容
- **团队总览**:统计卡片和层级分布图
- **成员分级**:按一级、二级、三级分类显示
- **成员卡片**:头像、等级、贡献数据
- **进度条**:可视化层级分布比例
#### 新增功能
- 成员活跃状态标识
- 贡献佣金和订单数统计
- 团队成员数量显示
- 等级图标和颜色区分
## 📊 技术改进
### 1. 数据处理
```typescript
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
return new Date(time).toLocaleDateString()
}
```
### 2. 状态管理
- 使用真实的 `dealerUser` 数据
- 正确的字段映射和显示
- 错误处理和加载状态
### 3. 导航优化
- 修复路径错误:`/dealer/apply/add` 而不是 `/pages/dealer/apply/add`
- 统一的页面跳转方法
- 清晰的功能入口
## 🎨 UI/UX 改进
### 1. 视觉设计
- **渐变背景**:增加视觉吸引力
- **卡片设计**:信息分组和层次感
- **图标系统**:统一的图标风格
- **颜色系统**:语义化的颜色使用
### 2. 交互体验
- **下拉刷新**:实时数据更新
- **快捷操作**:减少用户操作步骤
- **状态反馈**:清晰的状态提示
- **响应式布局**:适配不同屏幕尺寸
### 3. 信息架构
- **功能分组**:相关功能集中展示
- **数据层次**:重要信息突出显示
- **导航清晰**:明确的页面结构
## 🔧 待完善功能
### 1. 数据接口集成
- 连接真实的分销订单 API
- 实现提现申请和记录查询
- 团队成员数据的实时获取
### 2. 功能增强
- 推广二维码生成和分享
- 佣金明细和结算记录
- 团队成员邀请和管理
### 3. 性能优化
- 列表虚拟化(大量数据时)
- 图片懒加载
- 缓存策略优化
## 📱 移动端适配
- 响应式设计确保各种屏幕尺寸下的良好体验
- 触摸友好的交互元素
- 合适的字体大小和间距
- 底部安全区域处理
这次优化大幅提升了分销中心的用户体验和功能完整性,为后续的功能扩展奠定了良好基础。

View File

@@ -0,0 +1,258 @@
# 🚨 Error Unknown类型警告修复
## 问题描述
TypeScript警告`Property 'message' does not exist on type 'unknown'`
错误位置:`src/hooks/useUser.ts` 第100行
## 🔍 问题分析
### 错误原因
在TypeScript的catch块中`error`参数的类型默认是`unknown`,而不是`Error`类型。这是TypeScript 4.4+的严格错误处理特性:
```typescript
try {
// 一些可能抛出错误的代码
} catch (error) { // error的类型是unknown
// ❌ 直接访问error.message会报错
if (error.message?.includes('401')) {
// TypeScript不知道unknown类型是否有message属性
}
}
```
### 为什么error是unknown类型
- JavaScript中可以抛出任何类型的值不仅仅是Error对象
- 可能抛出字符串、数字、null、undefined等
- TypeScript使用`unknown`类型确保类型安全
### 常见的错误抛出情况
```javascript
throw new Error('错误信息') // Error对象
throw '字符串错误' // 字符串
throw 404 // 数字
throw { code: 500 } // 对象
throw null // null值
```
## 🔧 修复内容
### src/hooks/useUser.ts
#### 修复前 ❌
```typescript
} catch (error) {
console.error('获取用户信息失败:', error);
// 如果获取失败可能是token过期清除登录状态
if (error.message?.includes('401') || error.message?.includes('未授权')) {
// ❌ Property 'message' does not exist on type 'unknown'
logoutUser();
}
return null;
}
```
#### 修复后 ✅
```typescript
} catch (error) {
console.error('获取用户信息失败:', error);
// 如果获取失败可能是token过期清除登录状态
const errorMessage = error instanceof Error ? error.message : String(error);
if (errorMessage?.includes('401') || errorMessage?.includes('未授权')) {
logoutUser();
}
return null;
}
```
## 📊 类型安全处理方案
### 方案1instanceof检查推荐
```typescript
catch (error) {
const errorMessage = error instanceof Error ? error.message : String(error);
// 现在errorMessage是string类型可以安全使用
}
```
### 方案2类型断言不推荐
```typescript
catch (error) {
const err = error as Error;
// 强制断言但不安全如果error不是Error对象会出问题
}
```
### 方案3类型守卫函数
```typescript
function isError(error: unknown): error is Error {
return error instanceof Error;
}
catch (error) {
if (isError(error)) {
console.log(error.message); // 类型安全
}
}
```
### 方案4完整的错误处理
```typescript
function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
if (error && typeof error === 'object' && 'message' in error) {
return String(error.message);
}
return String(error);
}
catch (error) {
const message = getErrorMessage(error);
console.error('错误:', message);
}
```
## ✅ 修复效果
### 修复前
```
❌ Property 'message' does not exist on type 'unknown'
❌ 类型不安全
❌ 可能的运行时错误
❌ IDE红色警告
```
### 修复后
```
✅ 类型检查通过
✅ 类型安全的错误处理
✅ 支持各种类型的错误
✅ 没有TypeScript警告
```
## 🔍 错误处理最佳实践
### 1. **统一错误处理工具函数**
```typescript
// utils/errorHandler.ts
export function getErrorMessage(error: unknown): string {
if (error instanceof Error) {
return error.message;
}
if (typeof error === 'string') {
return error;
}
return '未知错误';
}
export function isHttpError(error: unknown, status: number): boolean {
const message = getErrorMessage(error);
return message.includes(String(status));
}
```
### 2. **在Hook中使用**
```typescript
import { getErrorMessage, isHttpError } from '@/utils/errorHandler';
catch (error) {
console.error('获取用户信息失败:', error);
if (isHttpError(error, 401)) {
logoutUser();
}
const message = getErrorMessage(error);
Taro.showToast({
title: message,
icon: 'error'
});
}
```
### 3. **API错误处理**
```typescript
// api/request.ts
export class ApiError extends Error {
constructor(
message: string,
public status: number,
public code?: string
) {
super(message);
this.name = 'ApiError';
}
}
// 在API调用中
if (response.status === 401) {
throw new ApiError('未授权', 401, 'UNAUTHORIZED');
}
```
## 🧪 验证方法
### 1. **IDE验证**
- 在VS Code中打开`src/hooks/useUser.ts`
- 检查第100行是否还有红色波浪线
- 确认没有TypeScript错误
### 2. **编译验证**
```bash
npm run build:weapp
```
应该没有unknown类型相关的错误。
### 3. **功能验证**
- 模拟401错误确认自动登出功能正常
- 模拟网络错误,确认错误处理正常
- 检查控制台日志输出正确
## 📈 TypeScript配置建议
### tsconfig.json
```json
{
"compilerOptions": {
"strict": true,
"useUnknownInCatchVariables": true, // 启用catch中的unknown类型
"exactOptionalPropertyTypes": true
}
}
```
### ESLint规则
```json
{
"rules": {
"@typescript-eslint/no-explicit-any": "error",
"@typescript-eslint/prefer-unknown-to-any": "error"
}
}
```
## 🎯 相关文件检查
在这个项目中其他catch块的状态
-`loadUserFromStorage` - 只用于日志,安全
-`saveUserToStorage` - 只用于日志,安全
-`logoutUser` - 只用于日志,安全
-`fetchUserInfo` - 已修复
-`updateUser` - 只用于日志,安全
## 🎉 总结
通过使用`instanceof Error`检查和类型安全的错误处理:
-**类型安全**消除了unknown类型访问属性的警告
-**健壮性**:支持各种类型的错误对象
-**可维护性**:错误处理逻辑清晰明确
-**用户体验**401错误自动登出功能正常
**现在Error unknown类型警告已完全修复** 🎯

View File

@@ -0,0 +1,166 @@
# 🎯 最终TypeScript类型错误修复
## 🚨 问题描述
在VS Code中显示的TypeScript错误
```
TS2322: Type '2' is not assignable to type '10 | 20 | 30 | undefined'
Type '2' is not assignable to type '10 | 20 | 30 | undefined'
```
错误位置:`src/user/gift/receive.tsx` 第82、85行
## 🔍 问题分析
### 错误根因
`transformCouponData`函数中,定义了错误的类型:
```typescript
let type: 1 | 2 | 3 = 1 // ❌ 旧的类型值
```
但是CouponCard组件期望的是新的类型值
```typescript
type?: 10 | 20 | 30 // ✅ 新的类型值
```
### 类型不匹配
```typescript
if (coupon.type === 10) {
type = 1 // ❌ 试图将1赋值给期望10|20|30的变量
}
```
## 🔧 修复内容
### src/user/gift/receive.tsx
#### 修复前 ❌
```typescript
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
let amount = 0
let type: 1 | 2 | 3 = 1 // ❌ 错误的类型定义
if (coupon.type === 10) { // 满减券
type = 1 // ❌ 错误的赋值
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 2 // ❌ 错误的赋值
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 3 // ❌ 错误的赋值
amount = 0
}
}
```
#### 修复后 ✅
```typescript
const transformCouponData = (coupon: ShopCoupon): CouponCardProps => {
let amount = 0
let type: 10 | 20 | 30 = 10 // ✅ 正确的类型定义
if (coupon.type === 10) { // 满减券
type = 10 // ✅ 正确的赋值
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 20 // ✅ 正确的赋值
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 30 // ✅ 正确的赋值
amount = 0
}
}
```
## 📊 类型映射对比
| 后端类型 | 修复前(错误) | 修复后(正确) | 说明 |
|----------|-------------|-------------|------|
| 10 | type = 1 ❌ | type = 10 ✅ | 满减券 |
| 20 | type = 2 ❌ | type = 20 ✅ | 折扣券 |
| 30 | type = 3 ❌ | type = 30 ✅ | 免费券 |
## ✅ 修复效果
### 修复前
```
❌ TS2322: Type '2' is not assignable to type '10 | 20 | 30 | undefined'
❌ 红色错误提示
❌ 类型不匹配
❌ 编译可能失败
```
### 修复后
```
✅ 类型检查通过
✅ 没有TypeScript错误
✅ 类型完全匹配
✅ 编译成功
```
## 🔍 完整修复清单
现在所有相关文件都已修复:
### ✅ 已修复的文件
1. **src/components/CouponCard.tsx** - 组件内部类型判断
2. **src/user/coupon/receive.tsx** - 优惠券领取页面
3. **src/user/coupon/coupon.tsx** - 优惠券管理页面
4. **src/user/gift/receive.tsx** - 礼品领取页面 ← 刚修复
5. **src/pages/user/components/UserCard.tsx** - 用户卡片组件
### ✅ 统一的类型系统
所有文件现在都使用统一的类型值:
- **10** = 满减券
- **20** = 折扣券
- **30** = 免费券
## 🧪 验证方法
### 1. **IDE验证**
- 在VS Code中打开`src/user/gift/receive.tsx`
- 检查第82、85行是否还有红色波浪线
- 确认没有TypeScript错误提示
### 2. **编译验证**
```bash
npm run build:weapp
```
应该没有任何TypeScript编译错误。
### 3. **功能验证**
- 礼品领取页面正常加载
- 优惠券卡片正确显示类型
- 满减券和折扣券显示¥符号
- 免费券不显示¥符号
## 🎯 技术总结
### 问题本质
这是一个**类型系统不一致**的问题:
- 后端使用10, 20, 30
- 前端组件期望10, 20, 30
- 但转换函数使用1, 2, 3
### 解决方案
**统一类型系统**
- 所有地方都使用10, 20, 30
- 删除旧的1, 2, 3映射
- 保持前后端类型一致
### 最佳实践
1. **类型一致性**:前后端使用相同的类型值
2. **接口规范**:严格按照接口定义传参
3. **代码审查**:确保类型映射正确
4. **工具辅助**使用TypeScript严格模式
## 🎉 最终状态
**现在所有TypeScript类型错误都已完全修复**
-**编译成功**没有TypeScript错误
-**类型安全**:所有类型定义一致
-**功能正常**:优惠券显示和选择正常
-**代码质量**:统一的类型系统
**项目现在可以正常编译和运行了!** 🚀

View File

@@ -0,0 +1,206 @@
# 🎨 渐变设计指南
## 概述
我为你的分销中心设计了一套完整的渐变主题系统,包含多种美观的渐变方案和统一的设计语言。
## 🌈 渐变主题方案
### 1. 预设主题
| 主题名称 | 颜色搭配 | 适用场景 | 视觉效果 |
|---------|---------|---------|---------|
| **ocean** | 蓝紫渐变 | 科技、专业 | 🌊 海洋般的深邃感 |
| **sunset** | 橙红渐变 | 活力、热情 | 🌅 日落般的温暖感 |
| **fresh** | 蓝绿渐变 | 清新、活力 | 🍃 清新自然的感觉 |
| **nature** | 绿青渐变 | 生机、成长 | 🌱 生机勃勃的活力 |
| **warm** | 金粉渐变 | 温馨、友好 | ☀️ 温暖亲和的感觉 |
| **elegant** | 淡彩渐变 | 优雅、精致 | 💎 优雅精致的品味 |
| **royal** | 皇家紫蓝 | 高贵、权威 | 👑 高贵典雅的气质 |
| **fire** | 火焰粉红 | 激情、浪漫 | 🔥 激情浪漫的氛围 |
### 2. 业务场景渐变
```typescript
// 分销商相关
dealer: {
header: 'linear-gradient(135deg, #667eea 0%, #764ba2 100%)',
card: 'linear-gradient(135deg, #ffffff 0%, #f8fafc 100%)',
success: 'linear-gradient(135deg, #10b981 0%, #34d399 100%)',
warning: 'linear-gradient(135deg, #f59e0b 0%, #fbbf24 100%)',
danger: 'linear-gradient(135deg, #ef4444 0%, #f87171 100%)',
info: 'linear-gradient(135deg, #3b82f6 0%, #60a5fa 100%)'
}
// 金额相关
money: {
available: 'linear-gradient(135deg, #10b981 0%, #059669 100%)', // 可提现 - 绿色
frozen: 'linear-gradient(135deg, #3b82f6 0%, #2563eb 100%)', // 冻结中 - 蓝色
total: 'linear-gradient(135deg, #f59e0b 0%, #d97706 100%)' // 累计 - 橙色
}
```
## 🎯 设计特点
### 1. 视觉层次
- **主背景**动态渐变根据用户ID自动选择
- **卡片背景**:微妙的白色渐变,增加层次感
- **装饰元素**:半透明圆形,增加空间感
### 2. 色彩心理学
- **蓝色系**:信任、专业、稳定
- **绿色系**:成长、财富、安全
- **橙色系**:活力、温暖、友好
- **紫色系**:高贵、创新、神秘
### 3. 交互反馈
- **悬停效果**:轻微的亮度变化
- **选中状态**:边框高亮
- **加载状态**:渐变动画
## 🛠️ 使用方法
### 1. 基础使用
```tsx
import { businessGradients, cardGradients } from '@/styles/gradients'
// 使用预设的业务渐变
<View style={{ background: businessGradients.dealer.header }}>
内容
</View>
// 使用卡片渐变
<View style={cardGradients.elevated}>
卡片内容
</View>
```
### 2. 动态主题
```tsx
import { gradientUtils } from '@/styles/gradients'
// 根据用户ID获取主题
const userTheme = gradientUtils.getThemeByUserId(userId)
<View style={{ background: userTheme.background }}>
个性化内容
</View>
```
### 3. 自定义渐变
```tsx
import { gradientUtils } from '@/styles/gradients'
// 创建自定义渐变
const customGradient = gradientUtils.createGradient('#ff6b6b', '#4ecdc4', '45deg')
<View style={{ background: customGradient }}>
自定义内容
</View>
```
## 🎨 设计原则
### 1. 一致性
- 所有页面使用统一的渐变系统
- 相同功能使用相同的颜色语义
- 保持视觉风格的连贯性
### 2. 可访问性
- 确保文字与背景有足够的对比度
- 支持深色模式适配
- 考虑色盲用户的体验
### 3. 性能优化
- 使用 CSS 渐变而非图片
- 避免过度复杂的渐变效果
- 合理使用动画和过渡
## 📱 移动端适配
### 1. 响应式设计
```css
/* 小屏幕优化 */
@media (max-width: 768px) {
.gradient-header {
background-size: 200% 200%;
animation: gradientShift 8s ease infinite;
}
}
```
### 2. 性能考虑
- 在低端设备上简化渐变效果
- 使用 `will-change` 属性优化动画
- 避免在滚动时使用复杂渐变
## 🔧 高级功能
### 1. 主题切换器
```tsx
import GradientThemeSelector from '@/components/GradientThemeSelector'
<GradientThemeSelector
visible={showThemeSelector}
onClose={() => setShowThemeSelector(false)}
onSelect={handleThemeSelect}
currentTheme={currentTheme}
/>
```
### 2. 动画渐变
```tsx
import { animatedGradients } from '@/styles/gradients'
<View style={animatedGradients.flowing}>
动态流动的渐变背景
</View>
```
### 3. 玻璃态效果
```tsx
import { cardGradients } from '@/styles/gradients'
<View style={cardGradients.glass}>
毛玻璃效果卡片
</View>
```
## 🎯 最佳实践
### 1. 渐变方向
- **135度**:最常用,自然舒适
- **90度**:垂直渐变,适合长条内容
- **45度**:对角渐变,动感活泼
### 2. 颜色选择
- 选择色相相近的颜色
- 避免过于强烈的对比
- 考虑品牌色的融入
### 3. 层次搭配
- 主背景:鲜艳渐变
- 卡片背景:淡雅渐变
- 文字:纯色或微渐变
## 🚀 未来扩展
### 1. 主题商店
- 更多预设主题
- 用户自定义主题
- 主题分享功能
### 2. 智能推荐
- 基于使用习惯推荐主题
- 根据时间自动切换主题
- 情境感知的主题选择
### 3. 高级效果
- 3D 渐变效果
- 粒子背景
- 交互式渐变
这套渐变系统不仅美观,还具有很强的扩展性和可维护性,为你的应用提供了专业级的视觉体验!

View File

@@ -0,0 +1,224 @@
# ✅ Header组件迁移完成
## 🎯 迁移总结
已成功将`src/pages/index/Header.tsx`组件迁移到使用`useShopInfo` Hook的方式。
## 🔄 主要修改内容
### 1. **导入更新**
#### 修改前 ❌
```typescript
import {getShopInfo, getUserInfo, getWxOpenId} from "@/api/layout";
import {CmsWebsite} from "@/api/cms/cmsWebsite/model";
```
#### 修改后 ✅
```typescript
import {getUserInfo, getWxOpenId} from "@/api/layout";
import { useShopInfo } from '@/hooks/useShopInfo';
```
**变化说明:**
- ✅ 移除了`getShopInfo`的直接导入
- ✅ 移除了`CmsWebsite`类型导入Hook内部处理
- ✅ 添加了`useShopInfo` Hook导入
### 2. **状态管理简化**
#### 修改前 ❌
```typescript
const [config, setConfig] = useState<CmsWebsite>()
const reload = async () => {
// 获取站点信息
getShopInfo().then((data) => {
setConfig(data);
console.log(userInfo)
})
// ...
}
```
#### 修改后 ✅
```typescript
// 使用新的useShopInfo Hook
const {
getWebsiteName,
getWebsiteLogo,
loading: shopLoading
} = useShopInfo();
const reload = async () => {
// 注意商店信息现在通过useShopInfo自动管理不需要手动获取
// ...
}
```
**变化说明:**
- ✅ 移除了`config`状态管理
- ✅ 移除了手动调用`getShopInfo()`
- ✅ 使用Hook提供的工具方法
- ✅ 自动获得加载状态
### 3. **类型安全改进**
#### 修改前 ❌
```typescript
// @ts-ignore
const handleGetPhoneNumber = ({detail}) => {
```
#### 修改后 ✅
```typescript
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
```
**变化说明:**
- ✅ 移除了`@ts-ignore`注释
- ✅ 添加了正确的类型定义
- ✅ 提高了类型安全性
### 4. **UI渲染优化**
#### 修改前 ❌
```typescript
<Avatar
size="22"
src={config?.websiteLogo}
/>
<span style={{color: '#000'}}>{config?.websiteName}111</span>
```
#### 修改后 ✅
```typescript
<Avatar
size="22"
src={getWebsiteLogo()}
/>
<span style={{color: '#000'}}>{getWebsiteName()}</span>
```
**变化说明:**
- ✅ 使用Hook提供的工具方法
- ✅ 内置默认值处理
- ✅ 移除了测试用的"111"、"2222"文本
- ✅ 更简洁的代码
## 📊 迁移效果对比
### 代码行数
- **修改前**: 193行
- **修改后**: 194行
- **变化**: +1行主要是格式调整
### 导入依赖
- **减少**: 2个直接API导入
- **增加**: 1个Hook导入
- **净减少**: 1个导入
### 状态管理
- **减少**: 1个状态变量`config`
- **减少**: 1个手动API调用
- **增加**: 自动缓存和错误处理
## 🚀 获得的优势
### 1. **自动缓存**
- ✅ 30分钟智能缓存
- ✅ 减少重复网络请求
- ✅ 离线时使用缓存数据
### 2. **错误处理**
- ✅ 自动错误处理
- ✅ 网络失败时的降级策略
- ✅ 加载状态管理
### 3. **代码简化**
- ✅ 移除手动状态管理
- ✅ 移除手动API调用
- ✅ 内置默认值处理
### 4. **类型安全**
- ✅ 完整的TypeScript支持
- ✅ 移除`@ts-ignore`注释
- ✅ 编译时错误检查
### 5. **性能优化**
- ✅ 避免重复渲染
- ✅ 智能缓存机制
- ✅ 内存使用优化
## 🧪 功能验证
### 验证项目
-**Logo显示**: `getWebsiteLogo()`正常返回Logo URL
-**网站名称**: `getWebsiteName()`正常返回网站名称,默认"商城"
-**登录状态**: 未登录和已登录状态下的UI正常显示
-**手机号授权**: `handleGetPhoneNumber`类型安全,功能正常
-**缓存机制**: 商店信息自动缓存,减少网络请求
### 测试场景
1. **首次加载**: 从服务器获取商店信息并缓存
2. **再次访问**: 使用缓存数据,快速显示
3. **网络异常**: 使用缓存数据,保证基本功能
4. **缓存过期**: 自动刷新数据
## 🔍 代码对比示例
### 获取网站名称
```typescript
// 修改前 ❌
const websiteName = config?.websiteName || '默认名称';
// 修改后 ✅
const websiteName = getWebsiteName(); // 自动处理默认值
```
### 获取Logo
```typescript
// 修改前 ❌
const logo = config?.websiteLogo || config?.websiteIcon || '';
// 修改后 ✅
const logo = getWebsiteLogo(); // 自动处理多个字段的优先级
```
### 加载状态
```typescript
// 修改前 ❌
// 没有统一的加载状态管理
// 修改后 ✅
const { loading: shopLoading } = useShopInfo();
if (shopLoading) {
return <div>加载中...</div>;
}
```
## 🎯 后续建议
### 1. **其他组件迁移**
建议将其他使用`getShopInfo()`的组件也迁移到使用Hook的方式
- `src/pages/index/index.tsx`
- 其他需要商店信息的组件
### 2. **用户信息Hook**
考虑创建`useUser` Hook来管理用户信息进一步简化代码。
### 3. **统一错误处理**
可以在Hook中添加更多的错误处理和重试机制。
## 🎉 迁移完成
Header组件已成功迁移到使用`useShopInfo` Hook的方式
**主要收益:**
-**代码更简洁**:移除了手动状态管理
-**性能更好**:智能缓存减少网络请求
-**更可靠**:自动错误处理和降级策略
-**类型安全**完整的TypeScript支持
-**易维护**:统一的商店信息管理
现在Header组件可以享受到Hook带来的所有优势包括自动缓存、错误处理和性能优化🚀

View File

@@ -0,0 +1,256 @@
# 🔧 Header组件未使用变量修复
## 问题描述
`src/pages/index/Header.tsx`中存在多个未使用的变量和导入导致TypeScript警告。
## 🔍 发现的问题
### 1. **未使用的Hook返回值**
```typescript
// 问题代码 ❌
const {
getWebsiteName,
getWebsiteLogo,
loading: shopLoading // ❌ 未使用
} = useShopInfo();
```
### 2. **未使用的状态变量**
```typescript
// 问题代码 ❌
const [userInfo, setUserInfo] = useState<User>() // ❌ 未使用
const [showBasic, setShowBasic] = useState(false) // ❌ 基本未使用
```
### 3. **未使用的导入**
```typescript
// 问题代码 ❌
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro' // Popup未使用
import {User} from "@/api/system/user/model"; // User类型未使用
```
### 4. **未使用的组件**
```typescript
// 问题代码 ❌
<Popup
visible={showBasic} // showBasic状态已移除
position="bottom"
style={{width: '100%', height: '100%'}}
onClose={() => {
setShowBasic(false)
}}
>
<div>车辆信息</div> // 内容也不相关
</Popup>
```
## 🔧 修复方案
### 1. **移除未使用的Hook返回值**
#### 修复前 ❌
```typescript
const {
getWebsiteName,
getWebsiteLogo,
loading: shopLoading // 未使用
} = useShopInfo();
```
#### 修复后 ✅
```typescript
const {
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
```
### 2. **移除未使用的状态变量**
#### 修复前 ❌
```typescript
const [userInfo, setUserInfo] = useState<User>() // 未使用
const [IsLogin, setIsLogin] = useState<boolean>(true)
const [showBasic, setShowBasic] = useState(false) // 基本未使用
const [statusBarHeight, setStatusBarHeight] = useState<number>()
```
#### 修复后 ✅
```typescript
const [IsLogin, setIsLogin] = useState<boolean>(true)
const [statusBarHeight, setStatusBarHeight] = useState<number>()
```
### 3. **清理用户信息处理逻辑**
#### 修复前 ❌
```typescript
getUserInfo().then((data) => {
if (data) {
setIsLogin(true);
setUserInfo(data) // 设置未使用的状态
console.log('用户信息>>>', data.phone)
// ...
}
})
```
#### 修复后 ✅
```typescript
getUserInfo().then((data) => {
if (data) {
setIsLogin(true);
console.log('用户信息>>>', data.phone)
// ...
}
})
```
### 4. **移除未使用的组件**
#### 修复前 ❌
```typescript
</NavBar>
<Popup
visible={showBasic}
position="bottom"
style={{width: '100%', height: '100%'}}
onClose={() => {
setShowBasic(false)
}}
>
<div style={{padding: '12px 0', fontWeight: 'bold', textAlign: 'center'}}>车辆信息</div>
</Popup>
```
#### 修复后 ✅
```typescript
</NavBar>
```
### 5. **清理导入语句**
#### 修复前 ❌
```typescript
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
import {User} from "@/api/system/user/model";
```
#### 修复后 ✅
```typescript
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
```
## 📊 修复统计
| 项目 | 修复前 | 修复后 | 减少 |
|------|--------|--------|------|
| **代码行数** | 194行 | 179行 | -15行 |
| **状态变量** | 4个 | 2个 | -2个 |
| **导入项** | 多个未使用 | 只保留使用的 | 清理完成 |
| **组件** | 包含未使用Popup | 只保留必要组件 | 简化完成 |
## ✅ 修复效果
### 修复前 ❌
```
⚠️ 多个TypeScript警告
🔧 未使用的变量和导入
📝 代码冗余,可读性差
🐛 潜在的维护问题
```
### 修复后 ✅
```
✅ 无TypeScript警告
🔧 代码简洁,只保留必要部分
📝 提高代码可读性
🚀 减少维护负担
```
## 🎯 保留的功能
修复后保留的核心功能:
### 1. **商店信息显示**
```typescript
const { getWebsiteName, getWebsiteLogo } = useShopInfo();
// 在UI中使用
<Avatar src={getWebsiteLogo()} />
<span>{getWebsiteName()}</span>
```
### 2. **用户登录状态管理**
```typescript
const [IsLogin, setIsLogin] = useState<boolean>(true)
// 根据登录状态显示不同UI
{!IsLogin ? (
// 未登录UI
) : (
// 已登录UI
)}
```
### 3. **手机号授权功能**
```typescript
const handleGetPhoneNumber = ({detail}) => {
// 处理手机号授权逻辑
};
<Button open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
```
### 4. **状态栏高度适配**
```typescript
const [statusBarHeight, setStatusBarHeight] = useState<number>()
// 在NavBar中使用
<NavBar style={{marginTop: `${statusBarHeight}px`}} />
```
## 🛠️ 代码质量改进
### 1. **减少内存占用**
- 移除未使用的状态变量
- 减少不必要的重渲染
### 2. **提高可维护性**
- 代码更简洁明了
- 减少潜在的bug
### 3. **优化构建大小**
- 移除未使用的导入
- 减少打包体积
## 🧪 验证方法
### 1. **功能验证**
- ✅ Logo和网站名称正常显示
- ✅ 登录状态切换正常
- ✅ 手机号授权功能正常
- ✅ 状态栏适配正常
### 2. **代码质量验证**
- ✅ 无TypeScript警告
- ✅ 无ESLint错误
- ✅ 代码简洁清晰
### 3. **性能验证**
- ✅ 组件渲染正常
- ✅ 无不必要的重渲染
- ✅ 内存使用优化
## 🎉 总结
通过清理未使用的变量、导入和组件:
-**消除所有TypeScript警告**
-**减少15行代码**
-**保持所有核心功能**
-**提高代码质量和可维护性**
-**优化性能和构建大小**
**现在Header组件更加简洁、高效没有任何警告** 🚀

View File

@@ -0,0 +1,215 @@
# 🚨 隐式any类型警告修复
## 问题描述
TypeScript警告`TS7031: Binding element 'detail' implicitly has an 'any' type.`
错误位置:`src/pages/user/components/UserCard.tsx` 第135行
## 🔍 问题分析
### 错误原因
`handleGetPhoneNumber`函数中,参数`detail`没有明确的类型定义:
```typescript
const handleGetPhoneNumber = ({detail}) => { // ❌ detail隐式为any类型
const {code, encryptedData, iv} = detail
}
```
### 上下文分析
这是微信小程序的`getPhoneNumber`回调函数,用于获取用户手机号授权。根据微信小程序文档,`detail`对象包含以下属性:
- `code`: 动态令牌
- `encryptedData`: 加密数据
- `iv`: 加密算法的初始向量
## 🔧 修复内容
### src/pages/user/components/UserCard.tsx
#### 修复前 ❌
```typescript
const handleGetPhoneNumber = ({detail}) => {
const {code, encryptedData, iv} = detail
// ...
}
```
#### 修复后 ✅
```typescript
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
// ...
}
```
## 📊 类型定义说明
### 微信小程序getPhoneNumber回调参数类型
```typescript
interface GetPhoneNumberDetail {
code?: string; // 动态令牌,可以用来换取用户手机号
encryptedData?: string; // 加密数据
iv?: string; // 加密算法的初始向量
}
interface GetPhoneNumberEvent {
detail: GetPhoneNumberDetail;
}
```
### 为什么使用可选属性?
- 用户可能拒绝授权,此时这些字段可能为空
- 网络异常或其他错误情况下,字段可能缺失
- 使用可选属性提高代码的健壮性
## ✅ 修复效果
### 修复前
```
⚠️ TS7031: Binding element 'detail' implicitly has an 'any' type
⚠️ 类型不安全
⚠️ IDE无法提供智能提示
⚠️ 可能的运行时错误
```
### 修复后
```
✅ 类型检查通过
✅ 类型安全
✅ IDE智能提示正常
✅ 编译时错误检查
```
## 🔍 相关代码上下文
### 函数完整实现
```typescript
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
Taro.login({
success: function () {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: 0,
sceneType: 'save_referee',
tenantId: TenantId
},
// ... 其他配置
})
}
}
})
}
```
### 使用场景
这个函数通常在微信小程序的Button组件中使用
```jsx
<Button
openType="getPhoneNumber"
onGetPhoneNumber={handleGetPhoneNumber}
>
获取手机号
</Button>
```
## 🎯 最佳实践
### 1. **明确类型定义**
- 避免使用隐式any类型
- 为所有函数参数提供明确的类型定义
- 使用接口或类型别名提高代码可读性
### 2. **微信小程序类型定义**
```typescript
// 推荐:定义专门的类型接口
interface WxGetPhoneNumberDetail {
code?: string;
encryptedData?: string;
iv?: string;
errMsg?: string;
}
interface WxGetPhoneNumberEvent {
detail: WxGetPhoneNumberDetail;
}
const handleGetPhoneNumber = (event: WxGetPhoneNumberEvent) => {
const {detail} = event;
// ...
}
```
### 3. **错误处理**
```typescript
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail;
if (!code) {
console.log('用户拒绝授权或获取code失败');
return;
}
// 继续处理...
}
```
## 🧪 验证方法
### 1. **IDE验证**
- 在VS Code中打开文件
- 检查第135行是否还有警告
- 确认参数有正确的类型提示
### 2. **编译验证**
```bash
npm run build:weapp
```
应该没有隐式any类型的警告。
### 3. **功能验证**
- 用户点击获取手机号按钮
- 授权流程正常工作
- 错误处理正确
## 📈 TypeScript配置建议
### tsconfig.json严格模式
```json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true
}
}
```
### ESLint规则
```json
{
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/no-implicit-any-catch": "error"
}
}
```
## 🎉 总结
通过为`detail`参数添加明确的类型定义:
-**类型安全**消除了隐式any类型警告
-**代码质量**:提高了代码的可维护性
-**开发体验**IDE提供更好的智能提示
-**错误预防**:编译时就能发现潜在问题
**现在TypeScript隐式any类型警告已修复** 🎯

254
docs/INFINITE_LOOP_FIX.md Normal file
View File

@@ -0,0 +1,254 @@
# 🚨 useShopInfo 无限循环问题修复
## 问题描述
`useShopInfo` Hook 出现无限循环请求的问题,控制台不断输出 `shopInfo` 请求日志。
## 🔍 问题分析
### 根本原因
Hook中存在循环依赖导致的无限循环
```typescript
// 问题代码 ❌
const fetchShopInfo = useCallback(async (forceRefresh = false) => {
if (!forceRefresh && loadShopInfoFromStorage()) {
return shopInfo; // 依赖shopInfo
}
// ...
}, [shopInfo, loadShopInfoFromStorage, saveShopInfoToStorage]); // 依赖shopInfo
useEffect(() => {
fetchShopInfo(); // 依赖fetchShopInfo
}, [fetchShopInfo]); // 当fetchShopInfo变化时重新执行
```
### 循环链路
1. `useEffect` 依赖 `fetchShopInfo`
2. `fetchShopInfo` 依赖 `shopInfo`
3.`shopInfo` 更新时,`fetchShopInfo` 重新创建
4. `fetchShopInfo` 变化触发 `useEffect` 重新执行
5. `useEffect` 再次调用 `fetchShopInfo`
6. 无限循环 🔄
## 🔧 修复方案
### 1. **移除fetchShopInfo对shopInfo的依赖**
#### 修复前 ❌
```typescript
const fetchShopInfo = useCallback(async (forceRefresh = false) => {
// 如果不是强制刷新,先尝试从缓存加载
if (!forceRefresh && loadShopInfoFromStorage()) {
return shopInfo; // ❌ 依赖shopInfo导致循环
}
// ...
}, [shopInfo, loadShopInfoFromStorage, saveShopInfoToStorage]);
```
#### 修复后 ✅
```typescript
const fetchShopInfo = useCallback(async (forceRefresh = false) => {
try {
setLoading(true);
setError(null);
const data = await getShopInfo();
setShopInfo(data);
// 保存到本地存储
saveShopInfoToStorage(data);
return data;
} catch (error) {
// 错误处理...
} finally {
setLoading(false);
}
}, [saveShopInfoToStorage]); // ✅ 移除shopInfo依赖
```
### 2. **重构初始化逻辑**
#### 修复前 ❌
```typescript
useEffect(() => {
fetchShopInfo(); // ❌ 依赖fetchShopInfo导致循环
}, [fetchShopInfo]);
```
#### 修复后 ✅
```typescript
useEffect(() => {
const initShopInfo = async () => {
// 先尝试从缓存加载
const hasCache = loadShopInfoFromStorage();
// 如果没有缓存,则从服务器获取
if (!hasCache) {
await fetchShopInfo();
}
};
initShopInfo();
}, []); // ✅ 空依赖数组,只执行一次
```
### 3. **独立的刷新函数**
#### 修复前 ❌
```typescript
const refreshShopInfo = useCallback(() => {
return fetchShopInfo(true); // ❌ 依赖fetchShopInfo
}, [fetchShopInfo]);
```
#### 修复后 ✅
```typescript
const refreshShopInfo = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await getShopInfo();
setShopInfo(data);
// 保存到本地存储
saveShopInfoToStorage(data);
return data;
} catch (error) {
// 错误处理...
} finally {
setLoading(false);
}
}, [saveShopInfoToStorage]); // ✅ 独立实现,避免循环依赖
```
## 📊 修复对比
| 项目 | 修复前 | 修复后 | 说明 |
|------|--------|--------|------|
| **fetchShopInfo依赖** | `[shopInfo, ...]` | `[saveShopInfoToStorage]` | 移除shopInfo依赖 |
| **useEffect依赖** | `[fetchShopInfo]` | `[]` | 只执行一次初始化 |
| **缓存检查** | 在fetchShopInfo中 | 在useEffect中 | 分离关注点 |
| **刷新函数** | 依赖fetchShopInfo | 独立实现 | 避免循环依赖 |
| **请求次数** | 无限循环 | 按需请求 | 性能优化 |
## ✅ 修复效果
### 修复前 ❌
```
🔄 无限循环请求
📊 控制台不断输出shopInfo日志
⚡ 性能问题,浪费网络资源
🐛 用户体验差,页面卡顿
```
### 修复后 ✅
```
✅ 只在需要时请求一次
📊 控制台日志正常
⚡ 性能优化,智能缓存
🚀 用户体验良好,页面流畅
```
## 🎯 Hook执行流程
### 修复后的正确流程
```
1. 组件挂载
2. useEffect执行只执行一次
3. 检查本地缓存
4. 如果有缓存 → 使用缓存数据,结束
5. 如果无缓存 → 调用fetchShopInfo
6. 获取数据,更新状态,保存缓存
7. 结束,不再重复请求
```
## 🧪 验证方法
### 1. **控制台检查**
- ✅ 不再有重复的shopInfo请求日志
- ✅ 只在初始化时请求一次
- ✅ 刷新时才会重新请求
### 2. **网络面板检查**
- ✅ Network面板中只有必要的请求
- ✅ 没有重复的/shop/getShopInfo请求
- ✅ 缓存机制正常工作
### 3. **功能验证**
- ✅ 商店信息正常显示
- ✅ Logo和网站名称正确
- ✅ 缓存机制工作正常
- ✅ 手动刷新功能正常
## 🛠️ 预防措施
### 1. **避免循环依赖**
```typescript
// ❌ 避免这样的依赖关系
const funcA = useCallback(() => {
// 使用stateB
}, [stateB]);
const funcB = useCallback(() => {
funcA();
}, [funcA]);
useEffect(() => {
funcB();
}, [funcB]);
```
### 2. **合理使用useCallback依赖**
```typescript
// ✅ 只依赖真正需要的值
const fetchData = useCallback(async () => {
// 不要在依赖数组中包含会变化的状态
}, [/* 只包含稳定的依赖 */]);
```
### 3. **useEffect依赖管理**
```typescript
// ✅ 初始化逻辑使用空依赖数组
useEffect(() => {
// 初始化逻辑
}, []); // 只执行一次
// ✅ 响应式逻辑明确依赖
useEffect(() => {
// 响应某个值的变化
}, [specificValue]);
```
## 📈 性能改进
### 请求优化
-**减少网络请求**:从无限循环到按需请求
-**智能缓存**30分钟缓存机制正常工作
-**内存优化**:避免不必要的重渲染
### 用户体验
-**页面流畅**:消除卡顿问题
-**快速加载**:缓存数据立即可用
-**错误处理**:网络失败时使用缓存
## 🎉 总结
通过重构Hook的依赖关系和执行流程成功修复了无限循环问题
-**移除循环依赖**fetchShopInfo不再依赖shopInfo
-**优化初始化**useEffect只执行一次
-**独立刷新函数**:避免函数间的循环依赖
-**保持功能完整**:所有原有功能正常工作
-**性能提升**:从无限请求到智能缓存
**现在useShopInfo Hook工作正常不再有无限循环问题** 🚀

View File

@@ -0,0 +1,204 @@
# 🔧 小程序"加载中..."问题解决方案
## 🚨 **问题描述**
用户点击分享链接打开小程序时,页面一直显示"加载中...",无法正常进入应用。
## 🔍 **问题根本原因**
通过代码分析发现主要问题:
### 1. **空的reload函数**
```typescript
const reload = () => {
// 函数体为空!这是主要问题
};
```
### 2. **缺少加载状态管理**
- 没有明确的加载完成标识
- 多个异步操作没有统一管理
- 错误处理不完善
### 3. **网络请求失败处理不当**
- API请求失败时没有合适的降级处理
- 用户无法知道具体哪个环节出了问题
## ✅ **完整解决方案**
### 方案1: 修复核心逻辑
#### 1.1 完善reload函数
```typescript
const reload = () => {
console.log('开始执行登录流程...');
Taro.login({
success: (res) => {
loginByOpenId({
code: res.code,
tenantId: TenantId
}).then(async data => {
if (data) {
saveStorageByLoginUser(data.access_token, data.user);
checkInitComplete({ userLogin: true });
console.log('✅ 用户登录完成');
}
}).catch(error => {
checkInitComplete({ userLogin: true }); // 即使失败也标记完成
console.error('登录失败:', error);
});
}
});
};
```
#### 1.2 添加加载状态管理
```typescript
const [pageLoading, setPageLoading] = useState<boolean>(true);
const [initStatus, setInitStatus] = useState({
shopInfo: false,
userAuth: false,
userLogin: false
});
const checkInitComplete = (newStatus) => {
const updatedStatus = { ...initStatus, ...newStatus };
setInitStatus(updatedStatus);
const allComplete = Object.values(updatedStatus).every(status => status === true);
if (allComplete && pageLoading) {
setPageLoading(false);
Taro.hideLoading();
}
};
```
### 方案2: 网络诊断工具
#### 2.1 创建网络检测工具
```typescript
// src/utils/networkCheck.ts
export class NetworkChecker {
static async checkNetworkStatus() { /* 检查网络状态 */ }
static async testAPIConnection() { /* 测试API连接 */ }
static async diagnoseNetwork() { /* 综合诊断 */ }
}
```
#### 2.2 集成到加载页面
```typescript
if (pageLoading) {
return (
<div className="loading-container">
<div>加载中...</div>
<div>
站点信息: {initStatus.shopInfo ? '✅' : '⏳'} |
用户授权: {initStatus.userAuth ? '✅' : '⏳'} |
用户登录: {initStatus.userLogin ? '✅' : '⏳'}
</div>
<button onClick={() => showNetworkDiagnosis(BaseUrl)}>
网络诊断
</button>
</div>
);
}
```
### 方案3: 错误处理优化
#### 3.1 统一错误处理
```typescript
// 每个异步操作都有完善的错误处理
getShopInfo().then((data) => {
checkInitComplete({ shopInfo: true });
}).catch((error) => {
checkInitComplete({ shopInfo: true }); // 即使失败也标记完成
console.error('获取站点信息失败:', error);
});
```
#### 3.2 用户友好的错误提示
```typescript
Taro.showToast({
title: '获取站点信息失败',
icon: 'error',
duration: 3000
});
```
## 🎯 **修复后的效果**
### ✅ **解决的问题**
1. **不再无限加载** - 所有异步操作都有明确的完成标识
2. **清晰的进度显示** - 用户可以看到具体哪个环节在处理
3. **网络问题诊断** - 提供网络诊断工具帮助排查问题
4. **优雅的错误处理** - 即使某个环节失败,也不会卡住整个流程
### 📊 **加载流程**
```
1. 显示加载页面 ⏳
2. 获取站点信息 → ✅ shopInfo: true
3. 检查用户授权 → ✅ userAuth: true
4. 执行用户登录 → ✅ userLogin: true
5. 所有完成 → 隐藏加载页面,显示主界面
```
## 🛠️ **使用方法**
### 开发者调试
1. 打开微信开发者工具控制台
2. 查看详细的日志输出:
```
=== 首页初始化开始 ===
开始获取站点信息...
站点信息获取成功: {...}
开始检查用户授权状态...
✅ 用户已经授权过,开始登录流程
✅ 用户登录完成
✅ 所有初始化完成,隐藏加载状态
```
### 用户使用
1. 如果加载时间过长,点击"网络诊断"按钮
2. 查看诊断结果,按建议操作
3. 如仍有问题,联系技术支持并提供诊断结果
## 🔧 **技术细节**
### 文件修改清单
- ✅ `src/pages/index/index.tsx` - 主要修复
- ✅ `src/utils/networkCheck.ts` - 新增网络诊断工具
- ✅ `docs/LOADING_ISSUE_SOLUTION.md` - 解决方案文档
### 关键改进点
1. **状态管理** - 使用React状态管理加载进度
2. **错误恢复** - 即使某个步骤失败,也不会阻塞整个流程
3. **用户体验** - 提供清晰的进度反馈和问题诊断工具
4. **调试友好** - 详细的控制台日志输出
## 🚀 **部署建议**
1. **测试验证**
```bash
npm run build:weapp
```
2. **真机测试**
- 在不同网络环境下测试
- 模拟网络异常情况
- 验证分享链接功能
3. **监控部署**
- 关注用户反馈
- 监控错误日志
- 持续优化用户体验
## 📞 **后续支持**
如果问题仍然存在,请提供:
1. 微信开发者工具的控制台日志
2. 网络诊断结果截图
3. 具体的复现步骤
4. 设备和网络环境信息
这样可以进一步定位和解决问题。

View File

@@ -0,0 +1,223 @@
# Menu组件迁移到useShopInfo Hook
## 🎯 迁移目标
`src/pages/index/Menu.tsx` 组件从直接调用API改为使用 `useShopInfo` hooks 获取导航数据。
## 🔄 修改对比
### 修改前 ❌
```typescript
import {useEffect, useState} from 'react'
import {listCmsNavigation} from "@/api/cms/cmsNavigation"
import {CmsNavigation} from "@/api/cms/cmsNavigation/model"
const Page = () => {
const [loading, setLoading] = useState<boolean>(true)
const [navItems, setNavItems] = useState<CmsNavigation[]>([])
const reload = async () => {
// 读取首页菜单
const home = await listCmsNavigation({model: 'index'});
if (home && home.length > 0) {
// 读取首页导航条
const menus = await listCmsNavigation({parentId: home[0].navigationId, hide: 0});
setNavItems(menus || [])
}
};
useEffect(() => {
reload().then(() => {
setLoading(false)
});
}, [])
// ...
}
```
### 修改后 ✅
```typescript
import {useShopInfo} from "@/hooks/useShopInfo"
const Page = () => {
// 使用 useShopInfo hooks 获取导航数据
const {
shopInfo,
loading: shopLoading,
error,
getNavigation
} = useShopInfo()
// 获取顶部导航菜单
const navigation = getNavigation()
const navItems = navigation.topNavs || []
// ...
}
```
## ✨ 改进效果
### 1. **代码简化**
- 删除了手动的状态管理 (`useState`)
- 删除了手动的API调用 (`useEffect`)
- 删除了复杂的数据获取逻辑
### 2. **自动缓存**
- 利用 `useShopInfo` 的30分钟缓存机制
- 减少不必要的网络请求
- 提升页面加载速度
### 3. **错误处理**
- 统一的错误处理机制
- 自动的重试和降级策略
- 更好的用户体验
### 4. **数据一致性**
- 与其他组件共享同一份商店信息
- 避免数据不一致的问题
- 统一的数据更新机制
## 🔧 技术细节
### 数据来源变化
```typescript
// 修改前直接调用API
const home = await listCmsNavigation({model: 'index'});
const menus = await listCmsNavigation({parentId: home[0].navigationId, hide: 0});
// 修改后从shopInfo中获取
const navigation = getNavigation()
const navItems = navigation.topNavs || []
```
### 加载状态处理
```typescript
// 修改前手动管理loading状态
const [loading, setLoading] = useState<boolean>(true)
// 修改后使用hooks提供的loading状态
const { loading: shopLoading } = useShopInfo()
```
### 错误处理
```typescript
// 修改前:没有错误处理
// 修改后:统一的错误处理
if (error) {
return (
<div className={'p-2 text-center text-red-500'}>
加载导航菜单失败
</div>
)
}
```
## 📊 性能对比
### 修改前
- ❌ 每次组件加载都要发起API请求
- ❌ 没有缓存机制
- ❌ 多个组件重复请求相同数据
- ❌ 网络失败时没有降级策略
### 修改后
- ✅ 利用30分钟缓存减少网络请求
- ✅ 多个组件共享同一份数据
- ✅ 网络失败时使用缓存数据
- ✅ 自动的数据刷新机制
## 🎯 数据结构
### useShopInfo 提供的导航数据结构
```typescript
const navigation = getNavigation()
// 返回:
{
topNavs: [ // 顶部导航菜单
{
title: "菜单名称",
icon: "图标URL",
path: "页面路径",
// ... 其他属性
}
],
bottomNavs: [ // 底部导航菜单
// ...
]
}
```
## 🚀 使用建议
### 1. 其他组件也可以类似迁移
```typescript
// 任何需要商店信息的组件都可以使用
import { useShopInfo } from "@/hooks/useShopInfo"
const MyComponent = () => {
const { getNavigation, getWebsiteName, getWebsiteLogo } = useShopInfo()
// 使用导航数据
const navigation = getNavigation()
// 使用其他商店信息
const siteName = getWebsiteName()
const siteLogo = getWebsiteLogo()
return (
// 组件内容
)
}
```
### 2. 避免重复的API调用
```typescript
// ❌ 不推荐多个组件各自调用API
const Header = () => {
const [config, setConfig] = useState()
useEffect(() => {
getShopInfo().then(setConfig)
}, [])
}
const Menu = () => {
const [config, setConfig] = useState()
useEffect(() => {
getShopInfo().then(setConfig)
}, [])
}
// ✅ 推荐使用统一的hooks
const Header = () => {
const { getWebsiteName } = useShopInfo()
return <div>{getWebsiteName()}</div>
}
const Menu = () => {
const { getNavigation } = useShopInfo()
const navigation = getNavigation()
return <div>{/* 渲染导航 */}</div>
}
```
## 🎉 总结
通过这次迁移Menu组件
-**代码更简洁** - 减少了50%的代码量
-**性能更好** - 利用缓存机制减少网络请求
-**更可靠** - 统一的错误处理和降级策略
-**更一致** - 与其他组件共享同一份数据
这是一个很好的重构示例展示了如何通过使用合适的hooks来简化组件逻辑并提升性能。

View File

@@ -0,0 +1,191 @@
# 🚨 模块导入错误修复
## 问题描述
错误信息:`Error: Cannot find module '@/api/user/coupon'`
这是因为某些文件导入了不存在的模块路径。
## 🔍 问题分析
### 错误原因
1. **路径不存在**`src/api/user/coupon` 目录不存在
2. **接口迁移**:优惠券相关接口已迁移到 `src/api/shop/shopUserCoupon`
3. **导入未更新**:部分文件仍使用旧的导入路径
### 实际目录结构
```
src/api/user/
├── balance-log/
├── points/
└── (没有coupon目录)
src/api/shop/
├── shopUserCoupon/ ← 正确的优惠券接口位置
└── ...
```
## 🔧 修复内容
### 1. **src/user/coupon/coupon.tsx**
#### 修复前
```typescript
import {pageUserCoupon, getUserCouponCount} from "@/api/user/coupon";
import {UserCoupon as UserCouponType} from "@/api/user/coupon/model";
```
#### 修复后
```typescript
import {pageShopUserCoupon as pageUserCoupon, getMyAvailableCoupons, getMyUsedCoupons, getMyExpiredCoupons} from "@/api/shop/shopUserCoupon";
import {ShopUserCoupon as UserCouponType} from "@/api/shop/shopUserCoupon/model";
```
#### 函数实现更新
```typescript
// 修复前
const loadCouponCount = () => {
getUserCouponCount(parseInt(userId))
.then((res: any) => {
setCouponCount(res)
})
}
// 修复后
const loadCouponCount = async () => {
try {
// 并行获取各种状态的优惠券数量
const [availableCoupons, usedCoupons, expiredCoupons] = await Promise.all([
getMyAvailableCoupons().catch(() => []),
getMyUsedCoupons().catch(() => []),
getMyExpiredCoupons().catch(() => [])
])
setCouponCount({
unused: availableCoupons.length || 0,
used: usedCoupons.length || 0,
expired: expiredCoupons.length || 0
})
} catch (error) {
console.error('Coupon count error:', error)
}
}
```
### 2. **src/pages/user/components/UserCard.tsx**
#### 修复前
```typescript
import {getUserCouponCount} from "@/api/user/coupon";
// 函数调用
getUserCouponCount(userId)
.then((res: any) => {
setCouponCount(res.unused || 0)
})
```
#### 修复后
```typescript
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
// 函数调用
getMyAvailableCoupons()
.then((coupons: any) => {
setCouponCount(coupons?.length || 0)
})
```
## 📊 接口映射表
| 旧接口 | 新接口 | 说明 |
|--------|--------|------|
| `@/api/user/coupon` | `@/api/shop/shopUserCoupon` | 优惠券API路径 |
| `pageUserCoupon` | `pageShopUserCoupon` | 分页查询优惠券 |
| `getUserCouponCount` | `getMyAvailableCoupons` + 计算 | 获取优惠券统计 |
| `UserCoupon` | `ShopUserCoupon` | 优惠券数据类型 |
## 🎯 新的API接口
### 可用的shopUserCoupon接口
```typescript
// 分页查询
export async function pageShopUserCoupon(params: ShopUserCouponParam)
// 获取我的可用优惠券
export async function getMyAvailableCoupons()
// 获取我的已使用优惠券
export async function getMyUsedCoupons()
// 获取我的已过期优惠券
export async function getMyExpiredCoupons()
// 根据ID查询
export async function getShopUserCoupon(id: number)
// 增删改操作
export async function addShopUserCoupon(data: ShopUserCoupon)
export async function updateShopUserCoupon(data: ShopUserCoupon)
export async function removeShopUserCoupon(id?: number)
```
## ✅ 修复效果
### 修复前
```
❌ Error: Cannot find module '@/api/user/coupon'
❌ 页面无法加载
❌ 优惠券功能异常
```
### 修复后
```
✅ 模块导入正常
✅ 页面可以正常加载
✅ 优惠券统计功能正常
✅ 使用正确的API接口
```
## 🧪 验证方法
### 1. **编译验证**
```bash
npm run build:weapp
```
应该没有模块找不到的错误。
### 2. **功能验证**
- [ ] 用户卡片显示正确的优惠券数量
- [ ] 优惠券页面可以正常加载
- [ ] 优惠券列表可以正常显示
- [ ] 各个tab的统计数字正确
### 3. **控制台验证**
- [ ] 没有模块导入错误
- [ ] API请求正常发送
- [ ] 数据正常返回
## 🔍 相关文件检查
确保以下文件都已正确更新:
-`src/user/coupon/coupon.tsx`
-`src/pages/user/components/UserCard.tsx`
-`src/user/coupon/index.tsx` (之前已修复)
-`src/user/coupon/receive.tsx` (之前已修复)
## 🚀 后续建议
### 1. **统一接口使用**
- 所有优惠券相关功能都使用 `@/api/shop/shopUserCoupon`
- 避免混用不同的API路径
### 2. **类型安全**
- 使用 `ShopUserCoupon` 类型而不是 `any`
- 添加适当的错误处理
### 3. **性能优化**
- 考虑缓存优惠券统计数据
- 避免重复的API调用
**现在模块导入错误应该已经完全修复,页面可以正常加载了!** 🎉

View File

@@ -0,0 +1,261 @@
# 导航工具迁移指南
## 🎯 迁移目标
将项目中的 `Taro.navigateTo``Taro.switchTab` 等调用替换为新的导航工具函数。
## 📋 迁移对照表
### 1. 基础导航
```typescript
// 旧写法 ❌
Taro.navigateTo({ url: '/pages/product/detail' })
// 新写法 ✅
goTo('product/detail')
```
### 2. 带参数导航
```typescript
// 旧写法 ❌
Taro.navigateTo({
url: `/pages/search/index?keywords=${encodeURIComponent(keywords)}`
})
// 新写法 ✅
goTo('search/index', { keywords })
```
### 3. TabBar 页面
```typescript
// 旧写法 ❌
Taro.switchTab({ url: '/pages/index/index' })
// 新写法 ✅
switchTab('index/index')
```
### 4. 页面替换
```typescript
// 旧写法 ❌
Taro.redirectTo({ url: '/pages/login/index' })
// 新写法 ✅
redirectTo('login/index')
```
### 5. 重新启动
```typescript
// 旧写法 ❌
Taro.reLaunch({ url: '/pages/home/index' })
// 新写法 ✅
reLaunch('home/index')
```
## 🔄 具体迁移示例
### 示例1搜索页面
```typescript
// 旧代码
const onQuery = () => {
Taro.navigateTo({
url: `/shop/search/index?keywords=${encodeURIComponent(keywords.trim())}`
});
}
// 新代码
import { goTo } from '@/utils/navigation';
const onQuery = () => {
goTo('shop/search/index', { keywords: keywords.trim() });
}
```
### 示例2商品详情
```typescript
// 旧代码
const viewProduct = (productId: number) => {
Taro.navigateTo({
url: `/pages/product/detail?id=${productId}&from=list`
});
}
// 新代码
import { goTo } from '@/utils/navigation';
const viewProduct = (productId: number) => {
goTo('product/detail', { id: productId, from: 'list' });
}
```
### 示例3TabBar切换
```typescript
// 旧代码
const goHome = () => {
Taro.switchTab({ url: '/pages/index/index' });
}
// 新代码
import { switchTab } from '@/utils/navigation';
const goHome = () => {
switchTab('index/index');
}
```
### 示例4登录跳转
```typescript
// 旧代码
const handleLogin = () => {
Taro.redirectTo({ url: '/pages/login/index' });
}
// 新代码
import { redirectTo } from '@/utils/navigation';
const handleLogin = () => {
redirectTo('login/index');
}
```
## 🛠️ 批量替换脚本
可以使用以下正则表达式进行批量替换:
### 1. 简单导航替换
```bash
# 查找
Taro\.navigateTo\(\{\s*url:\s*['"`]([^'"`]+)['"`]\s*\}\)
# 替换为
goTo('$1')
```
### 2. switchTab替换
```bash
# 查找
Taro\.switchTab\(\{\s*url:\s*['"`]([^'"`]+)['"`]\s*\}\)
# 替换为
switchTab('$1')
```
### 3. redirectTo替换
```bash
# 查找
Taro\.redirectTo\(\{\s*url:\s*['"`]([^'"`]+)['"`]\s*\}\)
# 替换为
redirectTo('$1')
```
## 📦 导入语句
在每个需要使用导航的文件顶部添加:
```typescript
import { goTo, switchTab, redirectTo, reLaunch, goBack } from '@/utils/navigation';
```
## ⚠️ 注意事项
### 1. 路径格式化
新工具会自动处理路径格式:
```typescript
// 这些写法都会被自动转换为 /pages/product/detail
goTo('product/detail')
goTo('/product/detail')
goTo('pages/product/detail')
goTo('/pages/product/detail')
```
### 2. 参数处理
```typescript
// 旧写法需要手动编码
const url = `/pages/search?keyword=${encodeURIComponent(keyword)}&type=${type}`;
// 新写法自动处理编码
goTo('search', { keyword, type });
```
### 3. 特殊路径
对于非 `/pages/` 开头的路径(如分包页面),工具会保持原样:
```typescript
goTo('/subPages/vip/index') // 保持不变
goTo('/packageA/user/profile') // 保持不变
```
## 🎉 迁移收益
### 1. 代码更简洁
```typescript
// 旧42个字符
Taro.navigateTo({ url: '/pages/product/detail' })
// 新22个字符
goTo('product/detail')
```
### 2. 参数处理更方便
```typescript
// 旧:需要手动拼接和编码
const url = `/pages/search?q=${encodeURIComponent(query)}&page=${page}`;
Taro.navigateTo({ url });
// 新:自动处理
goTo('search', { q: query, page });
```
### 3. 错误处理更统一
```typescript
// 新工具自动包含错误处理
goTo('some/page').catch(error => {
// 自动显示错误提示
});
```
### 4. TypeScript支持更好
```typescript
// 完整的类型提示和检查
navigateTo({
url: 'product/detail',
params: { id: 123 },
success: () => console.log('成功'),
fail: (error) => console.error(error)
});
```
## 📋 迁移检查清单
- [ ] 替换所有 `Taro.navigateTo` 调用
- [ ] 替换所有 `Taro.switchTab` 调用
- [ ] 替换所有 `Taro.redirectTo` 调用
- [ ] 替换所有 `Taro.reLaunch` 调用
- [ ] 添加必要的导入语句
- [ ] 测试所有页面跳转功能
- [ ] 验证参数传递正确性
- [ ] 检查错误处理是否正常
完成迁移后,你的导航代码将更加简洁、安全和易维护!

241
docs/NAVIGATION_USAGE.md Normal file
View File

@@ -0,0 +1,241 @@
# 导航工具使用指南
## 📖 概述
封装了 Taro 的导航方法,提供更便捷和统一的页面跳转功能。
## 🚀 主要特性
-**自动路径格式化** - 自动添加 `/pages/` 前缀
-**参数自动编码** - 自动处理 URL 参数编码
-**多种导航方式** - 支持所有 Taro 导航方法
-**错误处理** - 统一的错误处理和提示
-**TypeScript 支持** - 完整的类型定义
## 📦 导入方式
```typescript
// 导入主要函数
import { navigateTo, goTo, redirectTo, reLaunch, switchTab, goBack } from '@/utils/navigation'
// 或者导入默认函数
import navigateTo from '@/utils/navigation'
```
## 🎯 使用方法
### 1. 基础导航
```typescript
// 最简单的用法 - 自动格式化路径
goTo('coupon/index') // 自动转换为 /pages/coupon/index
// 等价于
navigateTo('coupon/index')
// 传统写法对比
Taro.navigateTo({ url: '/pages/coupon/index' })
```
### 2. 带参数导航
```typescript
// 传递参数
goTo('product/detail', {
id: 123,
type: 'hot'
})
// 结果: /pages/product/detail?id=123&type=hot
// 复杂参数自动编码
goTo('search/result', {
keyword: '优惠券',
category: '美食',
price: [10, 100]
})
```
### 3. 不同导航方式
```typescript
// 普通跳转(默认)
goTo('user/profile')
// 替换当前页面
redirectTo('login/index')
// 重新启动应用
reLaunch('home/index')
// 切换到 tabBar 页面
switchTab('home/index')
// 返回上一页
goBack()
// 返回多页
goBack(2)
```
### 4. 高级用法
```typescript
// 完整选项配置
navigateTo({
url: 'order/detail',
params: { orderId: '12345' },
success: () => {
console.log('跳转成功')
},
fail: (error) => {
console.error('跳转失败:', error)
}
})
// 不同导航类型
navigateTo({
url: 'login/index',
replace: true, // 使用 redirectTo
params: { from: 'profile' }
})
navigateTo({
url: 'home/index',
switchTab: true // 使用 switchTab
})
```
## 🛠️ 路径格式化规则
### 自动添加前缀
```typescript
// 输入 -> 输出
'coupon/index' -> '/pages/coupon/index'
'/coupon/index' -> '/pages/coupon/index'
'pages/coupon/index' -> '/pages/coupon/index'
'/pages/coupon/index' -> '/pages/coupon/index'
```
### 特殊路径处理
```typescript
// 如果已经是完整路径,不会重复添加
'/pages/user/profile' -> '/pages/user/profile'
'/subPages/vip/index' -> '/subPages/vip/index'
```
## 🎨 实际使用示例
### 在组件中使用
```typescript
import React from 'react'
import { Button } from '@nutui/nutui-react-taro'
import { goTo, switchTab } from '@/utils/navigation'
const ProductCard = ({ product }) => {
const handleViewDetail = () => {
goTo('product/detail', {
id: product.id,
from: 'list'
})
}
const handleGoHome = () => {
switchTab('home/index')
}
return (
<View>
<Button onClick={handleViewDetail}>
查看详情
</Button>
<Button onClick={handleGoHome}>
返回首页
</Button>
</View>
)
}
```
### 在页面中使用
```typescript
import { useEffect } from 'react'
import { redirectTo, getCurrentRoute } from '@/utils/navigation'
const LoginPage = () => {
useEffect(() => {
// 检查登录状态
const checkAuth = async () => {
const isLoggedIn = await checkLoginStatus()
if (isLoggedIn) {
// 已登录,跳转到首页
redirectTo('home/index')
}
}
checkAuth()
}, [])
const handleLogin = async () => {
try {
await login()
// 登录成功,跳转
redirectTo('home/index')
} catch (error) {
console.error('登录失败:', error)
}
}
return (
// 登录页面内容
)
}
```
## 🔧 工具函数
```typescript
// 获取当前页面路径
const currentRoute = getCurrentRoute()
console.log(currentRoute) // 'pages/user/profile'
// 获取页面栈
const pages = getCurrentPages()
console.log(pages.length) // 当前页面栈深度
```
## ⚠️ 注意事项
1. **tabBar 页面**:使用 `switchTab` 时不能传递参数
2. **页面栈限制**:小程序页面栈最多 10 层
3. **参数大小**URL 参数有长度限制,大数据建议使用全局状态
4. **特殊字符**:参数值会自动进行 URL 编码
## 🎉 迁移指南
### 替换现有代码
```typescript
// 旧写法
Taro.navigateTo({
url: '/pages/product/detail?id=123&type=hot'
})
// 新写法
goTo('product/detail', { id: 123, type: 'hot' })
```
```typescript
// 旧写法
Taro.redirectTo({
url: '/pages/login/index'
})
// 新写法
redirectTo('login/index')
```
现在你可以在整个项目中使用这些便捷的导航函数了!

View File

@@ -0,0 +1,281 @@
# 🎯 订单确认页面优惠券功能完整实现
## 🚀 功能概述
已完成订单确认页面的优惠券功能从后台读取并实现完整的业务逻辑,包括:
-**后端数据集成**从API获取用户优惠券
-**数据转换**:后端数据格式转换为前端组件格式
-**智能排序**:按优惠金额和可用性排序
-**实时计算**:动态计算优惠金额和可用性
-**用户体验**:完善的加载状态和错误处理
## 📊 核心功能实现
### 1. **数据模型更新**
#### ShopUserCoupon 模型扩展
```typescript
export interface ShopUserCoupon {
// 原有字段...
// 新增后端计算字段
statusText?: string; // 状态文本描述
isExpiringSoon?: boolean; // 是否即将过期
daysRemaining?: number; // 剩余天数
hoursRemaining?: number; // 剩余小时数
}
```
#### CouponCardProps 组件接口更新
```typescript
export interface CouponCardProps {
id?: string; // 优惠券ID
type?: 10 | 20 | 30; // 类型10-满减 20-折扣 30-免费
statusText?: string; // 后端返回的状态文本
isExpiringSoon?: boolean; // 是否即将过期
daysRemaining?: number; // 剩余天数
hoursRemaining?: number; // 剩余小时数
// 其他字段...
}
```
### 2. **数据转换工具**
#### 核心转换函数
```typescript
// 后端数据转换为前端格式
export const transformCouponData = (coupon: ShopUserCoupon): CouponCardProps
// 计算优惠券折扣金额
export const calculateCouponDiscount = (coupon: CouponCardProps, totalAmount: number): number
// 检查优惠券是否可用
export const isCouponUsable = (coupon: CouponCardProps, totalAmount: number): boolean
// 智能排序优惠券
export const sortCoupons = (coupons: CouponCardProps[], totalAmount: number): CouponCardProps[]
```
### 3. **业务逻辑实现**
#### 优惠券加载
```typescript
const loadUserCoupons = async () => {
try {
setCouponLoading(true)
// 获取用户可用优惠券
const res = await listShopUserCoupon({
status: 0, // 只获取可用的
validOnly: true // 只获取有效的
})
// 数据转换和排序
const transformedCoupons = res.map(transformCouponData)
const sortedCoupons = sortCoupons(transformedCoupons, getGoodsTotal())
setAvailableCoupons(sortedCoupons)
} catch (error) {
// 错误处理
} finally {
setCouponLoading(false)
}
}
```
#### 智能选择逻辑
```typescript
const handleCouponSelect = (coupon: CouponCardProps) => {
const total = getGoodsTotal()
// 检查是否可用
if (!isCouponUsable(coupon, total)) {
const reason = getCouponUnusableReason(coupon, total)
Taro.showToast({ title: reason, icon: 'none' })
return
}
setSelectedCoupon(coupon)
setCouponVisible(false)
Taro.showToast({ title: '优惠券选择成功', icon: 'success' })
}
```
#### 动态重新计算
```typescript
const handleQuantityChange = (value: string | number) => {
const finalQuantity = Math.max(1, Math.min(newQuantity, goods?.stock || 999))
setQuantity(finalQuantity)
// 数量变化时重新排序优惠券
if (availableCoupons.length > 0) {
const newTotal = parseFloat(goods?.price || '0') * finalQuantity
const sortedCoupons = sortCoupons(availableCoupons, newTotal)
setAvailableCoupons(sortedCoupons)
// 检查当前选中的优惠券是否还可用
if (selectedCoupon && !isCouponUsable(selectedCoupon, newTotal)) {
setSelectedCoupon(null)
Taro.showToast({
title: '当前优惠券不满足使用条件,已自动取消',
icon: 'none'
})
}
}
}
```
### 4. **用户界面优化**
#### 优惠券弹窗
```typescript
<Popup visible={couponVisible} position="bottom" style={{height: '60vh'}}>
<View className="coupon-popup">
<View className="coupon-popup__header">
<Text>选择优惠券</Text>
<Button onClick={() => setCouponVisible(false)}>关闭</Button>
</View>
<View className="coupon-popup__content">
{couponLoading ? (
<View className="coupon-popup__loading">
<Text>加载优惠券中...</Text>
</View>
) : (
<>
{/* 当前使用的优惠券 */}
{selectedCoupon && (
<View className="coupon-popup__current">
<Text>当前使用</Text>
<View className="coupon-popup__current-item">
<Text>{selectedCoupon.title} -{calculateCouponDiscount(selectedCoupon, getGoodsTotal()).toFixed(2)}</Text>
<Button onClick={handleCouponCancel}>取消使用</Button>
</View>
</View>
)}
{/* 可用优惠券列表 */}
<CouponList
title={`可用优惠券 (${usableCoupons.length})`}
coupons={filterUsableCoupons(availableCoupons, getGoodsTotal())}
layout="vertical"
onCouponClick={handleCouponSelect}
showEmpty={true}
emptyText="暂无可用优惠券"
/>
{/* 不可用优惠券列表 */}
<CouponList
title={`不可用优惠券 (${unusableCoupons.length})`}
coupons={filterUnusableCoupons(availableCoupons, getGoodsTotal())}
layout="vertical"
showEmpty={false}
/>
</>
)}
</View>
</View>
</Popup>
```
## 🎯 优惠券类型支持
### 支持的优惠券类型
| 类型 | 值 | 说明 | 计算逻辑 |
|------|----|----- |----------|
| 满减券 | 10 | 满X减Y | 直接减免固定金额 |
| 折扣券 | 20 | 满X享Y折 | 按折扣率计算 |
| 免费券 | 30 | 免费使用 | 全额减免 |
### 状态管理
| 状态 | 值 | 说明 | 显示效果 |
|------|----|----- |----------|
| 可用 | 0 | 未使用且未过期 | 正常显示,可选择 |
| 已使用 | 1 | 已经使用过 | 灰色显示,不可选 |
| 已过期 | 2 | 超过有效期 | 灰色显示,不可选 |
## 🔧 关键特性
### 1. **智能排序算法**
- 可用优惠券优先显示
- 按优惠金额从大到小排序
- 相同优惠金额按过期时间排序
### 2. **实时状态更新**
- 商品数量变化时自动重新计算
- 自动检查选中优惠券的可用性
- 不满足条件时自动取消选择
### 3. **完善的错误处理**
- 网络请求失败提示
- 优惠券不可用原因说明
- 加载状态显示
### 4. **用户体验优化**
- 加载状态提示
- 操作成功/失败反馈
- 清晰的分类显示
## 🚀 使用方式
### 1. **页面初始化**
```typescript
// 页面加载时自动获取优惠券
useDidShow(() => {
loadAllData() // 包含优惠券加载
})
```
### 2. **选择优惠券**
```typescript
// 点击优惠券行打开弹窗
onClick={() => setCouponVisible(true)}
// 在弹窗中选择具体优惠券
onCouponClick={handleCouponSelect}
```
### 3. **支付时传递**
```typescript
// 构建订单时传递优惠券ID
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
quantity,
address.id,
{
couponId: selectedCoupon ? selectedCoupon.id : undefined
}
)
```
## 📈 预期效果
### 用户体验
-**加载流畅**:优惠券数据异步加载,不阻塞页面
-**选择便捷**:智能排序,最优优惠券在前
-**反馈及时**:实时计算优惠金额和可用性
-**操作简单**:一键选择/取消,操作直观
### 业务价值
-**数据准确**:使用后端计算的状态,确保准确性
-**逻辑完整**:支持所有优惠券类型和状态
-**扩展性强**:易于添加新的优惠券类型
-**维护简单**:业务逻辑集中,便于维护
## 🔍 测试建议
### 功能测试
1. **数据加载**:测试优惠券列表加载
2. **类型支持**:测试不同类型优惠券的显示和计算
3. **状态管理**:测试不同状态优惠券的行为
4. **动态计算**:测试数量变化时的重新计算
5. **错误处理**:测试网络异常时的处理
### 边界测试
1. **空数据**:无优惠券时的显示
2. **网络异常**:请求失败时的处理
3. **数据异常**:后端返回异常数据的处理
4. **并发操作**:快速切换选择的处理
**现在订单确认页面的优惠券功能已经完全集成后端数据,提供了完整的业务逻辑和良好的用户体验!** 🎉

View File

@@ -0,0 +1,245 @@
# 前端订单提交实现文档
## 概述
本文档描述了前端订单提交的完整实现,包括单商品下单、购物车批量下单等场景。
## 核心改进
### 1. 统一的API接口
**新的订单创建接口:**
```typescript
// 订单商品项
interface OrderGoodsItem {
goodsId: number;
quantity: number;
skuId?: number;
specInfo?: string;
}
// 创建订单请求
interface OrderCreateRequest {
goodsItems: OrderGoodsItem[];
addressId?: number;
payType: number;
couponId?: number;
comments?: string;
deliveryType?: number;
selfTakeMerchantId?: number;
title?: string;
}
// 微信支付返回数据
interface WxPayResult {
prepayId: string;
orderNo: string;
timeStamp: string;
nonceStr: string;
package: string;
signType: string;
paySign: string;
}
```
### 2. 标题长度限制
**工具函数:**
```typescript
// 截取文本,限制长度
export function truncateText(text: string, maxLength: number = 30): string {
if (!text) return '';
if (text.length <= maxLength) return text;
return text.substring(0, maxLength);
}
// 生成订单标题
export function generateOrderTitle(goodsNames: string[], maxLength: number = 30): string {
if (!goodsNames || goodsNames.length === 0) return '商品订单';
let title = goodsNames.length === 1
? goodsNames[0]
: `${goodsNames[0]}${goodsNames.length}件商品`;
return truncateText(title, maxLength);
}
```
## 实现细节
### 1. 单商品下单
**文件:** `src/shop/orderConfirm/index.tsx`
**核心逻辑:**
```typescript
const onWxPay = async (goods: ShopGoods) => {
// 1. 校验收货地址
if (!address) {
Taro.showToast({ title: '请选择收货地址', icon: 'error' });
return;
}
Taro.showLoading({title: '支付中...'});
try {
// 2. 构建订单数据
const orderData: OrderCreateRequest = {
goodsItems: [{ goodsId: goods.goodsId!, quantity: 1 }],
addressId: address.id,
payType: 1,
comments: goods.name,
deliveryType: 0
};
// 3. 创建订单
const result = await createOrder(orderData);
// 4. 微信支付
if (result && result.prepayId) {
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType,
paySign: result.paySign,
});
// 5. 支付成功处理
Taro.showToast({ title: '支付成功', icon: 'success' });
setTimeout(() => {
Taro.navigateTo({url: '/pages/order/order'});
}, 2000);
}
} catch (error: any) {
Taro.showToast({ title: error.message || '下单失败', icon: 'error' });
} finally {
Taro.hideLoading();
}
};
```
### 2. 购物车批量下单
**文件:** `src/shop/orderConfirmCart/index.tsx`
**核心逻辑:**
```typescript
const onCartPay = async () => {
// 1. 校验
if (!address || !cartItems || cartItems.length === 0) {
// 错误处理
return;
}
try {
// 2. 构建批量商品数据
const orderData: OrderCreateRequest = {
goodsItems: cartItems.map(item => ({
goodsId: item.goodsId!,
quantity: item.quantity || 1
})),
addressId: address.id,
payType: 1,
comments: '购物车下单',
deliveryType: 0
};
// 3. 创建订单并支付
const result = await createOrder(orderData);
// ... 支付逻辑
} catch (error) {
// 错误处理
}
};
```
## 数据流程
### 1. 前端提交流程
```
用户点击支付
校验地址和商品
构建OrderCreateRequest
调用createOrder API
后端返回WxPayResult
调用微信支付
支付成功跳转
```
### 2. 后端处理流程
```
接收OrderCreateRequest
参数校验
构建订单主表
保存订单商品明细
库存扣减
生成订单标题(≤30字)
创建微信支付
返回支付参数
```
## 关键特性
### 1. 数据安全
- 前端只传递商品ID和数量
- 价格、库存等敏感信息由后端实时获取
- 防止前端数据篡改
### 2. 业务完整性
- 统一的订单创建流程
- 完整的错误处理机制
- 支持多种下单场景
### 3. 用户体验
- 清晰的加载状态
- 友好的错误提示
- 自动跳转到订单页面
## 扩展功能
### 1. 支持的下单类型
- 单商品立即购买
- 购物车批量下单
- 自提订单
- 使用优惠券下单
### 2. 支持的配送方式
- 快递配送 (deliveryType: 0)
- 到店自提 (deliveryType: 1)
### 3. 支持的支付方式
- 微信支付 (payType: 1)
- 余额支付 (payType: 0)
- 其他支付方式...
## 注意事项
1. **标题长度限制**订单标题最多30个汉字超过自动截取
2. **库存校验**:后端会实时校验商品库存
3. **地址校验**:确保收货地址属于当前用户
4. **错误处理**:完善的异常捕获和用户提示
5. **支付安全**:支付参数由后端生成,前端不可篡改
## 测试建议
1. 测试单商品下单流程
2. 测试购物车批量下单
3. 测试各种异常情况(库存不足、地址无效等)
4. 测试支付成功和失败的处理
5. 测试订单标题长度限制功能

103
docs/ORDER_IMPROVEMENTS.md Normal file
View File

@@ -0,0 +1,103 @@
# 订单列表功能完善说明
## 完善的功能
### 1. 订单商品正确显示
- **问题**: 原来只显示订单基本信息,没有显示具体的商品信息
- **解决方案**:
- 扩展了订单接口,添加了 `OrderWithGoods` 类型
- 在加载订单列表时,同时获取每个订单的商品信息
- 使用 `listShopOrderGoods` API 获取订单商品详情
- 显示商品图片、名称、规格、数量和价格
### 2. 订单状态正确显示
- **问题**: 原来固定显示"待付款"状态
- **解决方案**:
- 添加了 `getOrderStatusText` 函数,根据订单的 `payStatus``deliveryStatus``orderStatus` 动态显示状态
- 支持的状态包括:待付款、待发货、待收货、已收货、已完成、已取消、退款申请中、退款成功等
### 3. 确认收货功能
- **新增功能**:
- 当订单状态为"待收货"时,显示"确认收货"按钮
- 点击确认收货后,更新订单状态为"已收货"和"已完成"
- 操作成功后显示提示信息并刷新列表
### 4. 取消订单功能
- **新增功能**:
- 当订单状态为"待付款"时,显示"取消订单"按钮
- 点击取消订单后,更新订单状态为"已取消"
- 操作成功后显示提示信息并刷新列表
### 5. 操作按钮优化
- **改进**: 根据订单状态动态显示不同的操作按钮
- 待付款:显示"取消订单"和"立即支付"按钮
- 待收货:显示"确认收货"按钮
- 已完成:显示"申请退款"按钮(预留功能)
### 6. 订单详情页面修复
- **问题**: 订单详情页面使用了错误的API
- **解决方案**:
- 修改为使用正确的 `listShopOrderGoods` API
- 直接显示商品信息,无需额外查询商品详情
- 优化了商品信息的显示格式
## 技术改进
### 1. 类型安全
- 添加了 `OrderWithGoods` 接口扩展
- 完善了 `OrderListProps` 接口定义
- 使用了正确的 TypeScript 类型
### 2. 错误处理
- 添加了完善的错误处理机制
- 操作失败时显示友好的错误提示
- 防止因单个订单商品获取失败而影响整个列表
### 3. 用户体验
- 添加了操作成功的提示信息
- 操作完成后自动刷新列表
- 阻止事件冒泡,避免误触
### 4. 数据一致性
- 操作完成后通知父组件刷新数据
- 确保订单状态的实时更新
## 使用说明
### 订单状态说明
- **待付款**: `payStatus = 0`
- **待发货**: `payStatus = 1 && deliveryStatus = 10`
- **待收货**: `deliveryStatus = 20`
- **已收货**: `deliveryStatus = 30`
- **已完成**: `orderStatus = 1`
- **已取消**: `orderStatus = 2`
### API 依赖
- `pageShopOrder`: 分页查询订单
- `listShopOrderGoods`: 查询订单商品
- `updateShopOrder`: 更新订单状态
### 组件结构
```
src/pages/order/
├── order.tsx # 订单主页面
├── components/
│ └── OrderList.tsx # 订单列表组件
└── test-order.tsx # 测试页面(可选)
```
## 测试建议
1. 创建不同状态的测试订单
2. 验证订单商品信息显示是否正确
3. 测试确认收货功能
4. 测试取消订单功能
5. 验证订单状态切换是否正常
## 后续优化建议
1. 添加订单搜索功能
2. 实现立即支付功能
3. 添加申请退款功能
4. 优化商品图片加载和缓存
5. 添加订单操作的二次确认

275
docs/PAYMENT_ISSUE_FIXED.md Normal file
View File

@@ -0,0 +1,275 @@
# ✅ 优惠券支付问题修复完成
## 🚨 修复的严重问题
### 问题描述
用户选择优惠券后支付失败,但系统仍然提示"支付成功",导致用户误以为支付完成。
### 根本原因
1. **双重成功提示** - OrderConfirm和PaymentHandler都显示成功提示
2. **支付状态验证缺失** - 没有验证实际支付状态
3. **错误处理不完善** - 错误信息不够详细和准确
## 🔧 修复内容
### 1. **修复双重成功提示问题**
#### OrderConfirm.tsx 修改
```typescript
// ❌ 修复前:双重提示
await PaymentHandler.pay(orderData, paymentType);
Taro.showToast({
title: '支付成功', // 第一次提示
icon: 'success'
})
// ✅ 修复后:移除重复提示
await PaymentHandler.pay(orderData, paymentType);
// 移除这里的成功提示让PaymentHandler统一处理
```
#### PaymentHandler 修改
```typescript
// ✅ 只有确认支付成功才显示提示
if (paymentSuccess) {
Taro.showToast({
title: '支付成功',
icon: 'success'
});
// 跳转逻辑
} else {
throw new Error('支付未完成');
}
```
### 2. **完善支付状态验证**
#### 余额支付验证增强
```typescript
// ❌ 修复前:只检查订单号
private static async handleBalancePay(result: any): Promise<void> {
if (!result || !result.orderNo) {
throw new Error('余额支付失败');
}
// 没有验证实际支付状态
}
// ✅ 修复后:完整验证
private static async handleBalancePay(result: any): Promise<boolean> {
// 检查支付状态
if (result.payStatus === false || result.payStatus === 0) {
throw new Error('余额不足或支付失败');
}
// 检查订单状态
if (result.orderStatus !== 1) {
throw new Error('订单状态异常,支付可能未成功');
}
// 验证扣款金额
if (result.payPrice && parseFloat(result.payPrice) <= 0) {
throw new Error('支付金额异常');
}
return true;
}
```
#### 微信支付验证增强
```typescript
// ✅ 增加参数验证和错误处理
private static async handleWechatPay(result: WxPayResult): Promise<void> {
// 验证必要参数
if (!result.timeStamp || !result.nonceStr || !result.package || !result.paySign) {
throw new Error('微信支付参数不完整');
}
try {
await Taro.requestPayment({...});
} catch (payError: any) {
// 处理微信支付特定错误
if (payError.errMsg?.includes('cancel')) {
throw new Error('用户取消支付');
} else if (payError.errMsg?.includes('fail')) {
throw new Error('微信支付失败,请重试');
}
throw new Error('微信支付失败');
}
}
```
### 3. **优化错误处理**
#### 详细错误分类
```typescript
private static getErrorMessage(error: any): string {
const message = error.message;
// 余额相关错误
if (message.includes('余额不足')) {
return '账户余额不足,请充值后重试';
}
// 优惠券相关错误
if (message.includes('优惠券')) {
return '优惠券使用失败,请重新选择';
}
// 库存相关错误
if (message.includes('库存')) {
return '商品库存不足,请减少购买数量';
}
// 其他错误分类...
return message;
}
```
#### 错误处理标记
```typescript
// ✅ 避免重复错误处理
catch (error: any) {
// 标记错误已处理
error.handled = true;
callback?.onError?.(errorMessage);
throw error;
}
// 在OrderConfirm中
catch (error: any) {
// 只处理未被PaymentHandler处理的错误
if (!error.handled) {
Taro.showToast({
title: errorMessage,
icon: 'error'
})
}
}
```
### 4. **优惠券相关修复**
#### 类型安全修复
```typescript
// ❌ 修复前:可能的类型问题
couponId: selectedCoupon ? selectedCoupon.id : undefined
// ✅ 修复后:确保数字类型
couponId: selectedCoupon ? Number(selectedCoupon.id) : undefined
```
#### 支付前验证
```typescript
// ✅ 支付前再次验证优惠券
if (selectedCoupon) {
const total = getGoodsTotal()
if (!isCouponUsable(selectedCoupon, total)) {
const reason = getCouponUnusableReason(selectedCoupon, total)
Taro.showToast({
title: reason || '优惠券不可用',
icon: 'error'
})
return;
}
}
```
### 5. **增强日志记录**
```typescript
// ✅ 关键节点日志
console.log('开始支付:', {
orderData,
paymentType,
selectedCoupon: selectedCoupon ? {
id: selectedCoupon.id,
title: selectedCoupon.title,
discount: getCouponDiscount()
} : null,
finalPrice: getFinalPrice()
});
console.log('订单创建结果:', result);
console.log('支付成功,订单号:', result.orderNo);
```
## 📊 修复效果对比
| 项目 | 修复前 ❌ | 修复后 ✅ |
|------|-----------|-----------|
| **成功提示** | 双重提示,误导用户 | 单一准确提示 |
| **支付验证** | 只检查订单号 | 完整状态验证 |
| **错误处理** | 通用错误信息 | 详细分类提示 |
| **优惠券** | 类型可能错误 | 类型安全处理 |
| **日志记录** | 信息不足 | 完整调试信息 |
| **用户体验** | 困惑和投诉 | 清晰准确反馈 |
## 🧪 测试验证
### 测试场景
- [x] **余额充足 + 优惠券** - 支付成功,显示正确金额
- [x] **余额不足 + 优惠券** - 显示"余额不足"错误
- [x] **微信支付 + 优惠券** - 正常调起微信支付
- [x] **用户取消支付** - 显示"用户取消支付"
- [x] **优惠券失效** - 支付前验证并提示
- [x] **网络异常** - 显示网络错误提示
### 验证要点
- [x] 支付成功时只显示一次成功提示
- [x] 支付失败时显示具体失败原因
- [x] 优惠券折扣正确应用
- [x] 最终扣款金额正确
- [x] 错误不会重复处理
## 🚀 性能优化
### 1. **减少重复操作**
- 移除双重成功提示
- 避免重复错误处理
- 优化日志输出
### 2. **提升用户体验**
- 详细错误分类提示
- 支付前预验证
- 清晰的状态反馈
### 3. **增强稳定性**
- 完整的参数验证
- 健壮的错误处理
- 详细的日志记录
## 🎯 关键改进点
### 🔥 **核心修复**
1.**消除双重提示** - 避免用户误解
2.**完善状态验证** - 确保支付真正成功
3.**优化错误处理** - 提供准确错误信息
### 🔶 **体验提升**
1.**详细错误分类** - 帮助用户理解问题
2.**支付前验证** - 减少支付失败
3.**完整日志记录** - 便于问题排查
### 🔵 **安全增强**
1.**类型安全处理** - 避免数据类型错误
2.**参数完整验证** - 防止支付参数异常
3.**状态一致性** - 确保前后端状态同步
## 🎉 修复总结
通过本次修复:
-**解决了严重的支付逻辑问题** - 消除双重成功提示
-**增强了支付状态验证** - 确保支付真正成功
-**优化了用户体验** - 提供准确清晰的反馈
-**提升了系统稳定性** - 完善错误处理机制
-**增加了调试能力** - 详细的日志记录
**现在支付流程更加可靠,用户不会再收到错误的成功提示!** 🚀
## 📝 后续建议
1. **监控支付成功率** - 观察修复效果
2. **收集用户反馈** - 持续优化体验
3. **完善测试用例** - 覆盖更多场景
4. **定期代码审查** - 防止类似问题

View File

@@ -0,0 +1,264 @@
# 支付逻辑重构指南
## 概述
本文档描述了支付逻辑的重构过程,将原本分散的支付代码统一整合,提高了代码的可维护性和复用性。
## 重构前后对比
### 重构前的问题
1. **代码重复**:每个支付方式都有重复的订单构建逻辑
2. **维护困难**:支付逻辑分散在多个方法中
3. **扩展性差**:添加新的支付方式需要修改多处代码
### 重构后的优势
1. **统一入口**:所有支付都通过 `onPay` 方法处理
2. **代码复用**:订单构建逻辑统一封装
3. **易于扩展**:新增支付方式只需在工具类中添加
4. **错误处理统一**:所有支付的错误处理逻辑一致
## 核心改进
### 1. 统一的支付工具类
**文件:** `src/utils/payment.ts`
```typescript
// 支付类型枚举
export enum PaymentType {
BALANCE = 0, // 余额支付
WECHAT = 1, // 微信支付
ALIPAY = 3, // 支付宝支付
}
// 统一支付处理类
export class PaymentHandler {
static async pay(
orderData: OrderCreateRequest,
paymentType: PaymentType,
callback?: PaymentCallback
): Promise<void> {
// 统一的支付处理逻辑
}
}
```
### 2. 订单数据构建函数
```typescript
// 单商品订单
export function buildSingleGoodsOrder(
goodsId: number,
quantity: number = 1,
addressId?: number,
options?: {
comments?: string;
deliveryType?: number;
couponId?: number;
}
): OrderCreateRequest
// 购物车订单
export function buildCartOrder(
cartItems: Array<{ goodsId: number; quantity: number }>,
addressId?: number,
options?: { ... }
): OrderCreateRequest
```
### 3. 简化的支付入口
**重构前:**
```typescript
const onWxPay = async (goods: ShopGoods) => {
// 校验逻辑
// 构建订单数据
// 调用创建订单API
// 处理微信支付
// 错误处理
}
const onBalancePay = async (goods: ShopGoods) => {
// 重复的校验逻辑
// 重复的订单构建逻辑
// 处理余额支付
// 重复的错误处理
}
const onPay = async (goods: ShopGoods) => {
if (payment?.type == 0) {
await onBalancePay(goods)
}
if (payment?.type == 1) {
await onWxPay(goods)
}
}
```
**重构后:**
```typescript
const onPay = async (goods: ShopGoods) => {
// 基础校验
if (!address || !payment) {
// 错误提示
return;
}
// 构建订单数据
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
1,
address.id,
{ comments: goods.name }
);
// 选择支付类型
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
// 执行支付
await PaymentHandler.pay(orderData, paymentType);
};
```
## 使用示例
### 1. 单商品下单
```typescript
// 在 orderConfirm/index.tsx 中
const onPay = async (goods: ShopGoods) => {
if (!address || !payment) return;
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
1,
address.id,
{
comments: goods.name,
deliveryType: 0
}
);
const paymentType = payment.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
await PaymentHandler.pay(orderData, paymentType);
};
```
### 2. 购物车批量下单
```typescript
// 在 orderConfirmCart/index.tsx 中
const onPay = async () => {
if (!address || !cartItems?.length) return;
const orderData = buildCartOrder(
cartItems.map(item => ({
goodsId: item.goodsId!,
quantity: item.quantity || 1
})),
address.id,
{
comments: '购物车下单',
deliveryType: 0
}
);
const paymentType = payment?.type === 0 ? PaymentType.BALANCE : PaymentType.WECHAT;
await PaymentHandler.pay(orderData, paymentType);
};
```
### 3. 使用优惠券下单
```typescript
const onPayWithCoupon = async (goods: ShopGoods, couponId: number) => {
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
1,
address.id,
{
comments: `使用优惠券购买${goods.name}`,
couponId: couponId
}
);
await PaymentHandler.pay(orderData, PaymentType.WECHAT);
};
```
### 4. 自提订单
```typescript
const onSelfPickupOrder = async (goods: ShopGoods, merchantId: number) => {
const orderData = buildSingleGoodsOrder(
goods.goodsId!,
1,
address.id,
{
comments: `自提订单 - ${goods.name}`,
deliveryType: 1,
selfTakeMerchantId: merchantId
}
);
await PaymentHandler.pay(orderData, PaymentType.WECHAT);
};
```
## 扩展新的支付方式
### 1. 添加支付类型
```typescript
// 在 PaymentType 枚举中添加
export enum PaymentType {
BALANCE = 0,
WECHAT = 1,
ALIPAY = 3,
UNIONPAY = 4, // 新增银联支付
}
```
### 2. 实现支付处理方法
```typescript
// 在 PaymentHandler 类中添加
private static async handleUnionPay(result: any): Promise<void> {
// 银联支付逻辑
}
```
### 3. 在支付分发中添加
```typescript
// 在 PaymentHandler.pay 方法中添加
switch (paymentType) {
case PaymentType.WECHAT:
await this.handleWechatPay(result);
break;
case PaymentType.BALANCE:
await this.handleBalancePay(result);
break;
case PaymentType.UNIONPAY: // 新增
await this.handleUnionPay(result);
break;
// ...
}
```
## 优势总结
1. **代码简洁**:支付入口方法从 50+ 行减少到 20+ 行
2. **逻辑清晰**:订单构建、支付处理、错误处理分离
3. **易于测试**:每个功能模块独立,便于单元测试
4. **维护性强**:修改支付逻辑只需在工具类中修改
5. **扩展性好**:新增支付方式无需修改业务代码
## 注意事项
1. **向后兼容**:确保重构后的接口与原有调用方式兼容
2. **错误处理**:统一的错误处理机制,确保用户体验一致
3. **类型安全**:使用 TypeScript 枚举确保支付类型的类型安全
4. **测试覆盖**:对重构后的代码进行充分的测试

View File

@@ -0,0 +1,229 @@
# 第一阶段优化完成报告
## 🎉 已完成的优化
### 1. ✅ API请求层优化
#### 主要改进
- **完善的错误处理机制**:支持网络错误、超时错误、业务错误、认证错误的分类处理
- **请求/响应拦截器**:自动添加认证头、处理响应数据
- **重试机制**:支持自动重试,递增延迟策略
- **超时处理**:可配置的请求超时时间
- **类型安全**完整的TypeScript类型定义
#### 使用示例
```typescript
import request, { ErrorType, RequestError } from '@/utils/request';
// 基础使用
const userInfo = await request.get<User.Info>('/api/user/info');
// 带参数的GET请求
const goodsList = await request.get<Product.Info[]>('/api/goods', {
categoryId: 1,
page: 1,
limit: 10
});
// POST请求
const result = await request.post<Order.Info>('/api/order/create', {
goods: [{ goodsId: 1, quantity: 2 }],
address: { /* 地址信息 */ }
});
// 自定义配置
const data = await request.get<any>('/api/data', null, {
timeout: 5000,
retry: 3,
showLoading: true,
showError: false
});
// 错误处理
try {
const result = await request.post('/api/action');
} catch (error) {
if (error instanceof RequestError) {
switch (error.type) {
case ErrorType.NETWORK_ERROR:
console.log('网络错误');
break;
case ErrorType.AUTH_ERROR:
console.log('认证失败');
break;
case ErrorType.BUSINESS_ERROR:
console.log('业务错误:', error.message);
break;
}
}
}
```
### 2. ✅ 全局错误处理机制
#### 主要改进
- **错误边界组件**捕获React组件树中的JavaScript错误
- **全局错误处理器**:统一处理各种类型的错误
- **错误分级**支持INFO、WARNING、ERROR、FATAL四个级别
- **错误上报**:支持错误信息收集和上报
- **用户友好提示**:根据错误类型显示合适的提示信息
#### 使用示例
```typescript
// 1. 在应用根组件中使用错误边界
import ErrorBoundary from '@/components/ErrorBoundary';
function App() {
return (
<ErrorBoundary
onError={(error, errorInfo) => {
console.log('捕获到错误:', error, errorInfo);
}}
>
<YourAppContent />
</ErrorBoundary>
);
}
// 2. 使用全局错误处理器
import { handleError, handleFatalError, ErrorLevel } from '@/utils/errorHandler';
// 处理普通错误
try {
// 一些可能出错的代码
} catch (error) {
handleError(error, ErrorLevel.ERROR, 'UserAction');
}
// 处理致命错误
handleFatalError(new Error('应用崩溃'), { userId: '123' });
// 处理警告
handleWarning('数据加载缓慢', { loadTime: 5000 });
```
### 3. ✅ 类型定义完善
#### 主要改进
- **全局基础类型**ID、Timestamp、分页参数等通用类型
- **业务类型定义**:用户、商品、订单、购物车等业务实体类型
- **组件类型定义**各种UI组件的Props类型定义
- **严格的TypeScript配置**启用strict模式提高类型安全
#### 使用示例
```typescript
// 1. 使用业务类型
const user: User.Info = {
userId: '123',
username: 'john',
nickname: 'John Doe',
roles: [
{
roleCode: 'user',
roleName: '普通用户'
}
]
};
// 2. 使用组件类型
const GoodsListComponent: React.FC<ComponentProps.GoodsList> = ({
goods,
loading,
onItemClick,
onAddToCart
}) => {
// 组件实现
};
// 3. 使用API类型
const fetchUserInfo = async (userId: ID): Promise<User.Info> => {
return await request.get<User.Info>(`/api/user/${userId}`);
};
// 4. 使用分页类型
const fetchGoodsList = async (
params: Product.QueryParams
): Promise<BasePaginationResponse<Product.Info>> => {
return await request.get('/api/goods', params);
};
```
## 🔧 如何应用到现有代码
### 1. 更新API调用
将现有的API调用替换为新的request工具
```typescript
// 旧代码
import { request } from '@/utils/request';
const result = await request({ url: '/api/user', method: 'GET' });
// 新代码
import request from '@/utils/request';
const result = await request.get<User.Info>('/api/user');
```
### 2. 添加错误边界
在关键组件外层添加错误边界:
```typescript
// 在页面组件中
import ErrorBoundary from '@/components/ErrorBoundary';
export default function UserPage() {
return (
<ErrorBoundary>
<UserContent />
</ErrorBoundary>
);
}
```
### 3. 使用类型定义
为现有组件添加类型定义:
```typescript
// 旧代码
function UserCard({ user, onClick }) {
// 组件实现
}
// 新代码
interface UserCardProps {
user: User.Info;
onClick: (user: User.Info) => void;
}
function UserCard({ user, onClick }: UserCardProps) {
// 组件实现
}
```
## 📊 改进效果
### 代码质量提升
- ✅ 类型安全性大幅提升
- ✅ 错误处理更加完善
- ✅ 代码可维护性增强
- ✅ 开发体验改善
### 用户体验提升
- ✅ 更友好的错误提示
- ✅ 更稳定的应用运行
- ✅ 更快的错误恢复
- ✅ 更好的离线处理
### 开发效率提升
- ✅ 更好的IDE支持
- ✅ 更早的错误发现
- ✅ 更清晰的代码结构
- ✅ 更容易的代码重构
## 🚀 下一步计划
第一阶段优化已完成,建议继续进行:
1. **第二阶段**:性能优化、用户体验优化、状态管理优化
2. **第三阶段**:测试覆盖、代码规范工具、安全性增强、文档完善
你可以选择继续进行第二阶段的优化,或者先在项目中应用这些改进并测试效果。

View File

@@ -0,0 +1,214 @@
# 微信小程序扫码登录集成文档
## 概述
本文档介绍如何在微信小程序中集成扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。
## 功能特性
-**多平台支持** - 支持网页端、移动APP、微信小程序
-**安全可靠** - Token有效期控制防重复使用
-**用户体验好** - 5分钟有效期实时状态反馈
-**微信集成** - 自动获取微信用户信息
-**组件化设计** - 提供多种使用方式
## 后端接口
### 1. 生成扫码token
```
POST /api/qr-login/generate
```
### 2. 检查登录状态
```
GET /api/qr-login/status/{token}
```
### 3. 确认登录(通用)
```
POST /api/qr-login/confirm
```
### 4. 微信小程序专用确认接口
```
POST /api/qr-login/wechat-confirm
```
## 前端集成
### 1. API接口层
文件:`src/api/qr-login/index.ts`
提供了完整的扫码登录API接口封装
- `generateQRToken()` - 生成扫码token
- `checkQRLoginStatus()` - 检查登录状态
- `confirmQRLogin()` - 确认登录
- `confirmWechatQRLogin()` - 微信小程序专用确认
### 2. Hook层
文件:`src/hooks/useQRLogin.ts`
提供了扫码登录的状态管理和业务逻辑:
```typescript
const {
state, // 当前状态
error, // 错误信息
result, // 登录结果
isLoading, // 是否加载中
startScan, // 开始扫码
cancel, // 取消扫码
reset, // 重置状态
handleScanResult, // 处理扫码结果
canScan // 是否可以扫码
} = useQRLogin();
```
### 3. 组件层
#### QRLoginScanner 完整扫码组件
文件:`src/components/QRLoginScanner.tsx`
功能完整的扫码登录组件,包含状态显示和错误处理:
```tsx
<QRLoginScanner
onSuccess={(result) => console.log('登录成功', result)}
onError={(error) => console.log('登录失败', error)}
buttonText="扫码登录"
showStatus={true}
/>
```
#### QRLoginButton 简化按钮组件
文件:`src/components/QRLoginButton.tsx`
简化的按钮组件,支持两种模式:
```tsx
{/* 直接扫码模式 */}
<QRLoginButton
text="扫码登录"
onSuccess={handleSuccess}
onError={handleError}
/>
{/* 页面跳转模式 */}
<QRLoginButton
text="扫码登录"
usePageMode={true}
/>
```
### 4. 页面层
文件:`src/passport/qr-login/index.tsx`
专门的扫码登录页面,提供完整的用户体验:
- 用户信息展示
- 扫码功能
- 使用说明
- 登录历史
- 安全提示
## 使用方式
### 方式一:在现有组件中集成
```tsx
import { useQRLogin } from '@/hooks/useQRLogin';
const MyComponent = () => {
const { startScan, isLoading } = useQRLogin();
const handleScan = async () => {
try {
await startScan();
} catch (error) {
console.error('扫码失败:', error);
}
};
return (
<Button loading={isLoading} onClick={handleScan}>
扫码登录
</Button>
);
};
```
### 方式二:使用预制组件
```tsx
import QRLoginButton from '@/components/QRLoginButton';
const MyComponent = () => {
return (
<QRLoginButton
text="扫码登录"
usePageMode={true}
/>
);
};
```
### 方式三:跳转到专门页面
```tsx
import Taro from '@tarojs/taro';
const handleQRLogin = () => {
Taro.navigateTo({
url: '/passport/qr-login/index'
});
};
```
## 工作流程
### 网页端流程
1. 用户访问登录页面
2. 点击"扫码登录"按钮
3. 调用 `POST /api/qr-login/generate` 生成token
4. 显示包含token的二维码
5. 每2秒调用 `GET /api/qr-login/status/{token}` 检查状态
6. 状态变为 `confirmed` 时获取JWT token
7. 自动跳转到主页面
### 小程序端流程
1. 用户在小程序中点击扫码登录
2. 调用 `Taro.scanCode()` 扫描二维码
3. 解析二维码获取token
4. 调用 `POST /api/qr-login/wechat-confirm` 确认登录
5. 传递用户ID和微信用户信息
6. 显示登录确认成功提示
## 安全考虑
1. **Token有效期** - 默认5分钟有效期防止长期暴露
2. **一次性使用** - Token确认后立即失效防止重复使用
3. **用户确认** - 需要用户主动确认,防止误操作
4. **来源验证** - 只扫描官方网站的二维码
5. **权限检查** - 确保用户已登录小程序
## 错误处理
常见错误及处理方式:
1. **用户未登录** - 提示用户先登录小程序
2. **扫码失败** - 提示重新扫码或检查二维码
3. **Token无效** - 提示二维码已过期,请刷新
4. **网络错误** - 提示检查网络连接
5. **权限拒绝** - 引导用户开启相机权限
## 测试建议
1. **功能测试** - 测试完整的扫码登录流程
2. **异常测试** - 测试各种异常情况的处理
3. **性能测试** - 测试扫码响应速度和网络请求
4. **兼容性测试** - 测试不同设备和微信版本
5. **安全测试** - 测试Token安全性和权限控制
## 注意事项
1. 确保后端接口已正确实现
2. 配置正确的API基础URL
3. 处理好用户权限和登录状态
4. 提供清晰的用户提示和错误信息
5. 考虑网络异常和超时情况

284
docs/QR_LOGIN_USAGE.md Normal file
View File

@@ -0,0 +1,284 @@
# 微信小程序扫码登录使用指南
## 概述
本项目已完整实现微信小程序扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。
## 功能特性
-**多种集成方式** - 按钮组件、弹窗组件、专门页面
-**自动解析二维码** - 支持多种二维码格式URL、JSON、纯token
-**安全可靠** - Token有效期控制防重复使用
-**用户体验好** - 实时状态反馈,错误处理完善
-**微信集成** - 自动获取微信用户信息
## 后端接口
### 1. 生成扫码token
```http
POST /api/qr-login/generate
```
### 2. 检查登录状态
```http
GET /api/qr-login/status/{token}
```
### 3. 确认登录
```http
POST /api/qr-login/confirm
```
### 4. 扫码操作(可选)
```http
POST /api/qr-login/scan/{token}
```
## 前端使用方式
### 方式1: 直接扫码按钮
最简单的使用方式点击按钮直接调用扫码API
```tsx
import QRLoginButton from '@/components/QRLoginButton';
// 基础使用
<QRLoginButton />
// 自定义配置
<QRLoginButton
text="扫码登录"
type="primary"
size="large"
onSuccess={(result) => console.log('登录成功', result)}
onError={(error) => console.log('登录失败', error)}
/>
```
### 方式2: 弹窗扫码
在当前页面弹出扫码弹窗:
```tsx
import { useState } from 'react';
import QRScanModal from '@/components/QRScanModal';
const MyComponent = () => {
const [showScan, setShowScan] = useState(false);
return (
<>
<Button onClick={() => setShowScan(true)}>
扫码登录
</Button>
<QRScanModal
visible={showScan}
onClose={() => setShowScan(false)}
onSuccess={(result) => {
console.log('登录成功', result);
setShowScan(false);
}}
onError={(error) => {
console.log('登录失败', error);
}}
/>
</>
);
};
```
### 方式3: 跳转到专门页面
跳转到专门的扫码登录页面:
```tsx
import QRLoginButton from '@/components/QRLoginButton';
// 使用页面模式
<QRLoginButton
text="进入扫码页面"
usePageMode={true}
/>
// 或者自定义跳转
<Button onClick={() => {
Taro.navigateTo({
url: '/passport/qr-login/index'
});
}}>
扫码登录
</Button>
```
### 方式4: 使用Hook
直接使用Hook进行更灵活的控制
```tsx
import { useQRLogin } from '@/hooks/useQRLogin';
const MyComponent = () => {
const {
startScan,
isLoading,
isSuccess,
isError,
error,
result,
canScan
} = useQRLogin();
return (
<Button
loading={isLoading}
disabled={!canScan()}
onClick={startScan}
>
{isLoading ? '扫码中...' : '扫码登录'}
</Button>
);
};
```
## 二维码格式支持
系统支持多种二维码格式:
### 1. URL格式
```
https://mp.websoft.top/qr-confirm?qrCodeKey=02278c578d3e4aad87dece6aab2f0296
https://example.com/login?token=abc123
```
### 2. JSON格式
```json
{
"token": "02278c578d3e4aad87dece6aab2f0296",
"type": "qr-login"
}
```
### 3. 简单格式
```
qr-login:02278c578d3e4aad87dece6aab2f0296
02278c578d3e4aad87dece6aab2f0296
```
## 页面配置
确保在 `app.config.ts` 中添加了相关页面:
```typescript
export default {
pages: [
// ... 其他页面
'passport/qr-login/index', // 扫码登录页面
'passport/qr-confirm/index', // 登录确认页面
],
// ...
}
```
## 使用示例
### 完整示例组件
```tsx
import React, { useState } from 'react';
import { View } from '@tarojs/components';
import { Button } from '@nutui/nutui-react-taro';
import QRLoginButton from '@/components/QRLoginButton';
import QRScanModal from '@/components/QRScanModal';
const LoginPage = () => {
const [showModal, setShowModal] = useState(false);
const handleSuccess = (result) => {
console.log('登录成功:', result);
// 处理登录成功逻辑
};
const handleError = (error) => {
console.error('登录失败:', error);
// 处理登录失败逻辑
};
return (
<View className="p-4">
{/* 方式1: 直接扫码 */}
<QRLoginButton
text="扫码登录"
onSuccess={handleSuccess}
onError={handleError}
/>
{/* 方式2: 弹窗扫码 */}
<Button onClick={() => setShowModal(true)}>
弹窗扫码
</Button>
{/* 方式3: 跳转页面 */}
<QRLoginButton
text="进入扫码页面"
usePageMode={true}
/>
<QRScanModal
visible={showModal}
onClose={() => setShowModal(false)}
onSuccess={handleSuccess}
onError={handleError}
/>
</View>
);
};
```
## 注意事项
1. **用户登录状态**: 使用扫码登录功能前,用户必须已在小程序中登录
2. **权限申请**: 确保小程序已申请摄像头权限
3. **网络环境**: 确保网络连接正常API接口可访问
4. **二维码有效期**: 二维码有5分钟有效期过期需重新生成
5. **安全性**: 只扫描来自官方网站的登录二维码
## 错误处理
常见错误及解决方案:
- `请先登录小程序`: 用户未登录,需要先完成小程序登录
- `无效的登录二维码`: 二维码格式不正确或已过期
- `登录确认失败`: 网络问题或服务器错误,可重试
- `扫码失败`: 摄像头权限问题或二维码不清晰
## API接口详情
详细的API接口文档请参考`src/api/qr-login/index.ts`
## 组件API
### QRLoginButton Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| type | string | 'primary' | 按钮类型 |
| size | string | 'normal' | 按钮大小 |
| text | string | '扫码登录' | 按钮文本 |
| showIcon | boolean | true | 是否显示图标 |
| usePageMode | boolean | false | 是否使用页面模式 |
| onSuccess | function | - | 成功回调 |
| onError | function | - | 失败回调 |
### QRScanModal Props
| 属性 | 类型 | 默认值 | 说明 |
|------|------|--------|------|
| visible | boolean | false | 是否显示弹窗 |
| title | string | '扫描登录二维码' | 弹窗标题 |
| description | string | '扫描网页端显示的登录二维码' | 描述文本 |
| autoConfirm | boolean | true | 是否自动确认登录 |
| onClose | function | - | 关闭回调 |
| onSuccess | function | - | 成功回调 |
| onError | function | - | 失败回调 |

View File

@@ -0,0 +1,76 @@
# Request 系统清理总结
## 🎯 清理目标
完全移除 `request-legacy.ts` 及其相关文件,统一使用新的 `request.ts`
## 🗑️ 已删除的文件
### 核心文件
-`src/utils/request-legacy.ts` - 旧版兼容层
### 脚本文件
-`scripts/fix-all-api-imports.sh` - API导入修复脚本
-`scripts/update-api-imports.js` - API导入更新脚本
### 文档文件
-`docs/API_IMPORT_FIX_SUMMARY.md` - API导入修复总结
-`docs/FINAL_FIX_REPORT.md` - 最终修复报告
-`docs/TYPESCRIPT_ERROR_FIXES.md` - TypeScript错误修复指南
-`docs/RUNTIME_ERROR_FIX.md` - 运行时错误修复报告
## ✅ 保留的文件
### 核心文件
-`src/utils/request.ts` - 新版统一请求工具
### 文档文件
-`docs/REQUEST_USAGE.md` - 请求工具使用说明
## 🔧 代码清理
### API文件统一
- 所有API文件现在都使用 `import request from '@/utils/request'`
- 移除了所有 `request-legacy` 的引用
- 保持了原有的API调用方式`res.code``res.data``res.message`
### 调试信息优化
- 简化了开发环境的调试信息
- 移除了冗余的日志输出
- 保留了关键的错误信息
## 🚀 最终状态
### 统一的请求系统
现在项目只有一个请求工具:`src/utils/request.ts`
### 完整的功能支持
- ✅ 自动错误处理和提示
- ✅ 网络错误、超时错误、业务错误处理
- ✅ 认证错误自动跳转
- ✅ 请求重试机制
- ✅ 加载状态管理
### API调用方式
```typescript
// 标准方式返回完整ApiResult
const res = await request.get<ApiResult<User>>('/api/user');
if (res.code === 0) {
return res.data;
}
// 便捷方式自动提取data
const user = await request.getData<User>('/api/user');
```
## 📋 验证清单
- [x] 删除所有 `request-legacy` 相关文件
- [x] 确认没有代码引用已删除的文件
- [x] 构建测试通过
- [x] 错误处理正常工作
- [x] API调用正常工作
## 🎉 清理完成
项目现在使用统一的 `request.ts` 系统,代码更加简洁,维护更加容易!

83
docs/REQUEST_USAGE.md Normal file
View File

@@ -0,0 +1,83 @@
# Request 工具使用说明
## 概述
`src/utils/request.ts` 已经适配了后台生成的 API 代码格式,支持两种使用方式:
## 1. 标准方式(适配后台生成代码)
后台生成的代码使用 `request.get<ApiResult>` 格式,返回完整的 `ApiResult` 响应:
```typescript
// 后台生成的代码格式
export async function pageCmsNavigation(params: CmsNavigationParam) {
const res = await request.get<ApiResult<PageResult<CmsNavigation>>>(
'/cms/cms-navigation/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
```
## 2. 便捷方式(自动提取数据)
如果你想要自动提取 `data` 字段,可以使用 `getData` 系列方法:
```typescript
import request from '@/utils/request';
// 自动提取 data 字段
const data = await request.getData<CmsNavigation[]>('/cms/cms-navigation');
// 等价于
const res = await request.get<ApiResult<CmsNavigation[]>>('/cms/cms-navigation');
const data = res.data;
```
## 可用方法
### 标准方法(返回完整 ApiResult
- `request.get<T>(url, params?, config?)`
- `request.post<T>(url, data?, config?)`
- `request.put<T>(url, data?, config?)`
- `request.del<T>(url, data?, config?)`
- `request.patch<T>(url, data?, config?)`
### 便捷方法(自动提取 data
- `request.getData<T>(url, params?, config?)`
- `request.postData<T>(url, data?, config?)`
- `request.putData<T>(url, data?, config?)`
- `request.delData<T>(url, data?, config?)`
## 配置选项
```typescript
interface RequestConfig {
url: string;
method?: 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';
data?: any;
header?: Record<string, string>;
timeout?: number;
retry?: number;
showLoading?: boolean;
showError?: boolean;
returnRaw?: boolean; // 内部使用,控制返回格式
}
```
## 错误处理
所有请求都会自动处理:
- 网络错误
- 超时错误
- 认证错误(自动清除登录信息并跳转)
- 业务错误(显示错误提示)
## 注意事项
1. **后台生成的代码无需修改**,直接使用 `request.get<ApiResult>` 格式
2. **新写的代码**可以选择使用 `getData` 系列方法来简化代码
3. 所有方法都支持自动重试、错误处理、认证等功能

View File

@@ -0,0 +1,287 @@
# 🎉 useShopInfo Hook 创建完成!
## ✅ 已完成的工作
我已经为`getShopInfo()`接口创建了一个完整的React Hook方便全站使用。
### 📁 创建的文件
1. **`src/hooks/useShopInfo.ts`** - 主要的Hook实现
2. **`docs/USE_SHOP_INFO_HOOK.md`** - 详细的使用指南
3. **`src/pages/index/HeaderWithHook.tsx`** - 使用Hook的Header组件示例
4. **`src/pages/index/IndexWithHook.tsx`** - 使用Hook的首页组件示例
## 🚀 主要特性
### ✨ **智能缓存系统**
- 🕐 **30分钟缓存**:减少不必要的网络请求
- 🔄 **自动过期**:缓存过期时自动刷新
- 📱 **离线支持**:网络失败时使用缓存数据
- 🧹 **缓存管理**:提供清除和强制刷新功能
### 🛠️ **丰富的工具方法**
```typescript
const {
// 基础信息
getWebsiteName, // 网站名称
getWebsiteLogo, // 网站Logo
getDarkLogo, // 深色Logo
getDomain, // 域名
// 联系信息
getPhone, // 电话
getEmail, // 邮箱
getAddress, // 地址
getIcpNo, // 备案号
// 高级功能
getStatus, // 网站状态
getConfig, // 网站配置
getNavigation, // 导航菜单
isSearchEnabled, // 搜索功能
getVersionInfo // 版本信息
} = useShopInfo();
```
### 🎯 **TypeScript支持**
- 完整的类型定义
- 智能代码提示
- 编译时错误检查
## 📊 使用对比
### 修改前 ❌
```typescript
const [config, setConfig] = useState<CmsWebsite>();
const [loading, setLoading] = useState(true);
useEffect(() => {
getShopInfo().then((data) => {
setConfig(data);
setLoading(false);
}).catch((error) => {
console.error('获取失败:', error);
setLoading(false);
});
}, []);
// 使用时需要检查
const websiteName = config?.websiteName || '商城';
const websiteLogo = config?.websiteLogo || '';
```
### 修改后 ✅
```typescript
const {
shopInfo,
loading,
error,
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
// 直接使用,内置默认值
const websiteName = getWebsiteName(); // 自动返回 '商城' 如果为空
const websiteLogo = getWebsiteLogo(); // 自动处理空值
```
## 🔄 迁移步骤
### 1. **导入新Hook**
```typescript
import { useShopInfo } from '@/hooks/useShopInfo';
```
### 2. **替换状态管理**
```typescript
// 删除旧代码
const [config, setConfig] = useState<CmsWebsite>();
// 使用新Hook
const { shopInfo: config, loading, error } = useShopInfo();
```
### 3. **删除手动API调用**
```typescript
// 删除这些代码
useEffect(() => {
getShopInfo().then((data) => {
setConfig(data);
});
}, []);
```
### 4. **使用工具方法**
```typescript
// 推荐使用工具方法
const websiteName = getWebsiteName();
const websiteLogo = getWebsiteLogo();
```
## 🎯 实际应用场景
### 1. **页面标题设置**
```typescript
const { getWebsiteName } = useShopInfo();
useEffect(() => {
Taro.setNavigationBarTitle({
title: getWebsiteName()
});
}, [getWebsiteName]);
```
### 2. **分享配置**
```typescript
const { getWebsiteName, getWebsiteLogo } = useShopInfo();
useShareAppMessage(() => ({
title: `精选商品 - ${getWebsiteName()}`,
imageUrl: getWebsiteLogo()
}));
```
### 3. **头部组件**
```typescript
const { getWebsiteName, getWebsiteLogo, loading } = useShopInfo();
return (
<NavBar
left={
<div style={{display: 'flex', alignItems: 'center'}}>
<Avatar src={getWebsiteLogo()} />
<span>{getWebsiteName()}</span>
</div>
}
/>
);
```
### 4. **联系页面**
```typescript
const { getPhone, getEmail, getAddress } = useShopInfo();
return (
<div>
<div>电话: {getPhone()}</div>
<div>邮箱: {getEmail()}</div>
<div>地址: {getAddress()}</div>
</div>
);
```
## 🔧 高级功能
### **缓存控制**
```typescript
const { refreshShopInfo, clearCache } = useShopInfo();
// 强制刷新
await refreshShopInfo();
// 清除缓存
clearCache();
```
### **错误处理**
```typescript
const { shopInfo, loading, error, refreshShopInfo } = useShopInfo();
if (error) {
return (
<div>
<div>加载失败: {error}</div>
<button onClick={refreshShopInfo}>重试</button>
</div>
);
}
```
### **条件渲染**
```typescript
const { shopInfo, loading } = useShopInfo();
if (loading) {
return <Skeleton />;
}
return (
<div>
{shopInfo && (
<img src={shopInfo.websiteLogo} alt="Logo" />
)}
</div>
);
```
## 📈 性能优势
### **减少重复请求**
- ✅ 多个组件共享同一份数据
- ✅ 智能缓存避免重复网络请求
- ✅ 自动管理加载状态
### **内存优化**
- ✅ 使用useCallback避免不必要的重渲染
- ✅ 合理的缓存策略
- ✅ 自动清理过期数据
### **用户体验**
- ✅ 离线时使用缓存数据
- ✅ 加载状态管理
- ✅ 错误处理和重试机制
## 🧪 测试建议
### **功能测试**
```typescript
const TestComponent = () => {
const {
shopInfo,
loading,
error,
getWebsiteName,
refreshShopInfo
} = useShopInfo();
return (
<div>
<div>加载状态: {loading ? '加载中' : '已完成'}</div>
<div>错误信息: {error || '无'}</div>
<div>网站名称: {getWebsiteName()}</div>
<button onClick={refreshShopInfo}>刷新数据</button>
</div>
);
};
```
## 🎉 总结
通过创建`useShopInfo` Hook我们实现了
-**统一的商店信息管理**
-**智能缓存机制**
-**丰富的工具方法**
-**完整的TypeScript支持**
-**简单的迁移路径**
-**优秀的用户体验**
现在你可以在整个应用中轻松使用商店信息无需担心重复请求、缓存管理或错误处理。Hook会自动处理这些复杂的逻辑让你专注于业务功能的实现。
**开始使用:**
```typescript
import { useShopInfo } from '@/hooks/useShopInfo';
const MyComponent = () => {
const { getWebsiteName, getWebsiteLogo } = useShopInfo();
return (
<div>
<img src={getWebsiteLogo()} alt="Logo" />
<h1>{getWebsiteName()}</h1>
</div>
);
};
```
🚀 **现在就开始使用这个强大的Hook吧**

View File

@@ -0,0 +1,319 @@
# 🔍 SpecSelector规格选择组件深度分析
## 📋 组件概述
`SpecSelector`是商品规格选择组件,用于处理多规格商品的选择逻辑。通过分析代码发现了多个关键问题需要改进。
## 🚨 **发现的主要问题**
### 1. **核心功能缺失 - 规格选择逻辑未实现**
#### 问题描述
```typescript
// ❌ 关键函数被注释掉了
// const handleSpecSelect = (specName: string, specValue: string) => {
// setSelectedSpecs(prev => ({
// ...prev,
// [specName]: specValue
// }));
// };
```
#### 影响
- **无法选择规格**:用户无法点击选择具体的规格值
- **SKU匹配失效**无法根据选择的规格找到对应的SKU
- **价格库存不更新**:选择规格后价格和库存不会动态更新
### 2. **规格可用性检查缺失**
#### 问题描述
```typescript
// ❌ 规格可用性检查函数被注释
// const isSpecValueAvailable = (specName: string, specValue: string) => {
// // 检查规格值是否有库存、是否可选
// };
```
#### 影响
- **无库存规格仍可选**:用户可能选择无库存的规格组合
- **用户体验差**:无法提前知道哪些规格组合不可用
- **订单失败风险**:可能生成无效订单
### 3. **UI与数据逻辑分离**
#### 问题描述
```typescript
// ❌ 硬编码的UI与动态数据不匹配
<Radio.Group defaultValue="1" direction="horizontal">
<Radio shape="button" value="1">选项1</Radio>
<Radio shape="button" value="2">选项2</Radio>
<Radio shape="button" value="3">选项3</Radio>
</Radio.Group>
```
#### 影响
- **静态选项**:显示固定的"选项1、选项2、选项3"
- **数据不匹配**:与实际的`specs`数据完全脱节
- **功能失效**:选择操作不会影响实际状态
### 4. **数量选择功能缺失**
#### 问题描述
```typescript
// ❌ 没有数量选择器
const [quantity, setQuantity] = useState(1); // 状态存在但无UI
```
#### 影响
- **无法调整数量**用户只能购买1个商品
- **批量购买不支持**:无法满足批量购买需求
### 5. **错误处理和验证不足**
#### 问题描述
```typescript
// ❌ 确认按钮没有充分验证
const handleConfirm = () => {
if (!selectedSku) {
return; // 静默失败,用户不知道为什么无法确认
}
onConfirm(selectedSku, quantity, action);
};
```
#### 影响
- **静默失败**:用户不知道为什么无法确认
- **缺少提示**:没有错误提示和引导
- **体验差**:用户困惑为什么按钮无响应
## 📊 **数据流分析**
### 当前数据流(有问题)
```
specs数据 → specGroups ❌ UI显示硬编码选项
selectedSpecs ❌ 永远为空对象
selectedSku ❌ 永远为null
确认按钮 ❌ 永远被禁用
```
### 期望数据流(正确)
```
specs数据 → specGroups → 动态生成UI选项
用户选择 → selectedSpecs更新 → SKU匹配
SKU匹配 → selectedSku更新 → 价格库存更新
用户确认 → 验证通过 → 回调执行
```
## 🔧 **需要实现的核心功能**
### 1. **动态规格选择器**
```typescript
// 需要实现
const renderSpecOptions = () => {
return specGroups.map(group => (
<Cell key={group.specName}>
<Space direction="vertical">
<View className="title">{group.specName}</View>
<Radio.Group
value={selectedSpecs[group.specName]}
onChange={(value) => handleSpecSelect(group.specName, value)}
>
{group.values.map(value => (
<Radio
key={value}
shape="button"
value={value}
disabled={!isSpecValueAvailable(group.specName, value)}
>
{value}
</Radio>
))}
</Radio.Group>
</Space>
</Cell>
));
};
```
### 2. **规格选择处理**
```typescript
const handleSpecSelect = (specName: string, specValue: string) => {
setSelectedSpecs(prev => ({
...prev,
[specName]: specValue
}));
};
```
### 3. **规格可用性检查**
```typescript
const isSpecValueAvailable = (specName: string, specValue: string) => {
const testSpecs = { ...selectedSpecs, [specName]: specValue };
// 如果还有其他规格未选择,则认为可选
if (Object.keys(testSpecs).length < specGroups.length) {
return true;
}
// 构建规格值字符串并查找SKU
const sortedSpecNames = specGroups.map(g => g.specName).sort();
const specValues = sortedSpecNames.map(name => testSpecs[name]).join('|');
const sku = skus.find(s => s.sku === specValues);
return sku && sku.stock && sku.stock > 0 && sku.status === 0;
};
```
### 4. **数量选择器**
```typescript
const renderQuantitySelector = () => (
<Cell>
<Space direction="vertical">
<View className="title">数量</View>
<InputNumber
value={quantity}
min={1}
max={selectedSku?.stock || 999}
onChange={setQuantity}
/>
</Space>
</Cell>
);
```
### 5. **完善的确认逻辑**
```typescript
const handleConfirm = () => {
// 验证规格选择
if (Object.keys(selectedSpecs).length < specGroups.length) {
Taro.showToast({
title: '请选择完整规格',
icon: 'none'
});
return;
}
// 验证SKU
if (!selectedSku) {
Taro.showToast({
title: '所选规格暂无库存',
icon: 'none'
});
return;
}
// 验证库存
if (!selectedSku.stock || selectedSku.stock < quantity) {
Taro.showToast({
title: '库存不足',
icon: 'none'
});
return;
}
onConfirm(selectedSku, quantity, action);
};
```
## 🎯 **SKU匹配逻辑分析**
### 当前实现问题
```typescript
// ❌ 问题:规格值排序可能不一致
const specValues = sortedSpecNames.map(name => selectedSpecs[name]).join('|');
const sku = skus.find(s => s.sku === specValues);
```
### 改进建议
```typescript
// ✅ 更健壮的SKU匹配
const findMatchingSku = (specs: Record<string, string>) => {
return skus.find(sku => {
if (!sku.sku) return false;
const skuSpecs = sku.sku.split('|');
const selectedValues = Object.values(specs);
// 检查是否所有选中的规格值都在SKU中
return selectedValues.every(value => skuSpecs.includes(value)) &&
selectedValues.length === skuSpecs.length;
});
};
```
## 🚀 **性能优化建议**
### 1. **规格数据预处理**
```typescript
// 在组件外部或useMemo中预处理
const processedSpecs = useMemo(() => {
// 预处理规格数据,建立索引
const specIndex = new Map();
specs.forEach(spec => {
// 建立规格名称到值的映射
});
return specIndex;
}, [specs]);
```
### 2. **SKU查找优化**
```typescript
// 使用Map提高查找效率
const skuMap = useMemo(() => {
const map = new Map();
skus.forEach(sku => {
if (sku.sku) {
map.set(sku.sku, sku);
}
});
return map;
}, [skus]);
```
## 🧪 **测试场景**
### 1. **基础功能测试**
- [ ] 规格选项正确显示
- [ ] 规格选择状态更新
- [ ] SKU匹配正确
- [ ] 价格库存动态更新
### 2. **边界情况测试**
- [ ] 无库存规格处理
- [ ] 单规格商品处理
- [ ] 数据为空的处理
- [ ] 网络错误处理
### 3. **用户体验测试**
- [ ] 选择流程顺畅
- [ ] 错误提示清晰
- [ ] 加载状态友好
- [ ] 响应速度快
## 📈 **改进优先级**
### 🔥 **高优先级(必须修复)**
1. **实现规格选择逻辑** - 核心功能
2. **修复UI与数据绑定** - 基础可用性
3. **添加数量选择器** - 基本需求
4. **完善确认验证** - 防止错误
### 🔶 **中优先级(重要改进)**
1. **规格可用性检查** - 用户体验
2. **错误处理优化** - 稳定性
3. **性能优化** - 响应速度
### 🔵 **低优先级(锦上添花)**
1. **动画效果** - 视觉体验
2. **高级功能** - 额外特性
## 🎉 **总结**
SpecSelector组件目前存在**严重的功能缺失**
-**核心功能未实现**:规格选择逻辑被注释
-**UI与数据脱节**:显示硬编码选项
-**用户体验差**:无法正常使用
-**缺少验证**:错误处理不足
**建议立即进行重构**,实现完整的规格选择功能,确保组件可用性和用户体验。

View File

@@ -0,0 +1,236 @@
# 🚨 Tabs组件onChange类型错误修复
## 问题描述
TypeScript错误
```
TS2322: Type '(value: string) => void' is not assignable to type '(index: string | number) => void'
```
错误位置:`src/user/gift/index.tsx` Tabs组件的`onChange`属性
## 🔍 问题分析
### 错误原因
Tabs组件的`onChange`回调函数期望接收`string | number`类型的参数,但我们定义的`handleTabChange`函数只接受`string`类型:
```typescript
// 错误的类型定义 ❌
const handleTabChange = (value: string) => {
setActiveTab(value) // activeTab也是string类型
}
// Tabs组件期望的类型 ✅
onChange: (index: string | number) => void
```
### 类型不匹配链
1. `activeTab` 状态定义为 `string`
2. `handleTabChange` 参数定义为 `string`
3. `Tabs.onChange` 期望 `string | number`
4. 导致类型不兼容
## 🔧 修复内容
### 1. 更新状态类型定义
#### 修复前 ❌
```typescript
const [activeTab, setActiveTab] = useState('0') // string类型
```
#### 修复后 ✅
```typescript
const [activeTab, setActiveTab] = useState<string | number>('0') // string | number类型
```
### 2. 更新事件处理函数
#### 修复前 ❌
```typescript
const handleTabChange = (value: string) => {
// @ts-ignore
setActiveTab(value)
// ...
}
```
#### 修复后 ✅
```typescript
const handleTabChange = (value: string | number) => {
setActiveTab(value) // 不再需要@ts-ignore
// ...
}
```
### 3. 更新相关函数
#### 修复前 ❌
```typescript
const getStatusFilter = () => {
switch (activeTab) { // activeTab可能是number类型
case '0': // 只匹配字符串
return { useStatus: 0 }
// ...
}
}
```
#### 修复后 ✅
```typescript
const getStatusFilter = () => {
switch (String(activeTab)) { // 确保转换为字符串进行比较
case '0':
return { useStatus: 0 }
// ...
}
}
```
## 📊 修复对比表
| 组件/函数 | 修复前 | 修复后 | 说明 |
|-----------|--------|--------|------|
| `activeTab` 状态 | `string` | `string \| number` | 支持Tabs组件的类型要求 |
| `handleTabChange` | `(value: string)` | `(value: string \| number)` | 匹配onChange回调类型 |
| `getStatusFilter` | `switch (activeTab)` | `switch (String(activeTab))` | 确保字符串比较 |
| TypeScript注释 | `// @ts-ignore` | 移除 | 不再需要忽略类型检查 |
## ✅ 修复效果
### 修复前
```
❌ TS2322: Type '(value: string) => void' is not assignable to type '(index: string | number) => void'
❌ 需要@ts-ignore忽略类型检查
❌ 类型不安全
❌ IDE显示红色错误
```
### 修复后
```
✅ 类型检查通过
✅ 移除了@ts-ignore注释
✅ 类型安全
✅ 没有TypeScript错误
```
## 🔍 相关代码上下文
### Tabs组件使用
```typescript
<Tabs value={activeTab} onChange={handleTabChange}>
<TabPane title="可用" value="0">
</TabPane>
<TabPane title="已使用" value="1">
</TabPane>
<TabPane title="已过期" value="2">
</TabPane>
</Tabs>
```
### 完整的handleTabChange函数
```typescript
const handleTabChange = (value: string | number) => {
setActiveTab(value)
setPage(1)
setList([])
setHasMore(true)
// 延迟执行reload确保状态更新完成
setTimeout(() => {
reload(true)
}, 100)
}
```
## 🎯 最佳实践
### 1. **组件类型兼容性**
- 确保事件处理函数的参数类型与组件期望的类型匹配
- 避免使用`@ts-ignore`,而是修复根本的类型问题
### 2. **状态类型设计**
```typescript
// 推荐:根据使用场景定义合适的类型
const [activeTab, setActiveTab] = useState<string | number>('0')
// 或者更具体的类型
type TabValue = '0' | '1' | '2' | 0 | 1 | 2
const [activeTab, setActiveTab] = useState<TabValue>('0')
```
### 3. **类型转换处理**
```typescript
// 当需要字符串比较时,显式转换
switch (String(activeTab)) {
case '0':
// 处理逻辑
break
}
// 当需要数字比较时,显式转换
if (Number(activeTab) === 0) {
// 处理逻辑
}
```
## 🧪 验证方法
### 1. **IDE验证**
- 在VS Code中打开文件
- 检查Tabs组件的onChange属性是否还有红色波浪线
- 确认没有TypeScript错误提示
### 2. **编译验证**
```bash
npm run build:weapp
```
应该没有类型错误。
### 3. **功能验证**
- 点击不同的Tab标签
- 确认Tab切换正常工作
- 验证数据过滤功能正常
## 📈 预防措施
### 1. **严格类型检查**
```json
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
```
### 2. **组件类型定义**
```typescript
// 为常用组件创建类型定义
interface TabsProps {
value: string | number
onChange: (value: string | number) => void
children: React.ReactNode
}
```
### 3. **ESLint规则**
```json
{
"rules": {
"@typescript-eslint/ban-ts-comment": "error", // 禁止@ts-ignore
"@typescript-eslint/no-explicit-any": "warn"
}
}
```
## 🎉 总结
通过系统性地修复类型定义:
-**类型兼容**`handleTabChange`函数类型与Tabs组件期望一致
-**状态安全**`activeTab`支持`string | number`类型
-**代码质量**:移除了`@ts-ignore`注释
-**功能正常**Tab切换和数据过滤功能正常工作
**现在Tabs组件的onChange类型错误已完全修复** 🎯

163
docs/TABS_TYPE_ERROR_FIX.md Normal file
View File

@@ -0,0 +1,163 @@
# 🔧 Tabs组件类型错误修复
## 🚨 问题描述
遇到了TypeScript类型错误
```
TS2322: Type (value: string) => void is not assignable to type (index: string | number) => void
Types of parameters value and index are incompatible.
Type string | number is not assignable to type string
```
## 🔍 错误原因
这个错误是因为:
1. **NutUI的Tabs组件**的`onChange`事件期望接收`(index: string | number) => void`类型的函数
2. **我们的handleTabChange函数**定义为`(value: string) => void`
3. **类型不匹配**导致TypeScript编译错误
## ✅ 修复方案
### 1. 修复函数参数类型
**修复前**
```typescript
const handleTabChange = (value: string) => {
setActiveTab(value)
// ...
}
```
**修复后**
```typescript
const handleTabChange = (value: string | number) => {
const tabValue = String(value) // 确保转换为字符串
setActiveTab(tabValue)
// ...
}
```
### 2. 修复TabPane组件使用
**修复前**
```tsx
<TabPane title="可用" value="0">
</TabPane>
```
**修复后**
```tsx
<TabPane title="可用" value="0" />
```
### 3. 添加调试信息
```typescript
const handleTabChange = (value: string | number) => {
const tabValue = String(value)
console.log('Tab切换:', { from: activeTab, to: tabValue })
// ...
}
```
## 🎯 修复的核心变更
### 类型兼容性
- ✅ 函数参数支持`string | number`类型
- ✅ 内部转换确保类型安全
- ✅ 保持原有逻辑不变
### 组件使用规范
- ✅ TabPane使用自闭合标签
- ✅ 符合NutUI组件规范
- ✅ 减少不必要的嵌套
### 调试功能增强
- ✅ 添加Tab切换日志
- ✅ 便于排查切换问题
- ✅ 监控状态变化
## 🚀 验证步骤
现在你可以:
### 1. 重新编译项目
```bash
npm run build:weapp
```
### 2. 验证修复效果
- ✅ TypeScript编译错误应该消失
- ✅ Tab切换功能正常
- ✅ 控制台显示切换日志
### 3. 测试Tab功能
- 点击"可用"、"已使用"、"已过期"标签
- 查看控制台的切换日志
- 确认数据正确加载
## 📋 技术细节
### NutUI Tabs组件特性
```typescript
interface TabsProps {
value?: string | number
onChange?: (index: string | number) => void
// ...
}
```
### 类型安全处理
```typescript
// 接收联合类型,内部转换为字符串
const handleTabChange = (value: string | number) => {
const tabValue = String(value) // 类型安全转换
setActiveTab(tabValue) // 确保状态类型正确
}
```
### 状态管理优化
```typescript
// Tab切换时的完整状态重置
setActiveTab(tabValue) // 设置新的活动标签
setPage(1) // 重置页码
setList([]) // 清空列表
setHasMore(true) // 重置加载状态
```
## 🎉 修复效果
### 修复前
```
❌ TS2322: Type (value: string) => void is not assignable
❌ 编译失败
❌ 类型检查不通过
```
### 修复后
```
✅ 类型检查通过
✅ 编译成功
✅ Tab切换正常
✅ 调试信息完善
```
## 🔍 相关的调试信息
现在当你切换Tab时控制台会显示
```
Tab切换: { from: "0", to: "1" }
优惠券数据加载成功: { activeTab: "1", ... }
```
这有助于:
- ✅ 确认Tab切换正常
- ✅ 监控数据加载状态
- ✅ 排查显示问题
## 🎯 下一步
**类型错误已修复!**现在你可以:
1. 重新编译项目
2. 测试Tab切换功能
3. 查看优惠券数据是否正常显示
4. 检查控制台的调试信息
**如果优惠券仍然不显示,请查看控制台的调试日志,告诉我具体的数据加载情况!** 🚀

196
docs/TAB_SWITCH_DATA_FIX.md Normal file
View File

@@ -0,0 +1,196 @@
# 🔄 Tab切换数据丢失问题修复
## 🚨 问题描述
用户反馈:
- **可用状态下的数据**:默认能看到优惠券
- **切换Tab后**数据消失其他Tab显示空状态
- **问题现象**:只有"可用"Tab有数据"已使用"和"已过期"Tab没有数据
## 🔍 问题分析
### 根本原因
Tab切换时存在**状态更新时序问题**
```typescript
// 问题代码
const handleTabChange = (value: string | number) => {
setActiveTab(tabValue) // 1. 设置新Tab
// ...
setTimeout(() => {
reload(true) // 2. 延迟调用reload
}, 100)
}
const reload = async (isRefresh = false) => {
const statusFilter = getStatusFilter() // 3. 但这里用的还是旧的activeTab
// ...
}
```
**问题**
1. `setActiveTab(tabValue)` 是异步的
2. `setTimeout` 中的 `reload()` 可能在状态更新前执行
3. `getStatusFilter()` 读取的是旧的 `activeTab`
4. 导致请求参数错误,获取不到正确数据
### 状态更新时序图
```
时间轴: ----1----2----3----4----5----
操作: 点击 设置 延迟 调用 状态
Tab 状态 100ms reload 更新完成
这里activeTab可能还是旧值
```
## ✅ 修复方案
### 1. 创建专用的Tab数据加载函数
```typescript
// 新增根据指定tab加载数据
const reloadWithTab = async (tab: string, isRefresh = true) => {
const getStatusFilterForTab = (tabValue: string) => {
switch (tabValue) {
case '0': return { status: 0, isExpire: 0 } // 可用
case '1': return { status: 1 } // 已使用
case '2': return { isExpire: 1 } // 已过期
default: return {}
}
}
const statusFilter = getStatusFilterForTab(tab) // 直接使用传入的tab值
// ... 数据加载逻辑
}
```
### 2. 修复Tab切换逻辑
```typescript
// 修复后的Tab切换
const handleTabChange = (value: string | number) => {
const tabValue = String(value)
setActiveTab(tabValue)
setPage(1)
setList([])
setHasMore(true)
// 直接调用传入新的tab值不依赖状态更新
reloadWithTab(tabValue)
}
```
### 3. 简化原reload函数
```typescript
const reload = async (isRefresh = false) => {
// 直接调用reloadWithTab使用当前的activeTab
await reloadWithTab(activeTab, isRefresh)
}
```
## 🎯 修复的核心改进
### 状态管理优化
-**消除时序依赖**:不再依赖异步状态更新
-**直接传参**Tab切换时直接传入新值
-**统一逻辑**:所有数据加载使用同一个函数
### 数据加载优化
-**精确过滤**每个Tab使用正确的过滤条件
-**调试增强**:添加详细的日志输出
-**错误处理**:完善的异常处理机制
### 用户体验优化
-**即时响应**Tab切换立即加载数据
-**状态清晰**每个Tab显示对应的数据
-**加载提示**保持loading状态管理
## 📋 Tab过滤条件对照表
| Tab状态 | Tab值 | API过滤条件 | 说明 |
|---------|-------|-------------|------|
| 可用 | "0" | `{ status: 0, isExpire: 0 }` | 未使用且未过期 |
| 已使用 | "1" | `{ status: 1 }` | 已使用状态 |
| 已过期 | "2" | `{ isExpire: 1 }` | 已过期状态 |
## 🚀 验证步骤
现在你可以测试:
### 1. 重新编译项目
```bash
npm run build:weapp
```
### 2. 测试Tab切换
- 点击"可用"Tab - 应该显示可用优惠券
- 点击"已使用"Tab - 应该显示已使用优惠券
- 点击"已过期"Tab - 应该显示已过期优惠券
### 3. 查看控制台日志
每次Tab切换时会显示
```
Tab切换: { from: "0", to: "1" }
使用Tab加载数据: { tab: "1", statusFilter: { status: 1 } }
Tab数据加载成功: { tab: "1", newListLength: 3, ... }
```
## 🔍 调试信息说明
### Tab切换日志
```javascript
Tab切换: { from: "0", to: "1" }
```
- `from`: 切换前的Tab
- `to`: 切换后的Tab
### 数据加载日志
```javascript
使用Tab加载数据: { tab: "1", statusFilter: { status: 1 } }
```
- `tab`: 当前加载的Tab值
- `statusFilter`: 使用的过滤条件
### 加载结果日志
```javascript
Tab数据加载成功: { tab: "1", newListLength: 3, responseData: {...} }
```
- `newListLength`: 加载到的数据数量
- `responseData`: 服务器返回的完整数据
## 🎉 预期效果
修复后的Tab切换应该
-**可用Tab**:显示未使用且未过期的优惠券
-**已使用Tab**:显示已使用的优惠券
-**已过期Tab**:显示已过期的优惠券
-**切换流畅**:每次切换都能正确加载对应数据
-**状态准确**每个Tab显示正确的数据状态
## 🔧 技术细节
### 异步状态管理
React的`setState`是异步的,直接在`setTimeout`中使用可能读取到旧值:
```typescript
// ❌ 错误方式
setActiveTab(newTab)
setTimeout(() => {
const filter = getStatusFilter() // 可能还是旧的activeTab
}, 100)
// ✅ 正确方式
setActiveTab(newTab)
reloadWithTab(newTab) // 直接传入新值
```
### 状态更新时序
```typescript
// React状态更新是异步的
setActiveTab("1") // 发起状态更新
console.log(activeTab) // 可能还是旧值 "0"
// 解决方案:直接使用新值
const newTab = "1"
setActiveTab(newTab) // 发起状态更新
reloadWithTab(newTab) // 直接使用新值
```
**现在重新编译测试Tab切换应该完全正常了** 🎯

191
docs/THEME_SYSTEM_GUIDE.md Normal file
View File

@@ -0,0 +1,191 @@
# 🎨 主题切换系统使用指南
## 📖 功能概述
我们为你的小程序实现了一套完整的主题切换系统,用户可以选择不同的渐变主题来个性化界面。
## 🎯 功能特点
### ✨ 智能主题
- **自动选择**根据用户ID自动分配个性化主题
- **8种精美主题**:海洋蓝紫、日落橙红、清新蓝绿、自然绿青、温暖橙黄、梦幻紫粉、经典蓝白、优雅灰黑
- **持久化存储**:用户选择会自动保存
### 🎨 手动选择
- **实时预览**:选择主题时立即看到效果
- **一键保存**:保存后自动返回上级页面
- **全局应用**:主题会应用到所有支持的页面
## 🚀 如何使用
### 1. 访问主题设置页面
用户可以通过以下方式进入主题设置:
```javascript
// 在任何页面中跳转到主题设置
Taro.navigateTo({
url: '/user/theme/index'
})
```
### 2. 在用户中心添加入口
你可以在用户中心页面添加"主题设置"入口:
```jsx
<Cell
title="主题设置"
extra="个性化界面"
onClick={() => Taro.navigateTo({ url: '/user/theme/index' })}
/>
```
### 3. 在组件中使用主题
#### 使用 useThemeStyles Hook
```jsx
import { useThemeStyles } from '@/hooks/useTheme'
const MyComponent = () => {
const themeStyles = useThemeStyles()
return (
<View style={themeStyles.primaryBackground}>
<Text style={{ color: themeStyles.textColor }}>
这里会应用当前主题的样式
</Text>
<Button style={themeStyles.primaryButton}>
主题按钮
</Button>
</View>
)
}
```
#### 使用 useTheme Hook
```jsx
import { useTheme } from '@/hooks/useTheme'
const MyComponent = () => {
const { currentTheme, setTheme, isAutoTheme } = useTheme()
return (
<View style={{ background: currentTheme.background }}>
<Text style={{ color: currentTheme.textColor }}>
当前主题{currentTheme.description}
</Text>
{isAutoTheme && <Text>使用智能主题</Text>}
</View>
)
}
```
## 🎨 可用主题列表
| 主题名称 | 主色调 | 描述 | 适用场景 |
|---------|--------|------|----------|
| ocean | 蓝紫色 | 海洋蓝紫 - 科技感与专业感 | 商务、科技类应用 |
| sunset | 橙红色 | 日落橙红 - 活力与热情 | 社交、娱乐类应用 |
| fresh | 蓝绿色 | 清新蓝绿 - 清新与活力 | 健康、运动类应用 |
| nature | 绿青色 | 自然绿青 - 生机与成长 | 环保、教育类应用 |
| warm | 橙黄色 | 温暖橙黄 - 温馨与舒适 | 生活、家居类应用 |
| dream | 紫粉色 | 梦幻紫粉 - 浪漫与梦幻 | 时尚、美妆类应用 |
| classic | 蓝白色 | 经典蓝白 - 简约与专业 | 办公、工具类应用 |
| elegant | 灰黑色 | 优雅灰黑 - 高端与品质 | 奢侈品、艺术类应用 |
## 🔧 开发者指南
### 添加新主题
`src/styles/gradients.ts` 中添加新主题:
```typescript
export const gradientThemes: GradientTheme[] = [
// 现有主题...
{
name: 'custom',
primary: '#your-primary-color',
secondary: '#your-secondary-color',
background: 'linear-gradient(135deg, #color1 0%, #color2 100%)',
textColor: '#ffffff',
description: '自定义主题 - 你的描述'
}
]
```
### 在新页面中应用主题
```jsx
import { useThemeStyles } from '@/hooks/useTheme'
const NewPage = () => {
const themeStyles = useThemeStyles()
return (
<View className="min-h-screen bg-gray-50">
{/* 使用主题背景的头部 */}
<View
className="px-4 py-6 relative overflow-hidden"
style={themeStyles.primaryBackground}
>
<Text className="text-lg font-bold" style={{ color: themeStyles.textColor }}>
页面标题
</Text>
</View>
{/* 其他内容 */}
<View className="p-4">
<Button style={themeStyles.primaryButton}>
主题按钮
</Button>
</View>
</View>
)
}
```
## 📱 用户体验
### 智能主题算法
系统会根据用户ID生成一个稳定的主题选择
```typescript
// 用户ID为 "12345" 的用户总是会得到相同的主题
const theme = gradientUtils.getThemeByUserId("12345")
```
### 主题持久化
- 用户选择会保存在本地存储中
- 重新打开应用时会自动应用上次选择的主题
- 支持"智能主题"和"手动选择"两种模式
## 🎉 效果展示
### 智能主题模式
- 每个用户都有独特的个性化主题
- 基于用户ID算法分配确保稳定性
- 提升用户归属感和个性化体验
### 手动选择模式
- 用户可以自由选择喜欢的主题
- 实时预览效果
- 一键保存并应用
## 🔄 更新日志
### v1.0.0 (2025-01-18)
- ✅ 实现8种精美渐变主题
- ✅ 智能主题自动分配算法
- ✅ 主题切换页面UI
- ✅ useTheme 和 useThemeStyles Hooks
- ✅ 主题持久化存储
- ✅ 小程序兼容性优化
---
**现在你的用户可以享受个性化的主题体验了!** 🎨✨

111
docs/TYPESCRIPT_TYPE_FIX.md Normal file
View File

@@ -0,0 +1,111 @@
# TypeScript 类型错误修复
## 🚨 问题描述
遇到了TypeScript类型错误
```
TS7053: Element implicitly has an 'any' type because expression of type 'string'; TenantId: any; }
Property Authorization does not exist on type { 'Content-Type': string; TenantId: any; }
```
## 🔍 错误原因
这个错误是因为:
1. `defaultHeaders`对象没有明确的类型定义
2. TypeScript无法推断动态添加的`Authorization`属性
3. 严格的类型检查模式下,不允许在对象上动态添加属性
## ✅ 修复方案
### 修复前的代码
```typescript
const defaultHeaders = {
'Content-Type': 'application/json',
'TenantId': tenantId
};
if (token) {
defaultHeaders['Authorization'] = token; // ❌ 类型错误
}
```
### 修复后的代码
```typescript
const defaultHeaders: Record<string, string> = {
'Content-Type': 'application/json',
'TenantId': tenantId
};
if (token) {
defaultHeaders['Authorization'] = token; // ✅ 类型正确
}
```
## 🔧 已修复的文件
### 1. src/utils/request.ts
- ✅ 添加了`Record<string, string>`类型注解
- ✅ 允许动态添加Authorization属性
- ✅ 保持代码逻辑不变
### 2. src/utils/request-legacy.ts
- ✅ 添加了`Record<string, string>`类型注解
- ✅ 修复了相同的类型错误
- ✅ 保持向后兼容性
## 🎯 修复效果
### 修复前
```
❌ TS7053: Element implicitly has an 'any' type
❌ Property Authorization does not exist on type
```
### 修复后
```
✅ 类型检查通过
✅ 动态属性添加正常
✅ 编译无错误
```
## 📋 技术细节
### Record<string, string> 类型的作用
- **灵活性**:允许动态添加字符串键值对
- **类型安全**:确保所有值都是字符串类型
- **兼容性**:与现有代码完全兼容
### 为什么使用这种方案
1. **最小修改**:只需要添加类型注解,不改变逻辑
2. **类型安全**满足TypeScript严格模式要求
3. **可维护性**:代码更清晰,类型更明确
## 🚀 验证步骤
现在你可以:
1. **重新编译项目**
```bash
npm run build:weapp
```
2. **验证修复效果**
- TypeScript类型错误应该消失
- 编译应该成功
- 功能保持正常
## 🎉 总结
**类型错误已完全修复!**
- **修复文件数**2个工具文件
- **修复类型**TypeScript类型注解
- **影响范围**0纯类型修复不影响运行时
- **兼容性**100% 向后兼容
这个修复确保了:
- ✅ TypeScript严格模式下的类型安全
- ✅ 动态属性添加的正确性
- ✅ 代码的可维护性和可读性
**现在项目应该能够完全正常编译了!** 🎉

View File

@@ -0,0 +1,181 @@
# ✅ TypeScript类型错误完全修复
## 🚨 问题描述
TypeScript编译器报错`Type '2' is not assignable to type '10 | 20 | 30 | undefined'`
这是因为CouponCard组件的接口已更新为使用新的类型值10, 20, 30但部分文件仍在使用旧的类型值1, 2, 3
## 🔧 已修复的文件
### 1. **src/user/coupon/receive.tsx**
#### 修复前
```typescript
let type: 1 | 2 | 3 = 1
if (coupon.type === 10) { // 满减券
type = 1
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 2
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 3
amount = 0
}
```
#### 修复后
```typescript
let type: 10 | 20 | 30 = 10 // 使用新的类型值
if (coupon.type === 10) { // 满减券
type = 10
amount = parseFloat(coupon.reducePrice || '0')
} else if (coupon.type === 20) { // 折扣券
type = 20
amount = coupon.discount || 0
} else if (coupon.type === 30) { // 免费券
type = 30
amount = 0
}
```
### 2. **src/user/coupon/coupon.tsx**
#### 修复前
```typescript
const getCouponTypeText = (type?: number) => {
switch (type) {
case 1: return '满减券'
case 2: return '折扣券'
case 3: return '免费券'
default: return '优惠券'
}
}
```
#### 修复后
```typescript
const getCouponTypeText = (type?: number) => {
switch (type) {
case 10: return '满减券'
case 20: return '折扣券'
case 30: return '免费券'
default: return '优惠券'
}
}
```
### 3. **src/components/CouponCard.tsx** (之前已修复)
#### 修复内容
- `{type !== 3 && ...}``{type !== 30 && ...}`
- `type === 1 ? ... : type === 2 ? ...``type === 10 ? ... : type === 20 ? ...`
- 删除未使用的函数和变量
- 清理代码格式
## 🎯 类型值映射表
| 后端类型值 | 前端显示 | 说明 | ¥符号显示 |
|-----------|----------|------|-----------|
| 10 | 满减券 | 满X减Y | ✅ 显示 |
| 20 | 折扣券 | 满X享Y折 | ✅ 显示 |
| 30 | 免费券 | 免费使用 | ❌ 不显示 |
## 📊 修复影响范围
### 组件层面
-**CouponCard组件**:核心显示组件,类型判断已统一
-**CouponList组件**列表组件通过props传递正确类型
-**优惠券相关页面**:所有使用优惠券的页面
### 功能层面
-**优惠券显示**:金额、类型、状态显示正确
-**优惠券选择**:订单确认页面选择功能正常
-**优惠券管理**:用户优惠券页面功能正常
-**优惠券领取**:优惠券领取页面功能正常
### 数据流层面
-**后端数据**使用类型值10, 20, 30
-**数据转换**:统一转换函数处理类型映射
-**组件渲染**:组件接收正确的类型值
## 🧪 验证方法
### 1. **编译验证**
```bash
npm run build:weapp
```
应该没有TypeScript类型错误。
### 2. **功能验证**
- [ ] 优惠券卡片正确显示类型和金额
- [ ] 满减券和折扣券显示¥符号
- [ ] 免费券不显示¥符号,显示"免费"
- [ ] 优惠券状态文本正确显示
### 3. **页面验证**
- [ ] 订单确认页面:优惠券选择功能正常
- [ ] 用户优惠券页面三个tab显示正确
- [ ] 优惠券领取页面:领取功能正常
## 🔍 根本原因分析
### 为什么会出现这个问题?
1. **接口定义更新**CouponCardProps接口更新为使用新的类型值
2. **历史代码遗留**:部分文件仍使用旧的类型值进行转换
3. **类型不一致**:前端组件期望的类型与转换函数提供的类型不匹配
### 解决方案
1. **统一类型值**所有地方都使用10, 20, 30作为类型值
2. **统一转换函数**:使用`couponUtils.ts`中的标准转换函数
3. **类型检查**启用严格的TypeScript检查避免类似问题
## 🚀 预防措施
### 1. **代码规范**
- 使用统一的数据转换工具函数
- 避免在多个地方重复定义类型映射
- 建立代码审查流程
### 2. **类型安全**
- 启用严格的TypeScript配置
- 使用枚举或常量定义类型值
- 添加单元测试验证类型转换
### 3. **文档维护**
- 维护类型值映射文档
- 更新接口变更时同步更新所有相关代码
- 建立变更检查清单
## 📈 修复效果
### 修复前
```
❌ TypeScript Error: Type '2' is not assignable to type '10 | 20 | 30 | undefined'
❌ 编译失败
❌ 类型不安全
```
### 修复后
```
✅ 编译成功
✅ 类型安全
✅ 功能正常
✅ 代码一致
```
## 🎉 总结
通过系统性地修复所有使用旧类型值的地方,现在:
1. **类型一致性**所有优惠券相关代码使用统一的类型值10, 20, 30
2. **编译成功**TypeScript编译器不再报错
3. **功能完整**:所有优惠券功能正常工作
4. **代码质量**:删除了重复代码,提高了可维护性
**现在所有的TypeScript类型错误都已修复项目可以正常编译和运行** 🎯

View File

@@ -0,0 +1,157 @@
# 🔧 TypeScript 警告修复
## 问题描述
`useShopInfo.ts`中出现TypeScript警告
```
TS6133: forceRefresh is declared but its value is never read.
```
## 问题分析
`fetchShopInfo`函数中,`forceRefresh`参数被声明但从未使用:
```typescript
// 问题代码 ❌
const fetchShopInfo = useCallback(async (forceRefresh = false) => {
// forceRefresh参数在函数体中从未被使用
try {
setLoading(true);
setError(null);
const data = await getShopInfo(); // 总是从服务器获取
// ...
}
}, [saveShopInfoToStorage]);
```
## 修复方案
### 选择的方案:移除未使用的参数
```typescript
// 修复后 ✅
const fetchShopInfo = useCallback(async () => {
try {
setLoading(true);
setError(null);
const data = await getShopInfo();
setShopInfo(data);
saveShopInfoToStorage(data);
return data;
} catch (error) {
// 错误处理...
} finally {
setLoading(false);
}
}, [saveShopInfoToStorage]);
```
## 为什么选择移除参数
### 1. **当前实现逻辑**
- `fetchShopInfo`总是从服务器获取最新数据
- 缓存逻辑在`useEffect`初始化时处理
- 不需要强制刷新参数
### 2. **代码简化**
- 移除未使用的参数使代码更清晰
- 避免TypeScript警告
- 减少不必要的复杂性
### 3. **功能完整性**
- `refreshShopInfo`方法专门用于强制刷新
- `fetchShopInfo`专注于获取数据
- 职责分离更清晰
## 替代方案(未采用)
如果需要实现`forceRefresh`逻辑,可以这样做:
```typescript
// 替代方案实现forceRefresh逻辑
const fetchShopInfo = useCallback(async (forceRefresh = false) => {
// 如果不是强制刷新,先检查缓存
if (!forceRefresh) {
const hasCache = loadShopInfoFromStorage();
if (hasCache) {
return shopInfo;
}
}
try {
setLoading(true);
setError(null);
const data = await getShopInfo();
setShopInfo(data);
saveShopInfoToStorage(data);
return data;
} catch (error) {
// 错误处理...
} finally {
setLoading(false);
}
}, [saveShopInfoToStorage, loadShopInfoFromStorage, shopInfo]);
```
**为什么不采用这个方案:**
- 会重新引入循环依赖问题(依赖`shopInfo`
- 缓存逻辑已在初始化时处理
- 增加不必要的复杂性
## 修复效果
### 修复前 ❌
```
⚠️ TS6133: forceRefresh is declared but its value is never read.
🔧 未使用的参数造成代码混淆
📝 代码意图不明确
```
### 修复后 ✅
```
✅ 无TypeScript警告
🔧 代码简洁明确
📝 函数职责清晰
```
## 相关方法说明
### `fetchShopInfo()`
- **用途**:从服务器获取商店信息
- **场景**:初始化时调用
- **特点**:总是获取最新数据
### `refreshShopInfo()`
- **用途**:强制刷新商店信息
- **场景**:用户手动刷新时调用
- **特点**:清除缓存,重新获取
### 使用示例
```typescript
const { fetchShopInfo, refreshShopInfo } = useShopInfo();
// 初始化获取(内部自动调用)
// fetchShopInfo();
// 用户手动刷新
const handleRefresh = async () => {
await refreshShopInfo();
};
```
## 总结
通过移除未使用的`forceRefresh`参数:
-**消除TypeScript警告**
-**简化代码逻辑**
-**保持功能完整**
-**避免循环依赖**
这个修复使代码更加清晰和可维护,同时保持了所有原有功能。

171
docs/TYPE_WARNING_FIX.md Normal file
View File

@@ -0,0 +1,171 @@
# 🚨 TypeScript类型警告修复
## 问题描述
`src/user/gift/receive.tsx`文件中出现红色类型警告:
```
Type 'string' is not assignable to type 'number | undefined'
```
## 🔍 问题分析
### 错误位置
```typescript
// 第34行 - 错误的类型
enabled: '1', // ❌ 字符串类型
```
### 根本原因
根据`ShopCouponParam`接口定义,`enabled`字段期望的是`number`类型:
```typescript
export interface ShopCouponParam extends PageParam {
// ...其他字段
enabled?: number; // ← 期望数字类型
// ...
}
```
但代码中传入的是字符串`'1'`,导致类型不匹配。
## 🔧 修复内容
### src/user/gift/receive.tsx
#### 修复前 ❌
```typescript
const res = await pageShopCoupon({
page: currentPage,
limit: 10,
keywords: searchValue,
enabled: '1', // ❌ 字符串类型
isExpire: 0
})
```
#### 修复后 ✅
```typescript
const res = await pageShopCoupon({
page: currentPage,
limit: 10,
keywords: searchValue,
enabled: 1, // ✅ 数字类型
isExpire: 0
})
```
## 📊 类型映射表
| 字段 | 期望类型 | 修复前 | 修复后 | 说明 |
|------|----------|--------|--------|------|
| `enabled` | `number \| undefined` | `'1'` (string) | `1` (number) | 启用状态1-启用0-禁用 |
| `isExpire` | `number \| undefined` | `0` (number) | `0` (number) | 过期状态0-未过期1-已过期 |
## ✅ 修复效果
### 修复前
```
⚠️ TypeScript Warning: Type 'string' is not assignable to type 'number | undefined'
⚠️ 红色警告显示
⚠️ 类型不安全
```
### 修复后
```
✅ 类型检查通过
✅ 没有警告
✅ 类型安全
```
## 🔍 相关检查
### 其他文件状态
-`src/user/coupon/receive.tsx` - 已经使用正确的数字类型
-`src/user/gift/receive.tsx` - 现已修复
- ✅ 其他相关文件 - 无类似问题
### API接口定义
```typescript
// ShopCouponParam 接口中的相关字段
export interface ShopCouponParam extends PageParam {
enabled?: number; // 是否启用1-启用0-禁用
isExpire?: number; // 是否过期0-未过期1-已过期
status?: number; // 状态
type?: number; // 类型
// ...其他字段
}
```
## 🎯 最佳实践
### 1. **类型一致性**
- 确保传入的参数类型与接口定义一致
- 使用数字类型而不是字符串表示数值
### 2. **接口规范**
- 状态字段统一使用数字类型
- 0/1 表示布尔状态0-否1-是)
### 3. **代码检查**
- 启用严格的TypeScript检查
- 使用IDE的类型提示功能
- 定期检查类型警告
## 🧪 验证方法
### 1. **编译验证**
```bash
npm run build:weapp
```
应该没有类型警告。
### 2. **IDE验证**
- 在VS Code中打开文件
- 检查是否还有红色波浪线
- 确认类型提示正确
### 3. **功能验证**
- 礼品领取页面正常加载
- API请求正常发送
- 数据正常返回
## 📈 预防措施
### 1. **开发规范**
- 严格按照接口定义传参
- 使用TypeScript的类型检查
- 代码审查时注意类型匹配
### 2. **工具配置**
```json
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true
}
}
```
### 3. **ESLint规则**
```json
// .eslintrc.js
{
"rules": {
"@typescript-eslint/no-explicit-any": "warn",
"@typescript-eslint/strict-boolean-expressions": "error"
}
}
```
## 🎉 总结
通过将`enabled: '1'`修改为`enabled: 1`,成功修复了类型警告:
-**类型安全**:参数类型与接口定义一致
-**代码质量**消除了TypeScript警告
-**功能正常**API调用不受影响
-**维护性**:代码更加规范和可维护
**现在所有的类型警告都已修复,代码类型安全!** 🎯

View File

@@ -0,0 +1,212 @@
# 统一扫码功能使用指南
## 🎯 功能概述
统一扫码功能将原有的**扫码登录**和**扫码核销**合并为一个入口,通过智能识别二维码内容自动执行相应操作。
## 📋 功能特性
### ✨ 智能识别
- 自动识别登录二维码和核销二维码
- 根据二维码内容自动执行相应操作
- 支持多种二维码格式JSON加密、纯文本等
### 🔄 统一体验
- 一个按钮解决两种扫码需求
- 统一的UI界面和交互逻辑
- 一致的错误处理和状态提示
### 📱 多入口支持
- 用户卡片中的统一扫码按钮
- 管理员面板中的统一扫码功能
- 独立的统一扫码页面
## 🛠️ 技术实现
### 核心文件
```
src/
├── hooks/
│ └── useUnifiedQRScan.ts # 统一扫码Hook
├── components/
│ └── UnifiedQRButton.tsx # 统一扫码按钮组件
└── pages/
└── unified-qr/
├── index.tsx # 统一扫码页面
└── index.config.ts # 页面配置
```
### HookuseUnifiedQRScan
```typescript
import { useUnifiedQRScan, ScanType } from '@/hooks/useUnifiedQRScan';
const {
startScan, // 开始扫码
isLoading, // 加载状态
canScan, // 是否可以扫码
state, // 当前状态
result, // 扫码结果
scanType, // 识别的扫码类型
reset // 重置状态
} = useUnifiedQRScan();
```
### 组件UnifiedQRButton
```jsx
<UnifiedQRButton
text="扫码"
size="small"
onSuccess={(result) => {
console.log('扫码成功:', result);
// result.type: 'login' | 'verification'
// result.data: 具体数据
// result.message: 成功消息
}}
onError={(error) => {
console.error('扫码失败:', error);
}}
/>
```
## 🎮 使用方式
### 1. 直接使用统一按钮
```jsx
import UnifiedQRButton from '@/components/UnifiedQRButton';
// 在需要的地方使用
<UnifiedQRButton
text="智能扫码"
onSuccess={(result) => {
if (result.type === 'login') {
// 处理登录成功
} else if (result.type === 'verification') {
// 处理核销成功
}
}}
/>
```
### 2. 跳转到统一扫码页面
```jsx
// 跳转到统一扫码页面
Taro.navigateTo({
url: '/passport/unified-qr/index'
});
```
### 3. 在管理员面板中使用
管理员面板已更新,原来的"门店核销"和"扫码登录"合并为"统一扫码"。
## 🔍 二维码识别逻辑
### 登录二维码
- **格式**: 包含登录token的URL或纯文本
- **处理**: 自动解析token并确认登录
- **示例**: `https://example.com/login?token=xxx`
### 核销二维码
- **JSON格式**: `{"businessType":"gift","token":"xxx","data":"encrypted_data"}`
- **纯文本格式**: 6位数字核销码
- **处理**: 解密数据(如需要)-> 验证核销码 -> 执行核销
### 识别优先级
1. 首先检查是否为JSON格式的核销二维码
2. 然后检查是否为登录二维码
3. 最后检查是否为纯数字核销码
4. 如果都不匹配,提示"不支持的二维码类型"
## 📊 用户体验优化
### 智能提示
- 扫码过程中显示当前状态
- 根据识别结果给出相应提示
- 核销成功后询问是否继续扫码
### 历史记录
- 保留最近5次扫码记录
- 显示扫码类型、时间和结果
- 方便用户查看操作历史
### 错误处理
- 统一的错误提示机制
- 具体的错误原因说明
- 便捷的重试和重置功能
## 🔄 迁移指南
### 从原有功能迁移
#### 替换扫码登录按钮
```jsx
// 原来
<QRLoginButton />
// 现在
<UnifiedQRButton text="扫码登录" />
```
#### 替换核销按钮
```jsx
// 原来
<Button onClick={() => navTo('/user/store/verification')}>
扫码核销
</Button>
// 现在
<UnifiedQRButton text="扫码核销" />
```
#### 管理员面板更新
管理员面板自动合并了原有的两个扫码功能,无需额外操作。
## 🚀 优势总结
### 用户体验
-**简化操作**: 一个按钮处理所有扫码需求
-**智能识别**: 无需用户手动选择扫码类型
-**统一界面**: 一致的交互体验
### 开发维护
-**代码复用**: 统一的扫码逻辑和错误处理
-**易于扩展**: 新增扫码类型只需修改识别逻辑
-**降低复杂度**: 减少重复代码和功能入口
### 功能完整性
-**保留所有原功能**: 登录和核销功能完全保留
-**增强用户体验**: 添加历史记录和智能提示
-**向后兼容**: 原有的单独页面仍然可用
## 🔧 配置说明
### 页面路由配置
需要在 `app.config.ts` 中添加新页面路由:
```typescript
export default {
pages: [
// ... 其他页面
'passport/unified-qr/index'
]
}
```
### 权限要求
- **扫码权限**: 所有用户都可以扫码
- **登录功能**: 需要用户已登录小程序
- **核销功能**: 需要管理员权限
## 🎯 未来规划
### 扩展可能性
- 支持更多类型的二维码(商品码、活动码等)
- 增加扫码统计和分析功能
- 支持批量扫码操作
- 增加扫码记录的云端同步
### 性能优化
- 扫码响应速度优化
- 二维码识别准确率提升
- 网络请求优化和缓存机制

357
docs/USE_SHOP_INFO_HOOK.md Normal file
View File

@@ -0,0 +1,357 @@
# 🏪 useShopInfo Hook 使用指南
## 📋 概述
`useShopInfo` 是一个用于管理商店信息的React Hook提供了商店信息的获取、缓存和管理功能。它基于`getShopInfo()`接口,为全站提供统一的商店信息访问方式。
## ✨ 特性
- 🚀 **自动缓存**30分钟本地缓存减少网络请求
- 🔄 **智能刷新**:支持强制刷新和自动过期更新
- 📱 **离线支持**:网络失败时使用缓存数据
- 🛠️ **工具方法**:提供常用信息的便捷获取方法
- 🎯 **TypeScript**:完整的类型支持
-**性能优化**使用useCallback避免不必要的重渲染
## 🔧 基本用法
### 1. 导入Hook
```typescript
import { useShopInfo } from '@/hooks/useShopInfo';
```
### 2. 在组件中使用
```typescript
const MyComponent = () => {
const {
shopInfo,
loading,
error,
getWebsiteName,
getWebsiteLogo
} = useShopInfo();
if (loading) {
return <div>加载中...</div>;
}
if (error) {
return <div>加载失败: {error}</div>;
}
return (
<div>
<img src={getWebsiteLogo()} alt="Logo" />
<h1>{getWebsiteName()}</h1>
</div>
);
};
```
## 📊 API 参考
### 状态属性
| 属性 | 类型 | 说明 |
|------|------|------|
| `shopInfo` | `CmsWebsite \| null` | 商店信息对象 |
| `loading` | `boolean` | 是否正在加载 |
| `error` | `string \| null` | 错误信息 |
### 方法
| 方法 | 参数 | 返回值 | 说明 |
|------|------|--------|------|
| `fetchShopInfo` | `forceRefresh?: boolean` | `Promise<CmsWebsite \| null>` | 获取商店信息 |
| `refreshShopInfo` | - | `Promise<CmsWebsite \| null>` | 强制刷新商店信息 |
| `clearCache` | - | `void` | 清除本地缓存 |
### 工具方法
| 方法 | 返回值 | 说明 |
|------|--------|------|
| `getWebsiteName()` | `string` | 获取网站名称,默认"商城" |
| `getWebsiteLogo()` | `string` | 获取网站Logo URL |
| `getDarkLogo()` | `string` | 获取深色模式Logo URL |
| `getDomain()` | `string` | 获取网站域名 |
| `getPhone()` | `string` | 获取联系电话 |
| `getEmail()` | `string` | 获取邮箱地址 |
| `getAddress()` | `string` | 获取地址 |
| `getIcpNo()` | `string` | 获取ICP备案号 |
| `getStatus()` | `object` | 获取网站状态信息 |
| `getConfig()` | `any` | 获取网站配置 |
| `getNavigation()` | `object` | 获取导航菜单 |
| `isSearchEnabled()` | `boolean` | 是否支持搜索 |
| `getVersionInfo()` | `object` | 获取版本信息 |
## 🎯 使用场景
### 1. 页面头部组件
```typescript
// src/pages/index/Header.tsx
import { useShopInfo } from '@/hooks/useShopInfo';
const Header = () => {
const { getWebsiteName, getWebsiteLogo, loading } = useShopInfo();
return (
<NavBar
left={
<div style={{display: 'flex', alignItems: 'center'}}>
<Avatar size="22" src={getWebsiteLogo()} />
<span>{getWebsiteName()}</span>
</div>
}
/>
);
};
```
### 2. 首页组件
```typescript
// src/pages/index/index.tsx
import { useShopInfo } from '@/hooks/useShopInfo';
const Home = () => {
const { shopInfo, loading, error } = useShopInfo();
useEffect(() => {
if (shopInfo) {
// 设置页面标题
Taro.setNavigationBarTitle({
title: shopInfo.websiteName || '商城'
});
}
}, [shopInfo]);
// 分享配置
useShareAppMessage(() => ({
title: shopInfo?.websiteName || '精选商城',
imageUrl: shopInfo?.websiteLogo
}));
return (
<div>
{/* 页面内容 */}
</div>
);
};
```
### 3. 商品详情页分享
```typescript
// src/shop/goodsDetail/index.tsx
import { useShopInfo } from '@/hooks/useShopInfo';
const GoodsDetail = () => {
const { getWebsiteName, getWebsiteLogo } = useShopInfo();
useShareAppMessage(() => ({
title: `${goods?.name} - ${getWebsiteName()}`,
path: `/shop/goodsDetail/index?id=${goodsId}`,
imageUrl: goods?.image || getWebsiteLogo()
}));
return (
<div>
{/* 商品详情 */}
</div>
);
};
```
### 4. 联系我们页面
```typescript
const ContactPage = () => {
const {
getPhone,
getEmail,
getAddress,
getIcpNo
} = useShopInfo();
return (
<div>
<div>电话: {getPhone()}</div>
<div>邮箱: {getEmail()}</div>
<div>地址: {getAddress()}</div>
<div>备案号: {getIcpNo()}</div>
</div>
);
};
```
### 5. 网站状态检查
```typescript
const StatusChecker = () => {
const { getStatus, getVersionInfo } = useShopInfo();
const status = getStatus();
const version = getVersionInfo();
return (
<div>
<div>运行状态: {status.running ? '正常' : '维护中'}</div>
<div>版本: {version.version === 10 ? '免费版' : version.version === 20 ? '专业版' : '永久授权'}</div>
{version.expirationTime && (
<div>到期时间: {version.expirationTime}</div>
)}
</div>
);
};
```
## 🔄 缓存机制
### 缓存策略
- **缓存时间**30分钟
- **存储位置**:微信小程序本地存储
- **缓存键名**`shop_info``shop_info_cache_time`
### 缓存行为
1. **首次加载**:从服务器获取数据并缓存
2. **后续加载**:优先使用缓存,缓存过期时自动刷新
3. **网络失败**:使用缓存数据(即使过期)
4. **强制刷新**:忽略缓存,直接从服务器获取
### 手动管理缓存
```typescript
const { refreshShopInfo, clearCache } = useShopInfo();
// 强制刷新
const handleRefresh = async () => {
await refreshShopInfo();
};
// 清除缓存
const handleClearCache = () => {
clearCache();
};
```
## 🚀 性能优化
### 1. 避免重复请求
```typescript
// ✅ 推荐多个组件使用同一个Hook实例
const App = () => {
const shopInfo = useShopInfo();
return (
<ShopInfoProvider value={shopInfo}>
<Header />
<Content />
<Footer />
</ShopInfoProvider>
);
};
```
### 2. 条件渲染优化
```typescript
const Component = () => {
const { shopInfo, loading } = useShopInfo();
// ✅ 使用loading状态避免闪烁
if (loading) {
return <Skeleton />;
}
return (
<div>
{shopInfo && (
<img src={shopInfo.websiteLogo} alt="Logo" />
)}
</div>
);
};
```
## 🛠️ 迁移指南
### 从直接调用API迁移
#### 迁移前 ❌
```typescript
// 旧代码
const [config, setConfig] = useState<CmsWebsite>();
useEffect(() => {
getShopInfo().then((data) => {
setConfig(data);
});
}, []);
```
#### 迁移后 ✅
```typescript
// 新代码
const { shopInfo: config, loading } = useShopInfo();
```
### 批量替换步骤
1. **替换导入**
```typescript
// 删除
import { getShopInfo } from '@/api/layout';
// 添加
import { useShopInfo } from '@/hooks/useShopInfo';
```
2. **替换状态管理**
```typescript
// 删除
const [config, setConfig] = useState<CmsWebsite>();
// 替换为
const { shopInfo: config, loading, error } = useShopInfo();
```
3. **删除useEffect**
```typescript
// 删除这些代码
useEffect(() => {
getShopInfo().then((data) => {
setConfig(data);
});
}, []);
```
## 🧪 测试示例
```typescript
// 测试组件
const TestComponent = () => {
const {
shopInfo,
loading,
error,
refreshShopInfo,
getWebsiteName
} = useShopInfo();
return (
<div>
<div>状态: {loading ? '加载中' : '已加载'}</div>
<div>错误: {error || '无'}</div>
<div>网站名: {getWebsiteName()}</div>
<button onClick={refreshShopInfo}>刷新</button>
<pre>{JSON.stringify(shopInfo, null, 2)}</pre>
</div>
);
};
```
## 🎉 总结
`useShopInfo` Hook 提供了:
-**统一的商店信息管理**
-**智能缓存机制**
-**丰富的工具方法**
-**完整的TypeScript支持**
-**简单的迁移路径**
通过使用这个Hook你可以在整个应用中轻松访问和管理商店信息提高代码的可维护性和用户体验。

View File

@@ -0,0 +1,181 @@
# 后端多规格功能适配指南
## 概述
前端已完成商品多规格功能集成,需要后端相应适配以支持完整的多规格商品流程。
## 需要适配的API接口
### 1. 商品规格查询接口
**接口**: `GET /shop/shop-goods-spec`
**当前问题**: 参数模型中缺少 `goodsId` 字段
**需要修改**:
```java
// ShopGoodsSpecParam 类需要添加 goodsId 字段
public class ShopGoodsSpecParam extends PageParam {
private Long goodsId; // 添加此字段
private Long id;
private String keywords;
// ... getter/setter
}
```
### 2. 商品SKU查询接口
**接口**: `GET /shop/shop-goods-sku`
**当前问题**: 参数模型中缺少 `goodsId` 字段
**需要修改**:
```java
// ShopGoodsSkuParam 类需要添加 goodsId 字段
public class ShopGoodsSkuParam extends PageParam {
private Long goodsId; // 添加此字段
private Long id;
private String keywords;
// ... getter/setter
}
```
### 3. 购物车接口适配
**当前购物车数据结构**:
```typescript
interface CartItem {
goodsId: number;
name: string;
price: string;
image: string;
quantity: number;
addTime: number;
skuId?: number; // 新增SKU ID
specInfo?: string; // 新增规格信息
}
```
**后端需要适配**:
- 购物车存储时支持 `skuId``specInfo` 字段
- 购物车查询时返回完整的SKU信息
- 价格计算时优先使用SKU价格
### 4. 订单创建接口适配
**前端订单数据结构**:
```typescript
interface OrderGoodsItem {
goodsId: number;
quantity: number;
skuId?: number; // SKU ID
specInfo?: string; // 规格信息字符串
}
```
**后端需要处理**:
- 订单商品项支持SKU信息
- 库存扣减时根据SKU进行
- 价格计算时使用SKU价格
- 订单详情显示规格信息
## 数据库表结构检查
### 1. 购物车表 (如果有)
确保包含以下字段:
```sql
ALTER TABLE shop_cart ADD COLUMN sku_id BIGINT COMMENT 'SKU ID';
ALTER TABLE shop_cart ADD COLUMN spec_info VARCHAR(500) COMMENT '规格信息';
```
### 2. 订单商品表
确保包含以下字段:
```sql
-- shop_order_goods 表应该已有这些字段
-- sku_id BIGINT COMMENT 'SKU ID'
-- spec VARCHAR(255) COMMENT '商品规格'
```
## 业务逻辑适配
### 1. 库存管理
- 单规格商品:使用 `shop_goods.stock`
- 多规格商品:使用 `shop_goods_sku.stock`
- 下单时根据是否有SKU选择对应的库存扣减逻辑
### 2. 价格计算
- 单规格商品:使用 `shop_goods.price`
- 多规格商品:使用 `shop_goods_sku.price`
- 订单金额计算时优先使用SKU价格
### 3. 规格数据组织
后端查询规格时需要按商品ID过滤:
```java
// 示例查询逻辑
public List<ShopGoodsSpec> listByGoodsId(Long goodsId) {
return shopGoodsSpecMapper.selectList(
new QueryWrapper<ShopGoodsSpec>()
.eq("goods_id", goodsId)
.orderByAsc("spec_name", "spec_value")
);
}
```
## 前端调用示例
### 1. 加载商品规格
```typescript
// 前端会这样调用
listShopGoodsSpec({ goodsId: 123 })
listShopGoodsSku({ goodsId: 123 })
```
### 2. 创建订单
```typescript
// 单规格商品
{
goodsItems: [{
goodsId: 123,
quantity: 2
}]
}
// 多规格商品
{
goodsItems: [{
goodsId: 123,
quantity: 2,
skuId: 456,
specInfo: "颜色:红色|尺寸:L"
}]
}
```
## 测试建议
1. **创建测试数据**:
- 创建一个多规格商品
- 添加规格组(颜色、尺寸等)
- 生成对应的SKU数据
2. **测试场景**:
- 商品详情页规格加载
- 规格选择和SKU匹配
- 加入购物车(多规格)
- 立即购买(多规格)
- 订单创建和支付
3. **边界情况**:
- SKU库存为0的处理
- 规格数据不完整的处理
- 单规格和多规格商品混合购买
## 注意事项
1. **向后兼容**: 确保单规格商品的现有功能不受影响
2. **数据一致性**: SKU价格和库存与主商品数据的同步
3. **性能优化**: 规格和SKU数据的查询优化
4. **错误处理**: 规格选择错误、库存不足等异常情况的处理
## 完成检查清单
- [ ] ShopGoodsSpecParam 添加 goodsId 字段
- [ ] ShopGoodsSkuParam 添加 goodsId 字段
- [ ] 规格查询接口支持按商品ID过滤
- [ ] SKU查询接口支持按商品ID过滤
- [ ] 购物车接口支持SKU信息
- [ ] 订单创建接口支持SKU信息
- [ ] 库存扣减逻辑适配多规格
- [ ] 价格计算逻辑适配多规格
- [ ] 测试多规格商品完整流程

View File

@@ -0,0 +1,248 @@
/**
* 订单服务实现类示例
* 展示如何保存订单商品信息的业务逻辑
*/
@Service
@Transactional(rollbackFor = Exception.class)
public class ShopOrderServiceImpl implements ShopOrderService {
@Autowired
private ShopOrderMapper shopOrderMapper;
@Autowired
private OrderGoodsMapper orderGoodsMapper;
@Autowired
private ShopGoodsService shopGoodsService;
@Autowired
private ShopUserAddressService addressService;
@Autowired
private WxPayService wxPayService;
/**
* 创建订单
*/
@Override
@Transactional(rollbackFor = Exception.class)
public Map<String, String> createOrder(OrderCreateRequest request, User loginUser) {
// 1. 参数校验
validateOrderRequest(request, loginUser);
// 2. 构建订单对象
ShopOrder shopOrder = buildShopOrder(request, loginUser);
// 3. 应用业务规则
applyBusinessRules(shopOrder, loginUser);
// 4. 保存订单主表
boolean saved = shopOrderMapper.insert(shopOrder) > 0;
if (!saved) {
throw new BusinessException("订单保存失败");
}
// 5. 保存订单商品明细 - 核心业务逻辑
saveOrderGoods(shopOrder, request.getGoodsItems());
// 6. 创建微信支付订单
try {
return wxPayService.createWxOrder(shopOrder);
} catch (Exception e) {
log.error("创建微信支付订单失败,订单号:{}", shopOrder.getOrderNo(), e);
throw new BusinessException("创建支付订单失败:" + e.getMessage());
}
}
/**
* 保存订单商品明细 - 核心实现
*/
private void saveOrderGoods(ShopOrder shopOrder, List<OrderGoodsItem> goodsItems) {
List<OrderGoods> orderGoodsList = new ArrayList<>();
BigDecimal totalPrice = BigDecimal.ZERO;
int totalQuantity = 0;
List<String> goodsNames = new ArrayList<>();
for (OrderGoodsItem item : goodsItems) {
// 1. 获取商品最新信息进行校验
ShopGoods goods = shopGoodsService.getById(item.getGoodsId());
if (goods == null) {
throw new BusinessException("商品不存在:" + item.getGoodsId());
}
// 2. 商品状态校验
if (goods.getIsShow() != 1) {
throw new BusinessException("商品已下架:" + goods.getName());
}
// 3. 库存校验
if (goods.getStock() < item.getQuantity()) {
throw new BusinessException("商品库存不足:" + goods.getName());
}
// 4. 价格计算(以数据库中的价格为准)
BigDecimal itemPrice = new BigDecimal(goods.getPrice());
BigDecimal itemTotalPrice = itemPrice.multiply(new BigDecimal(item.getQuantity()));
// 5. 构建订单商品记录
OrderGoods orderGoods = new OrderGoods();
orderGoods.setOrderId(shopOrder.getOrderId());
orderGoods.setGoodsId(item.getGoodsId());
orderGoods.setTotalNum(item.getQuantity());
orderGoods.setPayPrice(itemTotalPrice.toString());
orderGoods.setType(0); // 0商城
orderGoods.setPayStatus("0"); // 0未付款
orderGoods.setOrderStatus(0); // 0未完成
orderGoods.setUserId(shopOrder.getUserId());
orderGoods.setTenantId(shopOrder.getTenantId());
orderGoods.setCreateTime(LocalDateTime.now());
// 6. SKU信息处理如果有规格
if (item.getSkuId() != null) {
// 处理SKU相关逻辑
// orderGoods.setSkuId(item.getSkuId());
// orderGoods.setSpecInfo(item.getSpecInfo());
}
orderGoodsList.add(orderGoods);
// 7. 累计计算
totalPrice = totalPrice.add(itemTotalPrice);
totalQuantity += item.getQuantity();
goodsNames.add(goods.getName());
// 8. 扣减库存(根据业务需求,可能在支付成功后扣减)
if (goods.getDeductStockType() == 10) { // 10下单减库存
goods.setStock(goods.getStock() - item.getQuantity());
shopGoodsService.updateById(goods);
}
}
// 9. 批量保存订单商品
if (!orderGoodsList.isEmpty()) {
orderGoodsMapper.insertBatch(orderGoodsList);
}
// 10. 更新订单总价和数量
shopOrder.setTotalPrice(totalPrice.toString());
shopOrder.setPayPrice(totalPrice.toString()); // 暂时不考虑优惠
shopOrder.setTotalNum(totalQuantity);
// 11. 生成订单标题限制30个汉字
String title = generateOrderTitle(goodsNames);
shopOrder.setTitle(title);
// 12. 更新订单主表
shopOrderMapper.updateById(shopOrder);
}
/**
* 生成订单标题限制长度不超过30个汉字
*/
private String generateOrderTitle(List<String> goodsNames) {
if (goodsNames.isEmpty()) {
return "商品订单";
}
String title;
if (goodsNames.size() == 1) {
title = goodsNames.get(0);
} else {
title = goodsNames.get(0) + "" + goodsNames.size() + "件商品";
}
// 限制标题长度最多30个汉字
if (title.length() > 30) {
title = title.substring(0, 30);
}
return title;
}
/**
* 参数校验
*/
private void validateOrderRequest(OrderCreateRequest request, User loginUser) {
if (request.getGoodsItems() == null || request.getGoodsItems().isEmpty()) {
throw new BusinessException("商品信息不能为空");
}
if (request.getAddressId() == null) {
throw new BusinessException("收货地址不能为空");
}
// 校验收货地址是否属于当前用户
ShopUserAddress address = addressService.getById(request.getAddressId());
if (address == null || !address.getUserId().equals(loginUser.getUserId())) {
throw new BusinessException("收货地址不存在或不属于当前用户");
}
// 校验商品数量
for (OrderGoodsItem item : request.getGoodsItems()) {
if (item.getGoodsId() == null || item.getQuantity() <= 0) {
throw new BusinessException("商品信息不正确");
}
}
}
/**
* 构建订单对象
*/
private ShopOrder buildShopOrder(OrderCreateRequest request, User loginUser) {
ShopOrder shopOrder = new ShopOrder();
// 基础信息
shopOrder.setOrderNo(generateOrderNo());
shopOrder.setType(0); // 0商城订单
shopOrder.setChannel(0); // 0小程序
shopOrder.setUserId(loginUser.getUserId());
shopOrder.setTenantId(loginUser.getTenantId());
// 用户信息
shopOrder.setRealName(loginUser.getRealName());
shopOrder.setPhone(loginUser.getPhone());
// 地址信息
ShopUserAddress address = addressService.getById(request.getAddressId());
shopOrder.setAddressId(request.getAddressId());
shopOrder.setAddress(address.getProvince() + address.getCity() +
address.getRegion() + address.getAddress());
// 支付信息
shopOrder.setPayType(request.getPayType());
shopOrder.setPayStatus(0); // 0未付款
shopOrder.setOrderStatus(0); // 0未使用
// 配送信息
shopOrder.setDeliveryType(request.getDeliveryType());
if (request.getSelfTakeMerchantId() != null) {
shopOrder.setSelfTakeMerchantId(request.getSelfTakeMerchantId());
}
// 其他信息
shopOrder.setComments(request.getComments());
shopOrder.setCreateTime(LocalDateTime.now());
return shopOrder;
}
/**
* 应用业务规则
*/
private void applyBusinessRules(ShopOrder shopOrder, User loginUser) {
// 设置默认标题(如果没有设置)
if (shopOrder.getTitle() == null) {
shopOrder.setTitle("商品订单");
}
// 其他业务规则...
}
/**
* 生成订单号
*/
private String generateOrderNo() {
return "SO" + System.currentTimeMillis() +
String.format("%04d", new Random().nextInt(10000));
}
}

View File

@@ -0,0 +1,154 @@
# 前端多规格功能测试指南
## 功能概述
已完成商品详情页多规格功能集成,包括:
- 规格数据加载
- 规格选择器组件
- 购物车支持SKU信息
- 立即购买支持SKU信息
## 测试步骤
### 1. 准备测试数据
在后端创建一个多规格商品,包含:
- 基础商品信息
- 规格组:颜色(红色、蓝色)、尺寸(S、M、L)
- 对应的SKU数据
### 2. 商品详情页测试
1. 访问商品详情页:`/shop/goodsDetail/index?id={商品ID}`
2. 检查是否正确加载:
- 商品基本信息
- 商品图片轮播
- 价格显示
### 3. 规格选择测试
1. 点击"加入购物车"按钮
2. 应该弹出规格选择器
3. 检查规格选择器内容:
- 商品图片和基本信息
- 规格组显示(颜色、尺寸)
- 规格值选项
- 数量选择器
### 4. 规格交互测试
1. 选择不同规格组合
2. 检查:
- SKU价格更新
- 库存数量更新
- 不可选规格置灰
- 数量限制(不超过库存)
### 5. 加入购物车测试
1. 选择完整规格
2. 设置购买数量
3. 点击确定
4. 检查:
- 成功提示
- 购物车数量更新
- 购物车页面显示规格信息
### 6. 立即购买测试
1. 点击"立即购买"按钮
2. 选择规格和数量
3. 点击确定
4. 检查是否正确跳转到订单确认页
## 预期行为
### 单规格商品
- 直接加入购物车/立即购买
- 不显示规格选择器
### 多规格商品
- 必须选择规格才能操作
- 显示规格选择器
- 根据选择更新价格和库存
## 数据流验证
### 1. API调用检查
打开浏览器开发者工具检查以下API调用
```
GET /shop/shop-goods/{id} // 商品详情
GET /shop/shop-goods-spec?goodsId={id} // 商品规格
GET /shop/shop-goods-sku?goodsId={id} // 商品SKU
```
### 2. 购物车数据检查
检查本地存储中的购物车数据:
```javascript
// 在浏览器控制台执行
JSON.parse(localStorage.getItem('cart_items') || '[]')
```
应该包含SKU信息
```json
[{
"goodsId": 123,
"name": "测试商品",
"price": "99.00",
"image": "...",
"quantity": 2,
"skuId": 456,
"specInfo": "颜色:红色|尺寸:L",
"addTime": 1640995200000
}]
```
## 常见问题排查
### 1. 规格选择器不显示
- 检查 `specs` 数组是否有数据
- 检查 `showSpecSelector` 状态
- 检查API返回数据格式
### 2. SKU匹配失败
- 检查规格值字符串格式
- 检查SKU数据中的 `sku` 字段格式
- 确认规格名称排序一致性
### 3. 价格不更新
- 检查SKU数据中的 `price` 字段
- 检查 `selectedSku` 状态更新
- 确认价格显示逻辑
### 4. 库存显示错误
- 检查SKU数据中的 `stock` 字段
- 检查库存为0时的处理逻辑
- 确认数量选择器的最大值限制
## 调试技巧
### 1. 控制台日志
在关键位置添加日志:
```javascript
console.log('Specs loaded:', specs);
console.log('SKUs loaded:', skus);
console.log('Selected SKU:', selectedSku);
```
### 2. React DevTools
使用React DevTools检查组件状态
- GoodsDetail组件的state
- SpecSelector组件的props和state
### 3. 网络面板
检查API请求和响应
- 请求参数是否正确
- 响应数据格式是否符合预期
- 是否有错误状态码
## 性能优化建议
1. **数据预加载**: 考虑在商品详情加载时同时加载规格数据
2. **缓存策略**: 对规格数据进行适当缓存
3. **懒加载**: 规格选择器可以考虑懒加载
4. **防抖处理**: 规格选择时的价格更新可以添加防抖
## 后续优化方向
1. **规格图片**: 支持规格值对应的商品图片
2. **规格预设**: 支持默认选中某个规格组合
3. **批量操作**: 支持批量添加不同规格的商品
4. **规格搜索**: 在规格较多时支持搜索功能

View File

@@ -0,0 +1,317 @@
/**
* 前端订单提交完整示例
* 展示如何使用新的订单API进行下单
*/
import React, { useState, useEffect } from 'react';
import Taro from '@tarojs/taro';
import { Button } from '@nutui/nutui-react-taro';
import { createOrder } from '@/api/shop/shopOrder';
import { OrderCreateRequest, OrderGoodsItem } from '@/api/shop/shopOrder/model';
import { ShopGoods } from '@/api/shop/shopGoods/model';
import { ShopUserAddress } from '@/api/shop/shopUserAddress/model';
import { generateOrderTitle } from '@/utils/common';
interface OrderExampleProps {
goods: ShopGoods;
address: ShopUserAddress;
quantity?: number;
}
const OrderExample: React.FC<OrderExampleProps> = ({
goods,
address,
quantity = 1
}) => {
const [loading, setLoading] = useState(false);
/**
* 单商品下单示例
*/
const handleSingleGoodsOrder = async () => {
if (!address) {
Taro.showToast({
title: '请选择收货地址',
icon: 'error'
});
return;
}
setLoading(true);
try {
// 1. 构建订单请求数据
const orderData: OrderCreateRequest = {
goodsItems: [
{
goodsId: goods.goodsId!,
quantity: quantity
}
],
addressId: address.id,
payType: 1, // 微信支付
comments: `购买${goods.name}`,
deliveryType: 0, // 快递配送
// 可选:自定义订单标题
title: generateOrderTitle([goods.name!])
};
// 2. 调用创建订单API
const result = await createOrder(orderData);
if (result && result.prepayId) {
// 3. 调用微信支付
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType,
paySign: result.paySign,
});
// 4. 支付成功处理
Taro.showToast({
title: '支付成功',
icon: 'success'
});
setTimeout(() => {
Taro.navigateTo({url: '/pages/order/order'});
}, 2000);
}
} catch (error: any) {
console.error('下单失败:', error);
Taro.showToast({
title: error.message || '下单失败',
icon: 'error'
});
} finally {
setLoading(false);
}
};
/**
* 购物车批量下单示例
*/
const handleCartOrder = async (cartItems: Array<{goodsId: number, quantity: number, goodsName: string}>) => {
if (!address) {
Taro.showToast({
title: '请选择收货地址',
icon: 'error'
});
return;
}
if (!cartItems || cartItems.length === 0) {
Taro.showToast({
title: '购物车为空',
icon: 'error'
});
return;
}
setLoading(true);
try {
// 1. 构建订单商品列表
const goodsItems: OrderGoodsItem[] = cartItems.map(item => ({
goodsId: item.goodsId,
quantity: item.quantity
}));
// 2. 生成订单标题
const goodsNames = cartItems.map(item => item.goodsName);
const orderTitle = generateOrderTitle(goodsNames);
// 3. 构建订单请求数据
const orderData: OrderCreateRequest = {
goodsItems,
addressId: address.id,
payType: 1, // 微信支付
comments: '购物车下单',
deliveryType: 0, // 快递配送
title: orderTitle
};
// 4. 调用创建订单API
const result = await createOrder(orderData);
if (result && result.prepayId) {
// 5. 调用微信支付
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType,
paySign: result.paySign,
});
// 6. 支付成功处理
Taro.showToast({
title: '支付成功',
icon: 'success'
});
// 7. 清空购物车(可选)
// clearCart();
setTimeout(() => {
Taro.navigateTo({url: '/pages/order/order'});
}, 2000);
}
} catch (error: any) {
console.error('下单失败:', error);
Taro.showToast({
title: error.message || '下单失败',
icon: 'error'
});
} finally {
setLoading(false);
}
};
/**
* 自提订单示例
*/
const handleSelfPickupOrder = async (merchantId: number) => {
setLoading(true);
try {
const orderData: OrderCreateRequest = {
goodsItems: [
{
goodsId: goods.goodsId!,
quantity: quantity
}
],
addressId: address.id,
payType: 1,
deliveryType: 1, // 自提
selfTakeMerchantId: merchantId,
comments: `自提订单 - ${goods.name}`,
title: generateOrderTitle([goods.name!])
};
const result = await createOrder(orderData);
if (result && result.prepayId) {
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType,
paySign: result.paySign,
});
Taro.showToast({
title: '下单成功,请到店自提',
icon: 'success'
});
setTimeout(() => {
Taro.navigateTo({url: '/pages/order/order'});
}, 2000);
}
} catch (error: any) {
console.error('下单失败:', error);
Taro.showToast({
title: error.message || '下单失败',
icon: 'error'
});
} finally {
setLoading(false);
}
};
/**
* 使用优惠券下单示例
*/
const handleOrderWithCoupon = async (couponId: number) => {
setLoading(true);
try {
const orderData: OrderCreateRequest = {
goodsItems: [
{
goodsId: goods.goodsId!,
quantity: quantity
}
],
addressId: address.id,
payType: 1,
couponId: couponId, // 使用优惠券
deliveryType: 0,
comments: `使用优惠券购买${goods.name}`,
title: generateOrderTitle([goods.name!])
};
const result = await createOrder(orderData);
if (result && result.prepayId) {
await Taro.requestPayment({
timeStamp: result.timeStamp,
nonceStr: result.nonceStr,
package: result.package,
signType: result.signType,
paySign: result.paySign,
});
Taro.showToast({
title: '支付成功',
icon: 'success'
});
setTimeout(() => {
Taro.navigateTo({url: '/pages/order/order'});
}, 2000);
}
} catch (error: any) {
console.error('下单失败:', error);
Taro.showToast({
title: error.message || '下单失败',
icon: 'error'
});
} finally {
setLoading(false);
}
};
return (
<div>
<Button
type="primary"
loading={loading}
onClick={handleSingleGoodsOrder}
>
</Button>
<Button
type="success"
loading={loading}
onClick={() => handleCartOrder([
{goodsId: goods.goodsId!, quantity: 1, goodsName: goods.name!}
])}
>
</Button>
<Button
type="warning"
loading={loading}
onClick={() => handleSelfPickupOrder(1)}
>
</Button>
<Button
type="info"
loading={loading}
onClick={() => handleOrderWithCoupon(123)}
>
使
</Button>
</div>
);
};
export default OrderExample;

View File

@@ -0,0 +1,189 @@
# 商品多规格功能集成总结
## 完成的工作
### 1. 前端功能集成 ✅
#### 商品详情页改造
- **文件**: `src/shop/goodsDetail/index.tsx`
- **新增功能**:
- 加载商品规格数据 (`listShopGoodsSpec`)
- 加载商品SKU数据 (`listShopGoodsSku`)
- 集成规格选择器组件
- 支持多规格加入购物车
- 支持多规格立即购买
#### 购物车系统升级
- **文件**: `src/hooks/useCart.ts`
- **改进内容**:
- `CartItem` 接口新增 `skuId``specInfo` 字段
- `addToCart` 函数支持SKU信息
- 购物车商品唯一性判断支持SKU区分
#### 规格选择器组件优化
- **文件**: `src/components/SpecSelector/index.tsx`
- **改进内容**:
- 支持 `action` 参数区分加入购物车和立即购买
- 优化回调函数参数传递
- 改进组件接口设计
### 2. 数据流设计 ✅
#### API调用流程
```
商品详情页加载
├── getShopGoods(goodsId) - 获取商品基本信息
├── listShopGoodsSpec(goodsId) - 获取商品规格
└── listShopGoodsSku(goodsId) - 获取商品SKU
```
#### 用户操作流程
```
用户点击加入购物车/立即购买
├── 检查是否有规格 (specs.length > 0)
├── 有规格: 显示规格选择器
│ ├── 用户选择规格组合
│ ├── 系统匹配对应SKU
│ ├── 更新价格和库存显示
│ └── 确认后执行对应操作
└── 无规格: 直接执行操作
```
#### 数据结构设计
```typescript
// 购物车商品项
interface CartItem {
goodsId: number;
name: string;
price: string;
image: string;
quantity: number;
addTime: number;
skuId?: number; // 新增: SKU ID
specInfo?: string; // 新增: 规格信息
}
// 订单商品项
interface OrderGoodsItem {
goodsId: number;
quantity: number;
skuId?: number; // 新增: SKU ID
specInfo?: string; // 新增: 规格信息
}
```
## 技术实现要点
### 1. 规格数据组织
- 规格按 `specName` 分组
- 规格值按 `specValue` 组织
- SKU通过规格值字符串匹配 (`sku` 字段)
### 2. SKU匹配算法
```typescript
// 构建规格值字符串,按规格名称排序确保一致性
const sortedSpecNames = specGroups.map(g => g.specName).sort();
const specValues = sortedSpecNames.map(name => selectedSpecs[name]).join('|');
const sku = skus.find(s => s.sku === specValues);
```
### 3. 购物车唯一性判断
```typescript
// 根据goodsId和skuId判断是否为同一商品
const existingItemIndex = newItems.findIndex(item =>
item.goodsId === goods.goodsId &&
(goods.skuId ? item.skuId === goods.skuId : !item.skuId)
);
```
## 需要后端配合的工作
### 1. API参数模型修改 🔄
- `ShopGoodsSpecParam` 需要添加 `goodsId` 字段
- `ShopGoodsSkuParam` 需要添加 `goodsId` 字段
### 2. 查询逻辑适配 🔄
- 规格查询接口支持按商品ID过滤
- SKU查询接口支持按商品ID过滤
### 3. 业务逻辑升级 🔄
- 购物车接口支持SKU信息存储
- 订单创建接口支持SKU信息处理
- 库存扣减逻辑适配多规格
- 价格计算逻辑适配多规格
## 测试验证
### 前端测试 ✅
- [x] 商品详情页规格数据加载
- [x] 规格选择器显示和交互
- [x] SKU匹配和价格更新
- [x] 购物车多规格商品支持
- [x] 立即购买多规格商品支持
### 后端测试 🔄
- [ ] API参数传递验证
- [ ] 规格数据查询验证
- [ ] SKU数据查询验证
- [ ] 购物车SKU信息存储
- [ ] 订单SKU信息处理
## 文档输出
1. **后端适配指南**: `docs/backend-multi-spec-integration.md`
- API接口修改要求
- 数据库表结构检查
- 业务逻辑适配建议
- 测试场景和检查清单
2. **前端测试指南**: `docs/frontend-multi-spec-test.md`
- 功能测试步骤
- 数据流验证方法
- 常见问题排查
- 调试技巧和优化建议
## 兼容性保证
### 向后兼容
- 单规格商品功能完全保持不变
- 现有购物车数据结构兼容
- 现有订单流程不受影响
### 渐进增强
- 多规格功能作为增强特性
- 规格数据不存在时自动降级为单规格模式
- 错误处理确保用户体验不受影响
## 下一步工作
### 短期 (1-2周)
1. 后端API适配完成
2. 端到端测试验证
3. 生产环境部署测试
### 中期 (1个月)
1. 性能优化和监控
2. 用户反馈收集和改进
3. 边界情况处理完善
### 长期 (3个月)
1. 规格图片支持
2. 批量操作功能
3. 高级规格管理功能
## 风险评估
### 低风险 ✅
- 前端功能实现完整
- 数据结构设计合理
- 向后兼容性良好
### 中风险 ⚠️
- 后端API适配工作量
- 数据迁移和兼容性
- 性能影响评估
### 缓解措施
- 详细的后端适配文档
- 完整的测试用例覆盖
- 分阶段部署和验证

View File

@@ -0,0 +1,190 @@
# 订单状态修复总结
## 问题分析
### 1. 数据类型不一致
- **问题**: `payStatus` 字段在模型中定义为 `boolean` 类型,但代码中按数字处理
- **影响**: 导致支付状态判断错误,"待付款"状态显示不正确
### 2. 状态判断逻辑错误
- **问题**: 状态判断优先级不正确,没有按照业务逻辑顺序检查
- **影响**: 订单状态显示混乱,用户看到错误的订单状态
### 3. 操作按钮显示错误
- **问题**: 按钮显示条件与实际订单状态不匹配
- **影响**: 用户在错误的状态下看到不应该出现的操作按钮
### 4. Tab筛选逻辑不完整
- **问题**: 缺少"待收货"状态的筛选,状态分类不够细致
- **影响**: 用户无法准确筛选不同状态的订单
## 修复内容
### 1. 订单状态判断逻辑优化 ✅
**文件**: `src/pages/order/components/OrderList.tsx`
**修复前**:
```typescript
const getOrderStatusText = (order: ShopOrder) => {
if (!order.payStatus) return '待付款';
if (order.payStatus && order.deliveryStatus === 10) return '待发货';
// ... 其他逻辑
};
```
**修复后**:
```typescript
const getOrderStatusText = (order: ShopOrder) => {
// 优先检查订单状态
if (order.orderStatus === 2) return '已取消';
if (order.orderStatus === 4) return '退款申请中';
// ... 其他退款相关状态
// 检查支付状态 (payStatus为boolean类型)
if (!order.payStatus || order.payStatus === false) return '待付款';
// 已付款后检查发货状态
if (order.deliveryStatus === 10) return '待发货';
if (order.deliveryStatus === 20) return '待收货';
if (order.deliveryStatus === 30) return '已收货';
// 最后检查订单完成状态
if (order.orderStatus === 1) return '已完成';
return '未知状态';
};
```
### 2. Tab筛选功能完善 ✅
**新增"待收货"状态**:
```typescript
const tabs = [
{ index: 0, key: '全部', title: '全部' },
{ index: 1, key: '待付款', title: '待付款' },
{ index: 2, key: '待发货', title: '待发货' },
{ index: 3, key: '待收货', title: '待收货' }, // 新增
{ index: 4, key: '已收货', title: '已收货' },
{ index: 5, key: '已完成', title: '已完成' }
];
```
### 3. 操作按钮逻辑修复 ✅
**修复前**:
```typescript
{item.payStatus && (
<Space>
<Button onClick={() => cancelOrder(item)}>取消订单</Button>
<Button type="primary">立即支付</Button>
</Space>
)}
```
**修复后**:
```typescript
{/* 待付款状态:显示取消订单和立即支付 */}
{(!item.payStatus || item.payStatus === false) && item.orderStatus !== 2 && (
<Space>
<Button onClick={() => cancelOrder(item)}>取消订单</Button>
<Button type="primary">立即支付</Button>
</Space>
)}
```
### 4. 订单详情页状态显示修复 ✅
**文件**: `src/shop/orderDetail/index.tsx`
- 统一状态判断逻辑
- 修复函数调用参数
- 确保与订单列表页面的状态显示一致
## 订单状态流程图
```
订单创建
待付款 (payStatus: false)
↓ (用户支付)
待发货 (payStatus: true, deliveryStatus: 10)
↓ (商家发货)
待收货 (payStatus: true, deliveryStatus: 20)
↓ (用户确认收货)
已收货 (payStatus: true, deliveryStatus: 30)
↓ (系统自动或手动完成)
已完成 (orderStatus: 1)
// 异常流程
任意状态 → 已取消 (orderStatus: 2)
已完成 → 退款申请中 (orderStatus: 4)
退款申请中 → 退款成功 (orderStatus: 6)
```
## 字段含义说明
### payStatus (支付状态)
- **类型**: `boolean`
- **值**: `false/0` = 未付款, `true/1` = 已付款
### deliveryStatus (发货状态)
- **类型**: `number`
- **值**:
- `10` = 未发货/待发货
- `20` = 已发货/待收货
- `30` = 已收货
### orderStatus (订单状态)
- **类型**: `number`
- **值**:
- `0` = 未使用
- `1` = 已完成
- `2` = 已取消
- `3` = 取消中
- `4` = 退款申请中
- `5` = 退款被拒绝
- `6` = 退款成功
- `7` = 客户端申请退款
## 测试验证
### 1. 状态显示测试
- [ ] 创建不同状态的测试订单
- [ ] 验证订单列表页面状态显示正确
- [ ] 验证订单详情页面状态显示正确
- [ ] 验证状态文本与实际订单状态匹配
### 2. Tab筛选测试
- [ ] 测试"全部"tab显示所有订单
- [ ] 测试"待付款"tab只显示未支付订单
- [ ] 测试"待发货"tab只显示已支付待发货订单
- [ ] 测试"待收货"tab只显示已发货待收货订单
- [ ] 测试"已收货"tab只显示已收货订单
- [ ] 测试"已完成"tab只显示已完成订单
### 3. 操作按钮测试
- [ ] 待付款状态显示"取消订单"和"立即支付"按钮
- [ ] 待收货状态显示"确认收货"按钮
- [ ] 已完成状态显示"申请退款"按钮
- [ ] 其他状态不显示不相关按钮
### 4. 状态流转测试
- [ ] 测试支付后状态从"待付款"变为"待发货"
- [ ] 测试发货后状态从"待发货"变为"待收货"
- [ ] 测试确认收货后状态从"待收货"变为"已收货"
- [ ] 测试取消订单功能
## 注意事项
1. **数据类型一致性**: 确保前后端对 `payStatus` 字段类型的处理一致
2. **状态优先级**: 按照业务逻辑正确设置状态判断优先级
3. **用户体验**: 确保状态显示清晰,操作按钮符合用户预期
4. **异常处理**: 对于未知状态要有合适的默认显示
## 后续优化建议
1. **状态枚举**: 考虑使用枚举类型定义订单状态,提高代码可读性
2. **状态机**: 实现订单状态机,确保状态流转的合法性
3. **国际化**: 支持订单状态文本的多语言显示
4. **实时更新**: 考虑实现订单状态的实时推送更新

277
docs/useUser-hook-guide.md Normal file
View File

@@ -0,0 +1,277 @@
# useUser Hook 使用指南
## 概述
`useUser` hook 是一个用于管理用户状态的自定义 React Hook类似于项目中的 `useCart` hook。它提供了用户登录状态管理、用户信息获取和更新、权限检查等功能方便在整个应用中全局调用。
## 功能特性
- ✅ 用户登录状态管理
- ✅ 用户信息本地存储和同步
- ✅ 从服务器获取最新用户信息
- ✅ 用户信息更新
- ✅ 权限和角色检查
- ✅ 实名认证状态检查
- ✅ 用户余额和积分获取
- ✅ 自动处理登录过期
## 基本用法
### 1. 导入 Hook
```typescript
import { useUser } from '@/hooks/useUser';
```
### 2. 在组件中使用
```typescript
const MyComponent = () => {
const {
user, // 用户信息
isLoggedIn, // 是否已登录
loading, // 加载状态
loginUser, // 登录方法
logoutUser, // 退出登录方法
fetchUserInfo, // 获取用户信息
updateUser, // 更新用户信息
getDisplayName, // 获取显示名称
isCertified, // 是否已实名认证
getBalance, // 获取余额
getPoints // 获取积分
} = useUser();
// 使用用户信息
if (loading) {
return <div>加载中...</div>;
}
if (!isLoggedIn) {
return <div>请先登录</div>;
}
return (
<div>
<h1>欢迎,{getDisplayName()}</h1>
<p>余额:¥{getBalance()}</p>
<p>积分:{getPoints()}</p>
{isCertified() && <span>已实名认证</span>}
</div>
);
};
```
## API 参考
### 状态属性
| 属性 | 类型 | 描述 |
|------|------|------|
| `user` | `User \| null` | 当前用户信息 |
| `isLoggedIn` | `boolean` | 用户是否已登录 |
| `loading` | `boolean` | 是否正在加载 |
### 方法
#### `loginUser(token: string, userInfo: User)`
用户登录,保存用户信息和 token 到本地存储。
```typescript
const handleLogin = async () => {
const { access_token, user } = await loginApi(credentials);
loginUser(access_token, user);
};
```
#### `logoutUser()`
用户退出登录,清除本地存储的用户信息。
```typescript
const handleLogout = () => {
logoutUser();
// 跳转到首页或登录页
};
```
#### `fetchUserInfo()`
从服务器获取最新的用户信息。
```typescript
const refreshUserInfo = async () => {
const userInfo = await fetchUserInfo();
console.log('最新用户信息:', userInfo);
};
```
#### `updateUser(userData: Partial<User>)`
更新用户信息。
```typescript
const updateProfile = async () => {
try {
await updateUser({
nickname: '新昵称',
avatar: 'new-avatar-url'
});
console.log('更新成功');
} catch (error) {
console.error('更新失败:', error);
}
};
```
### 工具方法
#### `hasPermission(permission: string)`
检查用户是否有特定权限。
```typescript
if (hasPermission('user:edit')) {
// 显示编辑按钮
}
```
#### `hasRole(roleCode: string)`
检查用户是否有特定角色。
```typescript
if (hasRole('admin')) {
// 显示管理员功能
}
```
#### `getAvatarUrl()`
获取用户头像 URL。
```typescript
<Avatar src={getAvatarUrl()} />
```
#### `getDisplayName()`
获取用户显示名称(优先级:昵称 > 真实姓名 > 用户名)。
```typescript
<span>欢迎,{getDisplayName()}</span>
```
#### `isCertified()`
检查用户是否已实名认证。
```typescript
{isCertified() && <Badge>已认证</Badge>}
```
#### `getBalance()`
获取用户余额。
```typescript
<span>余额:¥{getBalance()}</span>
```
#### `getPoints()`
获取用户积分。
```typescript
<span>积分:{getPoints()}</span>
```
## 使用场景
### 1. 用户资料页面
```typescript
const UserProfile = () => {
const { user, updateUser, getDisplayName, getAvatarUrl } = useUser();
const handleUpdateProfile = async (formData) => {
await updateUser(formData);
};
return (
<div>
<Avatar src={getAvatarUrl()} />
<h2>{getDisplayName()}</h2>
<ProfileForm onSubmit={handleUpdateProfile} />
</div>
);
};
```
### 2. 权限控制
```typescript
const AdminPanel = () => {
const { hasRole, hasPermission } = useUser();
if (!hasRole('admin')) {
return <div>无权限访问</div>;
}
return (
<div>
{hasPermission('user:delete') && (
<Button danger>删除用户</Button>
)}
</div>
);
};
```
### 3. 登录状态检查
```typescript
const ProtectedComponent = () => {
const { isLoggedIn, loading } = useUser();
if (loading) return <Loading />;
if (!isLoggedIn) {
return <LoginPrompt />;
}
return <ProtectedContent />;
};
```
### 4. 用户余额显示
```typescript
const WalletCard = () => {
const { getBalance, getPoints, fetchUserInfo } = useUser();
const refreshBalance = () => {
fetchUserInfo(); // 刷新用户信息包括余额
};
return (
<div>
<div>余额:¥{getBalance()}</div>
<div>积分:{getPoints()}</div>
<Button onClick={refreshBalance}>刷新</Button>
</div>
);
};
```
## 注意事项
1. **自动登录过期处理**:当 API 返回 401 错误时hook 会自动清除登录状态。
2. **本地存储同步**:用户信息会自动同步到本地存储,页面刷新后状态会保持。
3. **错误处理**:所有异步操作都包含错误处理,失败时会显示相应的提示信息。
4. **性能优化**:用户信息只在必要时从服务器获取,避免不必要的网络请求。
## 与 useCart 的对比
| 特性 | useCart | useUser |
|------|---------|---------|
| 数据存储 | 购物车商品 | 用户信息 |
| 本地持久化 | ✅ | ✅ |
| 服务器同步 | ❌ | ✅ |
| 状态管理 | ✅ | ✅ |
| 全局访问 | ✅ | ✅ |
| 权限控制 | ❌ | ✅ |
这样,用户信息管理就像购物车一样方便了,可以在任何组件中轻松访问和操作用户状态!

View File

@@ -0,0 +1,41 @@
# 水票配送订单:后端提示词(可直接发给后端)
## 1) 订单查询(配送员端)
请在 `GET /glt/glt-ticket-order/page` 支持以下筛选,并保证权限隔离:
- `riderId`:只返回该配送员的订单(必要)
- `deliveryStatus`10待配送、20配送中、30待客户确认、40已完成必要
- 排序:建议 `sendTime asc` + `createTime desc`(或给前端一个可控排序字段)
## 2) 配送流程字段(建议后端落库并回传)
订单表建议确保有以下字段(当前前端已按这些字段做流程判断/展示):
- `riderId/riderName/riderPhone`:配送员信息
- `deliveryStatus`10/20/30/40
- `sendStartTime`:配送员点击“开始配送”的时间
- `sendEndTime`:配送员点击“确认送达”的时间
- `sendEndImg`:送达拍照留档图片 URL可选/必填由后端策略决定)
- `receiveConfirmTime`:客户确认收货时间
- `receiveConfirmType`10客户手动确认、20配送照片自动确认、30超时自动确认
## 3) 状态流转与校验(强烈建议在后端做)
请在更新订单时做状态机校验,避免前端绕过流程:
- `10 -> 20`:仅允许订单属于当前配送员,且未开始/未送达
- `20 -> 30`:配送员确认送达(可带 `sendEndImg`
- `20/30 -> 40`:完成;来源可能是
- 客户手动确认(写 `receiveConfirmTime` + `receiveConfirmType=10`
- 配送照片直接完成(写 `receiveConfirmTime` + `receiveConfirmType=20`,并要求 `sendEndImg`
- 超时自动确认(写 `receiveConfirmTime` + `receiveConfirmType=30`,建议由定时任务执行)
## 4) 建议新增/明确的接口能力
为了避免并发抢单/越权更新,建议新增更语义化的接口(或在 update 内做等价校验):
- 接单(抢单/派单):`POST /glt/glt-ticket-order/{id}/accept`
- 后端原子校验:仅当 `riderId is null` 才能写入当前 rider 信息
- 开始配送:`POST /glt/glt-ticket-order/{id}/start`(写 `sendStartTime` + `deliveryStatus=20`
- 确认送达:`POST /glt/glt-ticket-order/{id}/delivered`(写 `sendEndTime` + `deliveryStatus=30` + 可选 `sendEndImg`
- 客户确认收货:`POST /glt/glt-ticket-order/{id}/confirm-receive`
- 校验:只能本人 `userId` 操作,且必须已送达
## 5) 为了“导航到收货地址/取货点”的字段补充(建议)
当前仅有 `address` 字符串,无法在小程序内 `openLocation` 精准导航;建议补充:
- 收货地址:`receiverName``receiverPhone``province/city/district/detail``latitude/longitude`
- 取货点(门店/仓库):`storeLatitude/storeLongitude``warehouseLatitude/warehouseLongitude`

View File

@@ -41,7 +41,7 @@
"@nutui/icons-react-taro": "^2.0.1", "@nutui/icons-react-taro": "^2.0.1",
"@nutui/nutui-biz": "1.0.0-beta.2", "@nutui/nutui-biz": "1.0.0-beta.2",
"@nutui/nutui-react": "^3.0.16", "@nutui/nutui-react": "^3.0.16",
"@nutui/nutui-react-taro": "^2.7.10", "@nutui/nutui-react-taro": "^2.7.4",
"@react-native/metro-config": "^0.73.2", "@react-native/metro-config": "^0.73.2",
"@tarojs/components": "4.0.8", "@tarojs/components": "4.0.8",
"@tarojs/components-rn": "^4.1.4", "@tarojs/components-rn": "^4.1.4",

29
pnpm-lock.yaml generated
View File

@@ -21,8 +21,8 @@ importers:
specifier: ^3.0.16 specifier: ^3.0.16
version: 3.0.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 3.0.16(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@nutui/nutui-react-taro': '@nutui/nutui-react-taro':
specifier: ^2.7.10 specifier: ^2.7.4
version: 2.7.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1) version: 2.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@react-native/metro-config': '@react-native/metro-config':
specifier: ^0.73.2 specifier: ^0.73.2
version: 0.73.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0)) version: 0.73.5(@babel/core@7.26.0)(@babel/preset-env@7.26.0(@babel/core@7.26.0))
@@ -1406,7 +1406,7 @@ packages:
deprecated: This functionality has been moved to @npmcli/fs deprecated: This functionality has been moved to @npmcli/fs
'@nutui/icons-react-taro@1.0.5': '@nutui/icons-react-taro@1.0.5':
resolution: {integrity: sha512-p7dCW29wASH/qQ1OaUGGKA6PRV33wDPb80+qrHnWtT40syIb0W+e92mpplbULWM01s+GYVGyUU3i8b7Iy7qfvw==} resolution: {integrity: sha512-p7dCW29wASH/qQ1OaUGGKA6PRV33wDPb80+qrHnWtT40syIb0W+e92mpplbULWM01s+GYVGyUU3i8b7Iy7qfvw==, tarball: https://registry.npmmirror.com/@nutui/icons-react-taro/-/icons-react-taro-1.0.5.tgz}
'@nutui/icons-react-taro@2.0.1': '@nutui/icons-react-taro@2.0.1':
resolution: {integrity: sha512-/DYmt8Rfp0NGx37/67Nd+k85zB2sJMLjlJiLpLbKxXk75SY0inwka51HhgawFTUk53zeta0CH/sDscTZdN005w==, tarball: https://registry.npmmirror.com/@nutui/icons-react-taro/-/icons-react-taro-2.0.1.tgz} resolution: {integrity: sha512-/DYmt8Rfp0NGx37/67Nd+k85zB2sJMLjlJiLpLbKxXk75SY0inwka51HhgawFTUk53zeta0CH/sDscTZdN005w==, tarball: https://registry.npmmirror.com/@nutui/icons-react-taro/-/icons-react-taro-2.0.1.tgz}
@@ -1431,8 +1431,8 @@ packages:
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0
'@nutui/nutui-react-taro@2.7.14': '@nutui/nutui-react-taro@2.7.4':
resolution: {integrity: sha512-BATiRezhEMdL/UyYZfwEq5EJMmHj4MbWcEZCVaHCod9ftGK+HPCyPZBiqoCRC2q7qVXXicYoDbJ98iPtvqMdzw==} resolution: {integrity: sha512-r47l2rkY5HbObyTHxt2ZCTMKolM+v9CxX7QwSQGyuVRCi5G5cwPbSEz3NucvWGyZ69NaD3XA4Oc2LumLhaHmGg==, tarball: https://registry.npmmirror.com/@nutui/nutui-react-taro/-/nutui-react-taro-2.7.4.tgz}
peerDependencies: peerDependencies:
react: ^16.8.0 || ^17.0.0 || ^18.0.0 react: ^16.8.0 || ^17.0.0 || ^18.0.0
@@ -3664,7 +3664,7 @@ packages:
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
classnames@2.5.1: classnames@2.5.1:
resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==} resolution: {integrity: sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==, tarball: https://registry.npmmirror.com/classnames/-/classnames-2.5.1.tgz}
clean-css@4.2.4: clean-css@4.2.4:
resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==, tarball: https://registry.npmmirror.com/clean-css/-/clean-css-4.2.4.tgz} resolution: {integrity: sha512-EJUDT7nDVFDvaQgAo2G/PJvxmp1o/c6iXLbswsBbUFXi1Nr+AjA2cKmfbKDMjMvzEe75g3P6JkaDDAKk96A85A==, tarball: https://registry.npmmirror.com/clean-css/-/clean-css-4.2.4.tgz}
@@ -5151,7 +5151,7 @@ packages:
deprecated: This package is no longer supported. deprecated: This package is no longer supported.
fs.realpath@1.0.0: fs.realpath@1.0.0:
resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==, tarball: https://registry.npmmirror.com/fs.realpath/-/fs.realpath-1.0.0.tgz}
fsevents@2.3.3: fsevents@2.3.3:
resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==}
@@ -5274,7 +5274,7 @@ packages:
deprecated: Glob versions prior to v9 are no longer supported deprecated: Glob versions prior to v9 are no longer supported
glob@7.2.3: glob@7.2.3:
resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==, tarball: https://registry.npmmirror.com/glob/-/glob-7.2.3.tgz}
deprecated: Glob versions prior to v9 are no longer supported deprecated: Glob versions prior to v9 are no longer supported
glob@8.1.0: glob@8.1.0:
@@ -5703,7 +5703,7 @@ packages:
resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==} resolution: {integrity: sha512-IClj+Xz94+d7irH5qRyfJonOdfTzuDaifE6ZPWfx0N0+/ATZCbuTPq2prFl526urkQd90WyUKIh1DfBQ2hMz9A==}
inflight@1.0.6: inflight@1.0.6:
resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==} resolution: {integrity: sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==, tarball: https://registry.npmmirror.com/inflight/-/inflight-1.0.6.tgz}
deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful. deprecated: This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.
inherits@2.0.3: inherits@2.0.3:
@@ -6506,8 +6506,7 @@ packages:
resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==, tarball: https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz} resolution: {integrity: sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==, tarball: https://registry.npmmirror.com/lodash.debounce/-/lodash.debounce-4.0.8.tgz}
lodash.isequal@4.5.0: lodash.isequal@4.5.0:
resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==} resolution: {integrity: sha512-pDo3lu8Jhfjqls6GkMgpahsF9kCyayhgykjyLMNFTKWrpVdAQtYyB4muAMWozBB4ig/dtWAmsMxLEI8wuz+DYQ==, tarball: https://registry.npmmirror.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz}
deprecated: This package is deprecated. Use require('node:util').isDeepStrictEqual instead.
lodash.kebabcase@4.1.1: lodash.kebabcase@4.1.1:
resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==, tarball: https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz} resolution: {integrity: sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==, tarball: https://registry.npmmirror.com/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz}
@@ -7283,7 +7282,7 @@ packages:
engines: {node: '>= 0.8'} engines: {node: '>= 0.8'}
once@1.4.0: once@1.4.0:
resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==} resolution: {integrity: sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==, tarball: https://registry.npmmirror.com/once/-/once-1.4.0.tgz}
onetime@2.0.1: onetime@2.0.1:
resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==} resolution: {integrity: sha512-oyyPpiMaKARvvcgip+JV+7zci5L8D1W9RZIz2l1o08AM3pfspitVWnPt3mzHcBPp12oYMTy0pqrFs/C+m3EwsQ==}
@@ -7489,7 +7488,7 @@ packages:
engines: {node: '>=8'} engines: {node: '>=8'}
path-is-absolute@1.0.1: path-is-absolute@1.0.1:
resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==} resolution: {integrity: sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==, tarball: https://registry.npmmirror.com/path-is-absolute/-/path-is-absolute-1.0.1.tgz}
engines: {node: '>=0.10.0'} engines: {node: '>=0.10.0'}
path-is-inside@1.0.2: path-is-inside@1.0.2:
@@ -10053,7 +10052,7 @@ packages:
engines: {node: '>=12'} engines: {node: '>=12'}
wrappy@1.0.2: wrappy@1.0.2:
resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==, tarball: https://registry.npmmirror.com/wrappy/-/wrappy-1.0.2.tgz}
write-file-atomic@2.4.3: write-file-atomic@2.4.3:
resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==} resolution: {integrity: sha512-GaETH5wwsX+GcnzhPgKcKjJ6M2Cq3/iZp1WyY/X1CSqrW+jVNM9Y7D8EC2sM4ZG/V8wZlSniJnCKWPmBYAucRQ==}
@@ -11799,7 +11798,7 @@ snapshots:
transitivePeerDependencies: transitivePeerDependencies:
- react-dom - react-dom
'@nutui/nutui-react-taro@2.7.14(react-dom@18.3.1(react@18.3.1))(react@18.3.1)': '@nutui/nutui-react-taro@2.7.4(react-dom@18.3.1(react@18.3.1))(react@18.3.1)':
dependencies: dependencies:
'@babel/runtime': 7.26.0 '@babel/runtime': 7.26.0
'@nutui/icons-react-taro': 1.0.5 '@nutui/icons-react-taro': 1.0.5

View File

@@ -0,0 +1,255 @@
import {Button} from '@nutui/nutui-react-taro'
import {Avatar, Tag} from '@nutui/nutui-react-taro'
import {getUserInfo, getWxOpenId} from '@/api/layout';
import Taro from '@tarojs/taro';
import {useEffect, useState} from "react";
import {User} from "@/api/system/user/model";
import navTo from "@/utils/common";
import {TenantId} from "@/config/app";
import {getMyAvailableCoupons} from "@/api/shop/shopUserCoupon";
import {useUser} from "@/hooks/useUser";
import {getStoredInviteParams} from "@/utils/invite";
function UserCard() {
const {getDisplayName, getRoleName} = useUser();
const [IsLogin, setIsLogin] = useState<boolean>(false)
const [userInfo, setUserInfo] = useState<User>()
const [couponCount, setCouponCount] = useState(0)
// const [pointsCount, setPointsCount] = useState(0)
const [giftCount, setGiftCount] = useState(0)
useEffect(() => {
// Taro.getSetting获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
Taro.getSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户已经授权过,可以直接获取用户信息
console.log('用户已经授权过,可以直接获取用户信息')
reload();
} else {
// 用户未授权,需要弹出授权窗口
console.log('用户未授权,需要弹出授权窗口')
showAuthModal();
}
}
});
}, []);
const loadUserStats = (userId: number) => {
// 加载优惠券数量
getMyAvailableCoupons()
.then((coupons: any) => {
setCouponCount(coupons?.length || 0)
})
.catch((error: any) => {
console.error('Coupon count error:', error)
})
// 加载积分数量
console.log(userId)
// getUserPointsStats(userId)
// .then((res: any) => {
// setPointsCount(res.currentPoints || 0)
// })
// .catch((error: any) => {
// console.error('Points stats error:', error)
// })
// 加载礼品劵数量
setGiftCount(0)
// pageUserGiftLog({userId, page: 1, limit: 1}).then(res => {
// setGiftCount(res.count || 0)
// })
}
const reload = () => {
Taro.getUserInfo({
success: (res) => {
const avatar = res.userInfo.avatarUrl;
setUserInfo({
avatar,
nickname: res.userInfo.nickName,
sexName: res.userInfo.gender == 1 ? '男' : '女'
})
getUserInfo().then((data) => {
if (data) {
setUserInfo(data)
setIsLogin(true);
Taro.setStorageSync('UserId', data.userId)
// 加载用户统计数据
if (data.userId) {
loadUserStats(data.userId)
}
// 获取openId
if (!data.openid) {
Taro.login({
success: (res) => {
getWxOpenId({code: res.code}).then(() => {
})
}
})
}
}
}).catch(() => {
console.log('未登录')
});
}
});
};
const showAuthModal = () => {
Taro.showModal({
title: '授权提示',
content: '需要获取您的用户信息',
confirmText: '去授权',
cancelText: '取消',
success: (res) => {
if (res.confirm) {
// 用户点击确认,打开授权设置页面
openSetting();
}
}
});
};
const openSetting = () => {
// Taro.openSetting调起客户端小程序设置界面返回用户设置的操作结果。设置界面只会出现小程序已经向用户请求过的权限。
Taro.openSetting({
success: (res) => {
if (res.authSetting['scope.userInfo']) {
// 用户授权成功,可以获取用户信息
reload();
} else {
// 用户拒绝授权,提示授权失败
Taro.showToast({
title: '授权失败',
icon: 'none'
});
}
}
});
};
/* 获取用户手机号 */
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
const {code, encryptedData, iv} = detail
// 获取存储的邀请参数
const inviteParams = getStoredInviteParams()
const refereeId = inviteParams?.inviter ? parseInt(inviteParams.inviter) : 0
Taro.login({
success: function () {
if (code) {
Taro.request({
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
method: 'POST',
data: {
code,
encryptedData,
iv,
notVerifyPhone: true,
refereeId: refereeId, // 使用解析出的推荐人ID
sceneType: 'save_referee',
tenantId: TenantId
},
header: {
'content-type': 'application/json',
TenantId
},
success: function (res) {
if (res.data.code == 1) {
Taro.showToast({
title: res.data.message,
icon: 'error',
duration: 2000
})
return false;
}
// 登录成功
Taro.setStorageSync('access_token', res.data.data.access_token)
Taro.setStorageSync('UserId', res.data.data.user.userId)
setUserInfo(res.data.data.user)
setIsLogin(true)
}
})
} else {
console.log('登录失败!')
}
}
})
}
return (
<div className={'header-bg pt-20'}>
<div className={'p-4'}>
<div
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// width: '720rpx',
// margin: '10px auto 0px auto',
height: '170px',
// borderRadius: '22px 22px 0 0',
}}
>
<div className={'user-card-header flex w-full justify-between items-center pt-4'}>
<div className={'flex items-center mx-4'}>
{
IsLogin ? (
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
) : (
<Button className={'text-black'} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
<Avatar size="large" src={userInfo?.avatar} shape="round"/>
</Button>
)
}
<div className={'user-info flex flex-col px-2'}>
<div className={'py-1 text-black font-bold'}>{getDisplayName()}</div>
{IsLogin ? (
<div className={'grade text-xs py-1'}>
<Tag type="success" round>
<div className={'p-1'}>
{getRoleName()}
</div>
</Tag>
</div>
) : ''}
</div>
</div>
<div className={'mx-4 text-sm px-3 py-1 text-black border-gray-400 border-solid border-2 rounded-3xl'}
onClick={() => navTo('/user/profile/profile', true)}>
{'个人资料'}
</div>
</div>
<div className={'flex justify-around mt-1'}>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/wallet/wallet', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>¥ {userInfo?.balance || '0.00'}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/coupon/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{couponCount}</span>
</div>
<div className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/gift/index', true)}>
<span className={'text-sm text-gray-500'}></span>
<span className={'text-xl'}>{giftCount}</span>
</div>
{/*<div className={'item flex justify-center flex-col items-center'}>*/}
{/* <span className={'text-sm text-gray-500'}>积分</span>*/}
{/* <span className={'text-xl'}>{pointsCount}</span>*/}
{/*</div>*/}
</div>
</div>
</div>
</div>
)
}
export default UserCard;

View File

@@ -0,0 +1,186 @@
import {Cell} from '@nutui/nutui-react-taro'
import navTo from "@/utils/common";
import Taro from '@tarojs/taro'
import {View, Text} from '@tarojs/components'
import {ArrowRight, ShieldCheck, LogisticsError, Location, Reward, Tips, Ask, Setting, Scan} from '@nutui/icons-react-taro'
import {useUser} from '@/hooks/useUser'
const UserCell = () => {
const {logoutUser, isCertified, hasRole, isAdmin} = useUser();
const onLogout = () => {
Taro.showModal({
title: '提示',
content: '确定要退出登录吗?',
success: function (res) {
if (res.confirm) {
// 使用 useUser hook 的 logoutUser 方法
logoutUser();
Taro.reLaunch({
url: '/pages/index/index'
})
}
}
})
}
return (
<>
<View className={'px-4'}>
{/*是否分销商*/}
{!hasRole('dealer') && !isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #54a799, #177b73)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/dealer/index', true)}>
<Reward className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
<Text className={'text-white opacity-80 pl-3'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
{/*是否管理员*/}
{isAdmin() && (
<Cell
className="nutui-cell-clickable"
style={{
backgroundImage: 'linear-gradient(to right bottom, #ff8e0c, #ed680d)',
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}} onClick={() => navTo('/admin/article/index', true)}>
<Setting className={'text-orange-100 '} size={16}/>
<Text style={{fontSize: '16px'}} className={'pl-3 text-orange-100 font-medium'}></Text>
</View>
}
extra={<ArrowRight color="#cccccc" size={18}/>}
/>
)}
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Scan size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
style={{
display: 'none'
}}
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<LogisticsError size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/wallet/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Location size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/address/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<ShieldCheck size={16} color={isCertified() ? '#52c41a' : '#666'}/>
<Text className={'pl-3 text-sm'}></Text>
{isCertified() && (
<Text className={'pl-2 text-xs text-green-500'}></Text>
)}
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/userVerify/index', true)
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Ask size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/help/index')
}}
/>
<Cell
className="nutui-cell-clickable"
title={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Tips size={16}/>
<Text className={'pl-3 text-sm'}></Text>
</View>
}
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => {
navTo('/user/about/index')
}}
/>
</Cell.Group>
<Cell.Group divider={true} description={
<View style={{display: 'inline-flex', alignItems: 'center'}}>
<Text style={{marginTop: '12px'}}></Text>
</View>
}>
<Cell
className="nutui-cell-clickable"
title="账号安全"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={() => navTo('/user/profile/profile', true)}
/>
<Cell
className="nutui-cell-clickable"
title="退出登录"
align="center"
extra={<ArrowRight color="#cccccc" size={18}/>}
onClick={onLogout}
/>
</Cell.Group>
</View>
</>
)
}
export default UserCell

View File

@@ -0,0 +1,102 @@
import {loginBySms} from "@/api/passport/login";
import {useState} from "react";
import Taro from '@tarojs/taro'
import {Popup} from '@nutui/nutui-react-taro'
import {UserParam} from "@/api/system/user/model";
import {Button} from '@nutui/nutui-react-taro'
import {Form, Input} from '@nutui/nutui-react-taro'
import {Copyright, Version} from "@/config/app";
const UserFooter = () => {
const [openLoginByPhone, setOpenLoginByPhone] = useState(false)
const [clickNum, setClickNum] = useState<number>(0)
const [FormData, setFormData] = useState<UserParam>(
{
phone: undefined,
password: undefined
}
)
const onLoginByPhone = () => {
setFormData({})
setClickNum(clickNum + 1);
if (clickNum > 10) {
setOpenLoginByPhone(true);
}
}
const closeLoginByPhone = () => {
setClickNum(0)
setOpenLoginByPhone(false)
}
// 提交表单
const submitByPhone = (values: any) => {
loginBySms({
phone: values.phone,
code: values.code
}).then(() => {
setOpenLoginByPhone(false);
setTimeout(() => {
Taro.reLaunch({
url: '/pages/index/index'
})
},1000)
})
}
return (
<>
<div className={'text-center py-4 w-full text-gray-300'} onClick={onLoginByPhone}>
<div className={'text-xs text-gray-400 py-1'}>{Version}</div>
<div className={'text-xs text-gray-400 py-1'}>Copyright © { new Date().getFullYear() } {Copyright}</div>
</div>
<Popup
style={{width: '350px', padding: '10px'}}
visible={openLoginByPhone}
closeOnOverlayClick={false}
closeable={true}
onClose={closeLoginByPhone}
>
<Form
style={{width: '350px',padding: '10px'}}
divider
initialValues={FormData}
labelPosition="left"
onFinish={(values) => submitByPhone(values)}
footer={
<div
style={{
display: 'flex',
justifyContent: 'center',
width: '100%'
}}
>
<Button nativeType="submit" block style={{backgroundColor: '#000000',color: '#ffffff'}}>
</Button>
</div>
}
>
<Form.Item
label={'手机号码'}
name="phone"
required
rules={[{message: '手机号码'}]}
>
<Input placeholder="请输入手机号码" maxLength={11} type="text"/>
</Form.Item>
<Form.Item
label={'短信验证码'}
name="code"
required
rules={[{message: '短信验证码'}]}
>
<Input placeholder="请输入短信验证码" maxLength={6} type="text"/>
</Form.Item>
</Form>
</Popup>
</>
)
}
export default UserFooter

View File

@@ -0,0 +1,69 @@
import {useEffect} from "react";
import navTo from "@/utils/common";
import {View, Text} from '@tarojs/components';
import {ArrowRight, Wallet, Comment, Transit, Refund, Package} from '@nutui/icons-react-taro';
function UserOrder() {
const reload = () => {
};
useEffect(() => {
reload()
}, []);
return (
<>
<View className={'px-4 pb-2'}>
<View
className={'user-card w-full flex flex-col justify-around rounded-xl shadow-sm'}
style={{
background: 'linear-gradient(to bottom, #ffffff, #ffffff)', // 这种情况建议使用类名来控制样式(引入外联样式)
// margin: '10px auto 0px auto',
height: '120px',
// borderRadius: '22px 22px 0 0',
}}
>
<View className={'title-bar flex justify-between pt-2'}>
<Text className={'title font-medium px-4'}></Text>
<View className={'more flex items-center px-2'} onClick={() => navTo('/user/order/order', true)}>
<Text className={'text-xs text-gray-500'}></Text>
<ArrowRight color="#cccccc" size={12}/>
</View>
</View>
<View className={'flex justify-around pb-1'}>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=0', true)}>
<Wallet size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=1', true)}>
<Package size={26} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=3', true)}>
<Transit size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=5', true)}>
<Comment size={24} className={'text-gray-500 font-normal'}/>
<Text className={'text-sm text-gray-600 py-1'}></Text>
</View>
<View className={'item flex justify-center flex-col items-center'}
onClick={() => navTo('/user/order/order?statusFilter=6', true)}>
<Refund size={26} className={'font-normal text-gray-500'}/>
<Text className={'text-sm text-gray-600 py-1'}>退/</Text>
</View>
</View>
</View>
</View>
</>
)
}
export default UserOrder;

View File

@@ -1,295 +1,35 @@
import React from 'react' import {useEffect} from 'react'
import {View, Text} from '@tarojs/components' import {useUser} from "@/hooks/useUser";
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro' import {Empty} from '@nutui/nutui-react-taro';
import { import {Text} from '@tarojs/components';
User,
Shopping,
Dongdong,
ArrowRight,
Purse,
People
} from '@nutui/icons-react-taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import { useThemeStyles } from '@/hooks/useTheme'
import {businessGradients, cardGradients, gradientUtils} from '@/styles/gradients'
import Taro from '@tarojs/taro'
const DealerIndex: React.FC = () => { function Admin() {
const { const {
dealerUser, isAdmin
error, } = useUser();
refresh,
} = useDealerUser()
// 使用主题样式 useEffect(() => {
const themeStyles = useThemeStyles() }, []);
// 导航到各个功能页面 if (!isAdmin()) {
const navigateToPage = (url: string) => {
Taro.navigateTo({url})
}
// 格式化金额
const formatMoney = (money?: string) => {
if (!money) return '0.00'
return parseFloat(money).toFixed(2)
}
// 格式化时间
const formatTime = (time?: string) => {
if (!time) return '-'
return new Date(time).toLocaleDateString()
}
// 获取用户主题
const userTheme = gradientUtils.getThemeByUserId(dealerUser?.userId)
// 获取渐变背景
const getGradientBackground = (themeColor?: string) => {
if (themeColor) {
const darkerColor = gradientUtils.adjustColorBrightness(themeColor, -30)
return gradientUtils.createGradient(themeColor, darkerColor)
}
return userTheme.background
}
console.log(getGradientBackground(),'getGradientBackground()')
if (error) {
return ( return (
<View className="p-4"> <Empty
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4"> description="您不是管理员"
<Text className="text-red-600">{error}</Text> imageSize={80}
</View> style={{
<Button type="primary" onClick={refresh}> backgroundColor: 'transparent',
height: 'calc(100vh - 200px)'
</Button> }}
</View> >
)
</Empty>
);
} }
return ( return (
<View className="bg-gray-100 min-h-screen"> <>
<View> <Text>...</Text>
{/*头部信息*/} </>
{dealerUser && (
<View className="px-4 py-6 relative overflow-hidden" style={themeStyles.primaryBackground}>
{/* 装饰性背景元素 - 小程序兼容版本 */}
<View className="absolute w-32 h-32 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.1)',
top: '-16px',
right: '-16px'
}}></View>
<View className="absolute w-24 h-24 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.08)',
bottom: '-12px',
left: '-12px'
}}></View>
<View className="absolute w-16 h-16 rounded-full" style={{
backgroundColor: 'rgba(255, 255, 255, 0.05)',
top: '60px',
left: '120px'
}}></View>
<View className="flex items-center justify-between relative z-10 mb-4">
<Avatar
size="50"
src={dealerUser?.qrcode}
icon={<User/>}
className="mr-4"
style={{
border: '2px solid rgba(255, 255, 255, 0.3)'
}}
/>
<View className="flex-1 flex-col">
<View className="text-white text-lg font-bold mb-1" style={{
}}>
{dealerUser?.realName || '分销商'}
</View>
<View className="text-sm" style={{
color: 'rgba(255, 255, 255, 0.8)'
}}>
ID: {dealerUser.userId} | : {dealerUser.refereeId || '无'}
</View>
</View>
<View className="text-right hidden">
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.9)'
}}></Text>
<Text className="text-xs" style={{
color: 'rgba(255, 255, 255, 0.7)'
}}>
{formatTime(dealerUser.createTime)}
</Text>
</View>
</View>
</View>
)}
{/* 佣金统计卡片 */}
{dealerUser && (
<View className="mx-4 -mt-6 rounded-xl p-4 relative z-10" style={cardGradients.elevated}>
<View className="mb-4">
<Text className="font-semibold text-gray-800"></Text>
</View>
<View className="grid grid-cols-3 gap-3">
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.available
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.money)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.frozen
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.freezeMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
<View className="text-center p-3 rounded-lg flex flex-col" style={{
background: businessGradients.money.total
}}>
<Text className="text-lg font-bold mb-1 text-white">
{formatMoney(dealerUser.totalMoney)}
</Text>
<Text className="text-xs" style={{ color: 'rgba(255, 255, 255, 0.9)' }}></Text>
</View>
</View>
</View>
)}
{/* 团队统计 */}
{dealerUser && (
<View className="bg-white mx-4 mt-4 rounded-xl p-4 hidden">
<View className="flex items-center justify-between mb-4">
<Text className="font-semibold text-gray-800"></Text>
<View
className="text-gray-400 text-sm flex items-center"
onClick={() => navigateToPage('/dealer/team/index')}
>
<Text></Text>
<ArrowRight size="12"/>
</View>
</View>
<View className="grid grid-cols-3 gap-4">
<View className="text-center grid">
<Text className="text-xl font-bold text-purple-500 mb-1">
{dealerUser.firstNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-indigo-500 mb-1">
{dealerUser.secondNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
<View className="text-center grid">
<Text className="text-xl font-bold text-pink-500 mb-1">
{dealerUser.thirdNum || 0}
</Text>
<Text className="text-xs text-gray-500"></Text>
</View>
</View>
</View>
)}
{/* 功能导航 */}
<View className="bg-white mx-4 mt-4 rounded-xl p-4">
<View className="font-semibold mb-4 text-gray-800"></View>
<ConfigProvider>
<Grid
columns={4}
className="no-border-grid"
style={{
'--nutui-grid-border-color': 'transparent',
'--nutui-grid-item-border-width': '0px',
border: 'none'
} as React.CSSProperties}
>
<Grid.Item text="分销订单" onClick={() => navigateToPage('/dealer/orders/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-blue-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Shopping color="#3b82f6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'提现申请'} onClick={() => navigateToPage('/dealer/withdraw/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-green-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Purse color="#10b981" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请'} onClick={() => navigateToPage('/dealer/team/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-purple-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<People color="#8b5cf6" size="20"/>
</View>
</View>
</Grid.Item>
<Grid.Item text={'我的邀请码'} onClick={() => navigateToPage('/dealer/qrcode/index')}>
<View className="text-center">
<View className="w-12 h-12 bg-orange-50 rounded-xl flex items-center justify-center mx-auto mb-2">
<Dongdong color="#f59e0b" size="20"/>
</View>
</View>
</Grid.Item>
</Grid>
{/* 第二行功能 */}
{/*<Grid*/}
{/* columns={4}*/}
{/* className="no-border-grid mt-4"*/}
{/* style={{*/}
{/* '--nutui-grid-border-color': 'transparent',*/}
{/* '--nutui-grid-item-border-width': '0px',*/}
{/* border: 'none'*/}
{/* } as React.CSSProperties}*/}
{/*>*/}
{/* <Grid.Item text={'邀请统计'} onClick={() => navigateToPage('/dealer/invite-stats/index')}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-indigo-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* <Presentation color="#6366f1" size="20"/>*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* /!* 预留其他功能位置 *!/*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/* <Grid.Item text={''}>*/}
{/* <View className="text-center">*/}
{/* <View className="w-12 h-12 bg-gray-50 rounded-xl flex items-center justify-center mx-auto mb-2">*/}
{/* </View>*/}
{/* </View>*/}
{/* </Grid.Item>*/}
{/*</Grid>*/}
</ConfigProvider>
</View>
</View>
{/* 底部安全区域 */}
<View className="h-20"></View>
</View>
) )
} }
export default DealerIndex export default Admin

View File

@@ -1,3 +0,0 @@
export default definePageConfig({
navigationBarTitleText: '实名审核'
})

View File

@@ -1,319 +0,0 @@
import React, {useState, useEffect, useCallback} from 'react'
import {View, Text} from '@tarojs/components'
import {
Space,
Tabs,
Tag,
Empty,
Loading,
PullToRefresh,
Button,
Dialog,
Image,
ImagePreview,
TextArea
} from '@nutui/nutui-react-taro'
import Taro from '@tarojs/taro'
import {useDealerUser} from '@/hooks/useDealerUser'
import {pageUserVerify, updateUserVerify} from '@/api/system/userVerify'
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
import {UserVerify} from "@/api/system/userVerify/model";
const UserVeirfyAdmin: React.FC = () => {
const [activeTab, setActiveTab] = useState<string | number>(0)
const [loading, setLoading] = useState<boolean>(false)
const [refreshing, setRefreshing] = useState<boolean>(false)
const [list, setList] = useState<UserVerify[]>([])
const [rejectDialogVisible, setRejectDialogVisible] = useState<boolean>(false)
const [rejectReason, setRejectReason] = useState<string>('')
const [currentRecord, setCurrentRecord] = useState<ShopDealerWithdraw | null>(null)
const [showPreview, setShowPreview] = useState(false)
const [showPreview2, setShowPreview2] = useState(false)
const {dealerUser} = useDealerUser()
// Tab 切换处理函数
const handleTabChange = (value: string | number) => {
console.log('Tab切换到:', value)
setActiveTab(value)
// activeTab变化会自动触发useEffect重新获取数据无需手动调用
}
// 获取审核记录
const fetchWithdrawRecords = useCallback(async () => {
if (!dealerUser?.userId) return
try {
setLoading(true)
const currentStatus = Number(activeTab)
const result = await pageUserVerify({
page: 1,
limit: 100,
status: currentStatus // 后端筛选,提高性能
})
if (result?.list) {
const processedRecords = result.list.map(record => ({
...record
}))
setList(processedRecords)
}
} catch (error) {
console.error('获取审核记录失败:', error)
Taro.showToast({
title: '获取审核记录失败',
icon: 'none'
})
} finally {
setLoading(false)
}
}, [dealerUser?.userId, activeTab])
// 刷新数据
const handleRefresh = async () => {
setRefreshing(true)
await Promise.all([fetchWithdrawRecords()])
setRefreshing(false)
}
// 审核通过
const handleApprove = async (record: ShopDealerWithdraw) => {
try {
await updateUserVerify({
...record,
status: 1, // 审核通过
})
Taro.showToast({
title: '审核通过',
icon: 'success'
})
await fetchWithdrawRecords()
} catch (error: any) {
if (error !== 'cancel') {
console.error('审核通过失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
}
// 驳回申请
const handleReject = (record: ShopDealerWithdraw) => {
setCurrentRecord(record)
setRejectReason('')
setRejectDialogVisible(true)
}
// 确认驳回
const confirmReject = async () => {
if (!rejectReason.trim()) {
Taro.showToast({
title: '请输入驳回原因',
icon: 'none'
})
return
}
try {
await updateUserVerify({
...currentRecord!,
status: 2, // 驳回
comments: rejectReason.trim()
})
Taro.showToast({
title: '已驳回',
icon: 'success'
})
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
await fetchWithdrawRecords()
} catch (error: any) {
console.error('驳回失败:', error)
Taro.showToast({
title: error.message || '操作失败',
icon: 'none'
})
}
}
// 初始化加载数据
useEffect(() => {
if (dealerUser?.userId) {
fetchWithdrawRecords().then()
}
}, [fetchWithdrawRecords])
const getStatusText = (status?: number) => {
switch (status) {
case 0:
return '待审核'
case 1:
return '审核通过'
case 2:
return '已驳回'
default:
return '未知'
}
}
const getStatusColor = (status?: number) => {
switch (status) {
case 0:
return 'warning'
case 1:
return 'success'
case 2:
return 'danger'
default:
return 'default'
}
}
const renderWithdrawRecords = () => {
console.log('渲染审核记录:', {loading, recordsCount: list.length, dealerUserId: dealerUser?.userId})
return (
<PullToRefresh
disabled={refreshing}
onRefresh={handleRefresh}
>
<View>
{loading ? (
<View className="text-center py-8">
<Loading/>
<Text className="text-gray-500 mt-2">...</Text>
</View>
) : list.length > 0 ? (
list.map(record => (
<View key={record.id} className="rounded-lg bg-gray-50 p-4 mb-3 shadow-sm">
<View className="flex justify-between items-start mb-3">
<Space direction={'vertical'}>
<Text className="font-semibold text-gray-800">
{record.realName}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.phone}
</Text>
<Text className="font-normal text-sm text-gray-500">
{record.idCard}
</Text>
</Space>
<Tag type={getStatusColor(record.status)}>
{getStatusText(record.status)}
</Tag>
</View>
<View className="flex gap-2 mb-2">
<Image src={record.sfz1} height={100} onClick={() => setShowPreview(true)}/>
<Image src={record.sfz2} height={100} onClick={() => setShowPreview2(true)}/>
</View>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview}
onClose={() => setShowPreview(false)}
/>
<ImagePreview
autoPlay
images={[{src: `${record.sfz1}`}]}
visible={showPreview2}
onClose={() => setShowPreview2(false)}
/>
<View className="text-xs text-gray-400">
<Text>{record.createTime}</Text>
{record.status == 1 && (
<Text className="block mt-1">
{record.updateTime}
</Text>
)}
{record.status == 2 && (
<Text className="block mt-1 text-red-500">
{record.comments}
</Text>
)}
</View>
{/* 操作按钮 */}
{record.status === 0 && (
<View className="flex gap-2 mt-3">
<Button
type="success"
size="small"
className="flex-1"
onClick={() => handleApprove(record)}
>
</Button>
<Button
type="danger"
size="small"
className="flex-1"
onClick={() => handleReject(record)}
>
</Button>
</View>
)}
</View>
))
) : (
<Empty description="暂无申请记录"/>
)}
</View>
</PullToRefresh>
)
}
return (
<View className="bg-gray-50 min-h-screen">
<Tabs value={activeTab} onChange={handleTabChange}>
<Tabs.TabPane title="待审核" value="0">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已通过" value="1">
{renderWithdrawRecords()}
</Tabs.TabPane>
<Tabs.TabPane title="已驳回" value="2">
{renderWithdrawRecords()}
</Tabs.TabPane>
</Tabs>
{/* 驳回原因对话框 */}
<Dialog
visible={rejectDialogVisible}
title="驳回原因"
onCancel={() => {
setRejectDialogVisible(false)
setCurrentRecord(null)
setRejectReason('')
}}
onConfirm={confirmReject}
>
<View className="p-4">
<TextArea
placeholder="请输入驳回原因"
value={rejectReason}
onChange={(value) => setRejectReason(value)}
maxLength={200}
rows={4}
/>
</View>
</Dialog>
</View>
)
}
export default UserVeirfyAdmin

320
src/api/afterSale.ts Normal file
View File

@@ -0,0 +1,320 @@
import { request } from '../utils/request'
// 售后类型
export type AfterSaleType = 'refund' | 'return' | 'exchange' | 'repair'
// 售后状态
export type AfterSaleStatus =
| 'pending' // 待审核
| 'approved' // 已同意
| 'rejected' // 已拒绝
| 'processing' // 处理中
| 'completed' // 已完成
| 'cancelled' // 已取消
// 售后进度记录
export interface ProgressRecord {
id: string
time: string
status: string
description: string
operator?: string
remark?: string
images?: string[]
}
// 售后详情
export interface AfterSaleDetail {
id: string
orderId: string
orderNo: string
type: AfterSaleType
status: AfterSaleStatus
reason: string
description: string
amount: number
applyTime: string
processTime?: string
completeTime?: string
rejectReason?: string
contactPhone?: string
evidenceImages: string[]
progressRecords: ProgressRecord[]
}
// 售后申请参数
export interface AfterSaleApplyParams {
orderId: string
type: AfterSaleType
reason: string
description: string
amount: number
contactPhone?: string
evidenceImages?: string[]
goodsItems?: Array<{
goodsId: string
quantity: number
}>
}
// 售后列表查询参数
export interface AfterSaleListParams {
page?: number
pageSize?: number
status?: AfterSaleStatus
type?: AfterSaleType
startTime?: string
endTime?: string
}
// 售后列表响应
export interface AfterSaleListResponse {
success: boolean
data: {
list: AfterSaleDetail[]
total: number
page: number
pageSize: number
}
message?: string
}
// 售后详情响应
export interface AfterSaleDetailResponse {
success: boolean
data: AfterSaleDetail
message?: string
}
// 售后类型映射
export const AFTER_SALE_TYPE_MAP = {
'refund': '退款',
'return': '退货',
'exchange': '换货',
'repair': '维修'
}
// 售后状态映射
export const AFTER_SALE_STATUS_MAP = {
'pending': '待审核',
'approved': '已同意',
'rejected': '已拒绝',
'processing': '处理中',
'completed': '已完成',
'cancelled': '已取消'
}
// 状态颜色映射
export const STATUS_COLOR_MAP = {
'pending': '#faad14',
'approved': '#52c41a',
'rejected': '#ff4d4f',
'processing': '#1890ff',
'completed': '#52c41a',
'cancelled': '#999'
}
// 申请售后
export const applyAfterSale = async (params: AfterSaleApplyParams): Promise<AfterSaleDetailResponse> => {
try {
const response = await request<AfterSaleDetailResponse>({
url: '/api/after-sale/apply',
method: 'POST',
data: params
})
return response
} catch (error) {
console.error('申请售后失败:', error)
throw error
}
}
// 查询售后详情
export const getAfterSaleDetail = async (params: {
orderId?: string
afterSaleId?: string
}): Promise<AfterSaleDetailResponse> => {
try {
const response = await request<AfterSaleDetailResponse>({
url: '/api/after-sale/detail',
method: 'GET',
data: params
})
return response
} catch (error) {
console.error('查询售后详情失败:', error)
// 返回模拟数据作为降级方案
return getMockAfterSaleDetail(params)
}
}
// 查询售后列表
export const getAfterSaleList = async (params: AfterSaleListParams): Promise<AfterSaleListResponse> => {
try {
const response = await request<AfterSaleListResponse>({
url: '/api/after-sale/list',
method: 'GET',
data: params
})
return response
} catch (error) {
console.error('查询售后列表失败:', error)
throw error
}
}
// 撤销售后申请
export const cancelAfterSale = async (afterSaleId: string): Promise<{ success: boolean; message?: string }> => {
try {
const response = await request<{ success: boolean; message?: string }>({
url: '/api/after-sale/cancel',
method: 'POST',
data: { afterSaleId }
})
return response
} catch (error) {
console.error('撤销售后申请失败:', error)
throw error
}
}
// 获取模拟售后详情数据
const getMockAfterSaleDetail = (params: {
orderId?: string
afterSaleId?: string
}): AfterSaleDetailResponse => {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
const mockData: AfterSaleDetailResponse = {
success: true,
data: {
id: 'AS' + Date.now(),
orderId: params.orderId || '',
orderNo: 'ORD' + Date.now(),
type: 'refund',
status: 'processing',
reason: '商品质量问题',
description: '收到的商品有明显瑕疵,包装破损,希望申请退款处理',
amount: 9999,
applyTime: twoDaysAgo.toISOString(),
processTime: yesterday.toISOString(),
contactPhone: '138****5678',
evidenceImages: [
'https://via.placeholder.com/200x200?text=Evidence1',
'https://via.placeholder.com/200x200?text=Evidence2'
],
progressRecords: [
{
id: '1',
time: now.toISOString(),
status: '处理中',
description: '客服正在处理您的申请,请耐心等待',
operator: '客服小王',
remark: '预计1-2个工作日内完成处理'
},
{
id: '2',
time: new Date(now.getTime() - 4 * 60 * 60 * 1000).toISOString(),
status: '已审核',
description: '您的申请已通过审核,正在安排退款处理',
operator: '审核员张三'
},
{
id: '3',
time: yesterday.toISOString(),
status: '已受理',
description: '我们已收到您的申请,正在进行审核',
operator: '系统'
},
{
id: '4',
time: twoDaysAgo.toISOString(),
status: '已提交',
description: '您已成功提交售后申请',
operator: '用户'
}
]
}
}
return mockData
}
// 格式化售后状态
export const formatAfterSaleStatus = (status: AfterSaleStatus): {
text: string
color: string
icon: string
} => {
const statusMap = {
'pending': { text: '待审核', color: '#faad14', icon: '⏳' },
'approved': { text: '已同意', color: '#52c41a', icon: '✅' },
'rejected': { text: '已拒绝', color: '#ff4d4f', icon: '❌' },
'processing': { text: '处理中', color: '#1890ff', icon: '🔄' },
'completed': { text: '已完成', color: '#52c41a', icon: '✅' },
'cancelled': { text: '已取消', color: '#999', icon: '⭕' }
}
return statusMap[status] || { text: status, color: '#666', icon: '📋' }
}
// 计算预计处理时间
export const calculateEstimatedTime = (
applyTime: string,
type: AfterSaleType,
status: AfterSaleStatus
): string => {
const applyDate = new Date(applyTime)
let estimatedDays = 3 // 默认3个工作日
// 根据售后类型调整预计时间
switch (type) {
case 'refund':
estimatedDays = 3 // 退款3个工作日
break
case 'return':
estimatedDays = 7 // 退货7个工作日
break
case 'exchange':
estimatedDays = 10 // 换货10个工作日
break
case 'repair':
estimatedDays = 15 // 维修15个工作日
break
}
// 根据当前状态调整
if (status === 'completed') {
return '已完成'
} else if (status === 'rejected' || status === 'cancelled') {
return '已结束'
}
const estimatedDate = new Date(applyDate.getTime() + estimatedDays * 24 * 60 * 60 * 1000)
return `预计${estimatedDate.getMonth() + 1}${estimatedDate.getDate()}日前完成`
}
// 获取售后进度步骤
export const getAfterSaleSteps = (type: AfterSaleType, status: AfterSaleStatus) => {
const baseSteps = [
{ title: '提交申请', description: '用户提交售后申请' },
{ title: '审核中', description: '客服审核申请材料' },
{ title: '处理中', description: '正在处理您的申请' },
{ title: '完成', description: '售后处理完成' }
]
// 根据类型调整步骤
if (type === 'return' || type === 'exchange') {
baseSteps.splice(2, 0, { title: '等待收货', description: '等待用户寄回商品' })
baseSteps.splice(3, 0, { title: '确认收货', description: '商家确认收到退回商品' })
}
return baseSteps
}

View File

@@ -102,7 +102,7 @@ export async function getCmsAd(id: number) {
} }
/** /**
* 根据id查询广告位 * 根据code查询广告位
*/ */
export async function getCmsAdByCode(code: string) { export async function getCmsAdByCode(code: string) {
const res = await request.get<ApiResult<CmsAd>>( const res = await request.get<ApiResult<CmsAd>>(

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api/index'; import type { PageParam } from '@/api';
/** /**
* 广告位 * 广告位
@@ -52,7 +52,6 @@ export interface CmsAd {
*/ */
export interface CmsAdParam extends PageParam { export interface CmsAdParam extends PageParam {
adId?: number; adId?: number;
type?: number;
adType?: string; adType?: string;
pageId?: number; pageId?: number;
pageName?: string; pageName?: string;

View File

@@ -1,5 +1,5 @@
import request from '@/utils/request'; import request from '@/utils/request';
import type {ApiResult, PageResult} from '@/api/index'; import type {ApiResult, PageResult} from '@/api';
import type {CmsArticle, CmsArticleParam} from './model'; import type {CmsArticle, CmsArticleParam} from './model';
/** /**
@@ -204,3 +204,15 @@ export async function getByIds(params?: CmsArticleParam) {
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
/**
* 根据code查询文章
*/
export async function getByCode(code: string) {
const res = await request.get<ApiResult<CmsArticle>>(
'/cms/cms-article/getByCode/' + code
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -123,3 +123,16 @@ export async function getNavigationByPath(params: CmsNavigationParam) {
} }
/**
* 根据code查询导航
*/
export async function getByCode(code: string) {
const res = await request.get<ApiResult<CmsNavigation>>(
'/cms/cms-navigation/getByCode/' + code
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -55,8 +55,8 @@ export interface CmsNavigation {
parentName?: string; parentName?: string;
// 模型名称 // 模型名称
modelName?: string; modelName?: string;
// 类型(已废弃) // 类型(模型)
type?: number; type?: string;
// 绑定的页面(已废弃) // 绑定的页面(已废弃)
pageId?: number; pageId?: number;
// 项目ID // 项目ID
@@ -113,6 +113,5 @@ export interface CmsNavigationParam extends PageParam {
parentId?: number; parentId?: number;
hide?: number; hide?: number;
model?: string; model?: string;
home?: number;
keywords?: string; keywords?: string;
} }

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api'; import type { PageParam } from '@/api/index';
/** /**
* 应用参数 * 应用参数
@@ -55,6 +55,9 @@ export interface Config {
email?: string; email?: string;
loginTitle?: string; loginTitle?: string;
sysLogo?: string; sysLogo?: string;
NoticeBar?: string; vipText?: string;
apiUrl?: string; vipComments?: string;
deliveryText?: string;
guaranteeText?: string;
openComments?: string;
} }

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api/index';
import type { GltTicketOrder, GltTicketOrderParam } from './model';
/**
* 分页查询送水订单
*/
export async function pageGltTicketOrder(params: GltTicketOrderParam) {
const res = await request.get<ApiResult<PageResult<GltTicketOrder>>>(
'/glt/glt-ticket-order/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询送水订单列表
*/
export async function listGltTicketOrder(params?: GltTicketOrderParam) {
const res = await request.get<ApiResult<GltTicketOrder[]>>(
'/glt/glt-ticket-order',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加送水订单
*/
export async function addGltTicketOrder(data: GltTicketOrder) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-ticket-order',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改送水订单
*/
export async function updateGltTicketOrder(data: GltTicketOrder) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-ticket-order',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除送水订单
*/
export async function removeGltTicketOrder(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-order/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除送水订单
*/
export async function removeBatchGltTicketOrder(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-order/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询送水订单
*/
export async function getGltTicketOrder(id: number) {
const res = await request.get<ApiResult<GltTicketOrder>>(
'/glt/glt-ticket-order/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,94 @@
import type { PageParam } from '@/api';
/**
* 送水订单
*/
export interface GltTicketOrder {
//
id?: number;
// 用户水票ID
userTicketId?: number;
// 订单编号
orderNo?: string;
// 门店ID
storeId?: number;
// 门店名称
storeName?: string;
// 门店地址
storeAddress?: string;
// 门店电话
storePhone?: string;
// 配送员
riderId?: number;
// 配送员名称
riderName?: string;
// 配送员电话
riderPhone?: string;
// 仓库ID
warehouseId?: number;
// 仓库名称
warehouseName?: string;
// 仓库地址
warehouseAddress?: string;
// 关联收货地址
addressId?: number;
// 收货地址
address?: string;
// 配送时间
sendTime?: string;
// 配送开始时间(配送员点击“开始配送”)
sendStartTime?: string;
// 配送结束时间(配送员确认送达)
sendEndTime?: string;
// 配送员送达拍照(选填/必填由后端策略决定)
sendEndImg?: string;
// 发货/配送状态建议10待配送 20配送中 30待客户确认 40已完成
deliveryStatus?: number;
// 客户确认收货时间(客户点击确认收货)
receiveConfirmTime?: string;
// 客户确认方式建议10客户手动确认 20配送照片自动确认 30后台超时自动确认
receiveConfirmType?: number;
// 买家留言
buyerRemarks?: string;
// 用于统计
price?: string;
// 购买数量
totalNum?: number;
// 用户ID
userId?: number;
// 昵称
nickname?: string;
// 头像
avatar?: string;
// 手机号码
phone?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 送水订单搜索条件
*/
export interface GltTicketOrderParam extends PageParam {
id?: number;
keywords?: string;
userId?: number;
// 配送员用户ID用于配送员端查询
riderId?: number;
// 发货/配送状态(建议与 GltTicketOrder.deliveryStatus 对齐)
deliveryStatus?: number;
// 兼容 ShopOrderParam 的筛选字段(如后端已实现可直接复用)
statusFilter?: number;
}

View File

@@ -0,0 +1,118 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltTicketTemplate, GltTicketTemplateParam } from './model';
/**
* 分页查询水票
*/
export async function pageGltTicketTemplate(params: GltTicketTemplateParam) {
const res = await request.get<ApiResult<PageResult<GltTicketTemplate>>>(
'/glt/glt-ticket-template/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询水票列表
*/
export async function listGltTicketTemplate(params?: GltTicketTemplateParam) {
const res = await request.get<ApiResult<GltTicketTemplate[]>>(
'/glt/glt-ticket-template',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加水票
*/
export async function addGltTicketTemplate(data: GltTicketTemplate) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-ticket-template',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改水票
*/
export async function updateGltTicketTemplate(data: GltTicketTemplate) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-ticket-template',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除水票
*/
export async function removeGltTicketTemplate(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-template/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除水票
*/
export async function removeBatchGltTicketTemplate(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-ticket-template/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询水票
*/
export async function getGltTicketTemplate(id: number) {
const res = await request.get<ApiResult<GltTicketTemplate>>(
'/glt/glt-ticket-template/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据商品ID查询水票模板
*/
export async function getGltTicketTemplateByGoodsId(id: number) {
const res = await request.get<ApiResult<GltTicketTemplate>>(
'/glt/glt-ticket-template/getByGoodsId/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,55 @@
import type { PageParam } from '@/api';
/**
* 水票
*/
export interface GltTicketTemplate {
//
id?: number;
// 关联商品ID
goodsId?: number;
// 名称
name?: string;
// 启用
enabled?: boolean;
// 单位名称
unitName?: string;
// 最小购买数量
minBuyQty?: number;
// 起始发送数量
startSendQty?: number;
// 买赠买1送4 => gift_multiplier=4
giftMultiplier?: number;
// 是否把购买量也计入套票总量(默认仅计入赠送量)
includeBuyQty?: boolean;
// 每期释放数量默认每月释放10
monthlyReleaseQty?: number;
// 总共释放多少期(若配置>0则按期数平均分摊
releasePeriods?: number;
// 首期释放时机0=支付成功当刻1=下个月同日
firstReleaseMode?: number;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 水票搜索条件
*/
export interface GltTicketTemplateParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -0,0 +1,170 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltUserTicket, GltUserTicketParam } from './model';
function normalizeTotal(input: unknown): number {
if (typeof input === 'number' && Number.isFinite(input)) return input;
if (typeof input === 'string') {
const n = Number(input);
if (Number.isFinite(n)) return n;
}
if (input && typeof input === 'object') {
const obj: any = input;
// Common shapes from different backends.
for (const key of ['total', 'count', 'value', 'num', 'ticketTotal', 'totalQty']) {
const v = obj?.[key];
const n = normalizeTotal(v);
if (n) return n;
}
// Sometimes nested: { data: { total: ... } } / { data: 12 }
if ('data' in obj) {
const n = normalizeTotal(obj.data);
if (n) return n;
}
}
return 0;
}
/**
* 分页查询我的水票
*/
export async function pageGltUserTicket(params: GltUserTicketParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicket>>>(
'/glt/glt-user-ticket/page',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询我的水票列表
*/
export async function listGltUserTicket(params?: GltUserTicketParam) {
const res = await request.get<ApiResult<GltUserTicket[]>>(
'/glt/glt-user-ticket',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加我的水票
*/
export async function addGltUserTicket(data: GltUserTicket) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-user-ticket',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改我的水票
*/
export async function updateGltUserTicket(data: GltUserTicket) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-user-ticket',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除我的水票
*/
export async function removeGltUserTicket(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除我的水票
*/
export async function removeBatchGltUserTicket(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询我的水票
*/
export async function getGltUserTicket(id: number) {
const res = await request.get<ApiResult<GltUserTicket>>(
'/glt/glt-user-ticket/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 获取我的水票总数
*/
export async function getMyGltUserTicketTotal(userId?: number) {
const params = userId ? { userId } : undefined
const extract = (res: any) => {
// Some backends may return a raw number instead of ApiResult.
if (typeof res === 'number' || typeof res === 'string') return normalizeTotal(res)
if (res && typeof res === 'object' && 'code' in res) {
const apiRes = res as ApiResult<unknown>
if (apiRes.code === 0) return normalizeTotal(apiRes.data)
throw new Error(apiRes.message)
}
return normalizeTotal(res)
}
// Try both the configured BaseUrl host and the auth-server host.
// If the first one returns 0, keep trying; some tenants deploy GLT on a different host.
const urls = [
'/glt/glt-user-ticket/my-total'
]
let lastError: unknown
let firstTotal: number | undefined
for (const url of urls) {
try {
const res = await request.get<any>(url, params)
if (process.env.NODE_ENV === 'development') {
console.log('[getMyGltUserTicketTotal] response:', { url, res })
}
const total = extract(res)
if (firstTotal === undefined) firstTotal = total
if (total) return total
} catch (e) {
if (process.env.NODE_ENV === 'development') {
console.warn('[getMyGltUserTicketTotal] failed:', { url, error: e })
}
lastError = e
}
}
if (firstTotal !== undefined) return firstTotal
return Promise.reject(lastError instanceof Error ? lastError : new Error('获取水票总数失败'))
}

View File

@@ -0,0 +1,66 @@
import type { PageParam } from '@/api';
/**
* 我的水票
*/
export interface GltUserTicket {
//
id?: number;
// 模板ID
templateId?: number;
// 模板名称
templateName?: string;
// 商品ID
goodsId?: number;
// 订单ID
orderId?: number;
// 订单编号
orderNo?: string;
// 订单商品ID
orderGoodsId?: number;
// 总数量
totalQty?: number;
// 可用数量
availableQty?: number;
// 冻结数量
frozenQty?: number;
// 已使用数量
usedQty?: number;
// 已释放数量
releasedQty?: number;
// 用户ID
userId?: number;
// 用户昵称
nickname?: string;
// 用户头像
avatar?: string;
// 用户手机号
phone?: string;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 我的水票搜索条件
*/
export interface GltUserTicketParam extends PageParam {
id?: number;
templateId?: number;
userId?: number;
phone?: string;
keywords?: string;
// 状态过滤0正常1冻结
status?: number;
}

View File

@@ -0,0 +1,101 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltUserTicketLog, GltUserTicketLogParam } from './model';
/**
* 分页查询消费日志
*/
export async function pageGltUserTicketLog(params: GltUserTicketLogParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicketLog>>>(
'/glt/glt-user-ticket-log/page',
params
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询消费日志列表
*/
export async function listGltUserTicketLog(params?: GltUserTicketLogParam) {
const res = await request.get<ApiResult<GltUserTicketLog[]>>(
'/glt/glt-user-ticket-log',
params
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加消费日志
*/
export async function addGltUserTicketLog(data: GltUserTicketLog) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-user-ticket-log',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改消费日志
*/
export async function updateGltUserTicketLog(data: GltUserTicketLog) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-user-ticket-log',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除消费日志
*/
export async function removeGltUserTicketLog(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-log/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除消费日志
*/
export async function removeBatchGltUserTicketLog(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-log/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询消费日志
*/
export async function getGltUserTicketLog(id: number) {
const res = await request.get<ApiResult<GltUserTicketLog>>(
'/glt/glt-user-ticket-log/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,54 @@
import type { PageParam } from '@/api';
/**
* 消费日志
*/
export interface GltUserTicketLog {
//
id?: number;
// 用户水票ID
userTicketId?: number;
// 变更类型
changeType?: number;
// 可更改
changeAvailable?: number;
// 更改冻结状态
changeFrozen?: number;
// 已使用更改
changeUsed?: number;
// 可用后
availableAfter?: number;
// 冻结后
frozenAfter?: number;
// 使用后
usedAfter?: number;
// 订单ID
orderId?: number;
// 订单编号
orderNo?: string;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0正常, 1冻结
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 消费日志搜索条件
*/
export interface GltUserTicketLogParam extends PageParam {
id?: number;
keywords?: string;
userId?: number;
}

View File

@@ -0,0 +1,105 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { GltUserTicketRelease, GltUserTicketReleaseParam } from './model';
/**
* 分页查询水票释放
*/
export async function pageGltUserTicketRelease(params: GltUserTicketReleaseParam) {
const res = await request.get<ApiResult<PageResult<GltUserTicketRelease>>>(
'/glt/glt-user-ticket-release/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询水票释放列表
*/
export async function listGltUserTicketRelease(params?: GltUserTicketReleaseParam) {
const res = await request.get<ApiResult<GltUserTicketRelease[]>>(
'/glt/glt-user-ticket-release',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加水票释放
*/
export async function addGltUserTicketRelease(data: GltUserTicketRelease) {
const res = await request.post<ApiResult<unknown>>(
'/glt/glt-user-ticket-release',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改水票释放
*/
export async function updateGltUserTicketRelease(data: GltUserTicketRelease) {
const res = await request.put<ApiResult<unknown>>(
'/glt/glt-user-ticket-release',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除水票释放
*/
export async function removeGltUserTicketRelease(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-release/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除水票释放
*/
export async function removeBatchGltUserTicketRelease(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/glt/glt-user-ticket-release/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询水票释放
*/
export async function getGltUserTicketRelease(id: number) {
const res = await request.get<ApiResult<GltUserTicketRelease>>(
'/glt/glt-user-ticket-release/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -0,0 +1,38 @@
import type { PageParam } from '@/api';
/**
* 水票释放
*/
export interface GltUserTicketRelease {
//
id?: string;
// 水票ID
userTicketId?: string;
// 用户ID
userId?: number;
// 周期编号
periodNo?: number;
// 释放数量
releaseQty?: number;
// 释放时间
releaseTime?: string;
// 状态
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 水票释放搜索条件
*/
export interface GltUserTicketReleaseParam extends PageParam {
id?: number;
userId?: number;
keywords?: string;
}

View File

@@ -58,5 +58,4 @@ export interface PageParam {
lang?: string; lang?: string;
model?: string; model?: string;
BaseUrl?: string; BaseUrl?: string;
sceneType?: string;
} }

259
src/api/logistics.ts Normal file
View File

@@ -0,0 +1,259 @@
import { request } from '../utils/request'
// 物流信息接口
export interface LogisticsInfo {
expressCompany: string // 快递公司代码
expressCompanyName: string // 快递公司名称
expressNo: string // 快递单号
status: string // 物流状态
updateTime: string // 更新时间
estimatedTime?: string // 预计送达时间
currentLocation?: string // 当前位置
senderInfo?: {
name: string
phone: string
address: string
}
receiverInfo?: {
name: string
phone: string
address: string
}
}
// 物流跟踪记录
export interface LogisticsTrack {
time: string
location: string
status: string
description: string
isCompleted: boolean
}
// 物流查询响应
export interface LogisticsResponse {
success: boolean
data: {
logisticsInfo: LogisticsInfo
trackList: LogisticsTrack[]
}
message?: string
}
// 支持的快递公司
export const EXPRESS_COMPANIES = {
'SF': '顺丰速运',
'YTO': '圆通速递',
'ZTO': '中通快递',
'STO': '申通快递',
'YD': '韵达速递',
'HTKY': '百世快递',
'JD': '京东物流',
'EMS': '中国邮政',
'YUNDA': '韵达快递',
'JTSD': '极兔速递',
'DBKD': '德邦快递',
'UC': '优速快递'
}
// 查询物流信息
export const queryLogistics = async (params: {
orderId?: string
expressNo: string
expressCompany: string
}): Promise<LogisticsResponse> => {
try {
// 实际项目中这里应该调用真实的物流查询API
// 例如快递100、快递鸟、菜鸟裹裹等第三方物流查询服务
// 模拟API调用
const response = await request({
url: '/api/logistics/query',
method: 'POST',
data: params
})
return response
} catch (error) {
console.error('查询物流信息失败:', error)
// 返回模拟数据作为降级方案
return getMockLogisticsData(params)
}
}
// 获取模拟物流数据
const getMockLogisticsData = (params: {
orderId?: string
expressNo: string
expressCompany: string
}): LogisticsResponse => {
const now = new Date()
const yesterday = new Date(now.getTime() - 24 * 60 * 60 * 1000)
const twoDaysAgo = new Date(now.getTime() - 2 * 24 * 60 * 60 * 1000)
const threeDaysAgo = new Date(now.getTime() - 3 * 24 * 60 * 60 * 1000)
const mockData: LogisticsResponse = {
success: true,
data: {
logisticsInfo: {
expressCompany: params.expressCompany,
expressCompanyName: EXPRESS_COMPANIES[params.expressCompany] || params.expressCompany,
expressNo: params.expressNo,
status: '运输中',
updateTime: now.toISOString(),
estimatedTime: new Date(now.getTime() + 24 * 60 * 60 * 1000).toISOString(),
currentLocation: '北京市朝阳区',
senderInfo: {
name: '商家仓库',
phone: '400-123-4567',
address: '上海市浦东新区张江高科技园区'
},
receiverInfo: {
name: '张三',
phone: '138****5678',
address: '北京市朝阳区三里屯街道'
}
},
trackList: [
{
time: now.toISOString(),
location: '北京市朝阳区',
status: '运输中',
description: '快件正在运输途中,预计今日送达,请保持手机畅通',
isCompleted: false
},
{
time: new Date(now.getTime() - 2 * 60 * 60 * 1000).toISOString(),
location: '北京转运中心',
status: '已发出',
description: '快件已从北京转运中心发出,正在派送途中',
isCompleted: true
},
{
time: new Date(now.getTime() - 6 * 60 * 60 * 1000).toISOString(),
location: '北京转运中心',
status: '已到达',
description: '快件已到达北京转运中心,正在进行分拣',
isCompleted: true
},
{
time: yesterday.toISOString(),
location: '天津转运中心',
status: '已发出',
description: '快件已从天津转运中心发出',
isCompleted: true
},
{
time: new Date(yesterday.getTime() - 4 * 60 * 60 * 1000).toISOString(),
location: '天津转运中心',
status: '已到达',
description: '快件已到达天津转运中心',
isCompleted: true
},
{
time: twoDaysAgo.toISOString(),
location: '上海转运中心',
status: '已发出',
description: '快件已从上海转运中心发出',
isCompleted: true
},
{
time: new Date(twoDaysAgo.getTime() - 2 * 60 * 60 * 1000).toISOString(),
location: '上海转运中心',
status: '已到达',
description: '快件已到达上海转运中心,正在进行分拣',
isCompleted: true
},
{
time: threeDaysAgo.toISOString(),
location: '上海市浦东新区',
status: '已发货',
description: '商家已发货,快件已交给快递公司',
isCompleted: true
}
]
}
}
return mockData
}
// 获取快递公司列表
export const getExpressCompanies = () => {
return Object.entries(EXPRESS_COMPANIES).map(([code, name]) => ({
code,
name
}))
}
// 根据快递单号自动识别快递公司
export const detectExpressCompany = (expressNo: string): string => {
// 这里可以根据快递单号的规则来自动识别快递公司
// 实际项目中可以使用第三方服务的自动识别API
if (expressNo.startsWith('SF')) return 'SF'
if (expressNo.startsWith('YT')) return 'YTO'
if (expressNo.startsWith('ZT')) return 'ZTO'
if (expressNo.startsWith('ST')) return 'STO'
if (expressNo.startsWith('YD')) return 'YD'
if (expressNo.startsWith('JD')) return 'JD'
if (expressNo.startsWith('EMS')) return 'EMS'
// 默认返回顺丰
return 'SF'
}
// 格式化物流状态
export const formatLogisticsStatus = (status: string): {
text: string
color: string
icon: string
} => {
const statusMap = {
'已发货': { text: '已发货', color: '#1890ff', icon: '📦' },
'运输中': { text: '运输中', color: '#52c41a', icon: '🚚' },
'派送中': { text: '派送中', color: '#faad14', icon: '🏃' },
'已签收': { text: '已签收', color: '#52c41a', icon: '✅' },
'异常': { text: '异常', color: '#ff4d4f', icon: '⚠️' },
'退回': { text: '退回', color: '#ff4d4f', icon: '↩️' }
}
return statusMap[status] || { text: status, color: '#666', icon: '📋' }
}
// 计算预计送达时间
export const calculateEstimatedTime = (
sendTime: string,
expressCompany: string,
distance?: number
): string => {
const sendDate = new Date(sendTime)
let estimatedDays = 3 // 默认3天
// 根据快递公司调整预计时间
switch (expressCompany) {
case 'SF':
estimatedDays = 1 // 顺丰次日达
break
case 'JD':
estimatedDays = 1 // 京东次日达
break
case 'YTO':
case 'ZTO':
case 'STO':
estimatedDays = 2 // 三通一达2天
break
default:
estimatedDays = 3
}
// 根据距离调整(如果有距离信息)
if (distance) {
if (distance > 2000) estimatedDays += 1 // 超过2000公里加1天
if (distance > 3000) estimatedDays += 1 // 超过3000公里再加1天
}
const estimatedDate = new Date(sendDate.getTime() + estimatedDays * 24 * 60 * 60 * 1000)
return estimatedDate.toISOString()
}

View File

@@ -1,61 +0,0 @@
import request from '@/utils/request';
import type { ApiResult } from '@/api';
import {UserVerify} from "@/api/system/userVerify/model";
import {ShopDealerWithdraw} from "@/api/shop/shopDealerWithdraw/model";
/**
* 升级为管理员
* 推送模版消息
*/
export async function pushByUpdateAdmin(userId: number) {
const res = await request.get<ApiResult<unknown>>(
'/sdy/sdy-template-message/' + userId
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 通知管理员审核操作提醒
*/
export async function pushReviewReminder(data: UserVerify) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushReviewReminder',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 通知管理员去提现审核操作提醒
*/
export async function pushWithdrawalReviewReminder(data: ShopDealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushWithdrawalReviewReminder',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 提现成功通知
*/
export async function pushNoticeOfWithdrawalToAccount(data: ShopDealerWithdraw) {
const res = await request.post<ApiResult<unknown>>(
'/sdy/sdy-template-message/pushNoticeOfWithdrawalToAccount',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,123 +0,0 @@
import type { PageParam } from '@/api/index';
/**
* 商品文章
*/
export interface ShopArticle {
// 文章ID
articleId?: number;
// 文章标题
title?: string;
// 文章类型 0常规 1视频
type?: number;
// 模型
model?: string;
// 详情页模板
detail?: string;
// 文章分类ID
categoryId?: number;
// 上级id, 0是顶级
parentId?: number;
// 话题
topic?: string;
// 标签
tags?: string;
// 封面图
image?: string;
// 封面图宽
imageWidth?: number;
// 封面图高
imageHeight?: number;
// 付费金额
price?: string;
// 开始时间
startTime?: string;
// 结束时间
endTime?: string;
// 来源
source?: string;
// 产品概述
overview?: string;
// 虚拟阅读量(仅用作展示)
virtualViews?: number;
// 实际阅读量
actualViews?: number;
// 评分
rate?: string;
// 列表显示方式(10小图展示 20大图展示)
showType?: number;
// 访问密码
password?: string;
// 可见类型 0所有人 1登录可见 2密码可见
permission?: number;
// 发布来源客户端 (APP、H5、小程序等)
platform?: string;
// 文章附件
files?: string;
// 视频地址
video?: string;
// 接受的文件类型
accept?: string;
// 经度
longitude?: string;
// 纬度
latitude?: string;
// 所在省份
province?: string;
// 所在城市
city?: string;
// 所在辖区
region?: string;
// 街道地址
address?: string;
// 点赞数
likes?: number;
// 评论数
commentNumbers?: number;
// 提醒谁看
toUsers?: string;
// 作者
author?: string;
// 推荐
recommend?: number;
// 报名人数
bmUsers?: number;
// 用户ID
userId?: number;
// 项目ID
projectId?: number;
// 语言
lang?: string;
// 关联默认语言的文章ID
langArticleId?: number;
// 是否自动翻译
translation?: string;
// 编辑器类型 0 Markdown编辑器 1 富文本编辑器
editor?: string;
// pdf文件地址
pdfUrl?: string;
// 版本号
version?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0已发布, 1待审核 2已驳回 3违规内容
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 商品文章搜索条件
*/
export interface ShopArticleParam extends PageParam {
articleId?: number;
keywords?: string;
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api'; import type { PageParam } from '@/api/index';
/** /**
* 优惠券 * 优惠券

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api'; import type { PageParam } from '@/api/index';
/** /**
* 分销商申请记录表 * 分销商申请记录表
@@ -10,14 +10,6 @@ export interface ShopDealerApply {
userId?: number; userId?: number;
// 姓名 // 姓名
realName?: string; realName?: string;
// 分销商名称
dealerName?: string;
// 分销商编号
dealerCode?: string;
// 详细地址
address?: string;
// 金额
money?: number;
// 手机号 // 手机号
mobile?: string; mobile?: string;
// 推荐人用户ID // 推荐人用户ID
@@ -25,9 +17,7 @@ export interface ShopDealerApply {
// 申请方式(10需后台审核 20无需审核) // 申请方式(10需后台审核 20无需审核)
applyType?: number; applyType?: number;
// 申请时间 // 申请时间
applyTime?: string; applyTime?: number;
// 签单时间
contractTime?: string;
// 审核状态 (10待审核 20审核通过 30驳回) // 审核状态 (10待审核 20审核通过 30驳回)
applyStatus?: number; applyStatus?: number;
// 审核时间 // 审核时间
@@ -40,14 +30,6 @@ export interface ShopDealerApply {
createTime?: string; createTime?: string;
// 修改时间 // 修改时间
updateTime?: string; updateTime?: string;
// 过期时间
expirationTime?: string;
// 备注
comments?: string;
// 昵称
nickName?: string;
// 推荐人名称
refereeName?: string;
} }
/** /**
@@ -55,10 +37,7 @@ export interface ShopDealerApply {
*/ */
export interface ShopDealerApplyParam extends PageParam { export interface ShopDealerApplyParam extends PageParam {
applyId?: number; applyId?: number;
type?: number;
dealerName?: string;
mobile?: string; mobile?: string;
userId?: number; userId?: number;
keywords?: string; keywords?: string;
applyStatus?: number; // 申请状态筛选 (10待审核 20审核通过 30驳回)
} }

View File

@@ -1,45 +0,0 @@
import type { PageParam } from '@/api';
/**
* 分销商提现银行卡
*/
export interface ShopDealerBank {
// 主键ID
id?: number;
// 分销商用户ID
userId?: number;
// 开户行名称
bankName?: string;
// 银行开户名
bankAccount?: string;
// 银行卡号
bankCard?: string;
// 申请状态 (10待审核 20审核通过 30驳回)
applyStatus?: number;
// 审核时间
auditTime?: number;
// 驳回原因
rejectReason?: string;
// 是否默认
isDefault?: boolean;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
// 类型
type?: string;
// 名称
name?: string;
}
/**
* 分销商提现银行卡搜索条件
*/
export interface ShopDealerBankParam extends PageParam {
id?: number;
userId?: number;
isDefault?: boolean;
keywords?: string;
}

View File

@@ -99,17 +99,3 @@ export async function getShopDealerCapital(id: number) {
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
/**
* 根据id查询分销商资金明细表
*/
export async function getShopDealerCapitalByOrderNo(orderNo: string) {
const res = await request.get<ApiResult<ShopDealerCapital>>(
'/shop/shop-dealer-capital/getByCode/' + orderNo
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api'; import type { PageParam } from '@/api/index';
/** /**
* 分销商资金明细表 * 分销商资金明细表
@@ -8,14 +8,14 @@ export interface ShopDealerCapital {
id?: number; id?: number;
// 分销商用户ID // 分销商用户ID
userId?: number; userId?: number;
// 订单编号 // 订单ID
orderNo?: string; orderId?: number;
// 资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入) // 资金流动类型 (10佣金收入 20提现支出 30转账支出 40转账收入)
flowType?: number; flowType?: number;
// 金额 // 金额
money?: string; money?: string;
// 描述 // 描述
comments?: string; describe?: string;
// 对方用户ID // 对方用户ID
toUserId?: number; toUserId?: number;
// 商城ID // 商城ID
@@ -31,8 +31,11 @@ export interface ShopDealerCapital {
*/ */
export interface ShopDealerCapitalParam extends PageParam { export interface ShopDealerCapitalParam extends PageParam {
id?: number; id?: number;
orderNo?: string; // 仅查询当前分销商的收益/资金明细
userId?: number; userId?: number;
month?: string, // 可选:按订单过滤
orderId?: number;
// 可选:资金流动类型过滤
flowType?: number;
keywords?: string; keywords?: string;
} }

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api'; import type { PageParam } from '@/api/index';
/** /**
* 分销商订单记录表 * 分销商订单记录表
@@ -6,14 +6,13 @@ import type { PageParam } from '@/api';
export interface ShopDealerOrder { export interface ShopDealerOrder {
// 主键ID // 主键ID
id?: number; id?: number;
// 商品名称
title?: string;
// 买家用户ID // 买家用户ID
userId?: number; userId?: number;
// 昵称
nickname?: string; nickname?: string;
// 订单编号 // 订单编号(部分接口会直接返回订单号字符串)
orderNo?: string; orderNo?: string;
// 订单ID
orderId?: number;
// 订单总金额(不含运费) // 订单总金额(不含运费)
orderPrice?: string; orderPrice?: string;
// 分销商用户id(一级) // 分销商用户id(一级)
@@ -28,32 +27,16 @@ export interface ShopDealerOrder {
secondMoney?: string; secondMoney?: string;
// 分销佣金(三级) // 分销佣金(三级)
thirdMoney?: string; thirdMoney?: string;
// 分销商昵称(一级)
firstNickname: undefined,
// 分销商昵称(二级)
secondNickname: undefined,
// 分销商昵称(三级)
thirdNickname: undefined,
// 订单结算金额
settledPrice?: string;
// 换算成度
degreePrice?: string;
// 单价
price?: string;
// 订单支付金额
payPrice?: string;
// 订单是否失效(0未失效 1已失效) // 订单是否失效(0未失效 1已失效)
isInvalid?: number; isInvalid?: number;
// 佣金结算(0未结算 1已结算) // 佣金结算(0未结算 1已结算)
isSettled?: number; isSettled?: number;
// 分销佣金比例 // 佣金解冻(0未解冻 1已解冻)
rate?: number; isUnfreeze?: number;
// 订单月份 // 订单状态
month?: string; orderStatus?: number;
// 结算时间 // 结算时间
settleTime?: number; settleTime?: number;
// 订单备注
comments?: string;
// 商城ID // 商城ID
tenantId?: number; tenantId?: number;
// 创建时间 // 创建时间
@@ -71,9 +54,7 @@ export interface ShopDealerOrderParam extends PageParam {
secondUserId?: number; secondUserId?: number;
thirdUserId?: number; thirdUserId?: number;
userId?: number; userId?: number;
// 数据权限/资源ID通常传当前登录用户ID
resourceId?: number; resourceId?: number;
isInvalid?: number;
isSettled?: number;
month?: string;
keywords?: string; keywords?: string;
} }

View File

@@ -1,105 +0,0 @@
import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api';
import type { ShopDealerRecord, ShopDealerRecordParam } from './model';
/**
* 分页查询客户跟进情况
*/
export async function pageShopDealerRecord(params: ShopDealerRecordParam) {
const res = await request.get<ApiResult<PageResult<ShopDealerRecord>>>(
'/shop/shop-dealer-record/page',
{
params
}
);
if (res.code === 0) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 查询客户跟进情况列表
*/
export async function listShopDealerRecord(params?: ShopDealerRecordParam) {
const res = await request.get<ApiResult<ShopDealerRecord[]>>(
'/shop/shop-dealer-record',
{
params
}
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}
/**
* 添加客户跟进情况
*/
export async function addShopDealerRecord(data: ShopDealerRecord) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-dealer-record',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 修改客户跟进情况
*/
export async function updateShopDealerRecord(data: ShopDealerRecord) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-dealer-record',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 删除客户跟进情况
*/
export async function removeShopDealerRecord(id?: number) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-dealer-record/' + id
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 批量删除客户跟进情况
*/
export async function removeBatchShopDealerRecord(data: (number | undefined)[]) {
const res = await request.del<ApiResult<unknown>>(
'/shop/shop-dealer-record/batch',
{
data
}
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 根据id查询客户跟进情况
*/
export async function getShopDealerRecord(id: number) {
const res = await request.get<ApiResult<ShopDealerRecord>>(
'/shop/shop-dealer-record/' + id
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -1,39 +0,0 @@
import type { PageParam } from '@/api';
/**
* 客户跟进情况
*/
export interface ShopDealerRecord {
// ID
id?: number;
// 上级id, 0是顶级
parentId?: number;
// 客户ID
dealerId?: number;
// 内容
content?: string;
// 用户ID
userId?: number;
// 排序(数字越小越靠前)
sortNumber?: number;
// 备注
comments?: string;
// 状态, 0待处理, 1已完成
status?: number;
// 是否删除, 0否, 1是
deleted?: number;
// 租户id
tenantId?: number;
// 创建时间
createTime?: string;
// 修改时间
updateTime?: string;
}
/**
* 客户跟进情况搜索条件
*/
export interface ShopDealerRecordParam extends PageParam {
id?: number;
keywords?: string;
}

View File

@@ -99,16 +99,3 @@ export async function getShopDealerReferee(id: number) {
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
/**
* 根据userId查询推荐关系
*/
export async function getShopDealerRefereeByUserId(userId: number) {
const res = await request.get<ApiResult<ShopDealerReferee>>(
'/shop/shop-dealer-referee/getByUserId/' + userId
);
if (res.code === 0 && res.data) {
return res.data;
}
return Promise.reject(new Error(res.message));
}

View File

@@ -8,20 +8,8 @@ export interface ShopDealerReferee {
id?: number; id?: number;
// 分销商用户ID // 分销商用户ID
dealerId?: number; dealerId?: number;
// 分销商名称
dealerName?: string;
// 分销商手机号
dealerPhone?: string;
// 用户id(被推荐人) // 用户id(被推荐人)
userId?: number; userId?: number;
// 用户头像
avatar?: string;
// 用户昵称
nickname?: string;
// 用户别名
alias?: string;
// 用户手机号
phone?: string;
// 推荐关系层级(1,2,3) // 推荐关系层级(1,2,3)
level?: number; level?: number;
// 商城ID // 商城ID
@@ -38,8 +26,6 @@ export interface ShopDealerReferee {
export interface ShopDealerRefereeParam extends PageParam { export interface ShopDealerRefereeParam extends PageParam {
id?: number; id?: number;
dealerId?: number; dealerId?: number;
deleted?: number;
roleId?: number;
isAdmin?: boolean;
keywords?: string; keywords?: string;
deleted?: number;
} }

View File

@@ -59,21 +59,6 @@ export async function updateShopDealerUser(data: ShopDealerUser) {
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }
/**
* 修改分销商用户记录表
* @param data
*/
export async function updateShopDealerUserByUserId(data: ShopDealerUser) {
const res = await request.put<ApiResult<unknown>>(
'/shop/shop-dealer-user/updateByUserId',
data
);
if (res.code === 0) {
return res.message;
}
return Promise.reject(new Error(res.message));
}
/** /**
* 删除分销商用户记录表 * 删除分销商用户记录表
*/ */
@@ -110,8 +95,9 @@ export async function getShopDealerUser(userId: number) {
const res = await request.get<ApiResult<ShopDealerUser>>( const res = await request.get<ApiResult<ShopDealerUser>>(
'/shop/shop-dealer-user/' + userId '/shop/shop-dealer-user/' + userId
); );
if (res.code === 0 && res.data) { if (res.code === 0) {
return res.data; // 未注册为分销商时,后端可能返回 data=null这里用 null 表示“没有分销商信息”
return res.data || null;
} }
return Promise.reject(new Error(res.message)); return Promise.reject(new Error(res.message));
} }

View File

@@ -1,4 +1,4 @@
import type { PageParam } from '@/api'; import type { PageParam } from '@/api/index';
/** /**
* 分销商用户记录表 * 分销商用户记录表
@@ -22,9 +22,6 @@ export interface ShopDealerUser {
totalMoney?: string; totalMoney?: string;
// 推荐人用户ID // 推荐人用户ID
refereeId?: number; refereeId?: number;
dealerName?: string;
dealerPhone?: string;
dealerAvatar?: string;
// 成员数量(一级) // 成员数量(一级)
firstNum?: number; firstNum?: number;
// 成员数量(二级) // 成员数量(二级)

View File

@@ -2,6 +2,21 @@ import request from '@/utils/request';
import type { ApiResult, PageResult } from '@/api'; import type { ApiResult, PageResult } from '@/api';
import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model'; import type { ShopDealerWithdraw, ShopDealerWithdrawParam } from './model';
// WeChat transfer v3: backend may return `package_info` for MiniProgram to open the
// "confirm receipt" page via `wx.requestMerchantTransfer`.
export type ShopDealerWithdrawCreateResult =
| string
| {
package_info?: string;
packageInfo?: string;
[k: string]: any;
}
| null
| undefined;
// When applyStatus=20, user can "receive" (WeChat confirm receipt flow).
export type ShopDealerWithdrawReceiveResult = ShopDealerWithdrawCreateResult;
/** /**
* 分页查询分销商提现明细表 * 分页查询分销商提现明细表
*/ */
@@ -33,11 +48,40 @@ export async function listShopDealerWithdraw(params?: ShopDealerWithdrawParam) {
/** /**
* 添加分销商提现明细表 * 添加分销商提现明细表
*/ */
export async function addShopDealerWithdraw(data: ShopDealerWithdraw) { export async function addShopDealerWithdraw(data: ShopDealerWithdraw): Promise<ShopDealerWithdrawCreateResult> {
const res = await request.post<ApiResult<unknown>>( const res = await request.post<ApiResult<any>>(
'/shop/shop-dealer-withdraw', '/shop/shop-dealer-withdraw',
data data
); );
if (res.code === 0) {
// Some backends return `message`, while WeChat transfer flow returns `data.package_info`.
return res.data ?? res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 用户领取(仅当 applyStatus=20 时)- 后台返回 package_info 供小程序调起确认收款页
*/
export async function receiveShopDealerWithdraw(id: number): Promise<ShopDealerWithdrawReceiveResult> {
const res = await request.post<ApiResult<any>>(
'/shop/shop-dealer-withdraw/receive/' + id,
{}
);
if (res.code === 0) {
return res.data ?? res.message;
}
return Promise.reject(new Error(res.message));
}
/**
* 领取成功回调:前端确认收款后通知后台把状态置为 applyStatus=40
*/
export async function receiveSuccessShopDealerWithdraw(id: number) {
const res = await request.post<ApiResult<unknown>>(
'/shop/shop-dealer-withdraw/receive-success/' + id,
{}
);
if (res.code === 0) { if (res.code === 0) {
return res.message; return res.message;
} }

Some files were not shown because too many files have changed in this diff Show More