remove(feature): 删除经销商申请、用户地址管理和聊天消息功能模块
- 移除经销商申请相关页面配置和业务逻辑代码 - 删除用户地址管理功能的所有配置文件和实现组件 - 清理聊天消息发送功能的相关页面配置和业务代码 - 移除相关的API调用和数据模型引用 - 删除页面导航配置和相关的工具函数引用
This commit is contained in:
@@ -1,226 +0,0 @@
|
|||||||
# 🎯 管理员模式切换方案
|
|
||||||
|
|
||||||
## 📋 **问题分析**
|
|
||||||
|
|
||||||
### 原始问题
|
|
||||||
- 用户卡片中有两个扫码入口(门店核销 + 扫码登录)
|
|
||||||
- 用户体验不友好,容易混淆
|
|
||||||
- 管理员功能分散,缺乏统一入口
|
|
||||||
|
|
||||||
### 解决思路
|
|
||||||
设计一个管理员模式切换系统,通过模式切换来统一管理所有管理员功能。
|
|
||||||
|
|
||||||
## 🚀 **解决方案**
|
|
||||||
|
|
||||||
### 方案概述
|
|
||||||
创建一个**管理员模式切换**系统,包含:
|
|
||||||
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. **用户友好** - 直观的界面和流畅的交互
|
|
||||||
|
|
||||||
现在管理员用户可以享受更加统一和专业的管理体验!🚀
|
|
||||||
@@ -1,298 +0,0 @@
|
|||||||
# 🔄 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字段结构,同时保持向后兼容!** 🚀
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
# 🔧 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功能正常工作!** 🚀
|
|
||||||
@@ -1,246 +0,0 @@
|
|||||||
# 🎯 优惠券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. **用户体验**
|
|
||||||
- 保持加载状态的显示
|
|
||||||
- 提供合适的错误提示
|
|
||||||
- 确保操作反馈及时
|
|
||||||
|
|
||||||
## 🎯 预期收益
|
|
||||||
|
|
||||||
### 开发效率
|
|
||||||
- ✅ **代码简化**:减少复杂的状态判断逻辑
|
|
||||||
- ✅ **维护便利**:业务逻辑更清晰
|
|
||||||
- ✅ **扩展性强**:易于添加新的状态类型
|
|
||||||
|
|
||||||
### 用户体验
|
|
||||||
- ✅ **响应更快**:减少数据处理时间
|
|
||||||
- ✅ **数据准确**:后端计算状态更可靠
|
|
||||||
- ✅ **功能稳定**:减少前端状态判断错误
|
|
||||||
|
|
||||||
### 系统性能
|
|
||||||
- ✅ **网络优化**:减少不必要的数据传输
|
|
||||||
- ✅ **服务器优化**:专门的查询更高效
|
|
||||||
- ✅ **缓存友好**:专门接口更容易缓存
|
|
||||||
|
|
||||||
**现在优惠券功能已经完全适配后端的专门接口,提供了更好的性能和用户体验!** 🎉
|
|
||||||
@@ -1,178 +0,0 @@
|
|||||||
# 🎨 优惠券卡片对齐问题修复
|
|
||||||
|
|
||||||
## 🚨 问题描述
|
|
||||||
|
|
||||||
从截图可以看出,优惠券卡片存在对齐问题:
|
|
||||||
- 右侧的优惠券信息和按钮没有垂直居中
|
|
||||||
- 整体布局看起来不够协调
|
|
||||||
- 视觉效果不够美观
|
|
||||||
|
|
||||||
## 🔍 问题分析
|
|
||||||
|
|
||||||
### 原始布局问题
|
|
||||||
```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布局优化
|
|
||||||
- **影响范围**: 所有优惠券卡片
|
|
||||||
- **视觉改进**: 垂直居中对齐
|
|
||||||
- **兼容性**: 保持所有功能不变
|
|
||||||
|
|
||||||
**现在重新编译查看效果,优惠券卡片应该完美对齐了!** 🎨
|
|
||||||
@@ -1,168 +0,0 @@
|
|||||||
# 🔍 优惠券显示问题调试
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
|
|
||||||
你反馈优惠券有数据了,但是没有显示出来。这是一个常见的前端数据渲染问题。
|
|
||||||
|
|
||||||
## 🚀 已添加的调试功能
|
|
||||||
|
|
||||||
我已经在优惠券页面添加了详细的调试信息,帮助我们找出问题所在:
|
|
||||||
|
|
||||||
### 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和样式
|
|
||||||
|
|
||||||
### 问题5:Tab切换问题
|
|
||||||
**症状**:某个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"
|
|
||||||
}]
|
|
||||||
```
|
|
||||||
|
|
||||||
**现在请重新运行应用,查看控制台的调试信息,然后告诉我你看到了什么!** 🔍
|
|
||||||
@@ -1,340 +0,0 @@
|
|||||||
# 🚨 优惠券支付问题分析
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
|
|
||||||
用户选择优惠券后支付失败,但系统仍然提示"支付成功",这是一个严重的用户体验问题。
|
|
||||||
|
|
||||||
## 🔍 问题分析
|
|
||||||
|
|
||||||
### 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
|
|
||||||
|
|
||||||
// ⚠️ 问题点9:selectedCoupon.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. **增加订单状态检查** - 支付后验证订单状态
|
|
||||||
|
|
||||||
**建议立即修复此问题,避免用户资金损失和投诉!** 🚨
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
# 🐛 优惠券状态显示问题调试
|
|
||||||
|
|
||||||
## 🔍 问题描述
|
|
||||||
|
|
||||||
用户反馈优惠券显示"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. **优化用户体验**:确保状态变化时的实时更新
|
|
||||||
|
|
||||||
**如果问题仍然存在,请检查浏览器控制台中的调试日志,并提供具体的错误信息。** 🔍
|
|
||||||
@@ -1,153 +0,0 @@
|
|||||||
# 🔧 优惠券组件警告修复
|
|
||||||
|
|
||||||
## 🚨 修复的警告
|
|
||||||
|
|
||||||
### 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组件应该没有任何警告,并且类型安全!** ✨
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
# 分销中心页面优化总结
|
|
||||||
|
|
||||||
## 🔍 原始问题分析
|
|
||||||
|
|
||||||
### 主要问题
|
|
||||||
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. 性能优化
|
|
||||||
- 列表虚拟化(大量数据时)
|
|
||||||
- 图片懒加载
|
|
||||||
- 缓存策略优化
|
|
||||||
|
|
||||||
## 📱 移动端适配
|
|
||||||
|
|
||||||
- 响应式设计确保各种屏幕尺寸下的良好体验
|
|
||||||
- 触摸友好的交互元素
|
|
||||||
- 合适的字体大小和间距
|
|
||||||
- 底部安全区域处理
|
|
||||||
|
|
||||||
这次优化大幅提升了分销中心的用户体验和功能完整性,为后续的功能扩展奠定了良好基础。
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
# 🚨 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;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 📊 类型安全处理方案
|
|
||||||
|
|
||||||
### 方案1:instanceof检查(推荐)
|
|
||||||
```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类型警告已完全修复!** 🎯
|
|
||||||
@@ -1,166 +0,0 @@
|
|||||||
# 🎯 最终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错误
|
|
||||||
- ✅ **类型安全**:所有类型定义一致
|
|
||||||
- ✅ **功能正常**:优惠券显示和选择正常
|
|
||||||
- ✅ **代码质量**:统一的类型系统
|
|
||||||
|
|
||||||
**项目现在可以正常编译和运行了!** 🚀
|
|
||||||
@@ -1,206 +0,0 @@
|
|||||||
# 🎨 渐变设计指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
我为你的分销中心设计了一套完整的渐变主题系统,包含多种美观的渐变方案和统一的设计语言。
|
|
||||||
|
|
||||||
## 🌈 渐变主题方案
|
|
||||||
|
|
||||||
### 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 渐变效果
|
|
||||||
- 粒子背景
|
|
||||||
- 交互式渐变
|
|
||||||
|
|
||||||
这套渐变系统不仅美观,还具有很强的扩展性和可维护性,为你的应用提供了专业级的视觉体验!
|
|
||||||
@@ -1,224 +0,0 @@
|
|||||||
# ✅ 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带来的所有优势,包括自动缓存、错误处理和性能优化!🚀
|
|
||||||
@@ -1,256 +0,0 @@
|
|||||||
# 🔧 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组件更加简洁、高效,没有任何警告!** 🚀
|
|
||||||
@@ -1,215 +0,0 @@
|
|||||||
# 🚨 隐式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类型警告已修复!** 🎯
|
|
||||||
@@ -1,254 +0,0 @@
|
|||||||
# 🚨 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工作正常,不再有无限循环问题!** 🚀
|
|
||||||
@@ -1,204 +0,0 @@
|
|||||||
# 🔧 小程序"加载中..."问题解决方案
|
|
||||||
|
|
||||||
## 🚨 **问题描述**
|
|
||||||
|
|
||||||
用户点击分享链接打开小程序时,页面一直显示"加载中...",无法正常进入应用。
|
|
||||||
|
|
||||||
## 🔍 **问题根本原因**
|
|
||||||
|
|
||||||
通过代码分析发现主要问题:
|
|
||||||
|
|
||||||
### 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. 设备和网络环境信息
|
|
||||||
|
|
||||||
这样可以进一步定位和解决问题。
|
|
||||||
@@ -1,223 +0,0 @@
|
|||||||
# 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来简化组件逻辑并提升性能。
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
# 🚨 模块导入错误修复
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
|
|
||||||
错误信息:`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调用
|
|
||||||
|
|
||||||
**现在模块导入错误应该已经完全修复,页面可以正常加载了!** 🎉
|
|
||||||
@@ -1,261 +0,0 @@
|
|||||||
# 导航工具迁移指南
|
|
||||||
|
|
||||||
## 🎯 迁移目标
|
|
||||||
|
|
||||||
将项目中的 `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' });
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 示例3:TabBar切换
|
|
||||||
|
|
||||||
```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` 调用
|
|
||||||
- [ ] 添加必要的导入语句
|
|
||||||
- [ ] 测试所有页面跳转功能
|
|
||||||
- [ ] 验证参数传递正确性
|
|
||||||
- [ ] 检查错误处理是否正常
|
|
||||||
|
|
||||||
完成迁移后,你的导航代码将更加简洁、安全和易维护!
|
|
||||||
@@ -1,241 +0,0 @@
|
|||||||
# 导航工具使用指南
|
|
||||||
|
|
||||||
## 📖 概述
|
|
||||||
|
|
||||||
封装了 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')
|
|
||||||
```
|
|
||||||
|
|
||||||
现在你可以在整个项目中使用这些便捷的导航函数了!
|
|
||||||
@@ -1,281 +0,0 @@
|
|||||||
# 🎯 订单确认页面优惠券功能完整实现
|
|
||||||
|
|
||||||
## 🚀 功能概述
|
|
||||||
|
|
||||||
已完成订单确认页面的优惠券功能从后台读取并实现完整的业务逻辑,包括:
|
|
||||||
|
|
||||||
- ✅ **后端数据集成**:从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. **并发操作**:快速切换选择的处理
|
|
||||||
|
|
||||||
**现在订单确认页面的优惠券功能已经完全集成后端数据,提供了完整的业务逻辑和良好的用户体验!** 🎉
|
|
||||||
@@ -1,245 +0,0 @@
|
|||||||
# 前端订单提交实现文档
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本文档描述了前端订单提交的完整实现,包括单商品下单、购物车批量下单等场景。
|
|
||||||
|
|
||||||
## 核心改进
|
|
||||||
|
|
||||||
### 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. 测试订单标题长度限制功能
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
# 订单列表功能完善说明
|
|
||||||
|
|
||||||
## 完善的功能
|
|
||||||
|
|
||||||
### 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. 添加订单操作的二次确认
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
# ✅ 优惠券支付问题修复完成
|
|
||||||
|
|
||||||
## 🚨 修复的严重问题
|
|
||||||
|
|
||||||
### 问题描述
|
|
||||||
用户选择优惠券后支付失败,但系统仍然提示"支付成功",导致用户误以为支付完成。
|
|
||||||
|
|
||||||
### 根本原因
|
|
||||||
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. **定期代码审查** - 防止类似问题
|
|
||||||
@@ -1,264 +0,0 @@
|
|||||||
# 支付逻辑重构指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本文档描述了支付逻辑的重构过程,将原本分散的支付代码统一整合,提高了代码的可维护性和复用性。
|
|
||||||
|
|
||||||
## 重构前后对比
|
|
||||||
|
|
||||||
### 重构前的问题
|
|
||||||
|
|
||||||
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. **测试覆盖**:对重构后的代码进行充分的测试
|
|
||||||
@@ -1,229 +0,0 @@
|
|||||||
# 第一阶段优化完成报告
|
|
||||||
|
|
||||||
## 🎉 已完成的优化
|
|
||||||
|
|
||||||
### 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. **第三阶段**:测试覆盖、代码规范工具、安全性增强、文档完善
|
|
||||||
|
|
||||||
你可以选择继续进行第二阶段的优化,或者先在项目中应用这些改进并测试效果。
|
|
||||||
@@ -1,214 +0,0 @@
|
|||||||
# 微信小程序扫码登录集成文档
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本文档介绍如何在微信小程序中集成扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- ✅ **多平台支持** - 支持网页端、移动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. 考虑网络异常和超时情况
|
|
||||||
@@ -1,284 +0,0 @@
|
|||||||
# 微信小程序扫码登录使用指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
|
|
||||||
本项目已完整实现微信小程序扫码登录功能,支持用户通过小程序扫描网页端二维码快速登录。
|
|
||||||
|
|
||||||
## 功能特性
|
|
||||||
|
|
||||||
- ✅ **多种集成方式** - 按钮组件、弹窗组件、专门页面
|
|
||||||
- ✅ **自动解析二维码** - 支持多种二维码格式(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 | - | 失败回调 |
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
# 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` 系统,代码更加简洁,维护更加容易!
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
# 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. 所有方法都支持自动重试、错误处理、认证等功能
|
|
||||||
@@ -1,287 +0,0 @@
|
|||||||
# 🎉 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吧!**
|
|
||||||
@@ -1,319 +0,0 @@
|
|||||||
# 🔍 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与数据脱节**:显示硬编码选项
|
|
||||||
- ❌ **用户体验差**:无法正常使用
|
|
||||||
- ❌ **缺少验证**:错误处理不足
|
|
||||||
|
|
||||||
**建议立即进行重构**,实现完整的规格选择功能,确保组件可用性和用户体验。
|
|
||||||
@@ -1,236 +0,0 @@
|
|||||||
# 🚨 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类型错误已完全修复!** 🎯
|
|
||||||
@@ -1,163 +0,0 @@
|
|||||||
# 🔧 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. 检查控制台的调试信息
|
|
||||||
|
|
||||||
**如果优惠券仍然不显示,请查看控制台的调试日志,告诉我具体的数据加载情况!** 🚀
|
|
||||||
@@ -1,196 +0,0 @@
|
|||||||
# 🔄 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切换应该完全正常了!** 🎯
|
|
||||||
@@ -1,191 +0,0 @@
|
|||||||
# 🎨 主题切换系统使用指南
|
|
||||||
|
|
||||||
## 📖 功能概述
|
|
||||||
|
|
||||||
我们为你的小程序实现了一套完整的主题切换系统,用户可以选择不同的渐变主题来个性化界面。
|
|
||||||
|
|
||||||
## 🎯 功能特点
|
|
||||||
|
|
||||||
### ✨ 智能主题
|
|
||||||
- **自动选择**:根据用户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
|
|
||||||
- ✅ 主题持久化存储
|
|
||||||
- ✅ 小程序兼容性优化
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
**现在你的用户可以享受个性化的主题体验了!** 🎨✨
|
|
||||||
@@ -1,111 +0,0 @@
|
|||||||
# 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严格模式下的类型安全
|
|
||||||
- ✅ 动态属性添加的正确性
|
|
||||||
- ✅ 代码的可维护性和可读性
|
|
||||||
|
|
||||||
**现在项目应该能够完全正常编译了!** 🎉
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
# ✅ 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类型错误都已修复,项目可以正常编译和运行!** 🎯
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
# 🔧 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警告**
|
|
||||||
- ✅ **简化代码逻辑**
|
|
||||||
- ✅ **保持功能完整**
|
|
||||||
- ✅ **避免循环依赖**
|
|
||||||
|
|
||||||
这个修复使代码更加清晰和可维护,同时保持了所有原有功能。
|
|
||||||
@@ -1,171 +0,0 @@
|
|||||||
# 🚨 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调用不受影响
|
|
||||||
- ✅ **维护性**:代码更加规范和可维护
|
|
||||||
|
|
||||||
**现在所有的类型警告都已修复,代码类型安全!** 🎯
|
|
||||||
@@ -1,212 +0,0 @@
|
|||||||
# 统一扫码功能使用指南
|
|
||||||
|
|
||||||
## 🎯 功能概述
|
|
||||||
|
|
||||||
统一扫码功能将原有的**扫码登录**和**扫码核销**合并为一个入口,通过智能识别二维码内容自动执行相应操作。
|
|
||||||
|
|
||||||
## 📋 功能特性
|
|
||||||
|
|
||||||
### ✨ 智能识别
|
|
||||||
- 自动识别登录二维码和核销二维码
|
|
||||||
- 根据二维码内容自动执行相应操作
|
|
||||||
- 支持多种二维码格式(JSON加密、纯文本等)
|
|
||||||
|
|
||||||
### 🔄 统一体验
|
|
||||||
- 一个按钮解决两种扫码需求
|
|
||||||
- 统一的UI界面和交互逻辑
|
|
||||||
- 一致的错误处理和状态提示
|
|
||||||
|
|
||||||
### 📱 多入口支持
|
|
||||||
- 用户卡片中的统一扫码按钮
|
|
||||||
- 管理员面板中的统一扫码功能
|
|
||||||
- 独立的统一扫码页面
|
|
||||||
|
|
||||||
## 🛠️ 技术实现
|
|
||||||
|
|
||||||
### 核心文件
|
|
||||||
```
|
|
||||||
src/
|
|
||||||
├── hooks/
|
|
||||||
│ └── useUnifiedQRScan.ts # 统一扫码Hook
|
|
||||||
├── components/
|
|
||||||
│ └── UnifiedQRButton.tsx # 统一扫码按钮组件
|
|
||||||
└── pages/
|
|
||||||
└── unified-qr/
|
|
||||||
├── index.tsx # 统一扫码页面
|
|
||||||
└── index.config.ts # 页面配置
|
|
||||||
```
|
|
||||||
|
|
||||||
### Hook:useUnifiedQRScan
|
|
||||||
|
|
||||||
```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'
|
|
||||||
]
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 权限要求
|
|
||||||
- **扫码权限**: 所有用户都可以扫码
|
|
||||||
- **登录功能**: 需要用户已登录小程序
|
|
||||||
- **核销功能**: 需要管理员权限
|
|
||||||
|
|
||||||
## 🎯 未来规划
|
|
||||||
|
|
||||||
### 扩展可能性
|
|
||||||
- 支持更多类型的二维码(商品码、活动码等)
|
|
||||||
- 增加扫码统计和分析功能
|
|
||||||
- 支持批量扫码操作
|
|
||||||
- 增加扫码记录的云端同步
|
|
||||||
|
|
||||||
### 性能优化
|
|
||||||
- 扫码响应速度优化
|
|
||||||
- 二维码识别准确率提升
|
|
||||||
- 网络请求优化和缓存机制
|
|
||||||
@@ -1,357 +0,0 @@
|
|||||||
# 🏪 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,你可以在整个应用中轻松访问和管理商店信息,提高代码的可维护性和用户体验。
|
|
||||||
@@ -1,181 +0,0 @@
|
|||||||
# 后端多规格功能适配指南
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
前端已完成商品多规格功能集成,需要后端相应适配以支持完整的多规格商品流程。
|
|
||||||
|
|
||||||
## 需要适配的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信息
|
|
||||||
- [ ] 库存扣减逻辑适配多规格
|
|
||||||
- [ ] 价格计算逻辑适配多规格
|
|
||||||
- [ ] 测试多规格商品完整流程
|
|
||||||
@@ -1,248 +0,0 @@
|
|||||||
/**
|
|
||||||
* 订单服务实现类示例
|
|
||||||
* 展示如何保存订单商品信息的业务逻辑
|
|
||||||
*/
|
|
||||||
@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));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
# 前端多规格功能测试指南
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
已完成商品详情页多规格功能集成,包括:
|
|
||||||
- 规格数据加载
|
|
||||||
- 规格选择器组件
|
|
||||||
- 购物车支持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. **规格搜索**: 在规格较多时支持搜索功能
|
|
||||||
@@ -1,317 +0,0 @@
|
|||||||
/**
|
|
||||||
* 前端订单提交完整示例
|
|
||||||
* 展示如何使用新的订单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;
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
# 商品多规格功能集成总结
|
|
||||||
|
|
||||||
## 完成的工作
|
|
||||||
|
|
||||||
### 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适配工作量
|
|
||||||
- 数据迁移和兼容性
|
|
||||||
- 性能影响评估
|
|
||||||
|
|
||||||
### 缓解措施
|
|
||||||
- 详细的后端适配文档
|
|
||||||
- 完整的测试用例覆盖
|
|
||||||
- 分阶段部署和验证
|
|
||||||
@@ -1,190 +0,0 @@
|
|||||||
# 订单状态修复总结
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
|
|
||||||
### 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. **实时更新**: 考虑实现订单状态的实时推送更新
|
|
||||||
@@ -1,277 +0,0 @@
|
|||||||
# 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 |
|
|
||||||
|------|---------|---------|
|
|
||||||
| 数据存储 | 购物车商品 | 用户信息 |
|
|
||||||
| 本地持久化 | ✅ | ✅ |
|
|
||||||
| 服务器同步 | ❌ | ✅ |
|
|
||||||
| 状态管理 | ✅ | ✅ |
|
|
||||||
| 全局访问 | ✅ | ✅ |
|
|
||||||
| 权限控制 | ❌ | ✅ |
|
|
||||||
|
|
||||||
这样,用户信息管理就像购物车一样方便了,可以在任何组件中轻松访问和操作用户状态!
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# 水票配送订单:后端提示词(可直接发给后端)
|
|
||||||
|
|
||||||
## 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`
|
|
||||||
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '商品文章管理',
|
|
||||||
navigationBarTextStyle: 'black'
|
|
||||||
})
|
|
||||||
@@ -1,271 +0,0 @@
|
|||||||
import {useState} from "react";
|
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
|
||||||
import {Button, Cell, CellGroup, Empty, ConfigProvider, SearchBar, Tag, InfiniteLoading, Loading, PullToRefresh} from '@nutui/nutui-react-taro'
|
|
||||||
import {Edit, Del, Eye} from '@nutui/icons-react-taro'
|
|
||||||
import {View} from '@tarojs/components'
|
|
||||||
import {CmsArticle} from "@/api/cms/cmsArticle/model";
|
|
||||||
import {pageCmsArticle, removeCmsArticle} from "@/api/cms/cmsArticle";
|
|
||||||
import FixedButton from "@/components/FixedButton";
|
|
||||||
import dayjs from "dayjs";
|
|
||||||
|
|
||||||
const ArticleArticleManage = () => {
|
|
||||||
const [list, setList] = useState<CmsArticle[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
// const [refreshing, setRefreshing] = useState(false)
|
|
||||||
const [hasMore, setHasMore] = useState(true)
|
|
||||||
const [searchValue, setSearchValue] = useState('')
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [total, setTotal] = useState(0)
|
|
||||||
|
|
||||||
const reload = async (isRefresh = false) => {
|
|
||||||
if (isRefresh) {
|
|
||||||
setPage(1)
|
|
||||||
setList([])
|
|
||||||
setHasMore(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const currentPage = isRefresh ? 1 : page
|
|
||||||
const res = await pageCmsArticle({
|
|
||||||
page: currentPage,
|
|
||||||
limit: 10,
|
|
||||||
keywords: searchValue
|
|
||||||
})
|
|
||||||
|
|
||||||
if (res && res.list) {
|
|
||||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
|
||||||
setList(newList)
|
|
||||||
setTotal(res.count || 0)
|
|
||||||
|
|
||||||
// 判断是否还有更多数据
|
|
||||||
setHasMore(res.list.length === 10) // 如果返回的数据等于limit,说明可能还有更多
|
|
||||||
|
|
||||||
if (!isRefresh) {
|
|
||||||
setPage(currentPage + 1)
|
|
||||||
} else {
|
|
||||||
setPage(2) // 刷新后下一页是第2页
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasMore(false)
|
|
||||||
setTotal(0)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取文章失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取文章失败',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 搜索功能
|
|
||||||
const handleSearch = (value: string) => {
|
|
||||||
setSearchValue(value)
|
|
||||||
reload(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下拉刷新
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
// setRefreshing(true)
|
|
||||||
await reload(true)
|
|
||||||
// setRefreshing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 删除文章
|
|
||||||
const handleDelete = async (id?: number) => {
|
|
||||||
Taro.showModal({
|
|
||||||
title: '确认删除',
|
|
||||||
content: '确定要删除这篇文章吗?',
|
|
||||||
success: async (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
try {
|
|
||||||
await removeCmsArticle(id)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '删除成功',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
reload(true);
|
|
||||||
} catch (error) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '删除失败',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 编辑文章
|
|
||||||
const handleEdit = (item: CmsArticle) => {
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: `/shop/shopArticle/add?id=${item.articleId}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看文章详情
|
|
||||||
const handleView = (item: CmsArticle) => {
|
|
||||||
// 这里可以跳转到文章详情页面
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: `/cms/detail/index?id=${item.articleId}`
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取状态标签
|
|
||||||
const getStatusTag = (status?: number) => {
|
|
||||||
switch (status) {
|
|
||||||
case 0:
|
|
||||||
return <Tag type="success">已发布</Tag>
|
|
||||||
case 1:
|
|
||||||
return <Tag type="warning">待审核</Tag>
|
|
||||||
case 2:
|
|
||||||
return <Tag type="danger">已驳回</Tag>
|
|
||||||
case 3:
|
|
||||||
return <Tag type="danger">违规内容</Tag>
|
|
||||||
default:
|
|
||||||
return <Tag>未知</Tag>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载更多
|
|
||||||
const loadMore = async () => {
|
|
||||||
if (!loading && hasMore) {
|
|
||||||
await reload(false) // 不刷新,追加数据
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useDidShow(() => {
|
|
||||||
reload(true).then()
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigProvider>
|
|
||||||
{/* 搜索栏 */}
|
|
||||||
<View className="py-2">
|
|
||||||
<SearchBar
|
|
||||||
placeholder="搜索关键词"
|
|
||||||
value={searchValue}
|
|
||||||
onChange={setSearchValue}
|
|
||||||
onSearch={handleSearch}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 统计信息 */}
|
|
||||||
{total > 0 && (
|
|
||||||
<View className="px-4 py-2 text-sm text-gray-500">
|
|
||||||
共找到 {total} 篇文章
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 文章列表 */}
|
|
||||||
<PullToRefresh
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
headHeight={60}
|
|
||||||
>
|
|
||||||
<View className="px-4" style={{ height: 'calc(100vh - 160px)', overflowY: 'auto' }} id="article-scroll">
|
|
||||||
{list.length === 0 && !loading ? (
|
|
||||||
<View className="flex flex-col justify-center items-center" style={{height: 'calc(100vh - 200px)'}}>
|
|
||||||
<Empty
|
|
||||||
description="暂无文章数据"
|
|
||||||
style={{backgroundColor: 'transparent'}}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<InfiniteLoading
|
|
||||||
target="article-scroll"
|
|
||||||
hasMore={hasMore}
|
|
||||||
onLoadMore={loadMore}
|
|
||||||
loadingText={
|
|
||||||
<View className="flex justify-center items-center py-4">
|
|
||||||
<Loading />
|
|
||||||
<View className="ml-2">加载中...</View>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
loadMoreText={
|
|
||||||
<View className="text-center py-4 text-gray-500">
|
|
||||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{list.map((item, index) => (
|
|
||||||
<CellGroup key={item.articleId || index} className="mb-4">
|
|
||||||
<Cell>
|
|
||||||
<View className="flex flex-col gap-3 w-full">
|
|
||||||
{/* 文章标题和状态 */}
|
|
||||||
<View className="flex justify-between items-start">
|
|
||||||
<View className="flex-1 pr-2">
|
|
||||||
<View className="text-lg font-bold text-gray-900 line-clamp-2">
|
|
||||||
{item.title}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{getStatusTag(item.status)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 文章概述 */}
|
|
||||||
{item.overview && (
|
|
||||||
<View className="text-sm text-gray-600 line-clamp-2">
|
|
||||||
{item.overview}
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 文章信息 */}
|
|
||||||
<View className="flex justify-between items-center text-xs text-gray-500">
|
|
||||||
<View className="flex items-center gap-4">
|
|
||||||
<View>阅读: {item.actualViews || 0}</View>
|
|
||||||
{item.price && <View>价格: ¥{item.price}</View>}
|
|
||||||
<View>创建: {dayjs(item.createTime).format('MM-DD HH:mm')}</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<View className="flex justify-end gap-2 pt-2 border-t border-gray-100">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
icon={<Eye/>}
|
|
||||||
onClick={() => handleView(item)}
|
|
||||||
>
|
|
||||||
查看
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
icon={<Edit/>}
|
|
||||||
onClick={() => handleEdit(item)}
|
|
||||||
>
|
|
||||||
编辑
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="danger"
|
|
||||||
fill="outline"
|
|
||||||
icon={<Del/>}
|
|
||||||
onClick={() => handleDelete(item.articleId)}
|
|
||||||
>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Cell>
|
|
||||||
</CellGroup>
|
|
||||||
))}
|
|
||||||
</InfiniteLoading>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PullToRefresh>
|
|
||||||
|
|
||||||
{/* 底部浮动按钮 */}
|
|
||||||
<FixedButton
|
|
||||||
text="发布文章"
|
|
||||||
icon={<Edit />}
|
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/shopArticle/add'})}
|
|
||||||
/>
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default ArticleArticleManage;
|
|
||||||
@@ -1,244 +0,0 @@
|
|||||||
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)
|
|
||||||
|
|
||||||
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)
|
|
||||||
// })
|
|
||||||
}
|
|
||||||
|
|
||||||
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'}>*/}
|
|
||||||
{/* <span className={'text-sm text-gray-500'}>积分</span>*/}
|
|
||||||
{/* <span className={'text-xl'}>{pointsCount}</span>*/}
|
|
||||||
{/*</div>*/}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default UserCard;
|
|
||||||
@@ -1,186 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,102 +0,0 @@
|
|||||||
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
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
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;
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '管理中心'
|
|
||||||
})
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
import {useEffect} from 'react'
|
|
||||||
import {useUser} from "@/hooks/useUser";
|
|
||||||
import {Empty} from '@nutui/nutui-react-taro';
|
|
||||||
import {Text} from '@tarojs/components';
|
|
||||||
|
|
||||||
function Admin() {
|
|
||||||
const {
|
|
||||||
isAdmin
|
|
||||||
} = useUser();
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
if (!isAdmin()) {
|
|
||||||
return (
|
|
||||||
<Empty
|
|
||||||
description="您不是管理员"
|
|
||||||
imageSize={80}
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
height: 'calc(100vh - 200px)'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
|
|
||||||
</Empty>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Text>待开发...</Text>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Admin
|
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
export default {
|
export default {
|
||||||
pages: [
|
pages: [
|
||||||
'pages/index/index',
|
'pages/index/index',
|
||||||
'pages/cart/cart',
|
|
||||||
'pages/find/find',
|
|
||||||
'pages/user/user'
|
'pages/user/user'
|
||||||
],
|
],
|
||||||
"subpackages": [
|
"subpackages": [
|
||||||
@@ -27,96 +25,14 @@ export default {
|
|||||||
"detail/index"
|
"detail/index"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"root": "coupon",
|
|
||||||
"pages": [
|
|
||||||
"index"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"root": "user",
|
"root": "user",
|
||||||
"pages": [
|
"pages": [
|
||||||
"order/order",
|
|
||||||
"order/logistics/index",
|
|
||||||
"order/evaluate/index",
|
|
||||||
"order/refund/index",
|
|
||||||
"order/progress/index",
|
|
||||||
"company/company",
|
|
||||||
"profile/profile",
|
"profile/profile",
|
||||||
"setting/setting",
|
"setting/setting",
|
||||||
"userVerify/index",
|
"userVerify/index",
|
||||||
"address/index",
|
|
||||||
"address/add",
|
|
||||||
"address/wxAddress",
|
|
||||||
"help/index",
|
"help/index",
|
||||||
"about/index",
|
"about/index"
|
||||||
"wallet/wallet",
|
|
||||||
"coupon/index",
|
|
||||||
"coupon/receive",
|
|
||||||
"coupon/detail",
|
|
||||||
"points/points",
|
|
||||||
"ticket/index",
|
|
||||||
"ticket/use",
|
|
||||||
"ticket/orders/index",
|
|
||||||
// "gift/index",
|
|
||||||
// "gift/redeem",
|
|
||||||
// "gift/detail",
|
|
||||||
// "gift/add",
|
|
||||||
"store/verification",
|
|
||||||
"store/orders/index",
|
|
||||||
"theme/index",
|
|
||||||
"poster/poster",
|
|
||||||
"chat/conversation/index",
|
|
||||||
"chat/message/index",
|
|
||||||
"chat/message/add",
|
|
||||||
"chat/message/detail"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"root": "dealer",
|
|
||||||
"pages": [
|
|
||||||
"index",
|
|
||||||
"apply/add",
|
|
||||||
"withdraw/index",
|
|
||||||
"orders/index",
|
|
||||||
"capital/index",
|
|
||||||
"team/index",
|
|
||||||
"qrcode/index",
|
|
||||||
"invite-stats/index",
|
|
||||||
"info"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"root": "shop",
|
|
||||||
"pages": [
|
|
||||||
'category/index',
|
|
||||||
'orderDetail/index',
|
|
||||||
'goodsDetail/index',
|
|
||||||
'orderConfirm/index',
|
|
||||||
'orderConfirmCart/index',
|
|
||||||
'comments/index',
|
|
||||||
'search/index']
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"root": "store",
|
|
||||||
"pages": [
|
|
||||||
"index",
|
|
||||||
"orders/index"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"root": "rider",
|
|
||||||
"pages": [
|
|
||||||
"index",
|
|
||||||
"orders/index",
|
|
||||||
"ticket/verification/index"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"root": "admin",
|
|
||||||
"pages": [
|
|
||||||
"index",
|
|
||||||
"article/index",
|
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -156,12 +72,6 @@ export default {
|
|||||||
selectedIconPath: "assets/tabbar/home-active.png",
|
selectedIconPath: "assets/tabbar/home-active.png",
|
||||||
text: "首页",
|
text: "首页",
|
||||||
},
|
},
|
||||||
{
|
|
||||||
pagePath: "pages/find/find",
|
|
||||||
iconPath: "assets/tabbar/shop.png",
|
|
||||||
selectedIconPath: "assets/tabbar/shop-active.png",
|
|
||||||
text: "网点",
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
pagePath: "pages/user/user",
|
pagePath: "pages/user/user",
|
||||||
iconPath: "assets/tabbar/user.png",
|
iconPath: "assets/tabbar/user.png",
|
||||||
|
|||||||
@@ -45,7 +45,7 @@ function Category() {
|
|||||||
useShareAppMessage(() => {
|
useShareAppMessage(() => {
|
||||||
return {
|
return {
|
||||||
title: `${nav?.categoryName}_易赊宝`,
|
title: `${nav?.categoryName}_易赊宝`,
|
||||||
path: `/shop/category/index?id=${categoryId}`,
|
path: `/cms/category/index?id=${categoryId}`,
|
||||||
success: function () {
|
success: function () {
|
||||||
console.log('分享成功');
|
console.log('分享成功');
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '领劵中心',
|
|
||||||
navigationBarTextStyle: 'black'
|
|
||||||
})
|
|
||||||
@@ -1,368 +0,0 @@
|
|||||||
import {useState} from "react";
|
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
|
||||||
import {
|
|
||||||
Empty,
|
|
||||||
ConfigProvider,
|
|
||||||
InfiniteLoading,
|
|
||||||
Loading,
|
|
||||||
PullToRefresh,
|
|
||||||
Tabs,
|
|
||||||
TabPane
|
|
||||||
} from '@nutui/nutui-react-taro'
|
|
||||||
import {View} from '@tarojs/components'
|
|
||||||
import {ShopCoupon} from "@/api/shop/shopCoupon/model";
|
|
||||||
import {pageShopCoupon} from "@/api/shop/shopCoupon";
|
|
||||||
import CouponList from "@/components/CouponList";
|
|
||||||
import CouponGuide from "@/components/CouponGuide";
|
|
||||||
import CouponFilter from "@/components/CouponFilter";
|
|
||||||
import {CouponCardProps} from "@/components/CouponCard";
|
|
||||||
import {takeCoupon} from "@/api/shop/shopUserCoupon";
|
|
||||||
|
|
||||||
const CouponReceiveCenter = () => {
|
|
||||||
const [list, setList] = useState<ShopCoupon[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [hasMore, setHasMore] = useState(true)
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [activeTab, setActiveTab] = useState('0') // 0-全部 1-满减券 2-折扣券 3-免费券
|
|
||||||
const [showGuide, setShowGuide] = useState(false)
|
|
||||||
const [showFilter, setShowFilter] = useState(false)
|
|
||||||
const [filters, setFilters] = useState({
|
|
||||||
type: [] as number[],
|
|
||||||
minAmount: undefined as number | undefined,
|
|
||||||
sortBy: 'createTime' as 'createTime' | 'amount' | 'expireTime',
|
|
||||||
sortOrder: 'desc' as 'asc' | 'desc'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 获取优惠券类型过滤条件
|
|
||||||
const getTypeFilter = () => {
|
|
||||||
switch (String(activeTab)) {
|
|
||||||
case '0': // 全部
|
|
||||||
return {}
|
|
||||||
case '1': // 满减券
|
|
||||||
return { type: 10 }
|
|
||||||
case '2': // 折扣券
|
|
||||||
return { type: 20 }
|
|
||||||
case '3': // 免费券
|
|
||||||
return { type: 30 }
|
|
||||||
default:
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据传入的值获取类型过滤条件
|
|
||||||
const getTypeFilterByValue = (value: string | number) => {
|
|
||||||
switch (String(value)) {
|
|
||||||
case '0': // 全部
|
|
||||||
return {}
|
|
||||||
case '1': // 满减券
|
|
||||||
return { type: 10 }
|
|
||||||
case '2': // 折扣券
|
|
||||||
return { type: 20 }
|
|
||||||
case '3': // 免费券
|
|
||||||
return { type: 30 }
|
|
||||||
default:
|
|
||||||
return {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据类型过滤条件加载优惠券
|
|
||||||
const loadCouponsByType = async (typeFilter: any) => {
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const currentPage = 1
|
|
||||||
// 获取可领取的优惠券(启用状态且未过期)
|
|
||||||
const res = await pageShopCoupon({
|
|
||||||
page: currentPage,
|
|
||||||
limit: 10,
|
|
||||||
keywords: '',
|
|
||||||
enabled: 1, // 启用状态
|
|
||||||
isExpire: 0, // 未过期
|
|
||||||
...typeFilter
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('API返回数据:', res)
|
|
||||||
if (res && res.list) {
|
|
||||||
setList(res.list)
|
|
||||||
setHasMore(res.list.length === 10)
|
|
||||||
setPage(2)
|
|
||||||
} else {
|
|
||||||
setList([])
|
|
||||||
setHasMore(false)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取优惠券失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取优惠券失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const reload = async (isRefresh = false) => {
|
|
||||||
if (isRefresh) {
|
|
||||||
setPage(1)
|
|
||||||
setList([])
|
|
||||||
setHasMore(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
setLoading(true)
|
|
||||||
try {
|
|
||||||
const currentPage = isRefresh ? 1 : page
|
|
||||||
const typeFilter = getTypeFilter()
|
|
||||||
console.log('reload - 当前activeTab:', activeTab, '类型过滤:', typeFilter)
|
|
||||||
|
|
||||||
// 获取可领取的优惠券(启用状态且未过期)
|
|
||||||
const res = await pageShopCoupon({
|
|
||||||
page: currentPage,
|
|
||||||
limit: 10,
|
|
||||||
keywords: '',
|
|
||||||
enabled: 1, // 启用状态
|
|
||||||
isExpire: 0, // 未过期
|
|
||||||
...typeFilter,
|
|
||||||
// 应用筛选条件
|
|
||||||
...(filters.type.length > 0 && { type: filters.type[0] }),
|
|
||||||
sortBy: filters.sortBy,
|
|
||||||
sortOrder: filters.sortOrder
|
|
||||||
})
|
|
||||||
|
|
||||||
console.log('reload - API返回数据:', res)
|
|
||||||
if (res && res.list) {
|
|
||||||
const newList = isRefresh ? res.list : [...list, ...res.list]
|
|
||||||
setList(newList)
|
|
||||||
|
|
||||||
// 判断是否还有更多数据
|
|
||||||
setHasMore(res.list.length === 10)
|
|
||||||
|
|
||||||
if (!isRefresh) {
|
|
||||||
setPage(currentPage + 1)
|
|
||||||
} else {
|
|
||||||
setPage(2)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
setHasMore(false)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取优惠券失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取优惠券失败',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 下拉刷新
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
await reload(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Tab切换
|
|
||||||
const handleTabChange = (value: string | number) => {
|
|
||||||
console.log('Tab切换到:', value)
|
|
||||||
setActiveTab(String(value))
|
|
||||||
setPage(1)
|
|
||||||
setList([])
|
|
||||||
setHasMore(true)
|
|
||||||
|
|
||||||
// 直接传递类型值,避免异步状态更新问题
|
|
||||||
const typeFilter = getTypeFilterByValue(value)
|
|
||||||
console.log('类型过滤条件:', typeFilter)
|
|
||||||
|
|
||||||
// 立即加载数据
|
|
||||||
loadCouponsByType(typeFilter)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 转换优惠券数据为CouponCard组件所需格式
|
|
||||||
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
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
id: coupon.id?.toString(),
|
|
||||||
amount,
|
|
||||||
type,
|
|
||||||
status: 0, // 可领取状态
|
|
||||||
minAmount: parseFloat(coupon.minPrice || '0'),
|
|
||||||
title: coupon.name || '优惠券',
|
|
||||||
description: coupon.description,
|
|
||||||
startTime: coupon.startTime,
|
|
||||||
endTime: coupon.endTime,
|
|
||||||
showReceiveBtn: true, // 显示领取按钮
|
|
||||||
onReceive: () => handleReceiveCoupon(coupon),
|
|
||||||
theme: getThemeByType(coupon.type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据优惠券类型获取主题色
|
|
||||||
const getThemeByType = (type?: number): 'red' | 'orange' | 'blue' | 'purple' | 'green' => {
|
|
||||||
switch (type) {
|
|
||||||
case 10: return 'red' // 满减券-红色
|
|
||||||
case 20: return 'orange' // 折扣券-橙色
|
|
||||||
case 30: return 'green' // 免费券-绿色
|
|
||||||
default: return 'blue'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 领取优惠券
|
|
||||||
const handleReceiveCoupon = async (coupon: ShopCoupon) => {
|
|
||||||
try {
|
|
||||||
// 检查是否已登录
|
|
||||||
const userId = Taro.getStorageSync('UserId')
|
|
||||||
if (!userId) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请先登录',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 调用领取接口
|
|
||||||
await takeCoupon({
|
|
||||||
couponId: coupon.id!,
|
|
||||||
userId: userId
|
|
||||||
})
|
|
||||||
|
|
||||||
Taro.showToast({
|
|
||||||
title: '领取成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
|
|
||||||
// 刷新列表
|
|
||||||
reload(true)
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('领取优惠券失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: error.message || '领取失败',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 筛选条件变更
|
|
||||||
const handleFiltersChange = (newFilters: any) => {
|
|
||||||
setFilters(newFilters)
|
|
||||||
reload(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 查看我的优惠券
|
|
||||||
const handleViewMyCoupons = () => {
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/user/coupon/index'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载更多
|
|
||||||
const loadMore = async () => {
|
|
||||||
if (!loading && hasMore) {
|
|
||||||
await reload(false) // 不刷新,追加数据
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useDidShow(() => {
|
|
||||||
reload(true).then()
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<ConfigProvider className="h-screen flex flex-col">
|
|
||||||
{/* Tab切换 */}
|
|
||||||
<View className="bg-white hidden">
|
|
||||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
|
||||||
<TabPane title="全部" value="0">
|
|
||||||
</TabPane>
|
|
||||||
<TabPane title="满减券" value="1">
|
|
||||||
</TabPane>
|
|
||||||
<TabPane title="折扣券" value="2">
|
|
||||||
</TabPane>
|
|
||||||
<TabPane title="免费券" value="3">
|
|
||||||
</TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 优惠券列表 - 占满剩余空间 */}
|
|
||||||
<View className="flex-1 overflow-hidden">
|
|
||||||
<PullToRefresh
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
headHeight={60}
|
|
||||||
>
|
|
||||||
<View
|
|
||||||
style={{
|
|
||||||
height: 'calc(100vh - 60px)',
|
|
||||||
overflowY: 'auto',
|
|
||||||
paddingTop: '24px',
|
|
||||||
paddingBottom: '32px'
|
|
||||||
}}
|
|
||||||
id="coupon-scroll"
|
|
||||||
>
|
|
||||||
{list.length === 0 && !loading ? (
|
|
||||||
<View className="flex flex-col justify-center items-center h-full">
|
|
||||||
<Empty
|
|
||||||
description="暂无可领取的优惠券"
|
|
||||||
style={{backgroundColor: 'transparent'}}
|
|
||||||
actions={[
|
|
||||||
{
|
|
||||||
text: '查看我的优惠券',
|
|
||||||
onClick: handleViewMyCoupons
|
|
||||||
}
|
|
||||||
]}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<InfiniteLoading
|
|
||||||
target="coupon-scroll"
|
|
||||||
hasMore={hasMore}
|
|
||||||
onLoadMore={loadMore}
|
|
||||||
loadingText={
|
|
||||||
<View className="flex justify-center items-center py-4">
|
|
||||||
<Loading />
|
|
||||||
<View className="ml-2">加载中...</View>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
loadMoreText={
|
|
||||||
<View className="text-center py-4 text-gray-500">
|
|
||||||
{list.length === 0 ? "暂无数据" : "没有更多了"}
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<CouponList
|
|
||||||
coupons={list.map(transformCouponData)}
|
|
||||||
showEmpty={false}
|
|
||||||
/>
|
|
||||||
</InfiniteLoading>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PullToRefresh>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 使用指南弹窗 */}
|
|
||||||
<CouponGuide
|
|
||||||
visible={showGuide}
|
|
||||||
onClose={() => setShowGuide(false)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 筛选弹窗 */}
|
|
||||||
<CouponFilter
|
|
||||||
visible={showFilter}
|
|
||||||
filters={filters}
|
|
||||||
onFiltersChange={handleFiltersChange}
|
|
||||||
onClose={() => setShowFilter(false)}
|
|
||||||
/>
|
|
||||||
</ConfigProvider>
|
|
||||||
);
|
|
||||||
|
|
||||||
};
|
|
||||||
|
|
||||||
export default CouponReceiveCenter;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '注册成为会员',
|
|
||||||
navigationBarTextStyle: 'black'
|
|
||||||
})
|
|
||||||
@@ -1,489 +0,0 @@
|
|||||||
import {useEffect, useState, useRef} from "react";
|
|
||||||
import {Loading, CellGroup, Input, Form, Avatar, Button, Space} from '@nutui/nutui-react-taro'
|
|
||||||
import {Edit} from '@nutui/icons-react-taro'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {View} from '@tarojs/components'
|
|
||||||
import FixedButton from "@/components/FixedButton";
|
|
||||||
import {useUser} from "@/hooks/useUser";
|
|
||||||
import {TenantId} from "@/config/app";
|
|
||||||
import {updateUser} from "@/api/system/user";
|
|
||||||
import {User} from "@/api/system/user/model";
|
|
||||||
import {getStoredInviteParams, handleInviteRelation} from "@/utils/invite";
|
|
||||||
import {addShopDealerUser} from "@/api/shop/shopDealerUser";
|
|
||||||
import {addUserRole, listUserRole, updateUserRole} from "@/api/system/userRole";
|
|
||||||
import { listRoles } from "@/api/system/role";
|
|
||||||
import type { UserRole } from "@/api/system/userRole/model";
|
|
||||||
|
|
||||||
// 类型定义
|
|
||||||
interface ChooseAvatarEvent {
|
|
||||||
detail: {
|
|
||||||
avatarUrl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InputEvent {
|
|
||||||
detail: {
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
const AddUserAddress = () => {
|
|
||||||
const {user, loginUser, fetchUserInfo} = useUser()
|
|
||||||
const [loading, setLoading] = useState<boolean>(true)
|
|
||||||
const [FormData, setFormData] = useState<User>()
|
|
||||||
const formRef = useRef<any>(null)
|
|
||||||
|
|
||||||
const reload = async () => {
|
|
||||||
const inviteParams = getStoredInviteParams()
|
|
||||||
if (inviteParams?.inviter) {
|
|
||||||
setFormData({
|
|
||||||
...user,
|
|
||||||
refereeId: Number(inviteParams.inviter),
|
|
||||||
// 清空昵称,强制用户手动输入
|
|
||||||
nickname: '',
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
// 如果没有邀请参数,也要确保昵称为空
|
|
||||||
setFormData({
|
|
||||||
...user,
|
|
||||||
nickname: '',
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
const uploadAvatar = ({detail}: ChooseAvatarEvent) => {
|
|
||||||
// 先更新本地显示的头像(临时显示)
|
|
||||||
const tempFormData = {
|
|
||||||
...FormData,
|
|
||||||
avatar: `${detail.avatarUrl}`,
|
|
||||||
}
|
|
||||||
setFormData(tempFormData)
|
|
||||||
|
|
||||||
Taro.uploadFile({
|
|
||||||
url: 'https://server.websoft.top/api/oss/upload',
|
|
||||||
filePath: detail.avatarUrl,
|
|
||||||
name: 'file',
|
|
||||||
header: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
TenantId
|
|
||||||
},
|
|
||||||
success: async (res) => {
|
|
||||||
const data = JSON.parse(res.data);
|
|
||||||
if (data.code === 0) {
|
|
||||||
const finalAvatarUrl = `${data.data.thumbnail}`
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 使用 useUser hook 的 updateUser 方法更新头像
|
|
||||||
await updateUser({
|
|
||||||
avatar: finalAvatarUrl
|
|
||||||
})
|
|
||||||
|
|
||||||
Taro.showToast({
|
|
||||||
title: '头像上传成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 1500
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
console.error('更新用户头像失败:', error)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 无论用户信息更新是否成功,都要更新本地FormData
|
|
||||||
const finalFormData = {
|
|
||||||
...tempFormData,
|
|
||||||
avatar: finalAvatarUrl
|
|
||||||
}
|
|
||||||
setFormData(finalFormData)
|
|
||||||
|
|
||||||
// 同步更新表单字段
|
|
||||||
if (formRef.current) {
|
|
||||||
formRef.current.setFieldsValue({
|
|
||||||
avatar: finalAvatarUrl
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 上传失败,恢复原来的头像
|
|
||||||
setFormData({
|
|
||||||
...FormData,
|
|
||||||
avatar: user?.avatar || ''
|
|
||||||
})
|
|
||||||
Taro.showToast({
|
|
||||||
title: '上传失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: (error) => {
|
|
||||||
console.error('上传头像失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '上传失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
// 恢复原来的头像
|
|
||||||
setFormData({
|
|
||||||
...FormData,
|
|
||||||
avatar: user?.avatar || ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 提交表单
|
|
||||||
const submitSucceed = async (values: User) => {
|
|
||||||
try {
|
|
||||||
// 验证必填字段
|
|
||||||
if (!values.phone && !FormData?.phone) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请先获取手机号',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证昵称:必须填写且不能是默认的微信昵称
|
|
||||||
const nickname = values.realName || FormData?.nickname || '';
|
|
||||||
if (!nickname || nickname.trim() === '') {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请上传头像和填写昵称',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为默认的微信昵称(常见的默认昵称)
|
|
||||||
const defaultNicknames = ['微信用户', 'WeChat User', '微信昵称'];
|
|
||||||
if (defaultNicknames.includes(nickname.trim())) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请填写真实昵称,不能使用默认昵称',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证昵称长度
|
|
||||||
if (nickname.trim().length < 2) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '昵称至少需要2个字符',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!values.avatar && !FormData?.avatar) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请上传头像',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
console.log(values,FormData)
|
|
||||||
|
|
||||||
if (!user?.userId) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '用户信息缺失,请先登录',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let roles: UserRole[] = [];
|
|
||||||
try {
|
|
||||||
roles = await listUserRole({userId: user.userId})
|
|
||||||
console.log(roles, 'roles...')
|
|
||||||
} catch (e) {
|
|
||||||
// 新用户/权限限制时可能查不到角色列表,不影响基础注册流程
|
|
||||||
console.warn('查询用户角色失败,将尝试直接写入默认角色:', e)
|
|
||||||
roles = []
|
|
||||||
}
|
|
||||||
|
|
||||||
// 准备提交的数据
|
|
||||||
await updateUser({
|
|
||||||
userId: user.userId,
|
|
||||||
nickname: values.realName || FormData?.nickname,
|
|
||||||
phone: values.phone || FormData?.phone,
|
|
||||||
avatar: values.avatar || FormData?.avatar,
|
|
||||||
refereeId: values.refereeId || FormData?.refereeId
|
|
||||||
});
|
|
||||||
|
|
||||||
await addShopDealerUser({
|
|
||||||
userId: user.userId,
|
|
||||||
realName: values.realName || FormData?.nickname,
|
|
||||||
mobile: values.phone || FormData?.phone,
|
|
||||||
refereeId: Number(values.refereeId) || Number(FormData?.refereeId)
|
|
||||||
})
|
|
||||||
|
|
||||||
// 通知其他页面(如“我的”页、分销中心页)刷新经销商信息
|
|
||||||
Taro.eventCenter.trigger('dealerUser:changed')
|
|
||||||
|
|
||||||
// 角色为空时这里会导致“注册成功但没有角色”,这里做一次兜底写入默认 user 角色
|
|
||||||
try {
|
|
||||||
// 1) 先尝试通过 roleCode=user 查询角色ID(避免硬编码)
|
|
||||||
// 2) 取不到就回退到旧的默认ID(1848)
|
|
||||||
let userRoleId: number | undefined;
|
|
||||||
try {
|
|
||||||
// 注意:当前 request.get 的封装不支持 axios 风格的 { params: ... },
|
|
||||||
// 某些自动生成的 API 可能无法按参数过滤;这里直接取全量再本地查找更稳。
|
|
||||||
const roleList = await listRoles();
|
|
||||||
userRoleId = roleList?.find(r => r.roleCode === 'user')?.roleId;
|
|
||||||
} catch (_) {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
if (!userRoleId) userRoleId = 1848;
|
|
||||||
|
|
||||||
const baseRolePayload = {
|
|
||||||
userId: user.userId,
|
|
||||||
tenantId: Number(TenantId),
|
|
||||||
roleId: userRoleId
|
|
||||||
};
|
|
||||||
|
|
||||||
// 后端若已创建 user-role 记录则更新;否则尝试“无id更新”触发创建(多数实现会 upsert)
|
|
||||||
if (roles.length > 0) {
|
|
||||||
await updateUserRole({
|
|
||||||
...roles[0],
|
|
||||||
roleId: userRoleId
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
try {
|
|
||||||
await addUserRole(baseRolePayload);
|
|
||||||
} catch (_) {
|
|
||||||
// 兼容后端仅支持 PUT upsert 的情况
|
|
||||||
await updateUserRole(baseRolePayload);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新一次用户信息,确保 roles 写回本地缓存,避免“我的”页显示为空/不一致
|
|
||||||
await fetchUserInfo();
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('写入默认角色失败(不影响注册成功):', e)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
Taro.showToast({
|
|
||||||
title: `注册成功`,
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
// “我的”是 tabBar 页面,注册完成后直接切到“我的”
|
|
||||||
Taro.switchTab({ url: '/pages/user/user' });
|
|
||||||
}, 1000);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('验证邀请人失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取微信昵称
|
|
||||||
const getWxNickname = (nickname: string) => {
|
|
||||||
// 更新表单数据
|
|
||||||
const updatedFormData = {
|
|
||||||
...FormData,
|
|
||||||
nickname: nickname
|
|
||||||
}
|
|
||||||
setFormData(updatedFormData);
|
|
||||||
|
|
||||||
// 同步更新表单字段
|
|
||||||
if (formRef.current) {
|
|
||||||
formRef.current.setFieldsValue({
|
|
||||||
realName: nickname
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 获取用户手机号 */
|
|
||||||
const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
|
|
||||||
const {code, encryptedData, iv} = detail
|
|
||||||
Taro.login({
|
|
||||||
success: (loginRes) => {
|
|
||||||
if (code) {
|
|
||||||
Taro.request({
|
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
authCode: loginRes.code,
|
|
||||||
code,
|
|
||||||
encryptedData,
|
|
||||||
iv,
|
|
||||||
notVerifyPhone: true,
|
|
||||||
refereeId: 0,
|
|
||||||
sceneType: 'save_referee',
|
|
||||||
tenantId: TenantId
|
|
||||||
},
|
|
||||||
header: {
|
|
||||||
'content-type': 'application/json',
|
|
||||||
TenantId
|
|
||||||
},
|
|
||||||
success: async function (res) {
|
|
||||||
if (res.data.code == 1) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: res.data.message,
|
|
||||||
icon: 'error',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 登录成功
|
|
||||||
const token = res.data.data.access_token;
|
|
||||||
const userData = res.data.data.user;
|
|
||||||
console.log(userData, 'userData...')
|
|
||||||
// 使用useUser Hook的loginUser方法更新状态
|
|
||||||
loginUser(token, userData);
|
|
||||||
|
|
||||||
if (userData.phone) {
|
|
||||||
console.log('手机号已获取', userData.phone)
|
|
||||||
const updatedFormData = {
|
|
||||||
...FormData,
|
|
||||||
phone: userData.phone,
|
|
||||||
// 不自动填充微信昵称,保持用户已输入的昵称
|
|
||||||
nickname: FormData?.nickname || '',
|
|
||||||
// 只在没有头像时才使用微信头像
|
|
||||||
avatar: FormData?.avatar || userData.avatar
|
|
||||||
}
|
|
||||||
setFormData(updatedFormData)
|
|
||||||
|
|
||||||
// 更新表单字段值
|
|
||||||
if (formRef.current) {
|
|
||||||
formRef.current.setFieldsValue({
|
|
||||||
phone: userData.phone,
|
|
||||||
// 不覆盖用户已输入的昵称
|
|
||||||
realName: FormData?.nickname || '',
|
|
||||||
avatar: FormData?.avatar || userData.avatar
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
Taro.showToast({
|
|
||||||
title: '手机号获取成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 1500
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
// 处理邀请关系
|
|
||||||
if (userData?.userId) {
|
|
||||||
try {
|
|
||||||
const inviteSuccess = await handleInviteRelation(userData.userId)
|
|
||||||
if (inviteSuccess) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '邀请关系建立成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理邀请关系失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示登录成功提示
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: '注册成功',
|
|
||||||
// icon: 'success',
|
|
||||||
// duration: 1500
|
|
||||||
// })
|
|
||||||
|
|
||||||
// 不需要重新启动小程序,状态已经通过useUser更新
|
|
||||||
// 可以选择性地刷新当前页面数据
|
|
||||||
// await reload();
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('登录失败!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理固定按钮点击事件
|
|
||||||
const handleFixedButtonClick = () => {
|
|
||||||
// 触发表单提交
|
|
||||||
formRef.current?.submit();
|
|
||||||
};
|
|
||||||
|
|
||||||
const submitFailed = (error: any) => {
|
|
||||||
console.log(error, 'err...')
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload().then(() => {
|
|
||||||
setLoading(false)
|
|
||||||
})
|
|
||||||
}, [user?.userId]); // 依赖用户ID,当用户变化时重新加载
|
|
||||||
|
|
||||||
// 当FormData变化时,同步更新表单字段值
|
|
||||||
useEffect(() => {
|
|
||||||
if (formRef.current && FormData) {
|
|
||||||
formRef.current.setFieldsValue({
|
|
||||||
refereeId: FormData.refereeId,
|
|
||||||
phone: FormData.phone,
|
|
||||||
avatar: FormData.avatar,
|
|
||||||
realName: FormData.nickname
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [FormData]);
|
|
||||||
|
|
||||||
if (loading) {
|
|
||||||
return <Loading className={'px-2'}>加载中</Loading>
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Form
|
|
||||||
ref={formRef}
|
|
||||||
divider
|
|
||||||
initialValues={FormData}
|
|
||||||
labelPosition="left"
|
|
||||||
onFinish={(values) => submitSucceed(values)}
|
|
||||||
onFinishFailed={(errors) => submitFailed(errors)}
|
|
||||||
>
|
|
||||||
<View className={'bg-gray-100 h-3'}></View>
|
|
||||||
<CellGroup style={{padding: '4px 0'}}>
|
|
||||||
{/*<Form.Item name="refereeId" label="邀请人ID" initialValue={FormData?.refereeId} required>*/}
|
|
||||||
{/* <Input placeholder="邀请人ID" disabled={false}/>*/}
|
|
||||||
{/*</Form.Item>*/}
|
|
||||||
<Form.Item name="phone" label="手机号" initialValue={FormData?.phone} required>
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<Input
|
|
||||||
placeholder="请填写手机号"
|
|
||||||
disabled={true}
|
|
||||||
maxLength={11}
|
|
||||||
value={FormData?.phone || ''}
|
|
||||||
/>
|
|
||||||
<Button style={{color: '#ffffff'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
|
||||||
<Space>
|
|
||||||
<Button size="small">点击获取</Button>
|
|
||||||
</Space>
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</Form.Item>
|
|
||||||
{
|
|
||||||
FormData?.phone && <Form.Item name="avatar" label="头像" initialValue={FormData?.avatar} required>
|
|
||||||
<Button open-type="chooseAvatar" style={{height: '58px'}} onChooseAvatar={uploadAvatar}>
|
|
||||||
<Avatar src={FormData?.avatar || user?.avatar} size="54"/>
|
|
||||||
</Button>
|
|
||||||
</Form.Item>
|
|
||||||
}
|
|
||||||
<Form.Item name="realName" label="昵称" initialValue="" required>
|
|
||||||
<Input
|
|
||||||
type="nickname"
|
|
||||||
className="info-content__input"
|
|
||||||
placeholder="请获取微信昵称"
|
|
||||||
value={FormData?.nickname || ''}
|
|
||||||
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
</CellGroup>
|
|
||||||
</Form>
|
|
||||||
|
|
||||||
{/* 底部浮动按钮 */}
|
|
||||||
<FixedButton
|
|
||||||
icon={<Edit/>}
|
|
||||||
text={'立即注册'}
|
|
||||||
onClick={handleFixedButtonClick}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
|
|
||||||
export default AddUserAddress;
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '收益明细'
|
|
||||||
})
|
|
||||||
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
/* Intentionally empty: styling is done via utility classes. */
|
|
||||||
|
|
||||||
@@ -1,199 +0,0 @@
|
|||||||
import React, {useCallback, useEffect, useState} from 'react'
|
|
||||||
import {View, Text, ScrollView} from '@tarojs/components'
|
|
||||||
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {pageShopDealerCapital} from '@/api/shop/shopDealerCapital'
|
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
|
||||||
import type {ShopDealerCapital} from '@/api/shop/shopDealerCapital/model'
|
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
|
||||||
|
|
||||||
const DealerCapital: React.FC = () => {
|
|
||||||
const {dealerUser} = useDealerUser()
|
|
||||||
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [refreshing, setRefreshing] = useState(false)
|
|
||||||
const [loadingMore, setLoadingMore] = useState(false)
|
|
||||||
const [currentPage, setCurrentPage] = useState(1)
|
|
||||||
const [hasMore, setHasMore] = useState(true)
|
|
||||||
const [records, setRecords] = useState<ShopDealerCapital[]>([])
|
|
||||||
|
|
||||||
const getFlowTypeText = (flowType?: number) => {
|
|
||||||
switch (flowType) {
|
|
||||||
case 10:
|
|
||||||
return '佣金收入'
|
|
||||||
case 20:
|
|
||||||
return '提现支出'
|
|
||||||
case 30:
|
|
||||||
return '转账支出'
|
|
||||||
case 40:
|
|
||||||
return '转账收入'
|
|
||||||
default:
|
|
||||||
return '资金变动'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getFlowTypeTag = (flowType?: number) => {
|
|
||||||
// 收入:success;支出:danger;其它:default
|
|
||||||
if (flowType === 10 || flowType === 40) return 'success'
|
|
||||||
if (flowType === 20 || flowType === 30) return 'danger'
|
|
||||||
return 'default'
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatMoney = (flowType?: number, money?: string) => {
|
|
||||||
const isIncome = flowType === 10 || flowType === 40
|
|
||||||
const isExpense = flowType === 20 || flowType === 30
|
|
||||||
const sign = isIncome ? '+' : isExpense ? '-' : ''
|
|
||||||
return `${sign}${money || '0.00'}`
|
|
||||||
}
|
|
||||||
|
|
||||||
const fetchRecords = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
|
||||||
if (!dealerUser?.userId) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isRefresh) {
|
|
||||||
setRefreshing(true)
|
|
||||||
} else if (page === 1) {
|
|
||||||
setLoading(true)
|
|
||||||
} else {
|
|
||||||
setLoadingMore(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await pageShopDealerCapital({
|
|
||||||
page,
|
|
||||||
limit: PAGE_SIZE,
|
|
||||||
// 只显示与当前登录用户相关的收益明细
|
|
||||||
userId: dealerUser.userId
|
|
||||||
})
|
|
||||||
|
|
||||||
const list = result?.list || []
|
|
||||||
if (page === 1) {
|
|
||||||
setRecords(list)
|
|
||||||
} else {
|
|
||||||
setRecords(prev => [...prev, ...list])
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasMore(list.length === PAGE_SIZE)
|
|
||||||
setCurrentPage(page)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取收益明细失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取收益明细失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setRefreshing(false)
|
|
||||||
setLoadingMore(false)
|
|
||||||
}
|
|
||||||
}, [dealerUser?.userId])
|
|
||||||
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
await fetchRecords(1, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleLoadMore = async () => {
|
|
||||||
if (!loadingMore && hasMore) {
|
|
||||||
await fetchRecords(currentPage + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (dealerUser?.userId) {
|
|
||||||
fetchRecords(1)
|
|
||||||
}
|
|
||||||
}, [fetchRecords, dealerUser?.userId])
|
|
||||||
|
|
||||||
if (!dealerUser) {
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="min-h-screen bg-gray-50">
|
|
||||||
<PullToRefresh
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
disabled={refreshing}
|
|
||||||
pullingText="下拉刷新"
|
|
||||||
canReleaseText="释放刷新"
|
|
||||||
refreshingText="刷新中..."
|
|
||||||
completeText="刷新完成"
|
|
||||||
>
|
|
||||||
<ScrollView
|
|
||||||
scrollY
|
|
||||||
className="h-screen"
|
|
||||||
onScrollToLower={handleLoadMore}
|
|
||||||
lowerThreshold={50}
|
|
||||||
>
|
|
||||||
<View className="p-4">
|
|
||||||
{loading && records.length === 0 ? (
|
|
||||||
<View className="text-center py-8">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
) : records.length > 0 ? (
|
|
||||||
<>
|
|
||||||
{records.map((item) => (
|
|
||||||
<View key={item.id} className="bg-white rounded-lg p-4 mb-3 shadow-sm">
|
|
||||||
<View className="flex justify-between items-start mb-1">
|
|
||||||
<Text className="font-semibold text-gray-800">
|
|
||||||
{item.describe || '收益明细'}
|
|
||||||
</Text>
|
|
||||||
<Tag type={getFlowTypeTag(item.flowType)}>
|
|
||||||
{getFlowTypeText(item.flowType)}
|
|
||||||
</Tag>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex justify-between items-center mb-1">
|
|
||||||
<Text className="text-sm text-gray-400">
|
|
||||||
佣金收入
|
|
||||||
</Text>
|
|
||||||
<Text
|
|
||||||
className={`text-sm font-semibold ${
|
|
||||||
item.flowType === 10 || item.flowType === 40 ? 'text-green-600' :
|
|
||||||
item.flowType === 20 || item.flowType === 30 ? 'text-red-500' :
|
|
||||||
'text-gray-700'
|
|
||||||
}`}
|
|
||||||
>
|
|
||||||
{formatMoney(item.flowType, item.money)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex justify-between items-center">
|
|
||||||
<Text className="text-sm text-gray-400">
|
|
||||||
{/*用户:{item.userId ?? '-'}*/}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-400">
|
|
||||||
{item.createTime || '-'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
|
|
||||||
{loadingMore && (
|
|
||||||
<View className="text-center py-4">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-1 text-sm">加载更多...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{!hasMore && records.length > 0 && (
|
|
||||||
<View className="text-center py-4">
|
|
||||||
<Text className="text-gray-400 text-sm">没有更多数据了</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无收益明细"/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</PullToRefresh>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DealerCapital
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '易赊宝分享中心'
|
|
||||||
})
|
|
||||||
@@ -1,295 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import {View, Text} from '@tarojs/components'
|
|
||||||
import {ConfigProvider, Button, Grid, Avatar} from '@nutui/nutui-react-taro'
|
|
||||||
import {
|
|
||||||
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 = () => {
|
|
||||||
const {
|
|
||||||
dealerUser,
|
|
||||||
error,
|
|
||||||
refresh,
|
|
||||||
} = useDealerUser()
|
|
||||||
|
|
||||||
// 使用主题样式
|
|
||||||
const themeStyles = useThemeStyles()
|
|
||||||
|
|
||||||
// 导航到各个功能页面
|
|
||||||
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 (
|
|
||||||
<View className="p-4">
|
|
||||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
|
||||||
<Text className="text-red-600">{error}</Text>
|
|
||||||
</View>
|
|
||||||
<Button type="primary" onClick={refresh}>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-100 min-h-screen">
|
|
||||||
<View>
|
|
||||||
{/*头部信息*/}
|
|
||||||
{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}
|
|
||||||
</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('/user/userVerify/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
|
|
||||||
@@ -1,157 +0,0 @@
|
|||||||
import React from 'react'
|
|
||||||
import { View, Text } from '@tarojs/components'
|
|
||||||
import { Button, Cell, CellGroup, Tag } from '@nutui/nutui-react-taro'
|
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
|
|
||||||
const DealerInfo: React.FC = () => {
|
|
||||||
const {
|
|
||||||
dealerUser,
|
|
||||||
loading,
|
|
||||||
error,
|
|
||||||
refresh,
|
|
||||||
} = useDealerUser()
|
|
||||||
|
|
||||||
// 跳转到申请页面
|
|
||||||
const navigateToApply = () => {
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/pages/dealer/apply/add'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<View className="p-4">
|
|
||||||
<View className="bg-red-50 border border-red-200 rounded-lg p-4 mb-4">
|
|
||||||
<Text className="text-red-600">{error}</Text>
|
|
||||||
</View>
|
|
||||||
<Button type="primary" onClick={refresh}>
|
|
||||||
重试
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen">
|
|
||||||
{/* 页面标题 */}
|
|
||||||
<View className="bg-white px-4 py-3 border-b border-gray-100">
|
|
||||||
<Text className="text-lg font-bold text-center">
|
|
||||||
经销商信息
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{!dealerUser ? (
|
|
||||||
// 非经销商状态
|
|
||||||
<View className="bg-white mx-4 mt-4 rounded-lg p-6">
|
|
||||||
<View className="text-center py-8">
|
|
||||||
<Text className="text-gray-500 mb-4">您还不是经销商</Text>
|
|
||||||
<Text className="text-sm text-gray-400 mb-6">
|
|
||||||
成为经销商后可享受专属价格和佣金收益
|
|
||||||
</Text>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
onClick={navigateToApply}
|
|
||||||
>
|
|
||||||
申请成为经销商
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
// 经销商信息展示
|
|
||||||
<View>
|
|
||||||
{/* 状态卡片 */}
|
|
||||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
|
||||||
<View className="flex items-center justify-between mb-4">
|
|
||||||
<Text className="text-lg font-semibold">经销商状态</Text>
|
|
||||||
<Tag>
|
|
||||||
{dealerUser.realName}
|
|
||||||
</Tag>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 基本信息 */}
|
|
||||||
<CellGroup>
|
|
||||||
<Cell
|
|
||||||
title="经销商ID"
|
|
||||||
extra={dealerUser.userId || '-'}
|
|
||||||
/>
|
|
||||||
<Cell
|
|
||||||
title="refereeId"
|
|
||||||
extra={dealerUser.refereeId || '-'}
|
|
||||||
/>
|
|
||||||
<Cell
|
|
||||||
title="成为经销商时间"
|
|
||||||
extra={
|
|
||||||
dealerUser.money
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
|
|
||||||
</CellGroup>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<View className="mt-6 gap-2">
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
loading={loading}
|
|
||||||
>
|
|
||||||
检查状态
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 经销商权益 */}
|
|
||||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
|
||||||
<Text className="font-semibold mb-3">经销商权益</Text>
|
|
||||||
<View className="gap-2">
|
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
• 享受经销商专属价格
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
• 获得推广佣金收益
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
• 优先获得新品信息
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
• 专属客服支持
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 佣金统计 */}
|
|
||||||
<View className="bg-white mx-4 mt-4 rounded-lg p-4">
|
|
||||||
<Text className="font-semibold mb-3">佣金统计</Text>
|
|
||||||
<View className="grid grid-cols-3 gap-4">
|
|
||||||
<View className="text-center">
|
|
||||||
<Text className="text-lg font-bold text-blue-600">0</Text>
|
|
||||||
<Text className="text-sm text-gray-500">今日佣金</Text>
|
|
||||||
</View>
|
|
||||||
<View className="text-center">
|
|
||||||
<Text className="text-lg font-bold text-green-600">0</Text>
|
|
||||||
<Text className="text-sm text-gray-500">本月佣金</Text>
|
|
||||||
</View>
|
|
||||||
<View className="text-center">
|
|
||||||
<Text className="text-lg font-bold text-orange-600">0</Text>
|
|
||||||
<Text className="text-sm text-gray-500">累计佣金</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 刷新按钮 */}
|
|
||||||
<View className="text-center py-4">
|
|
||||||
<Text
|
|
||||||
className="text-blue-500 text-sm"
|
|
||||||
onClick={refresh}
|
|
||||||
>
|
|
||||||
点击刷新数据
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DealerInfo
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '邀请统计',
|
|
||||||
navigationBarBackgroundColor: '#ffffff',
|
|
||||||
navigationBarTextStyle: 'black',
|
|
||||||
backgroundColor: '#f5f5f5',
|
|
||||||
enablePullDownRefresh: true
|
|
||||||
})
|
|
||||||
@@ -1,336 +0,0 @@
|
|||||||
import React, { useState, useEffect, useCallback } from 'react'
|
|
||||||
import { View, Text } from '@tarojs/components'
|
|
||||||
import {
|
|
||||||
Empty,
|
|
||||||
Tabs,
|
|
||||||
Loading,
|
|
||||||
PullToRefresh,
|
|
||||||
Card,
|
|
||||||
} from '@nutui/nutui-react-taro'
|
|
||||||
import {
|
|
||||||
User,
|
|
||||||
ArrowUp,
|
|
||||||
Calendar,
|
|
||||||
Share,
|
|
||||||
Target,
|
|
||||||
Gift
|
|
||||||
} from '@nutui/icons-react-taro'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import { useDealerUser } from '@/hooks/useDealerUser'
|
|
||||||
import {
|
|
||||||
getInviteStats,
|
|
||||||
getMyInviteRecords,
|
|
||||||
getInviteRanking
|
|
||||||
} from '@/api/invite'
|
|
||||||
import type {
|
|
||||||
InviteStats,
|
|
||||||
InviteRecord
|
|
||||||
} from '@/api/invite'
|
|
||||||
import { businessGradients } from '@/styles/gradients'
|
|
||||||
import {InviteRanking} from "@/api/invite/model";
|
|
||||||
|
|
||||||
const InviteStatsPage: React.FC = () => {
|
|
||||||
const [activeTab, setActiveTab] = useState<string>('stats')
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
|
||||||
const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
|
||||||
const [inviteRecords, setInviteRecords] = useState<InviteRecord[]>([])
|
|
||||||
const [ranking, setRanking] = useState<InviteRanking[]>([])
|
|
||||||
const [dateRange, setDateRange] = useState<string>('month')
|
|
||||||
const { dealerUser } = useDealerUser()
|
|
||||||
|
|
||||||
// 获取邀请统计数据
|
|
||||||
const fetchInviteStats = useCallback(async () => {
|
|
||||||
if (!dealerUser?.userId) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
const stats = await getInviteStats(dealerUser.userId)
|
|
||||||
stats && setInviteStats(stats)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取邀请统计失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取统计数据失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [dealerUser?.userId])
|
|
||||||
|
|
||||||
// 获取邀请记录
|
|
||||||
const fetchInviteRecords = useCallback(async () => {
|
|
||||||
if (!dealerUser?.userId) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
const result = await getMyInviteRecords({
|
|
||||||
page: 1,
|
|
||||||
limit: 50,
|
|
||||||
inviterId: dealerUser.userId
|
|
||||||
})
|
|
||||||
setInviteRecords(result?.list || [])
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取邀请记录失败:', error)
|
|
||||||
}
|
|
||||||
}, [dealerUser?.userId])
|
|
||||||
|
|
||||||
// 获取邀请排行榜
|
|
||||||
const fetchRanking = useCallback(async () => {
|
|
||||||
try {
|
|
||||||
const result = await getInviteRanking({
|
|
||||||
limit: 20,
|
|
||||||
period: dateRange as 'day' | 'week' | 'month'
|
|
||||||
})
|
|
||||||
setRanking(result || [])
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取排行榜失败:', error)
|
|
||||||
}
|
|
||||||
}, [dateRange])
|
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
await Promise.all([
|
|
||||||
fetchInviteStats(),
|
|
||||||
fetchInviteRecords(),
|
|
||||||
fetchRanking()
|
|
||||||
])
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (dealerUser?.userId) {
|
|
||||||
fetchInviteStats().then()
|
|
||||||
fetchInviteRecords().then()
|
|
||||||
fetchRanking().then()
|
|
||||||
}
|
|
||||||
}, [fetchInviteStats, fetchInviteRecords, fetchRanking])
|
|
||||||
|
|
||||||
// 获取状态显示文本
|
|
||||||
const getStatusText = (status: string) => {
|
|
||||||
const statusMap: Record<string, string> = {
|
|
||||||
'pending': '待注册',
|
|
||||||
'registered': '已注册',
|
|
||||||
'activated': '已激活'
|
|
||||||
}
|
|
||||||
return statusMap[status] || status
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取状态颜色
|
|
||||||
const getStatusColor = (status: string) => {
|
|
||||||
const colorMap: Record<string, string> = {
|
|
||||||
'pending': 'text-orange-500',
|
|
||||||
'registered': 'text-blue-500',
|
|
||||||
'activated': 'text-green-500'
|
|
||||||
}
|
|
||||||
return colorMap[status] || 'text-gray-500'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 渲染统计概览
|
|
||||||
const renderStatsOverview = () => (
|
|
||||||
<View className="px-4 space-y-4">
|
|
||||||
{/* 核心数据卡片 */}
|
|
||||||
<Card className="bg-white rounded-2xl shadow-sm">
|
|
||||||
<View className="p-4">
|
|
||||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请概览</Text>
|
|
||||||
{loading ? (
|
|
||||||
<View className="flex items-center justify-center py-8">
|
|
||||||
<Loading />
|
|
||||||
</View>
|
|
||||||
) : inviteStats ? (
|
|
||||||
<View className="grid grid-cols-2 gap-4">
|
|
||||||
<View className="text-center p-4 bg-blue-50 rounded-xl">
|
|
||||||
<ArrowUp size="24" className="text-blue-500 mx-auto mb-2" />
|
|
||||||
<Text className="text-2xl font-bold text-blue-600">
|
|
||||||
{inviteStats.totalInvites || 0}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-600">总邀请数</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="text-center p-4 bg-green-50 rounded-xl">
|
|
||||||
<User size="24" className="text-green-500 mx-auto mb-2" />
|
|
||||||
<Text className="text-2xl font-bold text-green-600">
|
|
||||||
{inviteStats.successfulRegistrations || 0}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-600">成功注册</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="text-center p-4 bg-purple-50 rounded-xl">
|
|
||||||
<Target size="24" className="text-purple-500 mx-auto mb-2" />
|
|
||||||
<Text className="text-2xl font-bold text-purple-600">
|
|
||||||
{inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-600">转化率</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="text-center p-4 bg-orange-50 rounded-xl">
|
|
||||||
<Calendar size="24" className="text-orange-500 mx-auto mb-2" />
|
|
||||||
<Text className="text-2xl font-bold text-orange-600">
|
|
||||||
{inviteStats.todayInvites || 0}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-600">今日邀请</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View className="text-center py-8">
|
|
||||||
<Text className="text-gray-500">暂无统计数据</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
|
|
||||||
{/* 邀请来源分析 */}
|
|
||||||
{inviteStats?.sourceStats && inviteStats.sourceStats.length > 0 && (
|
|
||||||
<Card className="bg-white rounded-2xl shadow-sm">
|
|
||||||
<View className="p-4">
|
|
||||||
<Text className="text-lg font-semibold text-gray-800 mb-4">邀请来源分析</Text>
|
|
||||||
<View className="space-y-3">
|
|
||||||
{inviteStats.sourceStats.map((source, index) => (
|
|
||||||
<View key={index} className="flex items-center justify-between p-3 bg-gray-50 rounded-lg">
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Share size="16" className="text-blue-500 mr-2" />
|
|
||||||
<Text className="font-medium text-gray-800">{source.source}</Text>
|
|
||||||
</View>
|
|
||||||
<View className="text-right">
|
|
||||||
<Text className="text-lg font-bold text-gray-800">{source.count}</Text>
|
|
||||||
<Text className="text-sm text-gray-500">
|
|
||||||
转化率 {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
// 渲染邀请记录
|
|
||||||
const renderInviteRecords = () => (
|
|
||||||
<View className="px-4">
|
|
||||||
{inviteRecords.length > 0 ? (
|
|
||||||
<View className="space-y-3">
|
|
||||||
{inviteRecords.map((record, index) => (
|
|
||||||
<Card key={record.id || index} className="bg-white rounded-xl shadow-sm">
|
|
||||||
<View className="p-4">
|
|
||||||
<View className="flex items-center justify-between mb-2">
|
|
||||||
<Text className="font-medium text-gray-800">
|
|
||||||
{record.inviteeName || `用户${record.inviteeId}`}
|
|
||||||
</Text>
|
|
||||||
<Text className={`text-sm font-medium ${getStatusColor(record.status || 'pending')}`}>
|
|
||||||
{getStatusText(record.status || 'pending')}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-center justify-between text-sm text-gray-500">
|
|
||||||
<Text>来源: {record.source || '未知'}</Text>
|
|
||||||
<Text>{record.inviteTime ? new Date(record.inviteTime).toLocaleDateString() : ''}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{record.registerTime && (
|
|
||||||
<Text className="text-xs text-green-600 mt-1">
|
|
||||||
注册时间: {new Date(record.registerTime).toLocaleString()}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无邀请记录" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
// 渲染排行榜
|
|
||||||
const renderRanking = () => (
|
|
||||||
<View className="px-4">
|
|
||||||
<View className="mb-4">
|
|
||||||
<Tabs value={dateRange} onChange={() => setDateRange}>
|
|
||||||
<Tabs.TabPane title="日榜" value="day" />
|
|
||||||
<Tabs.TabPane title="周榜" value="week" />
|
|
||||||
<Tabs.TabPane title="月榜" value="month" />
|
|
||||||
</Tabs>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{ranking.length > 0 ? (
|
|
||||||
<View className="space-y-3">
|
|
||||||
{ranking.map((item, index) => (
|
|
||||||
<Card key={item.inviterId} className="bg-white rounded-xl shadow-sm">
|
|
||||||
<View className="p-4 flex items-center">
|
|
||||||
<View className="flex items-center justify-center w-8 h-8 rounded-full bg-blue-100 mr-3">
|
|
||||||
{index < 3 ? (
|
|
||||||
<Gift size="16" className={index === 0 ? 'text-yellow-500' : index === 1 ? 'text-gray-400' : 'text-orange-400'} />
|
|
||||||
) : (
|
|
||||||
<Text className="text-sm font-bold text-gray-600">{index + 1}</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex-1">
|
|
||||||
<Text className="font-medium text-gray-800">{item.inviterName}</Text>
|
|
||||||
<Text className="text-sm text-gray-500">
|
|
||||||
邀请 {item.inviteCount} 人 · 转化率 {item.conversionRate ? `${(item.conversionRate * 100).toFixed(1)}%` : '0%'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Text className="text-lg font-bold text-blue-600">{item.successCount}</Text>
|
|
||||||
</View>
|
|
||||||
</Card>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无排行数据" />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!dealerUser) {
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
|
||||||
<Loading />
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen">
|
|
||||||
{/* 头部 */}
|
|
||||||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
|
||||||
background: businessGradients.dealer.header
|
|
||||||
}}>
|
|
||||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
top: '-16px',
|
|
||||||
right: '-16px'
|
|
||||||
}}></View>
|
|
||||||
|
|
||||||
<View className="relative z-10">
|
|
||||||
<Text className="text-2xl font-bold mb-2 text-white">邀请统计</Text>
|
|
||||||
<Text className="text-white text-opacity-80">
|
|
||||||
查看您的邀请效果和推广数据
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 标签页 */}
|
|
||||||
<View className="px-4 mb-4">
|
|
||||||
<Tabs value={activeTab} onChange={() => setActiveTab}>
|
|
||||||
<Tabs.TabPane title="统计概览" value="stats" />
|
|
||||||
<Tabs.TabPane title="邀请记录" value="records" />
|
|
||||||
<Tabs.TabPane title="排行榜" value="ranking" />
|
|
||||||
</Tabs>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 内容区域 */}
|
|
||||||
<PullToRefresh onRefresh={handleRefresh}>
|
|
||||||
<View className="pb-6">
|
|
||||||
{activeTab === 'stats' && renderStatsOverview()}
|
|
||||||
{activeTab === 'records' && renderInviteRecords()}
|
|
||||||
{activeTab === 'ranking' && renderRanking()}
|
|
||||||
</View>
|
|
||||||
</PullToRefresh>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default InviteStatsPage
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '分销订单'
|
|
||||||
})
|
|
||||||
@@ -1,205 +0,0 @@
|
|||||||
import React, {useState, useEffect, useCallback} from 'react'
|
|
||||||
import {View, Text, ScrollView} from '@tarojs/components'
|
|
||||||
import {Empty, Tag, PullToRefresh, Loading} from '@nutui/nutui-react-taro'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
|
||||||
import type {ShopDealerOrder} from '@/api/shop/shopDealerOrder/model'
|
|
||||||
|
|
||||||
interface OrderWithDetails extends ShopDealerOrder {
|
|
||||||
orderNo?: string
|
|
||||||
customerName?: string
|
|
||||||
userCommission?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const DealerOrders: React.FC = () => {
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
|
||||||
const [loadingMore, setLoadingMore] = useState<boolean>(false)
|
|
||||||
const [orders, setOrders] = useState<OrderWithDetails[]>([])
|
|
||||||
const [currentPage, setCurrentPage] = useState<number>(1)
|
|
||||||
const [hasMore, setHasMore] = useState<boolean>(true)
|
|
||||||
|
|
||||||
const {dealerUser} = useDealerUser()
|
|
||||||
|
|
||||||
// 获取订单数据
|
|
||||||
const fetchOrders = useCallback(async (page: number = 1, isRefresh: boolean = false) => {
|
|
||||||
// 需要当前登录用户ID(用于 resourceId 参数)
|
|
||||||
if (!dealerUser || !dealerUser.userId) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (isRefresh) {
|
|
||||||
setRefreshing(true)
|
|
||||||
} else if (page === 1) {
|
|
||||||
setLoading(true)
|
|
||||||
} else {
|
|
||||||
setLoadingMore(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await pageShopDealerOrder({
|
|
||||||
page,
|
|
||||||
limit: 10,
|
|
||||||
// 后端需要 resourceId=当前登录用户ID 才能正确过滤分销订单
|
|
||||||
resourceId: dealerUser.userId
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result?.list) {
|
|
||||||
const newOrders = result.list.map(order => ({
|
|
||||||
...order,
|
|
||||||
// 优先使用接口返回的订单号;没有则降级展示 orderId
|
|
||||||
orderNo: order.orderNo ?? (order.orderId != null ? String(order.orderId) : undefined),
|
|
||||||
customerName: `${order.nickname}${order.userId}`,
|
|
||||||
userCommission: order.firstMoney || '0.00'
|
|
||||||
}))
|
|
||||||
|
|
||||||
if (page === 1) {
|
|
||||||
setOrders(newOrders)
|
|
||||||
} else {
|
|
||||||
setOrders(prev => [...prev, ...newOrders])
|
|
||||||
}
|
|
||||||
|
|
||||||
setHasMore(newOrders.length === 10)
|
|
||||||
setCurrentPage(page)
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取分销订单失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取订单失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
setRefreshing(false)
|
|
||||||
setLoadingMore(false)
|
|
||||||
}
|
|
||||||
}, [dealerUser?.userId])
|
|
||||||
|
|
||||||
// 下拉刷新
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
await fetchOrders(1, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 加载更多
|
|
||||||
const handleLoadMore = async () => {
|
|
||||||
if (!loadingMore && hasMore) {
|
|
||||||
await fetchOrders(currentPage + 1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化加载数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (dealerUser?.userId) {
|
|
||||||
fetchOrders(1)
|
|
||||||
}
|
|
||||||
}, [fetchOrders])
|
|
||||||
|
|
||||||
const getStatusText = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
|
|
||||||
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return '已取消'
|
|
||||||
if (isInvalid === 1) return '已失效'
|
|
||||||
if (isUnfreeze === 1) return '已解冻'
|
|
||||||
if (isSettled === 1) return '已结算'
|
|
||||||
return '待结算'
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusColor = (isSettled?: number, isInvalid?: number, isUnfreeze?: number, orderStatus?: number) => {
|
|
||||||
if (orderStatus === 2 || orderStatus === 5 || orderStatus === 6) return 'default'
|
|
||||||
if (isInvalid === 1) return 'danger'
|
|
||||||
if (isUnfreeze === 1) return 'success'
|
|
||||||
if (isSettled === 1) return 'info'
|
|
||||||
return 'warning'
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleGoCapital = () => {
|
|
||||||
Taro.navigateTo({url: '/dealer/capital/index'})
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderOrderItem = (order: OrderWithDetails) => (
|
|
||||||
<View
|
|
||||||
key={order.id}
|
|
||||||
className="bg-white rounded-lg p-4 mb-3 shadow-sm"
|
|
||||||
onClick={handleGoCapital}
|
|
||||||
>
|
|
||||||
<View className="flex justify-between items-start mb-1">
|
|
||||||
<Text className="font-semibold text-gray-800">
|
|
||||||
订单号:{order.orderNo || '-'}
|
|
||||||
</Text>
|
|
||||||
<Tag type={getStatusColor(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}>
|
|
||||||
{getStatusText(order.isSettled, order.isInvalid, order.isUnfreeze,order.orderStatus)}
|
|
||||||
</Tag>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/*<View className="flex justify-between items-center mb-1">*/}
|
|
||||||
{/* <Text className="text-sm text-gray-400">*/}
|
|
||||||
{/* 订单金额:¥{order.orderPrice || '0.00'}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
|
|
||||||
<View className="flex justify-between items-center">
|
|
||||||
<Text className="text-sm text-gray-400">
|
|
||||||
{order.createTime}
|
|
||||||
</Text>
|
|
||||||
<Text className="text-sm text-gray-400">
|
|
||||||
订单金额:¥{order.orderPrice || '0.00'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
if (!dealerUser) {
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="min-h-screen bg-gray-50">
|
|
||||||
<PullToRefresh
|
|
||||||
onRefresh={handleRefresh}
|
|
||||||
disabled={refreshing}
|
|
||||||
pullingText="下拉刷新"
|
|
||||||
canReleaseText="释放刷新"
|
|
||||||
refreshingText="刷新中..."
|
|
||||||
completeText="刷新完成"
|
|
||||||
>
|
|
||||||
<ScrollView
|
|
||||||
scrollY
|
|
||||||
className="h-screen"
|
|
||||||
onScrollToLower={handleLoadMore}
|
|
||||||
lowerThreshold={50}
|
|
||||||
>
|
|
||||||
<View className="p-4">
|
|
||||||
{loading && orders.length === 0 ? (
|
|
||||||
<View className="text-center py-8">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
) : orders.length > 0 ? (
|
|
||||||
<>
|
|
||||||
{orders.map(renderOrderItem)}
|
|
||||||
{loadingMore && (
|
|
||||||
<View className="text-center py-4">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-1 text-sm">加载更多...</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
{!hasMore && orders.length > 0 && (
|
|
||||||
<View className="text-center py-4">
|
|
||||||
<Text className="text-gray-400 text-sm">没有更多数据了</Text>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无分销订单"/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</ScrollView>
|
|
||||||
</PullToRefresh>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DealerOrders
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '易赊宝分享中心',
|
|
||||||
// Enable "Share to friends" and "Share to Moments" (timeline) for this page.
|
|
||||||
enableShareAppMessage: true,
|
|
||||||
enableShareTimeline: true
|
|
||||||
})
|
|
||||||
@@ -1,522 +0,0 @@
|
|||||||
import React, {useState, useEffect} from 'react'
|
|
||||||
import {View, Text, Image} from '@tarojs/components'
|
|
||||||
import {Button, Loading} from '@nutui/nutui-react-taro'
|
|
||||||
import {Download, QrCode} from '@nutui/icons-react-taro'
|
|
||||||
import Taro, {useShareAppMessage} from '@tarojs/taro'
|
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
|
||||||
import {generateInviteCode} from '@/api/invite'
|
|
||||||
// import type {InviteStats} from '@/api/invite'
|
|
||||||
import {businessGradients} from '@/styles/gradients'
|
|
||||||
|
|
||||||
const DealerQrcode: React.FC = () => {
|
|
||||||
const [miniProgramCodeUrl, setMiniProgramCodeUrl] = useState<string>('')
|
|
||||||
const [codeLoading, setCodeLoading] = useState<boolean>(false)
|
|
||||||
const [saving, setSaving] = useState<boolean>(false)
|
|
||||||
// const [inviteStats, setInviteStats] = useState<InviteStats | null>(null)
|
|
||||||
// const [statsLoading, setStatsLoading] = useState<boolean>(false)
|
|
||||||
const {dealerUser, loading: dealerLoading, error, refresh} = useDealerUser()
|
|
||||||
|
|
||||||
// Enable "转发给朋友" + "分享到朋友圈" items in the share panel/menu.
|
|
||||||
useEffect(() => {
|
|
||||||
// Some clients require explicit call to show both share entries.
|
|
||||||
Taro.showShareMenu({
|
|
||||||
withShareTicket: true,
|
|
||||||
showShareItems: ['shareAppMessage', 'shareTimeline']
|
|
||||||
}).catch(() => {})
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 转发给朋友(分享小程序链接)
|
|
||||||
useShareAppMessage(() => {
|
|
||||||
const inviterRaw = dealerUser?.userId ?? Taro.getStorageSync('UserId')
|
|
||||||
const inviter = Number(inviterRaw)
|
|
||||||
const hasInviter = Number.isFinite(inviter) && inviter > 0
|
|
||||||
|
|
||||||
const user = Taro.getStorageSync('User') || {}
|
|
||||||
const nickname = (user && (user.nickname || user.realName || user.username)) || ''
|
|
||||||
const title = hasInviter ? `${nickname || '我'}邀请你加入易赊宝伙伴计划` : '易赊宝伙伴计划'
|
|
||||||
|
|
||||||
return {
|
|
||||||
title,
|
|
||||||
path: hasInviter
|
|
||||||
? `/pages/index/index?inviter=${inviter}&source=dealer_qrcode&t=${Date.now()}`
|
|
||||||
: `/pages/index/index`,
|
|
||||||
success: function () {
|
|
||||||
Taro.showToast({title: '分享成功', icon: 'success', duration: 2000})
|
|
||||||
},
|
|
||||||
fail: function () {
|
|
||||||
Taro.showToast({title: '分享失败', icon: 'none', duration: 2000})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// 生成小程序码
|
|
||||||
const generateMiniProgramCode = async () => {
|
|
||||||
if (!dealerUser?.userId) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setCodeLoading(true)
|
|
||||||
|
|
||||||
// 生成邀请小程序码
|
|
||||||
const codeUrl = await generateInviteCode(dealerUser.userId)
|
|
||||||
|
|
||||||
if (codeUrl) {
|
|
||||||
setMiniProgramCodeUrl(codeUrl)
|
|
||||||
} else {
|
|
||||||
throw new Error('返回的小程序码URL为空')
|
|
||||||
}
|
|
||||||
} catch (error: any) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: error.message || '生成小程序码失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
// 清空之前的二维码
|
|
||||||
setMiniProgramCodeUrl('')
|
|
||||||
} finally {
|
|
||||||
setCodeLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取邀请统计数据
|
|
||||||
// const fetchInviteStats = async () => {
|
|
||||||
// if (!dealerUser?.userId) return
|
|
||||||
//
|
|
||||||
// try {
|
|
||||||
// setStatsLoading(true)
|
|
||||||
// const stats = await getInviteStats(dealerUser.userId)
|
|
||||||
// stats && setInviteStats(stats)
|
|
||||||
// } catch (error) {
|
|
||||||
// // 静默处理错误,不影响用户体验
|
|
||||||
// } finally {
|
|
||||||
// setStatsLoading(false)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 初始化生成小程序码和获取统计数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (dealerUser?.userId) {
|
|
||||||
generateMiniProgramCode()
|
|
||||||
// fetchInviteStats()
|
|
||||||
}
|
|
||||||
}, [dealerUser?.userId])
|
|
||||||
|
|
||||||
const isAlbumAuthError = (errMsg?: string) => {
|
|
||||||
if (!errMsg) return false
|
|
||||||
// WeChat uses variants like: "saveImageToPhotosAlbum:fail auth deny",
|
|
||||||
// "saveImageToPhotosAlbum:fail auth denied", "authorize:fail auth deny"
|
|
||||||
return (
|
|
||||||
errMsg.includes('auth deny') ||
|
|
||||||
errMsg.includes('auth denied') ||
|
|
||||||
errMsg.includes('authorize') ||
|
|
||||||
errMsg.includes('scope.writePhotosAlbum')
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const ensureWriteAlbumPermission = async (): Promise<boolean> => {
|
|
||||||
try {
|
|
||||||
const setting = await Taro.getSetting()
|
|
||||||
if (setting?.authSetting?.['scope.writePhotosAlbum']) return true
|
|
||||||
|
|
||||||
await Taro.authorize({scope: 'scope.writePhotosAlbum'})
|
|
||||||
return true
|
|
||||||
} catch (error: any) {
|
|
||||||
const modal = await Taro.showModal({
|
|
||||||
title: '提示',
|
|
||||||
content: '需要您授权保存图片到相册,请在设置中开启相册权限',
|
|
||||||
confirmText: '去设置'
|
|
||||||
})
|
|
||||||
if (modal.confirm) {
|
|
||||||
await Taro.openSetting()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const downloadImageToLocalPath = async (url: string): Promise<string> => {
|
|
||||||
// saveImageToPhotosAlbum must receive a local temp path (e.g. `http://tmp/...` or `wxfile://...`).
|
|
||||||
// Some environments may return a non-existing temp path from getImageInfo, so we verify.
|
|
||||||
if (url.startsWith('http://tmp/') || url.startsWith('wxfile://')) {
|
|
||||||
return url
|
|
||||||
}
|
|
||||||
|
|
||||||
const token = Taro.getStorageSync('access_token')
|
|
||||||
const tenantId = Taro.getStorageSync('TenantId')
|
|
||||||
const header: Record<string, string> = {}
|
|
||||||
if (token) header.Authorization = token
|
|
||||||
if (tenantId) header.TenantId = tenantId
|
|
||||||
|
|
||||||
// 先下载到本地临时文件再保存到相册
|
|
||||||
const res = await Taro.downloadFile({url, header})
|
|
||||||
if (res.statusCode !== 200 || !res.tempFilePath) {
|
|
||||||
throw new Error(`图片下载失败(${res.statusCode || 'unknown'})`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Double-check file exists to avoid: saveImageToPhotosAlbum:fail no such file or directory
|
|
||||||
try {
|
|
||||||
await Taro.getFileInfo({filePath: res.tempFilePath})
|
|
||||||
} catch (_) {
|
|
||||||
throw new Error('图片临时文件不存在,请重试')
|
|
||||||
}
|
|
||||||
return res.tempFilePath
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存小程序码到相册
|
|
||||||
const saveMiniProgramCode = async () => {
|
|
||||||
if (!miniProgramCodeUrl) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '小程序码未生成',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (saving) return
|
|
||||||
setSaving(true)
|
|
||||||
Taro.showLoading({title: '保存中...'})
|
|
||||||
|
|
||||||
const hasPermission = await ensureWriteAlbumPermission()
|
|
||||||
if (!hasPermission) return
|
|
||||||
|
|
||||||
let filePath = await downloadImageToLocalPath(miniProgramCodeUrl)
|
|
||||||
try {
|
|
||||||
await Taro.saveImageToPhotosAlbum({filePath})
|
|
||||||
} catch (e: any) {
|
|
||||||
const msg = e?.errMsg || e?.message || ''
|
|
||||||
// Fallback: some devices/clients may fail to save directly from a temp path.
|
|
||||||
if (
|
|
||||||
msg.includes('no such file or directory') &&
|
|
||||||
(filePath.startsWith('http://tmp/') || filePath.startsWith('wxfile://'))
|
|
||||||
) {
|
|
||||||
const saved = (await Taro.saveFile({tempFilePath: filePath})) as unknown as { savedFilePath?: string }
|
|
||||||
if (saved?.savedFilePath) {
|
|
||||||
filePath = saved.savedFilePath
|
|
||||||
}
|
|
||||||
await Taro.saveImageToPhotosAlbum({filePath})
|
|
||||||
} else {
|
|
||||||
throw e
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Taro.showToast({
|
|
||||||
title: '保存成功',
|
|
||||||
icon: 'success'
|
|
||||||
})
|
|
||||||
} catch (error: any) {
|
|
||||||
const errMsg = error?.errMsg || error?.message
|
|
||||||
if (errMsg?.includes('cancel')) {
|
|
||||||
Taro.showToast({title: '已取消', icon: 'none'})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (isAlbumAuthError(errMsg)) {
|
|
||||||
const modal = await Taro.showModal({
|
|
||||||
title: '提示',
|
|
||||||
content: '需要您授权保存图片到相册',
|
|
||||||
confirmText: '去设置'
|
|
||||||
})
|
|
||||||
if (modal.confirm) {
|
|
||||||
await Taro.openSetting()
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Prefer a modal so we can show the real reason (e.g. domain whitelist / network error).
|
|
||||||
await Taro.showModal({
|
|
||||||
title: '保存失败',
|
|
||||||
content: errMsg || '保存失败,请稍后重试',
|
|
||||||
showCancel: false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} finally {
|
|
||||||
Taro.hideLoading()
|
|
||||||
setSaving(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 复制邀请信息
|
|
||||||
// const copyInviteInfo = () => {
|
|
||||||
// if (!dealerUser?.userId) {
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: '用户信息未加载',
|
|
||||||
// icon: 'error'
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// const inviteText = `🎉 邀请您加入我的团队!
|
|
||||||
//
|
|
||||||
// 扫描小程序码或搜索"易赊宝"小程序,即可享受优质商品和服务!
|
|
||||||
//
|
|
||||||
// 💰 成为我的团队成员,一起赚取丰厚佣金
|
|
||||||
// 🎁 新用户专享优惠等你来拿
|
|
||||||
//
|
|
||||||
// 邀请码:${dealerUser.userId}
|
|
||||||
// 快来加入我们吧!`
|
|
||||||
//
|
|
||||||
// Taro.setClipboardData({
|
|
||||||
// data: inviteText,
|
|
||||||
// success: () => {
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: '邀请信息已复制',
|
|
||||||
// icon: 'success'
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
// 分享小程序码
|
|
||||||
// const shareMiniProgramCode = () => {
|
|
||||||
// if (!dealerUser?.userId) {
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: '用户信息未加载',
|
|
||||||
// icon: 'error'
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // 小程序分享
|
|
||||||
// Taro.showShareMenu({
|
|
||||||
// withShareTicket: true,
|
|
||||||
// showShareItems: ['shareAppMessage']
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
if (dealerLoading) {
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (error) {
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
|
||||||
<Text className="text-gray-800 font-semibold">加载失败</Text>
|
|
||||||
<Text className="text-gray-500 text-sm mt-2">{error}</Text>
|
|
||||||
<Button className="mt-6" type="primary" onClick={refresh}>重试</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未成为分销商时给出明确引导,避免一直停留在“加载中”
|
|
||||||
if (!dealerUser) {
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen flex flex-col items-center justify-center p-6">
|
|
||||||
<Text className="text-gray-800 font-semibold">你还不是分销商</Text>
|
|
||||||
<Text className="text-gray-500 text-sm mt-2 text-center">申请成为分销商后即可生成分享码</Text>
|
|
||||||
<Button
|
|
||||||
className="mt-6"
|
|
||||||
type="primary"
|
|
||||||
onClick={() => Taro.navigateTo({url: '/dealer/apply/add'})}
|
|
||||||
>
|
|
||||||
去申请
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen">
|
|
||||||
{/* 头部卡片 */}
|
|
||||||
<View className="rounded-b-3xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
|
||||||
background: businessGradients.dealer.header
|
|
||||||
}}>
|
|
||||||
{/* 装饰背景 */}
|
|
||||||
<View className="absolute w-32 h-32 rounded-full" style={{
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
top: '-16px',
|
|
||||||
right: '-16px'
|
|
||||||
}}></View>
|
|
||||||
|
|
||||||
<View className="relative z-10 flex flex-col">
|
|
||||||
<Text className="text-2xl font-bold mb-2 text-white">我的分享码</Text>
|
|
||||||
<Text className="text-white text-opacity-80">
|
|
||||||
与好友“共享福利 一起省、一起赚”
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="px-4">
|
|
||||||
{/* 小程序码展示区 */}
|
|
||||||
<View className="bg-white rounded-2xl p-6 mb-6 shadow-sm">
|
|
||||||
<View className="text-center">
|
|
||||||
{codeLoading ? (
|
|
||||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-2">生成中...</Text>
|
|
||||||
</View>
|
|
||||||
) : miniProgramCodeUrl ? (
|
|
||||||
<View className="w-48 h-48 mx-auto mb-4 bg-white rounded-xl shadow-sm p-4">
|
|
||||||
<Image
|
|
||||||
src={miniProgramCodeUrl}
|
|
||||||
className="w-full h-full"
|
|
||||||
mode="aspectFit"
|
|
||||||
onError={() => {
|
|
||||||
Taro.showModal({
|
|
||||||
title: '二维码加载失败',
|
|
||||||
content: '请检查网络连接或联系管理员',
|
|
||||||
showCancel: true,
|
|
||||||
confirmText: '重新生成',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
generateMiniProgramCode();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View className="w-48 h-48 mx-auto mb-4 flex items-center justify-center bg-gray-50 rounded-xl">
|
|
||||||
<QrCode size="48" className="text-gray-400 mb-2"/>
|
|
||||||
<Text className="text-gray-500">小程序码生成失败</Text>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
className="mt-2"
|
|
||||||
onClick={generateMiniProgramCode}
|
|
||||||
>
|
|
||||||
重新生成
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View className="text-lg font-semibold text-gray-800 mb-2">
|
|
||||||
易赊宝伙伴计划
|
|
||||||
</View>
|
|
||||||
<View className="text-sm text-gray-500 mb-4">
|
|
||||||
自购省 | 分享赚 | 好友惠
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 操作按钮 */}
|
|
||||||
<View className={'gap-2'}>
|
|
||||||
<View className={'my-2'}>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
block
|
|
||||||
icon={<Download/>}
|
|
||||||
onClick={saveMiniProgramCode}
|
|
||||||
disabled={!miniProgramCodeUrl || codeLoading || saving}
|
|
||||||
>
|
|
||||||
保存小程序码到相册
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 推广说明 */}
|
|
||||||
<View className="bg-white rounded-2xl p-4 mt-6 hidden">
|
|
||||||
<Text className="font-semibold text-gray-800 mb-3">推广说明</Text>
|
|
||||||
<View className="space-y-2">
|
|
||||||
<View className="flex items-start">
|
|
||||||
<View className="w-2 h-2 bg-blue-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
好友通过您的二维码或链接注册成为您的团队成员
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex items-start">
|
|
||||||
<View className="w-2 h-2 bg-green-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
好友购买商品时,您可获得相应层级的分销佣金
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className="flex items-start">
|
|
||||||
<View className="w-2 h-2 bg-purple-500 rounded-full mt-2 mr-3 flex-shrink-0"></View>
|
|
||||||
<Text className="text-sm text-gray-600">
|
|
||||||
支持三级分销,团队越大收益越多
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 邀请统计数据 */}
|
|
||||||
{/*<View className="bg-white rounded-2xl p-4 mt-4 mb-6">*/}
|
|
||||||
{/* <Text className="font-semibold text-gray-800 mb-3">我的邀请数据</Text>*/}
|
|
||||||
{/* {statsLoading ? (*/}
|
|
||||||
{/* <View className="flex items-center justify-center py-8">*/}
|
|
||||||
{/* <Loading/>*/}
|
|
||||||
{/* <Text className="text-gray-500 mt-2">加载中...</Text>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* ) : inviteStats ? (*/}
|
|
||||||
{/* <View className="space-y-4">*/}
|
|
||||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
|
||||||
{/* <View className="text-center">*/}
|
|
||||||
{/* <Text className="text-2xl font-bold text-blue-500">*/}
|
|
||||||
{/* {inviteStats.totalInvites || 0}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/* <Text className="text-sm text-gray-500">总邀请数</Text>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* <View className="text-center">*/}
|
|
||||||
{/* <Text className="text-2xl font-bold text-green-500">*/}
|
|
||||||
{/* {inviteStats.successfulRegistrations || 0}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/* <Text className="text-sm text-gray-500">成功注册</Text>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
|
|
||||||
{/* <View className="grid grid-cols-2 gap-4">*/}
|
|
||||||
{/* <View className="text-center">*/}
|
|
||||||
{/* <Text className="text-2xl font-bold text-purple-500">*/}
|
|
||||||
{/* {inviteStats.conversionRate ? `${(inviteStats.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/* <Text className="text-sm text-gray-500">转化率</Text>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* <View className="text-center">*/}
|
|
||||||
{/* <Text className="text-2xl font-bold text-orange-500">*/}
|
|
||||||
{/* {inviteStats.todayInvites || 0}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/* <Text className="text-sm text-gray-500">今日邀请</Text>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
|
|
||||||
{/* /!* 邀请来源统计 *!/*/}
|
|
||||||
{/* {inviteStats.sourceStats && inviteStats.sourceStats.length > 0 && (*/}
|
|
||||||
{/* <View className="mt-4">*/}
|
|
||||||
{/* <Text className="text-sm font-medium text-gray-700 mb-2">邀请来源分布</Text>*/}
|
|
||||||
{/* <View className="space-y-2">*/}
|
|
||||||
{/* {inviteStats.sourceStats.map((source, index) => (*/}
|
|
||||||
{/* <View key={index} className="flex items-center justify-between py-2 px-3 bg-gray-50 rounded-lg">*/}
|
|
||||||
{/* <View className="flex items-center">*/}
|
|
||||||
{/* <View className="w-3 h-3 rounded-full bg-blue-500 mr-2"></View>*/}
|
|
||||||
{/* <Text className="text-sm text-gray-700">{source.source}</Text>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* <View className="text-right">*/}
|
|
||||||
{/* <Text className="text-sm font-medium text-gray-800">{source.count}</Text>*/}
|
|
||||||
{/* <Text className="text-xs text-gray-500">*/}
|
|
||||||
{/* {source.conversionRate ? `${(source.conversionRate * 100).toFixed(1)}%` : '0%'}*/}
|
|
||||||
{/* </Text>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* ))}*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* ) : (*/}
|
|
||||||
{/* <View className="text-center py-8">*/}
|
|
||||||
{/* <View className="text-gray-500">暂无邀请数据</View>*/}
|
|
||||||
{/* <Button*/}
|
|
||||||
{/* size="small"*/}
|
|
||||||
{/* type="primary"*/}
|
|
||||||
{/* className="mt-2"*/}
|
|
||||||
{/* onClick={fetchInviteStats}*/}
|
|
||||||
{/* >*/}
|
|
||||||
{/* 刷新数据*/}
|
|
||||||
{/* </Button>*/}
|
|
||||||
{/* </View>*/}
|
|
||||||
{/* )}*/}
|
|
||||||
{/*</View>*/}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DealerQrcode
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '邀请推广'
|
|
||||||
})
|
|
||||||
@@ -1,439 +0,0 @@
|
|||||||
import React, {useState, useEffect, useCallback} from 'react'
|
|
||||||
import {View, Text} from '@tarojs/components'
|
|
||||||
import {Phone, Edit, Message} from '@nutui/icons-react-taro'
|
|
||||||
import {Space, Empty, Avatar, Button} from '@nutui/nutui-react-taro'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
|
||||||
import {listShopDealerReferee} from '@/api/shop/shopDealerReferee'
|
|
||||||
import {pageShopDealerOrder} from '@/api/shop/shopDealerOrder'
|
|
||||||
import type {ShopDealerReferee} from '@/api/shop/shopDealerReferee/model'
|
|
||||||
import FixedButton from "@/components/FixedButton";
|
|
||||||
import navTo from "@/utils/common";
|
|
||||||
import {updateUser} from "@/api/system/user";
|
|
||||||
|
|
||||||
interface TeamMemberWithStats extends ShopDealerReferee {
|
|
||||||
name?: string
|
|
||||||
avatar?: string
|
|
||||||
nickname?: string;
|
|
||||||
alias?: string;
|
|
||||||
phone?: string;
|
|
||||||
orderCount?: number
|
|
||||||
commission?: string
|
|
||||||
status?: 'active' | 'inactive'
|
|
||||||
subMembers?: number
|
|
||||||
joinTime?: string
|
|
||||||
dealerAvatar?: string;
|
|
||||||
dealerName?: string;
|
|
||||||
dealerPhone?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 层级信息接口
|
|
||||||
interface LevelInfo {
|
|
||||||
dealerId: number
|
|
||||||
dealerName?: string
|
|
||||||
level: number
|
|
||||||
}
|
|
||||||
|
|
||||||
const DealerTeam: React.FC = () => {
|
|
||||||
const [teamMembers, setTeamMembers] = useState<TeamMemberWithStats[]>([])
|
|
||||||
const {dealerUser} = useDealerUser()
|
|
||||||
const [dealerId, setDealerId] = useState<number>()
|
|
||||||
// 层级栈,用于支持返回上一层
|
|
||||||
const [levelStack, setLevelStack] = useState<LevelInfo[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
// 当前查看的用户名称
|
|
||||||
const [currentDealerName, setCurrentDealerName] = useState<string>('')
|
|
||||||
|
|
||||||
// 异步加载成员统计数据
|
|
||||||
const loadMemberStats = async (members: TeamMemberWithStats[]) => {
|
|
||||||
// 分批处理,避免过多并发请求
|
|
||||||
const batchSize = 3
|
|
||||||
for (let i = 0; i < members.length; i += batchSize) {
|
|
||||||
const batch = members.slice(i, i + batchSize)
|
|
||||||
|
|
||||||
const batchStats = await Promise.all(
|
|
||||||
batch.map(async (member) => {
|
|
||||||
try {
|
|
||||||
// 并行获取订单统计和下级成员数量
|
|
||||||
const [orderResult, subMembersResult] = await Promise.all([
|
|
||||||
pageShopDealerOrder({
|
|
||||||
page: 1,
|
|
||||||
userId: member.userId
|
|
||||||
}),
|
|
||||||
listShopDealerReferee({
|
|
||||||
dealerId: member.userId,
|
|
||||||
deleted: 0
|
|
||||||
})
|
|
||||||
])
|
|
||||||
|
|
||||||
let orderCount = 0
|
|
||||||
let commission = '0.00'
|
|
||||||
let status: 'active' | 'inactive' = 'inactive'
|
|
||||||
|
|
||||||
if (orderResult?.list) {
|
|
||||||
const orders = orderResult.list
|
|
||||||
orderCount = orders.length
|
|
||||||
commission = orders.reduce((sum, order) => {
|
|
||||||
const levelCommission = member.level === 1 ? order.firstMoney :
|
|
||||||
member.level === 2 ? order.secondMoney :
|
|
||||||
order.thirdMoney
|
|
||||||
return sum + parseFloat(levelCommission || '0')
|
|
||||||
}, 0).toFixed(2)
|
|
||||||
|
|
||||||
// 判断活跃状态(30天内有订单为活跃)
|
|
||||||
const thirtyDaysAgo = new Date()
|
|
||||||
thirtyDaysAgo.setDate(thirtyDaysAgo.getDate() - 30)
|
|
||||||
const hasRecentOrder = orders.some(order =>
|
|
||||||
new Date(order.createTime || '') > thirtyDaysAgo
|
|
||||||
)
|
|
||||||
status = hasRecentOrder ? 'active' : 'inactive'
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
...member,
|
|
||||||
orderCount,
|
|
||||||
commission,
|
|
||||||
status,
|
|
||||||
subMembers: subMembersResult?.length || 0
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error(`获取成员${member.userId}数据失败:`, error)
|
|
||||||
return {
|
|
||||||
...member,
|
|
||||||
orderCount: 0,
|
|
||||||
commission: '0.00',
|
|
||||||
status: 'inactive' as const,
|
|
||||||
subMembers: 0
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
|
|
||||||
// 更新这一批成员的数据
|
|
||||||
setTeamMembers(prevMembers => {
|
|
||||||
const updatedMembers = [...prevMembers]
|
|
||||||
batchStats.forEach(updatedMember => {
|
|
||||||
const index = updatedMembers.findIndex(m => m.userId === updatedMember.userId)
|
|
||||||
if (index !== -1) {
|
|
||||||
updatedMembers[index] = updatedMember
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return updatedMembers
|
|
||||||
})
|
|
||||||
|
|
||||||
// 添加小延迟,避免请求过于密集
|
|
||||||
if (i + batchSize < members.length) {
|
|
||||||
await new Promise(resolve => setTimeout(resolve, 100))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取团队数据
|
|
||||||
const fetchTeamData = useCallback(async () => {
|
|
||||||
if (!dealerUser?.userId && !dealerId) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
console.log(dealerId, 'dealerId>>>>>>>>>')
|
|
||||||
// 获取团队成员关系
|
|
||||||
const refereeResult = await listShopDealerReferee({
|
|
||||||
dealerId: dealerId ? dealerId : dealerUser?.userId
|
|
||||||
})
|
|
||||||
|
|
||||||
if (refereeResult) {
|
|
||||||
console.log('团队成员原始数据:', refereeResult)
|
|
||||||
// 处理团队成员数据
|
|
||||||
const processedMembers: TeamMemberWithStats[] = refereeResult.map(member => ({
|
|
||||||
...member,
|
|
||||||
name: `${member.userId}`,
|
|
||||||
orderCount: 0,
|
|
||||||
commission: '0.00',
|
|
||||||
status: 'active' as const,
|
|
||||||
subMembers: 0,
|
|
||||||
joinTime: member.createTime
|
|
||||||
}))
|
|
||||||
|
|
||||||
// 先显示基础数据,然后异步加载详细统计
|
|
||||||
setTeamMembers(processedMembers)
|
|
||||||
setLoading(false)
|
|
||||||
|
|
||||||
// 异步加载每个成员的详细统计数据
|
|
||||||
loadMemberStats(processedMembers)
|
|
||||||
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取团队数据失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取团队数据失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [dealerUser?.userId, dealerId])
|
|
||||||
|
|
||||||
// 查看下级成员
|
|
||||||
const getNextUser = (item: TeamMemberWithStats) => {
|
|
||||||
// 检查层级限制:最多只能查看2层(levelStack.length >= 1 表示已经是第2层了)
|
|
||||||
if (levelStack.length >= 1) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果没有下级成员,不允许点击
|
|
||||||
if (!item.subMembers || item.subMembers === 0) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log('点击用户:', item.userId, item.name)
|
|
||||||
|
|
||||||
// 将当前层级信息推入栈中
|
|
||||||
const currentLevel: LevelInfo = {
|
|
||||||
dealerId: dealerId || dealerUser?.userId || 0,
|
|
||||||
dealerName: currentDealerName || (dealerId ? '上级' : dealerUser?.realName || '我'),
|
|
||||||
level: levelStack.length
|
|
||||||
}
|
|
||||||
setLevelStack(prev => [...prev, currentLevel])
|
|
||||||
|
|
||||||
// 切换到下级
|
|
||||||
setDealerId(item.userId)
|
|
||||||
setCurrentDealerName(item.nickname || item.dealerName || `用户${item.userId}`)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 返回上一层
|
|
||||||
const goBack = () => {
|
|
||||||
if (levelStack.length === 0) {
|
|
||||||
// 如果栈为空,返回首页或上一页
|
|
||||||
Taro.navigateBack()
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 从栈中弹出上一层信息
|
|
||||||
const prevLevel = levelStack[levelStack.length - 1]
|
|
||||||
setLevelStack(prev => prev.slice(0, -1))
|
|
||||||
|
|
||||||
if (prevLevel.dealerId === (dealerUser?.userId || 0)) {
|
|
||||||
// 返回到根层级
|
|
||||||
setDealerId(undefined)
|
|
||||||
setCurrentDealerName('')
|
|
||||||
} else {
|
|
||||||
setDealerId(prevLevel.dealerId)
|
|
||||||
setCurrentDealerName(prevLevel.dealerName || '')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 一键拨打
|
|
||||||
const makePhoneCall = (phone: string) => {
|
|
||||||
Taro.makePhoneCall({
|
|
||||||
phoneNumber: phone,
|
|
||||||
fail: () => {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '拨打取消',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 别名备注
|
|
||||||
const editAlias = (item: any, index: number) => {
|
|
||||||
Taro.showModal({
|
|
||||||
title: '备注',
|
|
||||||
// @ts-ignore
|
|
||||||
editable: true,
|
|
||||||
placeholderText: '真实姓名',
|
|
||||||
content: item.alias || '',
|
|
||||||
success: async (res: any) => {
|
|
||||||
if (res.confirm && res.content !== undefined) {
|
|
||||||
try {
|
|
||||||
// 更新跟进情况
|
|
||||||
await updateUser({
|
|
||||||
userId: item.userId,
|
|
||||||
alias: res.content.trim()
|
|
||||||
});
|
|
||||||
teamMembers[index].alias = res.content.trim()
|
|
||||||
setTeamMembers(teamMembers)
|
|
||||||
} catch (error) {
|
|
||||||
console.error('备注失败:', error);
|
|
||||||
Taro.showToast({
|
|
||||||
title: '备注失败,请重试',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 发送消息
|
|
||||||
const sendMessage = (item: TeamMemberWithStats) => {
|
|
||||||
return navTo(`/user/chat/message/add?id=${item.userId}`, true)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 监听数据变化,获取团队数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (dealerUser?.userId || dealerId) {
|
|
||||||
fetchTeamData().then()
|
|
||||||
}
|
|
||||||
}, [fetchTeamData])
|
|
||||||
|
|
||||||
// 初始化当前用户名称
|
|
||||||
useEffect(() => {
|
|
||||||
if (!dealerId && dealerUser?.realName && !currentDealerName) {
|
|
||||||
setCurrentDealerName(dealerUser.realName)
|
|
||||||
}
|
|
||||||
}, [dealerUser, dealerId, currentDealerName])
|
|
||||||
|
|
||||||
const renderMemberItem = (member: TeamMemberWithStats, index: number) => {
|
|
||||||
// 判断是否可以点击:有下级成员且未达到层级限制
|
|
||||||
const canClick = member.subMembers && member.subMembers > 0 && levelStack.length < 1
|
|
||||||
// 判断是否显示手机号:只有本级(levelStack.length === 0)才显示
|
|
||||||
const showPhone = levelStack.length === 0
|
|
||||||
// 判断数据是否还在加载中(初始值都是0或'0.00')
|
|
||||||
const isStatsLoading = member.orderCount === 0 && member.commission === '0.00' && member.subMembers === 0
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View
|
|
||||||
key={member.id}
|
|
||||||
className={`bg-white rounded-lg p-4 mb-3 shadow-sm ${
|
|
||||||
canClick ? 'cursor-pointer' : 'cursor-default opacity-75'
|
|
||||||
}`}
|
|
||||||
onClick={() => getNextUser(member)}
|
|
||||||
>
|
|
||||||
<View className="flex items-center mb-3">
|
|
||||||
<Avatar
|
|
||||||
size="40"
|
|
||||||
src={member.avatar}
|
|
||||||
className="mr-3"
|
|
||||||
/>
|
|
||||||
<View className="flex-1">
|
|
||||||
<View className="flex items-center justify-between mb-1">
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Space>
|
|
||||||
{member.alias ? <Text className="font-semibold text-blue-700 mr-2">{member.alias}</Text> :
|
|
||||||
<Text className="font-semibold text-gray-800 mr-2">{member.nickname}</Text>}
|
|
||||||
{/*别名备注*/}
|
|
||||||
<Edit size={16} className={'text-blue-500 mr-2'} onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
editAlias(member, index)
|
|
||||||
}}/>
|
|
||||||
{/*发送消息*/}
|
|
||||||
<Message size={16} className={'text-orange-500 mr-2'} onClick={(e) => {
|
|
||||||
e.stopPropagation()
|
|
||||||
sendMessage(member)
|
|
||||||
}}/>
|
|
||||||
</Space>
|
|
||||||
</View>
|
|
||||||
{/* 显示手机号(仅本级可见) */}
|
|
||||||
{showPhone && member.phone && (
|
|
||||||
<Text className="text-sm text-gray-500 hidden" onClick={(e) => {
|
|
||||||
e.stopPropagation();
|
|
||||||
makePhoneCall(member.phone || '');
|
|
||||||
}}>
|
|
||||||
{member.phone}
|
|
||||||
<Phone size={12} className="ml-1 text-green-500"/>
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<Text className="text-xs text-gray-500">
|
|
||||||
加入时间:{member.joinTime}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="grid grid-cols-3 gap-4 text-center">
|
|
||||||
<Space>
|
|
||||||
<Text className="text-xs text-gray-500">订单数</Text>
|
|
||||||
<Text className="text-sm font-semibold text-blue-600">
|
|
||||||
{isStatsLoading ? '-' : member.orderCount}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<Text className="text-xs text-gray-500">贡献佣金</Text>
|
|
||||||
<Text className="text-sm font-semibold text-green-600">
|
|
||||||
{isStatsLoading ? '-' : `¥${member.commission}`}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
<Space>
|
|
||||||
<Text className="text-xs text-gray-500">团队成员</Text>
|
|
||||||
<Text className={`text-sm font-semibold ${
|
|
||||||
canClick ? 'text-purple-600' : 'text-gray-400'
|
|
||||||
}`}>
|
|
||||||
{isStatsLoading ? '-' : (member.subMembers || 0)}
|
|
||||||
</Text>
|
|
||||||
</Space>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderOverview = () => (
|
|
||||||
<View className="rounded-xl p-4">
|
|
||||||
<View
|
|
||||||
className={'bg-white rounded-lg py-2 px-4 mb-3 shadow-sm text-right text-sm font-medium flex justify-between items-center'}>
|
|
||||||
<Text className="text-lg font-semibold">我的团队成员</Text>
|
|
||||||
<Text className={'text-gray-500 '}>成员数:{teamMembers.length}</Text>
|
|
||||||
</View>
|
|
||||||
{teamMembers.map(renderMemberItem)}
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
// 渲染顶部导航栏
|
|
||||||
const renderHeader = () => {
|
|
||||||
if (levelStack.length === 0) return null
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="bg-white p-4 mb-3 shadow-sm">
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Text className="text-lg font-semibold">
|
|
||||||
{currentDealerName}的团队成员
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
onClick={goBack}
|
|
||||||
className="bg-blue-500"
|
|
||||||
>
|
|
||||||
返回上一层
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dealerUser) {
|
|
||||||
return (
|
|
||||||
<Space className="flex items-center justify-center">
|
|
||||||
<Empty description="您还不是业务人员" style={{
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}} actions={[{text: '立即申请', onClick: () => navTo(`/dealer/apply/add`, true)}]}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
{renderHeader()}
|
|
||||||
|
|
||||||
{loading ? (
|
|
||||||
<View className="flex items-center justify-center mt-20">
|
|
||||||
<Text className="text-gray-500">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
) : teamMembers.length > 0 ? (
|
|
||||||
renderOverview()
|
|
||||||
) : (
|
|
||||||
<View className="flex items-center justify-center mt-20">
|
|
||||||
<Empty description="暂无成员" style={{
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}}/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<FixedButton text={'立即邀请'} onClick={() => navTo(`/dealer/qrcode/index`, true)}/>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DealerTeam;
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
import React, { useState } from 'react'
|
|
||||||
import { View, Text } from '@tarojs/components'
|
|
||||||
import { Tabs, Button } from '@nutui/nutui-react-taro'
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 提现功能调试组件
|
|
||||||
* 用于测试 Tabs 组件的点击和切换功能
|
|
||||||
*/
|
|
||||||
const WithdrawDebug: React.FC = () => {
|
|
||||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
|
||||||
const [clickCount, setClickCount] = useState(0)
|
|
||||||
|
|
||||||
// Tab 切换处理函数
|
|
||||||
const handleTabChange = (value: string | number) => {
|
|
||||||
console.log('Tab切换:', { from: activeTab, to: value, type: typeof value })
|
|
||||||
setActiveTab(value)
|
|
||||||
setClickCount(prev => prev + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 手动切换测试
|
|
||||||
const manualSwitch = (tab: string | number) => {
|
|
||||||
console.log('手动切换到:', tab)
|
|
||||||
setActiveTab(tab)
|
|
||||||
setClickCount(prev => prev + 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen p-4">
|
|
||||||
<View className="bg-white rounded-lg p-4 mb-4">
|
|
||||||
<Text className="text-lg font-bold mb-2">调试信息</Text>
|
|
||||||
<Text className="block mb-1">当前Tab: {String(activeTab)}</Text>
|
|
||||||
<Text className="block mb-1">切换次数: {clickCount}</Text>
|
|
||||||
<Text className="block mb-1">Tab类型: {typeof activeTab}</Text>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="bg-white rounded-lg p-4 mb-4">
|
|
||||||
<Text className="text-lg font-bold mb-2">手动切换测试</Text>
|
|
||||||
<View className="flex gap-2">
|
|
||||||
<Button size="small" onClick={() => manualSwitch('0')}>
|
|
||||||
切换到申请提现
|
|
||||||
</Button>
|
|
||||||
<Button size="small" onClick={() => manualSwitch('1')}>
|
|
||||||
切换到提现记录
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="bg-white rounded-lg">
|
|
||||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
|
||||||
<Tabs.TabPane title="申请提现" value="0">
|
|
||||||
<View className="p-4">
|
|
||||||
<Text className="text-center text-gray-600">申请提现页面内容</Text>
|
|
||||||
<Text className="text-center text-sm text-gray-400 mt-2">
|
|
||||||
当前Tab值: {String(activeTab)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<Tabs.TabPane title="提现记录" value="1">
|
|
||||||
<View className="p-4">
|
|
||||||
<Text className="text-center text-gray-600">提现记录页面内容</Text>
|
|
||||||
<Text className="text-center text-sm text-gray-400 mt-2">
|
|
||||||
当前Tab值: {String(activeTab)}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="bg-white rounded-lg p-4 mt-4">
|
|
||||||
<Text className="text-lg font-bold mb-2">事件日志</Text>
|
|
||||||
<Text className="text-sm text-gray-500">
|
|
||||||
请查看控制台输出以获取详细的切换日志
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default WithdrawDebug
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '提现申请'
|
|
||||||
})
|
|
||||||
@@ -1,642 +0,0 @@
|
|||||||
import React, {useState, useRef, useEffect, useCallback} from 'react'
|
|
||||||
import {View, Text} from '@tarojs/components'
|
|
||||||
import {
|
|
||||||
Space,
|
|
||||||
Button,
|
|
||||||
Form,
|
|
||||||
Input,
|
|
||||||
CellGroup,
|
|
||||||
Tabs,
|
|
||||||
Tag,
|
|
||||||
Empty,
|
|
||||||
Loading,
|
|
||||||
PullToRefresh
|
|
||||||
} from '@nutui/nutui-react-taro'
|
|
||||||
import {Wallet} from '@nutui/icons-react-taro'
|
|
||||||
import {businessGradients} from '@/styles/gradients'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {useDealerUser} from '@/hooks/useDealerUser'
|
|
||||||
import {myUserVerify} from '@/api/system/userVerify'
|
|
||||||
import {goTo} from '@/utils/navigation'
|
|
||||||
import {
|
|
||||||
pageShopDealerWithdraw,
|
|
||||||
addShopDealerWithdraw,
|
|
||||||
receiveShopDealerWithdraw,
|
|
||||||
receiveSuccessShopDealerWithdraw
|
|
||||||
} from '@/api/shop/shopDealerWithdraw'
|
|
||||||
import type {ShopDealerWithdraw} from '@/api/shop/shopDealerWithdraw/model'
|
|
||||||
|
|
||||||
interface WithdrawRecordWithDetails extends ShopDealerWithdraw {
|
|
||||||
accountDisplay?: string
|
|
||||||
// Backend may include these fields for WeChat "confirm receipt" flow after approval.
|
|
||||||
package_info?: string
|
|
||||||
packageInfo?: string
|
|
||||||
package?: string
|
|
||||||
}
|
|
||||||
|
|
||||||
const extractPackageInfo = (result: unknown): string | null => {
|
|
||||||
if (typeof result === 'string') return result
|
|
||||||
if (!result || typeof result !== 'object') return null
|
|
||||||
const r = result as any
|
|
||||||
return (
|
|
||||||
r.package_info ??
|
|
||||||
r.packageInfo ??
|
|
||||||
r.package ??
|
|
||||||
null
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
const canRequestMerchantTransferConfirm = (): boolean => {
|
|
||||||
try {
|
|
||||||
if (typeof (Taro as any).getEnv === 'function' && (Taro as any).ENV_TYPE) {
|
|
||||||
const env = (Taro as any).getEnv()
|
|
||||||
if (env !== (Taro as any).ENV_TYPE.WEAPP) return false
|
|
||||||
}
|
|
||||||
|
|
||||||
const api =
|
|
||||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
|
||||||
(Taro as any).requestMerchantTransfer
|
|
||||||
|
|
||||||
return typeof api === 'function'
|
|
||||||
} catch {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const requestMerchantTransferConfirm = (packageInfo: string): Promise<any> => {
|
|
||||||
if (!canRequestMerchantTransferConfirm()) {
|
|
||||||
return Promise.reject(new Error('请在微信小程序内完成收款确认'))
|
|
||||||
}
|
|
||||||
|
|
||||||
// Backend may wrap/format base64 with newlines; WeChat API requires a clean string.
|
|
||||||
const cleanPackageInfo = String(packageInfo).replace(/\s+/g, '')
|
|
||||||
|
|
||||||
const api =
|
|
||||||
(globalThis as any).wx?.requestMerchantTransfer ||
|
|
||||||
(Taro as any).requestMerchantTransfer
|
|
||||||
|
|
||||||
if (typeof api !== 'function') {
|
|
||||||
return Promise.reject(new Error('当前环境不支持商家转账收款确认(缺少 requestMerchantTransfer)'))
|
|
||||||
}
|
|
||||||
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
api({
|
|
||||||
// WeChat API uses `package`, backend returns `package_info`.
|
|
||||||
package: cleanPackageInfo,
|
|
||||||
mchId: '1737910695',
|
|
||||||
appId: 'wxad831ba00ad6a026',
|
|
||||||
success: (res: any) => resolve(res),
|
|
||||||
fail: (err: any) => reject(err)
|
|
||||||
})
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Some backends may return money fields as number; keep internal usage always as string.
|
|
||||||
const normalizeMoneyString = (money: unknown) => {
|
|
||||||
if (money === null || money === undefined || money === '') return '0.00'
|
|
||||||
return typeof money === 'string' ? money : String(money)
|
|
||||||
}
|
|
||||||
|
|
||||||
const DealerWithdraw: React.FC = () => {
|
|
||||||
const [activeTab, setActiveTab] = useState<string | number>('0')
|
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
|
||||||
const [refreshing, setRefreshing] = useState<boolean>(false)
|
|
||||||
const [submitting, setSubmitting] = useState<boolean>(false)
|
|
||||||
const [claimingId, setClaimingId] = useState<number | null>(null)
|
|
||||||
const [availableAmount, setAvailableAmount] = useState<string>('0.00')
|
|
||||||
const [withdrawRecords, setWithdrawRecords] = useState<WithdrawRecordWithDetails[]>([])
|
|
||||||
const formRef = useRef<any>(null)
|
|
||||||
|
|
||||||
const {dealerUser} = useDealerUser()
|
|
||||||
const [verifyStatus, setVerifyStatus] = useState<'unknown' | 'verified' | 'unverified' | 'pending' | 'rejected'>('unknown')
|
|
||||||
const [verifyStatusText, setVerifyStatusText] = useState<string>('')
|
|
||||||
|
|
||||||
// Tab 切换处理函数
|
|
||||||
const handleTabChange = (value: string | number) => {
|
|
||||||
console.log('Tab切换到:', value)
|
|
||||||
setActiveTab(value)
|
|
||||||
|
|
||||||
// 如果切换到提现记录页面,刷新数据
|
|
||||||
if (String(value) === '1') {
|
|
||||||
fetchWithdrawRecords()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取可提现余额
|
|
||||||
const fetchBalance = useCallback(async () => {
|
|
||||||
console.log(dealerUser, 'dealerUser...')
|
|
||||||
try {
|
|
||||||
setAvailableAmount(normalizeMoneyString(dealerUser?.money))
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取余额失败:', error)
|
|
||||||
}
|
|
||||||
}, [dealerUser])
|
|
||||||
|
|
||||||
// 获取提现记录
|
|
||||||
const fetchWithdrawRecords = useCallback(async () => {
|
|
||||||
if (!dealerUser?.userId) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
const result = await pageShopDealerWithdraw({
|
|
||||||
page: 1,
|
|
||||||
limit: 100,
|
|
||||||
userId: dealerUser.userId
|
|
||||||
})
|
|
||||||
|
|
||||||
if (result?.list) {
|
|
||||||
const processedRecords = result.list.map(record => ({
|
|
||||||
...record,
|
|
||||||
accountDisplay: getAccountDisplay(record)
|
|
||||||
}))
|
|
||||||
setWithdrawRecords(processedRecords)
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取提现记录失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: '获取提现记录失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}, [dealerUser?.userId])
|
|
||||||
|
|
||||||
// 格式化账户显示
|
|
||||||
const getAccountDisplay = (record: ShopDealerWithdraw) => {
|
|
||||||
if (record.payType === 10) {
|
|
||||||
return '微信钱包'
|
|
||||||
} else if (record.payType === 20 && record.alipayAccount) {
|
|
||||||
return `支付宝(${record.alipayAccount.slice(-4)})`
|
|
||||||
} else if (record.payType === 30 && record.bankCard) {
|
|
||||||
return `${record.bankName || '银行卡'}(尾号${record.bankCard.slice(-4)})`
|
|
||||||
}
|
|
||||||
return '未知账户'
|
|
||||||
}
|
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
const handleRefresh = async () => {
|
|
||||||
setRefreshing(true)
|
|
||||||
await Promise.all([fetchBalance(), fetchWithdrawRecords()])
|
|
||||||
setRefreshing(false)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化加载数据
|
|
||||||
useEffect(() => {
|
|
||||||
if (dealerUser?.userId) {
|
|
||||||
fetchBalance().then()
|
|
||||||
fetchWithdrawRecords().then()
|
|
||||||
}
|
|
||||||
}, [fetchBalance, fetchWithdrawRecords])
|
|
||||||
|
|
||||||
// 判断实名认证状态:提现前必须完成实名认证(已通过)
|
|
||||||
const fetchVerifyStatus = useCallback(async () => {
|
|
||||||
// Fast path: some pages store this flag after login.
|
|
||||||
if (String(Taro.getStorageSync('Certification')) === '1') {
|
|
||||||
setVerifyStatus('verified')
|
|
||||||
setVerifyStatusText('已实名认证')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
const r = await myUserVerify({})
|
|
||||||
if (!r) {
|
|
||||||
setVerifyStatus('unverified')
|
|
||||||
setVerifyStatusText('未实名认证')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
const s = Number((r as any).status)
|
|
||||||
const st = String((r as any).statusText || '')
|
|
||||||
|
|
||||||
// Common convention in this project: 0审核中/待审核, 1已通过, 2已驳回
|
|
||||||
if (s === 1) {
|
|
||||||
setVerifyStatus('verified')
|
|
||||||
setVerifyStatusText(st || '已实名认证')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (s === 0) {
|
|
||||||
setVerifyStatus('pending')
|
|
||||||
setVerifyStatusText(st || '审核中')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if (s === 2) {
|
|
||||||
setVerifyStatus('rejected')
|
|
||||||
setVerifyStatusText(st || '已驳回')
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
setVerifyStatus('unverified')
|
|
||||||
setVerifyStatusText(st || '未实名认证')
|
|
||||||
} catch (e) {
|
|
||||||
console.warn('获取实名认证状态失败,将按未认证处理:', e)
|
|
||||||
setVerifyStatus('unverified')
|
|
||||||
setVerifyStatusText('未实名认证')
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (!dealerUser?.userId) return
|
|
||||||
fetchVerifyStatus().then()
|
|
||||||
}, [dealerUser?.userId, fetchVerifyStatus])
|
|
||||||
|
|
||||||
const getStatusText = (status?: number) => {
|
|
||||||
switch (status) {
|
|
||||||
case 40:
|
|
||||||
return '已到账'
|
|
||||||
case 20:
|
|
||||||
return '待领取'
|
|
||||||
case 10:
|
|
||||||
return '待审核'
|
|
||||||
case 30:
|
|
||||||
return '已驳回'
|
|
||||||
default:
|
|
||||||
return '未知'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const getStatusColor = (status?: number) => {
|
|
||||||
switch (status) {
|
|
||||||
case 40:
|
|
||||||
return 'success'
|
|
||||||
case 20:
|
|
||||||
return 'info'
|
|
||||||
case 10:
|
|
||||||
return 'warning'
|
|
||||||
case 30:
|
|
||||||
return 'danger'
|
|
||||||
default:
|
|
||||||
return 'default'
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleSubmit = async (values: any) => {
|
|
||||||
if (!dealerUser?.userId) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '用户信息获取失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (verifyStatus !== 'verified') {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请先完成实名认证',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证提现金额
|
|
||||||
const amount = parseFloat(String(values.amount))
|
|
||||||
const available = parseFloat(normalizeMoneyString(availableAmount).replace(/,/g, ''))
|
|
||||||
|
|
||||||
if (isNaN(amount) || amount <= 0) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请输入有效的提现金额',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amount < 100) {
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: '最低提现金额为100元',
|
|
||||||
// icon: 'error'
|
|
||||||
// })
|
|
||||||
// return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (amount > available) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '提现金额超过可用余额',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setSubmitting(true)
|
|
||||||
|
|
||||||
const withdrawData: ShopDealerWithdraw = {
|
|
||||||
userId: dealerUser.userId,
|
|
||||||
money: values.amount,
|
|
||||||
// Only support WeChat wallet withdrawals.
|
|
||||||
payType: 10,
|
|
||||||
platform: 'MiniProgram'
|
|
||||||
}
|
|
||||||
|
|
||||||
// Security flow:
|
|
||||||
// 1) user submits => applyStatus=10 (待审核)
|
|
||||||
// 2) backend审核通过 => applyStatus=20 (待领取)
|
|
||||||
// 3) user goes to records to "领取" => applyStatus=40 (已到账)
|
|
||||||
await addShopDealerWithdraw(withdrawData)
|
|
||||||
Taro.showToast({title: '提现申请已提交,等待审核', icon: 'success'})
|
|
||||||
|
|
||||||
// 重置表单
|
|
||||||
formRef.current?.resetFields()
|
|
||||||
|
|
||||||
// 刷新数据
|
|
||||||
await handleRefresh()
|
|
||||||
|
|
||||||
// 切换到提现记录页面
|
|
||||||
setActiveTab('1')
|
|
||||||
|
|
||||||
} catch (error: any) {
|
|
||||||
console.error('提现申请失败:', error)
|
|
||||||
Taro.showToast({
|
|
||||||
title: error.message || '提现申请失败',
|
|
||||||
icon: 'error'
|
|
||||||
})
|
|
||||||
} finally {
|
|
||||||
setSubmitting(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const handleClaim = async (record: WithdrawRecordWithDetails) => {
|
|
||||||
if (!record?.id) {
|
|
||||||
Taro.showToast({title: '记录不存在', icon: 'error'})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.applyStatus !== 20) {
|
|
||||||
Taro.showToast({title: '当前状态不可领取', icon: 'none'})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (record.payType !== 10) {
|
|
||||||
Taro.showToast({title: '仅支持微信提现领取', icon: 'none'})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if (claimingId !== null) return
|
|
||||||
|
|
||||||
try {
|
|
||||||
setClaimingId(record.id)
|
|
||||||
|
|
||||||
if (!canRequestMerchantTransferConfirm()) {
|
|
||||||
throw new Error('当前环境不支持微信收款确认,请在微信小程序内操作')
|
|
||||||
}
|
|
||||||
|
|
||||||
const receiveResult = await receiveShopDealerWithdraw(record.id)
|
|
||||||
const packageInfo = extractPackageInfo(receiveResult)
|
|
||||||
if (!packageInfo) {
|
|
||||||
throw new Error('后台未返回 package_info,无法领取,请联系管理员')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await requestMerchantTransferConfirm(packageInfo)
|
|
||||||
} catch (e: any) {
|
|
||||||
const msg = String(e?.errMsg || e?.message || '')
|
|
||||||
if (/cancel/i.test(msg)) {
|
|
||||||
Taro.showToast({title: '已取消领取', icon: 'none'})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
throw new Error(msg || '领取失败,请稍后重试')
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await receiveSuccessShopDealerWithdraw(record.id)
|
|
||||||
Taro.showToast({title: '领取成功', icon: 'success'})
|
|
||||||
} catch (e: any) {
|
|
||||||
console.warn('领取成功,但状态同步失败:', e)
|
|
||||||
Taro.showToast({title: '已收款,状态更新失败,请稍后刷新', icon: 'none'})
|
|
||||||
} finally {
|
|
||||||
await handleRefresh()
|
|
||||||
}
|
|
||||||
} catch (e: any) {
|
|
||||||
console.error('领取失败:', e)
|
|
||||||
Taro.showToast({title: e?.message || '领取失败', icon: 'error'})
|
|
||||||
} finally {
|
|
||||||
setClaimingId(null)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const quickAmounts = ['100', '300', '500', '1000']
|
|
||||||
|
|
||||||
const setQuickAmount = (amount: string) => {
|
|
||||||
formRef.current?.setFieldsValue({amount})
|
|
||||||
}
|
|
||||||
|
|
||||||
const setAllAmount = () => {
|
|
||||||
formRef.current?.setFieldsValue({amount: normalizeMoneyString(availableAmount).replace(/,/g, '')})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 格式化金额
|
|
||||||
const formatMoney = (money?: unknown) => {
|
|
||||||
const n = parseFloat(normalizeMoneyString(money).replace(/,/g, ''))
|
|
||||||
return Number.isFinite(n) ? n.toFixed(2) : '0.00'
|
|
||||||
}
|
|
||||||
|
|
||||||
const goVerify = () => {
|
|
||||||
goTo('/user/userVerify/index')
|
|
||||||
}
|
|
||||||
|
|
||||||
const renderWithdrawForm = () => (
|
|
||||||
<View>
|
|
||||||
{(verifyStatus === 'unverified' || verifyStatus === 'pending' || verifyStatus === 'rejected') && (
|
|
||||||
<View className="rounded-lg bg-white px-4 py-3 mb-4 mx-4">
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<View className="flex flex-col">
|
|
||||||
<Text className="text-sm text-red-500">提现前请先完成实名认证</Text>
|
|
||||||
{verifyStatusText ? (
|
|
||||||
<Text className="text-xs text-gray-500 mt-1">当前状态:{verifyStatusText}</Text>
|
|
||||||
) : null}
|
|
||||||
</View>
|
|
||||||
<Text className="text-sm text-blue-600" onClick={goVerify}>
|
|
||||||
去认证
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
{/* 余额卡片 */}
|
|
||||||
<View className="rounded-xl p-6 mb-6 text-white relative overflow-hidden" style={{
|
|
||||||
background: businessGradients.dealer.header
|
|
||||||
}}>
|
|
||||||
{/* 装饰背景 - 小程序兼容版本 */}
|
|
||||||
<View className="absolute top-0 right-0 w-24 h-24 rounded-full" style={{
|
|
||||||
backgroundColor: 'rgba(255, 255, 255, 0.1)',
|
|
||||||
right: '-12px',
|
|
||||||
top: '-12px'
|
|
||||||
}}></View>
|
|
||||||
|
|
||||||
<View className="flex items-center justify-between relative z-10">
|
|
||||||
<View className={'flex flex-col'}>
|
|
||||||
<Text className="text-2xl font-bold text-white">{formatMoney(dealerUser?.money)}</Text>
|
|
||||||
<Text className="text-white text-opacity-80 text-sm mb-1">可提现余额</Text>
|
|
||||||
</View>
|
|
||||||
<View className="p-3 rounded-full" style={{
|
|
||||||
background: 'rgba(255, 255, 255, 0.2)'
|
|
||||||
}}>
|
|
||||||
<Wallet color="white" size="32"/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<View className="mt-4 pt-4 relative z-10" style={{
|
|
||||||
borderTop: '1px solid rgba(255, 255, 255, 0.3)'
|
|
||||||
}}>
|
|
||||||
<Text className="text-white text-opacity-80 text-xs">
|
|
||||||
手续费:免费
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<Form
|
|
||||||
ref={formRef}
|
|
||||||
onFinish={handleSubmit}
|
|
||||||
labelPosition="top"
|
|
||||||
>
|
|
||||||
<CellGroup>
|
|
||||||
<Form.Item name="amount" label="提现金额" required>
|
|
||||||
<Input
|
|
||||||
placeholder="请输入提现金额"
|
|
||||||
type="number"
|
|
||||||
/>
|
|
||||||
</Form.Item>
|
|
||||||
|
|
||||||
{/* 快捷金额 */}
|
|
||||||
<View className="px-4 py-2">
|
|
||||||
<Text className="text-sm text-gray-600 mb-2">快捷金额</Text>
|
|
||||||
<View className="flex flex-wrap gap-2">
|
|
||||||
{quickAmounts.map(amount => (
|
|
||||||
<Button
|
|
||||||
key={amount}
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
onClick={() => setQuickAmount(amount)}
|
|
||||||
>
|
|
||||||
{amount}
|
|
||||||
</Button>
|
|
||||||
))}
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
fill="outline"
|
|
||||||
onClick={setAllAmount}
|
|
||||||
>
|
|
||||||
全部
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="px-4 py-2">
|
|
||||||
<Text className="text-sm text-red-500">
|
|
||||||
注意事项:
|
|
||||||
1. 提取佣金必须完成实名认证。
|
|
||||||
2. 佣金非自动到账,再您提取佣金申请通过后,请手动到我的申请记录点击领取。
|
|
||||||
3. 易赊宝温馨提示,请您依法依规申报所得,缴税相关税费。
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</CellGroup>
|
|
||||||
|
|
||||||
<View className="mt-6 px-4">
|
|
||||||
<Button
|
|
||||||
block
|
|
||||||
type="primary"
|
|
||||||
nativeType="submit"
|
|
||||||
loading={submitting}
|
|
||||||
disabled={submitting || verifyStatus !== 'verified'}
|
|
||||||
>
|
|
||||||
{submitting ? '提交中...' : '申请提现'}
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</Form>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
|
|
||||||
const renderWithdrawRecords = () => {
|
|
||||||
console.log('渲染提现记录:', {loading, recordsCount: withdrawRecords.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>
|
|
||||||
) : withdrawRecords.length > 0 ? (
|
|
||||||
withdrawRecords.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>
|
|
||||||
<Text className="font-semibold text-gray-800 mb-1">
|
|
||||||
提现金额:¥{record.money}
|
|
||||||
</Text>
|
|
||||||
{/*<Text className="text-sm text-gray-500">*/}
|
|
||||||
{/* 提现账户:{record.accountDisplay}*/}
|
|
||||||
{/*</Text>*/}
|
|
||||||
</Space>
|
|
||||||
<Tag background="#999999" type={getStatusColor(record.applyStatus)} plain>
|
|
||||||
{getStatusText(record.applyStatus)}
|
|
||||||
</Tag>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
|
|
||||||
{record.applyStatus === 20 && record.payType === 10 && (
|
|
||||||
<View className="flex mb-5 justify-center">
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
loading={claimingId === record.id}
|
|
||||||
disabled={claimingId !== null}
|
|
||||||
onClick={() => handleClaim(record)}
|
|
||||||
>
|
|
||||||
立即领取
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<View className="flex justify-between items-center">
|
|
||||||
<View className="text-xs text-gray-400">
|
|
||||||
<Text>创建时间:{record.createTime}</Text>
|
|
||||||
{record.auditTime && (
|
|
||||||
<Text className="block mt-1">
|
|
||||||
审核时间:{record.auditTime}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
{record.rejectReason && (
|
|
||||||
<Text className="block mt-1 text-red-500">
|
|
||||||
驳回原因:{record.rejectReason}
|
|
||||||
</Text>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
</View>
|
|
||||||
))
|
|
||||||
) : (
|
|
||||||
<Empty description="暂无提现记录"/>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PullToRefresh>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!dealerUser) {
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen flex items-center justify-center">
|
|
||||||
<Loading/>
|
|
||||||
<Text className="text-gray-500 mt-2">加载中...</Text>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className="bg-gray-50 min-h-screen">
|
|
||||||
<Tabs value={activeTab} onChange={handleTabChange}>
|
|
||||||
<Tabs.TabPane title="申请提现" value="0">
|
|
||||||
{renderWithdrawForm()}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
|
|
||||||
<Tabs.TabPane title="提现记录" value="1">
|
|
||||||
{renderWithdrawRecords()}
|
|
||||||
</Tabs.TabPane>
|
|
||||||
</Tabs>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default DealerWithdraw
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '购物车',
|
|
||||||
navigationStyle: 'custom'
|
|
||||||
})
|
|
||||||
@@ -1,31 +0,0 @@
|
|||||||
// 购物车页面样式
|
|
||||||
.cart-page {
|
|
||||||
// 当购物车为空时,设置透明背景
|
|
||||||
&.empty {
|
|
||||||
page {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.cart-empty-container {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 空购物车容器样式
|
|
||||||
.cart-empty-container {
|
|
||||||
background-color: transparent !important;
|
|
||||||
|
|
||||||
// 确保 Empty 组件及其子元素也是透明的
|
|
||||||
.nut-empty {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nut-empty__image {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nut-empty__description {
|
|
||||||
background-color: transparent !important;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,352 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import Taro, {useShareAppMessage, useDidShow} from '@tarojs/taro';
|
|
||||||
import {
|
|
||||||
NavBar,
|
|
||||||
Checkbox,
|
|
||||||
Image,
|
|
||||||
InputNumber,
|
|
||||||
Button,
|
|
||||||
Empty,
|
|
||||||
Divider,
|
|
||||||
ConfigProvider
|
|
||||||
} from '@nutui/nutui-react-taro';
|
|
||||||
import {Del} from '@nutui/icons-react-taro';
|
|
||||||
import {View} from '@tarojs/components';
|
|
||||||
import {CartItem, useCart} from "@/hooks/useCart";
|
|
||||||
import './cart.scss';
|
|
||||||
import { ensureLoggedIn } from '@/utils/auth'
|
|
||||||
|
|
||||||
function Cart() {
|
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>(0);
|
|
||||||
const [selectedItems, setSelectedItems] = useState<number[]>([]);
|
|
||||||
const [isAllSelected, setIsAllSelected] = useState(false);
|
|
||||||
|
|
||||||
const {
|
|
||||||
cartItems,
|
|
||||||
cartCount,
|
|
||||||
updateQuantity,
|
|
||||||
removeFromCart,
|
|
||||||
clearCart,
|
|
||||||
loadCartFromStorage
|
|
||||||
} = useCart();
|
|
||||||
|
|
||||||
// InputNumber 主题配置
|
|
||||||
const customTheme = {
|
|
||||||
nutuiInputnumberButtonWidth: '28px',
|
|
||||||
nutuiInputnumberButtonHeight: '28px',
|
|
||||||
nutuiInputnumberInputWidth: '40px',
|
|
||||||
nutuiInputnumberInputHeight: '28px',
|
|
||||||
nutuiInputnumberInputBorderRadius: '4px',
|
|
||||||
nutuiInputnumberButtonBorderRadius: '4px',
|
|
||||||
}
|
|
||||||
|
|
||||||
useShareAppMessage(() => {
|
|
||||||
return {
|
|
||||||
title: '购物车 - 易赊宝',
|
|
||||||
success: function () {
|
|
||||||
console.log('分享成功');
|
|
||||||
},
|
|
||||||
fail: function () {
|
|
||||||
console.log('分享失败');
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 页面显示时刷新购物车数据
|
|
||||||
useDidShow(() => {
|
|
||||||
loadCartFromStorage();
|
|
||||||
});
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
Taro.getSystemInfo({
|
|
||||||
success: (res) => {
|
|
||||||
setStatusBarHeight(res.statusBarHeight || 0);
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 设置导航栏背景色
|
|
||||||
Taro.setNavigationBarColor({
|
|
||||||
backgroundColor: '#ffffff',
|
|
||||||
frontColor: 'black',
|
|
||||||
});
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 根据购物车状态动态设置页面背景色
|
|
||||||
useEffect(() => {
|
|
||||||
if (cartItems.length === 0) {
|
|
||||||
// 购物车为空时设置透明背景
|
|
||||||
Taro.setBackgroundColor({
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// 有商品时恢复默认背景
|
|
||||||
Taro.setBackgroundColor({
|
|
||||||
backgroundColor: '#f5f5f5'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}, [cartItems.length]);
|
|
||||||
|
|
||||||
// 处理单个商品选择
|
|
||||||
const handleItemSelect = (goodsId: number, checked: boolean) => {
|
|
||||||
if (checked) {
|
|
||||||
setSelectedItems([...selectedItems, goodsId]);
|
|
||||||
} else {
|
|
||||||
setSelectedItems(selectedItems.filter(id => id !== goodsId));
|
|
||||||
setIsAllSelected(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 处理全选
|
|
||||||
const handleSelectAll = (checked: boolean) => {
|
|
||||||
setIsAllSelected(checked);
|
|
||||||
if (checked) {
|
|
||||||
setSelectedItems(cartItems.map((item: CartItem) => item.goodsId));
|
|
||||||
} else {
|
|
||||||
setSelectedItems([]);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
// 更新商品数量
|
|
||||||
const handleQuantityChange = (goodsId: number, value: number) => {
|
|
||||||
updateQuantity(goodsId, value);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 删除商品
|
|
||||||
const handleRemoveItem = (goodsId: number) => {
|
|
||||||
Taro.showModal({
|
|
||||||
title: '确认删除',
|
|
||||||
content: '确定要删除这个商品吗?',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
removeFromCart(goodsId);
|
|
||||||
setSelectedItems(selectedItems.filter(id => id !== goodsId));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 计算选中商品的总价
|
|
||||||
const getSelectedTotalPrice = () => {
|
|
||||||
return cartItems
|
|
||||||
.filter((item: CartItem) => selectedItems.includes(item.goodsId))
|
|
||||||
.reduce((total: number, item: CartItem) => total + (parseFloat(item.price) * item.quantity), 0)
|
|
||||||
.toFixed(2);
|
|
||||||
};
|
|
||||||
|
|
||||||
// 去结算
|
|
||||||
const handleCheckout = () => {
|
|
||||||
if (selectedItems.length === 0) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请选择要结算的商品',
|
|
||||||
icon: 'none'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取选中的商品
|
|
||||||
const selectedCartItems = cartItems.filter((item: CartItem) =>
|
|
||||||
selectedItems.includes(item.goodsId)
|
|
||||||
);
|
|
||||||
|
|
||||||
// 将选中的商品信息存储到本地,供结算页面使用
|
|
||||||
Taro.setStorageSync('checkout_items', JSON.stringify(selectedCartItems));
|
|
||||||
|
|
||||||
// 未登录则引导去注册/登录;登录后回到购物车结算页
|
|
||||||
if (!ensureLoggedIn('/shop/orderConfirmCart/index')) return
|
|
||||||
|
|
||||||
// 跳转到购物车结算页面
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/shop/orderConfirmCart/index'
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
// 检查是否全选
|
|
||||||
useEffect(() => {
|
|
||||||
if (cartItems.length > 0 && selectedItems.length === cartItems.length) {
|
|
||||||
setIsAllSelected(true);
|
|
||||||
} else {
|
|
||||||
setIsAllSelected(false);
|
|
||||||
}
|
|
||||||
}, [selectedItems, cartItems]);
|
|
||||||
|
|
||||||
if (cartItems.length === 0) {
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<NavBar
|
|
||||||
fixed={true}
|
|
||||||
style={{marginTop: `${statusBarHeight}px`}}
|
|
||||||
right={
|
|
||||||
cartItems.length > 0 && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
fill="none"
|
|
||||||
onClick={() => {
|
|
||||||
Taro.showModal({
|
|
||||||
title: '确认清空',
|
|
||||||
content: '确定要清空购物车吗?',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
clearCart();
|
|
||||||
setSelectedItems([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="text-lg">购物车({cartCount})</span>
|
|
||||||
</NavBar>
|
|
||||||
|
|
||||||
{/* 垂直居中的空状态容器 */}
|
|
||||||
<View
|
|
||||||
className="flex items-center justify-center"
|
|
||||||
style={{
|
|
||||||
height: `calc(100vh - ${statusBarHeight + 150}px)`,
|
|
||||||
paddingTop: `${statusBarHeight + 50}px`,
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Empty
|
|
||||||
description="购物车空空如也"
|
|
||||||
actions={[{ text: '去逛逛' }]}
|
|
||||||
style={{
|
|
||||||
backgroundColor: 'transparent'
|
|
||||||
}}
|
|
||||||
onClick={() => Taro.switchTab({ url: '/pages/index/index' })}
|
|
||||||
/>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View style={{backgroundColor: '#f6f6f6', height: `${statusBarHeight}px`}}
|
|
||||||
className="fixed z-10 top-0 w-full"></View>
|
|
||||||
<NavBar
|
|
||||||
fixed={true}
|
|
||||||
style={{marginTop: `${statusBarHeight}px`}}
|
|
||||||
right={
|
|
||||||
cartItems.length > 0 && (
|
|
||||||
<Button
|
|
||||||
size="small"
|
|
||||||
type="primary"
|
|
||||||
fill="none"
|
|
||||||
onClick={() => {
|
|
||||||
Taro.showModal({
|
|
||||||
title: '确认清空',
|
|
||||||
content: '确定要清空购物车吗?',
|
|
||||||
success: (res) => {
|
|
||||||
if (res.confirm) {
|
|
||||||
clearCart();
|
|
||||||
setSelectedItems([]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
</Button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
>
|
|
||||||
<span className="text-lg">购物车({cartCount})</span>
|
|
||||||
</NavBar>
|
|
||||||
|
|
||||||
{/* 购物车内容 */}
|
|
||||||
<View className="pt-24">
|
|
||||||
{/* 商品列表 */}
|
|
||||||
<View className="bg-white">
|
|
||||||
{cartItems.map((item: CartItem, index: number) => (
|
|
||||||
<View key={item.goodsId}>
|
|
||||||
<View className="bg-white px-4 py-3 flex items-center gap-3">
|
|
||||||
{/* 选择框 */}
|
|
||||||
<Checkbox
|
|
||||||
checked={selectedItems.includes(item.goodsId)}
|
|
||||||
onChange={(checked) => handleItemSelect(item.goodsId, checked)}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 商品图片 */}
|
|
||||||
<Image
|
|
||||||
src={item.image}
|
|
||||||
width="80"
|
|
||||||
height="80"
|
|
||||||
lazyLoad={false}
|
|
||||||
radius="8"
|
|
||||||
className="flex-shrink-0"
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* 商品信息 */}
|
|
||||||
<View className="flex-1 min-w-0">
|
|
||||||
<View className="text-lg font-bold text-gray-900 truncate mb-1">
|
|
||||||
{item.name}
|
|
||||||
</View>
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
|
||||||
<span className={'text-xs'}>¥</span>
|
|
||||||
<span className={'font-bold text-lg'}>{item.price}</span>
|
|
||||||
</View>
|
|
||||||
<ConfigProvider theme={customTheme}>
|
|
||||||
<InputNumber
|
|
||||||
value={item.quantity}
|
|
||||||
min={1}
|
|
||||||
onChange={(value) => handleQuantityChange(item.goodsId, Number(value))}
|
|
||||||
/>
|
|
||||||
</ConfigProvider>
|
|
||||||
<Del className={'text-red-500'} size={14} onClick={() => handleRemoveItem(item.goodsId)}/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
{index < cartItems.length - 1 && <Divider/>}
|
|
||||||
</View>
|
|
||||||
))}
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 底部结算栏 */}
|
|
||||||
<View
|
|
||||||
className="fixed z-50 bottom-0 left-0 right-0 bg-white border-t border-gray-200 px-4 py-3 safe-area-bottom">
|
|
||||||
<View className="flex items-center justify-between">
|
|
||||||
<View className="flex items-center gap-3">
|
|
||||||
<Checkbox
|
|
||||||
checked={isAllSelected}
|
|
||||||
onChange={handleSelectAll}
|
|
||||||
>
|
|
||||||
全选
|
|
||||||
</Checkbox>
|
|
||||||
<View className="text-sm text-gray-600">
|
|
||||||
已选 {selectedItems.length} 件
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className="flex items-center gap-4">
|
|
||||||
<View className="text-right">
|
|
||||||
<View className="text-xs text-gray-500">合计:</View>
|
|
||||||
<div className={'flex text-red-500 text-xl items-baseline'}>
|
|
||||||
<span className={'text-xs'}>¥</span>
|
|
||||||
<span className={'font-bold text-lg'}>{getSelectedTotalPrice()}</span>
|
|
||||||
</div>
|
|
||||||
</View>
|
|
||||||
<Button
|
|
||||||
type="primary"
|
|
||||||
size="large"
|
|
||||||
disabled={selectedItems.length === 0}
|
|
||||||
onClick={handleCheckout}
|
|
||||||
className="px-6"
|
|
||||||
>
|
|
||||||
结算({selectedItems.length})
|
|
||||||
</Button>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
{/* 底部安全区域占位 */}
|
|
||||||
<View className="h-20"></View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Cart;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
export default definePageConfig({
|
|
||||||
navigationBarTitleText: '网点',
|
|
||||||
navigationBarTextStyle: 'black',
|
|
||||||
navigationBarBackgroundColor: '#ffffff'
|
|
||||||
})
|
|
||||||
@@ -1,144 +0,0 @@
|
|||||||
page {
|
|
||||||
background: #f7f7f7;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sitePage {
|
|
||||||
min-height: 100vh;
|
|
||||||
background: linear-gradient(180deg, #fde8ea 0%, #f7f7f7 320rpx, #f7f7f7 100%);
|
|
||||||
padding-bottom: calc(40rpx + env(safe-area-inset-bottom));
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchArea {
|
|
||||||
padding: 22rpx 24rpx 18rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchBox {
|
|
||||||
height: 86rpx;
|
|
||||||
background: #fff;
|
|
||||||
border: 2rpx solid #b51616;
|
|
||||||
border-radius: 12rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
overflow: hidden;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchInput {
|
|
||||||
flex: 1;
|
|
||||||
height: 86rpx;
|
|
||||||
padding: 0 20rpx;
|
|
||||||
font-size: 30rpx;
|
|
||||||
color: #222;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchPlaceholder {
|
|
||||||
color: #9e9e9e;
|
|
||||||
font-size: 30rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.searchIconWrap {
|
|
||||||
width: 88rpx;
|
|
||||||
height: 86rpx;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteList {
|
|
||||||
padding: 0 24rpx 24rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteCard {
|
|
||||||
background: #fff;
|
|
||||||
border-radius: 18rpx;
|
|
||||||
padding: 22rpx 22rpx 18rpx;
|
|
||||||
margin-top: 18rpx;
|
|
||||||
box-shadow: 0 10rpx 24rpx rgba(0, 0, 0, 0.04);
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteCardInner {
|
|
||||||
display: flex;
|
|
||||||
align-items: stretch;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteInfo {
|
|
||||||
flex: 1;
|
|
||||||
padding-right: 10rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteRow {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start;
|
|
||||||
padding: 10rpx 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteRowTop {
|
|
||||||
padding-top: 2rpx;
|
|
||||||
padding-bottom: 14rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteLabel {
|
|
||||||
width: 170rpx;
|
|
||||||
flex: 0 0 170rpx;
|
|
||||||
color: #9a9a9a;
|
|
||||||
font-size: 30rpx;
|
|
||||||
line-height: 1.6;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteValue {
|
|
||||||
flex: 1;
|
|
||||||
color: #222;
|
|
||||||
font-size: 30rpx;
|
|
||||||
line-height: 1.6;
|
|
||||||
word-break: break-all;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteValueStrong {
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteDivider {
|
|
||||||
height: 2rpx;
|
|
||||||
background: #ededed;
|
|
||||||
}
|
|
||||||
|
|
||||||
.siteSide {
|
|
||||||
width: 120rpx;
|
|
||||||
flex: 0 0 160rpx;
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column;
|
|
||||||
align-items: flex-end;
|
|
||||||
justify-content: center;
|
|
||||||
padding-left: 12rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.navArrow {
|
|
||||||
width: 14rpx;
|
|
||||||
height: 14rpx;
|
|
||||||
border-top: 5rpx solid #e60012;
|
|
||||||
border-right: 5rpx solid #e60012;
|
|
||||||
border-radius: 4rpx;
|
|
||||||
transform: rotate(45deg);
|
|
||||||
margin-right: 6rpx;
|
|
||||||
}
|
|
||||||
|
|
||||||
.distanceText {
|
|
||||||
margin-top: 18rpx;
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #e60012;
|
|
||||||
font-weight: 700;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyWrap {
|
|
||||||
padding: 40rpx 0;
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.emptyText {
|
|
||||||
font-size: 28rpx;
|
|
||||||
color: #9a9a9a;
|
|
||||||
}
|
|
||||||
|
|
||||||
.bottomSafe {
|
|
||||||
height: 20rpx;
|
|
||||||
}
|
|
||||||
@@ -1,258 +0,0 @@
|
|||||||
import { useMemo, useRef, useState } from 'react'
|
|
||||||
import Taro, { useDidShow } from '@tarojs/taro'
|
|
||||||
import {Input, Text, View} from '@tarojs/components'
|
|
||||||
import { Empty, InfiniteLoading, Loading, PullToRefresh } from '@nutui/nutui-react-taro'
|
|
||||||
import {Search} from '@nutui/icons-react-taro'
|
|
||||||
import { pageShopStore } from '@/api/shop/shopStore'
|
|
||||||
import type { ShopStore } from '@/api/shop/shopStore/model'
|
|
||||||
import { getCurrentLngLat } from '@/utils/location'
|
|
||||||
import './find.scss'
|
|
||||||
|
|
||||||
const PAGE_SIZE = 10
|
|
||||||
|
|
||||||
type LngLat = { lng: number; lat: number }
|
|
||||||
type ShopStoreView = ShopStore & { __distanceMeter?: number }
|
|
||||||
|
|
||||||
const parseLngLat = (raw: string | undefined): LngLat | null => {
|
|
||||||
const text = (raw || '').trim()
|
|
||||||
if (!text) return null
|
|
||||||
const parts = text.split(/[,\s]+/).filter(Boolean)
|
|
||||||
if (parts.length < 2) return null
|
|
||||||
const a = Number(parts[0])
|
|
||||||
const b = Number(parts[1])
|
|
||||||
if (!Number.isFinite(a) || !Number.isFinite(b)) return null
|
|
||||||
|
|
||||||
// Accept both "lng,lat" and "lat,lng".
|
|
||||||
const looksLikeLngLat = Math.abs(a) <= 180 && Math.abs(b) <= 90
|
|
||||||
const looksLikeLatLng = Math.abs(a) <= 90 && Math.abs(b) <= 180
|
|
||||||
if (looksLikeLngLat) return { lng: a, lat: b }
|
|
||||||
if (looksLikeLatLng) return { lng: b, lat: a }
|
|
||||||
return null
|
|
||||||
}
|
|
||||||
|
|
||||||
const distanceMeters = (a: LngLat, b: LngLat) => {
|
|
||||||
const toRad = (x: number) => (x * Math.PI) / 180
|
|
||||||
const R = 6371000
|
|
||||||
const dLat = toRad(b.lat - a.lat)
|
|
||||||
const dLng = toRad(b.lng - a.lng)
|
|
||||||
const lat1 = toRad(a.lat)
|
|
||||||
const lat2 = toRad(b.lat)
|
|
||||||
const sin1 = Math.sin(dLat / 2)
|
|
||||||
const sin2 = Math.sin(dLng / 2)
|
|
||||||
const h = sin1 * sin1 + Math.cos(lat1) * Math.cos(lat2) * sin2 * sin2
|
|
||||||
return 2 * R * Math.asin(Math.min(1, Math.sqrt(h)))
|
|
||||||
}
|
|
||||||
|
|
||||||
const formatDistance = (meter: number | undefined) => {
|
|
||||||
if (!Number.isFinite(meter as number)) return ''
|
|
||||||
const m = Math.max(0, Math.round(meter as number))
|
|
||||||
if (m < 1000) return `${m}米`
|
|
||||||
const km = m / 1000
|
|
||||||
return `${km.toFixed(km >= 10 ? 0 : 1)}km`
|
|
||||||
}
|
|
||||||
|
|
||||||
const Find = () => {
|
|
||||||
const [keyword, setKeyword] = useState<string>('')
|
|
||||||
const [storeList, setStoreList] = useState<ShopStoreView[]>([])
|
|
||||||
const [loading, setLoading] = useState(false)
|
|
||||||
const [hasMore, setHasMore] = useState(true)
|
|
||||||
const [userLngLat, setUserLngLat] = useState<LngLat | null>(null)
|
|
||||||
|
|
||||||
const pageRef = useRef(1)
|
|
||||||
const latestListRef = useRef<ShopStoreView[]>([])
|
|
||||||
const loadingRef = useRef(false)
|
|
||||||
const coordsRef = useRef<LngLat | null>(null)
|
|
||||||
|
|
||||||
const viewList = useMemo<ShopStoreView[]>(() => {
|
|
||||||
const me = userLngLat
|
|
||||||
if (!me) return storeList
|
|
||||||
|
|
||||||
// Keep backend order; only attach distance for display.
|
|
||||||
return storeList.map((s) => {
|
|
||||||
const coords = parseLngLat(s.lngAndLat || s.location)
|
|
||||||
if (!coords) return s
|
|
||||||
return { ...s, __distanceMeter: distanceMeters(me, coords) }
|
|
||||||
})
|
|
||||||
}, [storeList, userLngLat])
|
|
||||||
|
|
||||||
const loadStores = async (isRefresh = true, keywordsOverride?: string) => {
|
|
||||||
if (loadingRef.current) return
|
|
||||||
loadingRef.current = true
|
|
||||||
setLoading(true)
|
|
||||||
|
|
||||||
if (isRefresh) {
|
|
||||||
pageRef.current = 1
|
|
||||||
latestListRef.current = []
|
|
||||||
setStoreList([])
|
|
||||||
setHasMore(true)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (!coordsRef.current) {
|
|
||||||
const me = await getCurrentLngLat('为您展示附近网点,需要获取定位信息。')
|
|
||||||
const lng = me ? Number(me.lng) : NaN
|
|
||||||
const lat = me ? Number(me.lat) : NaN
|
|
||||||
coordsRef.current = Number.isFinite(lng) && Number.isFinite(lat) ? { lng, lat } : null
|
|
||||||
setUserLngLat(coordsRef.current)
|
|
||||||
}
|
|
||||||
|
|
||||||
const currentPage = pageRef.current
|
|
||||||
const kw = (keywordsOverride ?? keyword).trim()
|
|
||||||
const res = await pageShopStore({
|
|
||||||
page: currentPage,
|
|
||||||
limit: PAGE_SIZE,
|
|
||||||
keywords: kw || undefined
|
|
||||||
})
|
|
||||||
|
|
||||||
const resList = res?.list || []
|
|
||||||
const nextList = isRefresh ? resList : [...latestListRef.current, ...resList]
|
|
||||||
latestListRef.current = nextList
|
|
||||||
setStoreList(nextList)
|
|
||||||
|
|
||||||
const count = typeof res?.count === 'number' ? res.count : nextList.length
|
|
||||||
setHasMore(nextList.length < count)
|
|
||||||
|
|
||||||
if (resList.length > 0) {
|
|
||||||
pageRef.current = currentPage + 1
|
|
||||||
} else {
|
|
||||||
setHasMore(false)
|
|
||||||
}
|
|
||||||
} catch (e) {
|
|
||||||
console.error('获取网点列表失败:', e)
|
|
||||||
Taro.showToast({ title: '获取网点失败', icon: 'none' })
|
|
||||||
setHasMore(false)
|
|
||||||
} finally {
|
|
||||||
loadingRef.current = false
|
|
||||||
setLoading(false)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useDidShow(() => {
|
|
||||||
loadStores(true).then()
|
|
||||||
})
|
|
||||||
|
|
||||||
const onNavigate = (item: ShopStore) => {
|
|
||||||
const coords = parseLngLat(item.lngAndLat || item.location)
|
|
||||||
if (!coords) {
|
|
||||||
Taro.showToast({ title: '网点暂无坐标,无法导航', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Taro.openLocation({
|
|
||||||
latitude: coords.lat,
|
|
||||||
longitude: coords.lng,
|
|
||||||
name: item.name || item.city || '网点',
|
|
||||||
address: item.address || ''
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const onCall = (phone: string | undefined) => {
|
|
||||||
const p = (phone || '').trim()
|
|
||||||
if (!p) {
|
|
||||||
Taro.showToast({ title: '暂无联系电话', icon: 'none' })
|
|
||||||
return
|
|
||||||
}
|
|
||||||
Taro.makePhoneCall({ phoneNumber: p })
|
|
||||||
}
|
|
||||||
|
|
||||||
const onSearch = () => {
|
|
||||||
loadStores(true).then()
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<View className='sitePage'>
|
|
||||||
<View className='searchArea'>
|
|
||||||
<View className='searchBox'>
|
|
||||||
<Input
|
|
||||||
className='searchInput'
|
|
||||||
value={keyword}
|
|
||||||
placeholder='请输入城市名称查询'
|
|
||||||
placeholderClass='searchPlaceholder'
|
|
||||||
confirmType='search'
|
|
||||||
onInput={(e) => setKeyword(e.detail.value)}
|
|
||||||
onConfirm={onSearch}
|
|
||||||
/>
|
|
||||||
<View className='searchIconWrap' onClick={onSearch}>
|
|
||||||
<Search size={18} color='#b51616' />
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<PullToRefresh onRefresh={() => loadStores(true)} headHeight={60}>
|
|
||||||
<View style={{ height: 'calc(100vh) - 160px', overflowY: 'auto' }} id='store-scroll'>
|
|
||||||
{viewList.length === 0 && !loading ? (
|
|
||||||
<View className='emptyWrap'>
|
|
||||||
<Empty description='暂无网点' style={{ backgroundColor: 'transparent' }} />
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View className='siteList'>
|
|
||||||
<InfiniteLoading
|
|
||||||
target='store-scroll'
|
|
||||||
hasMore={hasMore}
|
|
||||||
onLoadMore={() => loadStores(false)}
|
|
||||||
loadingText={
|
|
||||||
<View className='emptyWrap'>
|
|
||||||
<Loading />
|
|
||||||
<Text className='emptyText' style={{ marginLeft: '8px' }}>
|
|
||||||
加载中...
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
loadMoreText={
|
|
||||||
<View className='emptyWrap'>
|
|
||||||
<Text className='emptyText'>
|
|
||||||
{viewList.length === 0 ? '暂无网点' : '没有更多了'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{viewList.map((item, idx) => {
|
|
||||||
const name = item?.name || item?.city || item?.province || '网点'
|
|
||||||
const contact = item?.managerName || '--'
|
|
||||||
const distanceText = formatDistance(item?.__distanceMeter)
|
|
||||||
return (
|
|
||||||
<View key={String(item?.id ?? `${name}-${idx}`)} className='siteCard'>
|
|
||||||
<View className='siteCardInner'>
|
|
||||||
<View className='siteInfo'>
|
|
||||||
<View className='siteRow siteRowTop'>
|
|
||||||
<Text className='siteLabel'>网点名称:</Text>
|
|
||||||
<Text className='siteValue siteValueStrong'>{name}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='siteDivider' />
|
|
||||||
<View className='siteRow'>
|
|
||||||
<Text className='siteLabel'>网点地址:</Text>
|
|
||||||
<Text className='siteValue'>{item?.address || '--'}</Text>
|
|
||||||
</View>
|
|
||||||
<View className='siteRow'>
|
|
||||||
<Text className='siteLabel'>联系电话:</Text>
|
|
||||||
<Text className='siteValue' onClick={() => onCall(item?.phone)}>
|
|
||||||
{item?.phone || '--'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
<View className='siteRow'>
|
|
||||||
<Text className='siteLabel'>联系人:</Text>
|
|
||||||
<Text className='siteValue'>{contact}</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
|
|
||||||
<View className='siteSide' onClick={() => onNavigate(item)}>
|
|
||||||
<View className='navArrow' />
|
|
||||||
<Text className='distanceText'>
|
|
||||||
{distanceText ? `${distanceText}` : '查看导航'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</InfiniteLoading>
|
|
||||||
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
</PullToRefresh>
|
|
||||||
|
|
||||||
<View className='bottomSafe' />
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default Find
|
|
||||||
@@ -1,141 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import {Image} from '@nutui/nutui-react-taro'
|
|
||||||
import {Share} from '@nutui/icons-react-taro'
|
|
||||||
import {View, Text} from '@tarojs/components';
|
|
||||||
import Taro, {useShareAppMessage, useShareTimeline} from "@tarojs/taro";
|
|
||||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
|
||||||
import {pageShopGoods} from "@/api/shop/shopGoods";
|
|
||||||
import './BestSellers.scss'
|
|
||||||
|
|
||||||
|
|
||||||
const BestSellers = () => {
|
|
||||||
const [list, setList] = useState<ShopGoods[]>([])
|
|
||||||
const [goods, setGoods] = useState<ShopGoods>()
|
|
||||||
|
|
||||||
const reload = () => {
|
|
||||||
pageShopGoods({}).then(res => {
|
|
||||||
setList(res?.list || []);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理分享点击
|
|
||||||
const handleShare = (item: ShopGoods) => {
|
|
||||||
setGoods(item);
|
|
||||||
|
|
||||||
// 显示分享选项菜单
|
|
||||||
Taro.showActionSheet({
|
|
||||||
itemList: ['分享给好友', '分享到朋友圈'],
|
|
||||||
success: (res) => {
|
|
||||||
if (res.tapIndex === 0) {
|
|
||||||
// 分享给好友 - 触发转发
|
|
||||||
Taro.showShareMenu({
|
|
||||||
withShareTicket: true,
|
|
||||||
success: () => {
|
|
||||||
// 提示用户点击右上角分享
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请点击右上角分享给好友',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else if (res.tapIndex === 1) {
|
|
||||||
// 分享到朋友圈
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请点击右上角分享到朋友圈',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.log('显示分享菜单失败', err);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 分享给好友
|
|
||||||
useShareAppMessage(() => {
|
|
||||||
return {
|
|
||||||
title: goods?.name || '精选商品',
|
|
||||||
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
|
||||||
imageUrl: goods?.image, // 分享图片
|
|
||||||
success: function (res: any) {
|
|
||||||
console.log('分享成功', res);
|
|
||||||
Taro.showToast({
|
|
||||||
title: '分享成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
},
|
|
||||||
fail: function (res: any) {
|
|
||||||
console.log('分享失败', res);
|
|
||||||
Taro.showToast({
|
|
||||||
title: '分享失败',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
// 分享到朋友圈
|
|
||||||
useShareTimeline(() => {
|
|
||||||
return {
|
|
||||||
title: `${goods?.name || '精选商品'} - 易赊宝`,
|
|
||||||
path: `/shop/goodsDetail/index?id=${goods?.goodsId}`,
|
|
||||||
imageUrl: goods?.image
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View className={'py-1'}>
|
|
||||||
<View className={'flex flex-col justify-between items-center rounded-lg px-2'}>
|
|
||||||
{list?.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<View key={index} className={'flex flex-col rounded-lg bg-white shadow-sm w-full mb-5'}>
|
|
||||||
<Image src={item.image} mode={'aspectFit'} lazyLoad={false}
|
|
||||||
radius="10px 10px 0 0" height="180"
|
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
|
||||||
<View className={'flex flex-col p-2 rounded-lg'}>
|
|
||||||
<View>
|
|
||||||
<View className={'car-no text-sm'}>{item.name}</View>
|
|
||||||
<View className={'flex justify-between text-xs py-1'}>
|
|
||||||
<Text className={'text-orange-500'}>{item.comments}</Text>
|
|
||||||
<Text className={'text-gray-400'}>已售 {item.sales}</Text>
|
|
||||||
</View>
|
|
||||||
<View className={'flex justify-between items-center py-2'}>
|
|
||||||
<View className={'flex text-red-500 text-xl items-baseline'}>
|
|
||||||
<Text className={'text-xs'}>¥</Text>
|
|
||||||
<Text className={'font-bold text-2xl'}>{item.price}</Text>
|
|
||||||
</View>
|
|
||||||
<View className={'buy-btn'}>
|
|
||||||
<View className={'cart-icon flex items-center'}>
|
|
||||||
<View
|
|
||||||
className={'flex flex-col justify-center items-center text-white px-3 gap-1 text-nowrap whitespace-nowrap cursor-pointer'}
|
|
||||||
onClick={() => handleShare(item)}
|
|
||||||
>
|
|
||||||
<Share size={20}/>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
<Text className={'text-white pl-4 pr-5'}
|
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default BestSellers
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import {Tabs, TabPane} from '@nutui/nutui-react-taro'
|
|
||||||
|
|
||||||
const list = [
|
|
||||||
{
|
|
||||||
title: '今天',
|
|
||||||
id: 1
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '昨天',
|
|
||||||
id: 2
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '过去7天',
|
|
||||||
id: 3
|
|
||||||
},
|
|
||||||
{
|
|
||||||
title: '过去30天',
|
|
||||||
id: 4
|
|
||||||
}
|
|
||||||
]
|
|
||||||
const Chart = () => {
|
|
||||||
const [tapIndex, setTapIndex] = useState<string | number>('0')
|
|
||||||
const reload = () => {
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Tabs
|
|
||||||
align={'left'}
|
|
||||||
tabStyle={{position: 'sticky', top: '0px'}}
|
|
||||||
value={tapIndex}
|
|
||||||
onChange={(paneKey) => {
|
|
||||||
setTapIndex(paneKey)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{
|
|
||||||
list?.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<TabPane key={index} title={item.title}/>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</Tabs>
|
|
||||||
{
|
|
||||||
list?.map((item, index) => {
|
|
||||||
console.log(item.title)
|
|
||||||
return (
|
|
||||||
<div key={index} className={'px-3'}>
|
|
||||||
{
|
|
||||||
tapIndex != index ? null :
|
|
||||||
<div className={'bg-white rounded-lg p-4 flex justify-center items-center text-center text-gray-300'} style={{height: '200px'}}>
|
|
||||||
线状图
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
</>
|
|
||||||
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default Chart
|
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {Button} from '@nutui/nutui-react-taro'
|
|
||||||
import {Target, Scan, Truck} from '@nutui/icons-react-taro'
|
|
||||||
import {getUserInfo} from "@/api/layout";
|
|
||||||
import navTo from "@/utils/common";
|
|
||||||
|
|
||||||
const ExpirationTime = () => {
|
|
||||||
const [isAdmin, setIsAdmin] = useState<boolean>(false)
|
|
||||||
const [roleName, setRoleName] = useState<string>()
|
|
||||||
const onScanCode = () => {
|
|
||||||
Taro.scanCode({
|
|
||||||
onlyFromCamera: true,
|
|
||||||
scanType: ['qrCode'],
|
|
||||||
success: (res) => {
|
|
||||||
console.log(res, 'qrcode...')
|
|
||||||
Taro.navigateTo({url: '/hjm/query?id=' + res.result})
|
|
||||||
},
|
|
||||||
fail: (res) => {
|
|
||||||
console.log(res, '扫码失败')
|
|
||||||
Taro.showToast({
|
|
||||||
title: '扫码失败',
|
|
||||||
icon: 'none',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
const navToCarList = () => {
|
|
||||||
if (isAdmin) {
|
|
||||||
navTo('/hjm/list', true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
getUserInfo().then((data) => {
|
|
||||||
if (data) {
|
|
||||||
if(data.certification){
|
|
||||||
setIsAdmin( true)
|
|
||||||
}
|
|
||||||
data.roles?.map((item, index) => {
|
|
||||||
if (index == 0) {
|
|
||||||
setRoleName(item.roleCode)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<div className={'mb-3 fixed top-36 z-20'} style={{width: '96%', marginLeft: '3%'}}>
|
|
||||||
<div className={'w-full flex justify-around items-center py-3 rounded-lg'}>
|
|
||||||
<>
|
|
||||||
<Button size={'large'}
|
|
||||||
style={{background: 'linear-gradient(to right, #f3f2f7, #805de1)', borderColor: '#f3f2f7'}}
|
|
||||||
icon={<Truck/>} onClick={navToCarList}>车辆列表</Button>
|
|
||||||
<Button size={'large'}
|
|
||||||
style={{background: 'linear-gradient(to right, #fffbe6, #ffc53d)', borderColor: '#f3f2f7'}}
|
|
||||||
icon={<Scan/>}
|
|
||||||
onClick={onScanCode}>扫一扫
|
|
||||||
</Button>
|
|
||||||
</>
|
|
||||||
|
|
||||||
{
|
|
||||||
roleName == 'youzheng' && <Button size={'large'} style={{
|
|
||||||
background: 'linear-gradient(to right, #eaff8f, #7cb305)',
|
|
||||||
borderColor: '#f3f2f7'
|
|
||||||
}} icon={<Target/>} onClick={() => Taro.navigateTo({url: '/hjm/fence'})}>电子围栏</Button>
|
|
||||||
}
|
|
||||||
|
|
||||||
{
|
|
||||||
roleName == 'kuaidiyuan' && <Button size={'large'} style={{
|
|
||||||
background: 'linear-gradient(to right, #ffa39e, #ff4d4f)',
|
|
||||||
borderColor: '#f3f2f7'
|
|
||||||
}} icon={<Target/>} onClick={() => Taro.navigateTo({url: '/hjm/bx/bx-add'})}>一键报险</Button>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default ExpirationTime
|
|
||||||
@@ -1,67 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import {Image} from '@nutui/nutui-react-taro'
|
|
||||||
import {Share} from '@nutui/icons-react-taro'
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {ShopGoods} from "@/api/shop/shopGoods/model";
|
|
||||||
import {pageShopGoods} from "@/api/shop/shopGoods";
|
|
||||||
import './GoodsList.scss'
|
|
||||||
|
|
||||||
|
|
||||||
const BestSellers = () => {
|
|
||||||
const [list, setList] = useState<ShopGoods[]>([])
|
|
||||||
|
|
||||||
const reload = () => {
|
|
||||||
pageShopGoods({}).then(res => {
|
|
||||||
setList(res?.list || []);
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={'py-3'}>
|
|
||||||
<div className={'flex flex-wrap justify-between items-start rounded-lg px-2'}>
|
|
||||||
{list?.map((item, index) => {
|
|
||||||
return (
|
|
||||||
<div key={index} className={'flex flex-col rounded-lg bg-white shadow-sm mb-5'} style={{
|
|
||||||
width: '48%'
|
|
||||||
}}>
|
|
||||||
<Image src={item.image} mode={'scaleToFill'} lazyLoad={false}
|
|
||||||
radius="10px 10px 0 0" height="180"
|
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
|
||||||
<div className={'flex flex-col p-2 rounded-lg'}>
|
|
||||||
<div>
|
|
||||||
<div className={'car-no text-sm'}>{item.name}</div>
|
|
||||||
<div className={'flex justify-between text-xs py-1'}>
|
|
||||||
<span className={'text-orange-500'}>{item.comments}</span>
|
|
||||||
<span className={'text-gray-400'}>已售 {item.sales}</span>
|
|
||||||
</div>
|
|
||||||
<div className={'flex justify-between items-center py-2'}>
|
|
||||||
<div className={'flex text-red-500 text-xl items-baseline'}>
|
|
||||||
<span className={'text-xs'}>¥</span>
|
|
||||||
<span className={'font-bold text-2xl'}>{item.price}</span>
|
|
||||||
</div>
|
|
||||||
<div className={'buy-btn'}>
|
|
||||||
<div className={'cart-icon'}>
|
|
||||||
<Share size={20} className={'mx-4 mt-2'}
|
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}/>
|
|
||||||
</div>
|
|
||||||
<div className={'text-white pl-4 pr-5'}
|
|
||||||
onClick={() => Taro.navigateTo({url: '/shop/goodsDetail/index?id=' + item.goodsId})}>购买
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
})}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default BestSellers
|
|
||||||
@@ -1,58 +0,0 @@
|
|||||||
import {useEffect, useState} from 'react'
|
|
||||||
import {Grid} from '@nutui/nutui-react-taro'
|
|
||||||
import {Avatar, Divider} from '@nutui/nutui-react-taro'
|
|
||||||
import {View, Text} from '@tarojs/components'
|
|
||||||
import {listCmsNavigation} from "@/api/cms/cmsNavigation";
|
|
||||||
import {CmsNavigation} from "@/api/cms/cmsNavigation/model";
|
|
||||||
import navTo from "@/utils/common";
|
|
||||||
|
|
||||||
const MyGrid = () => {
|
|
||||||
const [list, setList] = useState<CmsNavigation[]>([])
|
|
||||||
const reload = async () => {
|
|
||||||
// 读取首页菜单
|
|
||||||
const home = await listCmsNavigation({model: 'index'});
|
|
||||||
const homeId = home[0].navigationId;
|
|
||||||
if(homeId){
|
|
||||||
const menu = await listCmsNavigation({home: 0, parentId: homeId, hide: 0})
|
|
||||||
setList(menu)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload().then()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
if (list.length == 0) {
|
|
||||||
return <></>
|
|
||||||
}
|
|
||||||
|
|
||||||
// @ts-ignore
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View className={'p-4'}>
|
|
||||||
<View className={' bg-white rounded-2xl py-4'}>
|
|
||||||
<View className={'title font-medium px-4'}>功能菜单</View>
|
|
||||||
<Divider />
|
|
||||||
<Grid columns={3} square style={{
|
|
||||||
// @ts-ignore
|
|
||||||
'--nutui-grid-border-color': 'transparent',
|
|
||||||
}}>
|
|
||||||
{
|
|
||||||
list.map((item) => (
|
|
||||||
<Grid.Item key={item.navigationId} onClick={() => navTo(`${item.path}`,true)}>
|
|
||||||
<Avatar src={item.icon} className={'mb-2'} shape="square" style={{
|
|
||||||
backgroundColor: 'transparent',
|
|
||||||
}}/>
|
|
||||||
<Text className={'text-gray-600'} style={{
|
|
||||||
fontSize: '16px'
|
|
||||||
}}>{item.title}</Text>
|
|
||||||
</Grid.Item>
|
|
||||||
))
|
|
||||||
}
|
|
||||||
</Grid>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default MyGrid
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
.header-bg{
|
|
||||||
background: linear-gradient(to bottom, #03605c, #18ae4f);
|
|
||||||
height: 335px;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
.header-bg2{
|
|
||||||
background: linear-gradient(to bottom, #03605c, #18ae4f);
|
|
||||||
height: 200px;
|
|
||||||
width: 100%;
|
|
||||||
top: 0;
|
|
||||||
position: absolute;
|
|
||||||
z-index: 0;
|
|
||||||
}
|
|
||||||
@@ -1,275 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import Taro from '@tarojs/taro';
|
|
||||||
import {Space} from '@nutui/nutui-react-taro'
|
|
||||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
|
||||||
import {Avatar, NavBar} from '@nutui/nutui-react-taro'
|
|
||||||
import {getWxOpenId} from "@/api/layout";
|
|
||||||
// import {TenantId} from "@/config/app";
|
|
||||||
import {getOrganization} from "@/api/system/organization";
|
|
||||||
import {myUserVerify} from "@/api/system/userVerify";
|
|
||||||
import {useShopInfo} from '@/hooks/useShopInfo';
|
|
||||||
import {useUser} from '@/hooks/useUser';
|
|
||||||
// import {handleInviteRelation} from "@/utils/invite";
|
|
||||||
import {View, Text} from '@tarojs/components'
|
|
||||||
import MySearch from "./MySearch";
|
|
||||||
import './Header.scss';
|
|
||||||
import navTo from "@/utils/common";
|
|
||||||
import UnifiedQRButton from "@/components/UnifiedQRButton";
|
|
||||||
import {getShopDealerRefereeByUserId} from "@/api/shop/shopDealerReferee";
|
|
||||||
|
|
||||||
const Header = (props: any) => {
|
|
||||||
// 使用新的useShopInfo Hook
|
|
||||||
const {
|
|
||||||
getWebsiteLogo,
|
|
||||||
getWebsiteName
|
|
||||||
} = useShopInfo();
|
|
||||||
|
|
||||||
// 使用useUser Hook管理用户状态
|
|
||||||
const {
|
|
||||||
user,
|
|
||||||
isLoggedIn,
|
|
||||||
fetchUserInfo
|
|
||||||
} = useUser();
|
|
||||||
|
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
|
||||||
|
|
||||||
const reload = async () => {
|
|
||||||
Taro.getSystemInfo({
|
|
||||||
success: (res) => {
|
|
||||||
setStatusBarHeight(res.statusBarHeight)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 检查用户是否已登录并且有头像和昵称
|
|
||||||
if (isLoggedIn) {
|
|
||||||
const hasAvatar = user?.avatar || Taro.getStorageSync('Avatar');
|
|
||||||
const hasNickname = user?.nickname || Taro.getStorageSync('Nickname');
|
|
||||||
|
|
||||||
if (!hasAvatar || !hasNickname) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '您还没有上传头像和昵称',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/user/profile/profile'
|
|
||||||
})
|
|
||||||
}, 3000)
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果已登录,获取最新用户信息
|
|
||||||
if (isLoggedIn) {
|
|
||||||
try {
|
|
||||||
const data = await fetchUserInfo();
|
|
||||||
if (data) {
|
|
||||||
console.log('用户信息>>>', data.phone)
|
|
||||||
// 获取openId
|
|
||||||
if (!data.openid) {
|
|
||||||
Taro.login({
|
|
||||||
success: (res) => {
|
|
||||||
getWxOpenId({code: res.code}).then(() => {
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 是否已认证
|
|
||||||
if (data.certification) {
|
|
||||||
Taro.setStorageSync('Certification', '1')
|
|
||||||
}
|
|
||||||
// 机构ID
|
|
||||||
Taro.setStorageSync('OrganizationId', data.organizationId)
|
|
||||||
// 父级机构ID
|
|
||||||
if (Number(data.organizationId) > 0) {
|
|
||||||
getOrganization(Number(data.organizationId)).then(res => {
|
|
||||||
Taro.setStorageSync('OrganizationParentId', res.parentId)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
// 管理员
|
|
||||||
const isKdy = data.roles?.findIndex(item => item.roleCode == 'admin')
|
|
||||||
if (isKdy != -1) {
|
|
||||||
Taro.setStorageSync('RoleName', '管理')
|
|
||||||
Taro.setStorageSync('RoleCode', 'admin')
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 注册用户
|
|
||||||
const isUser = data.roles?.findIndex(item => item.roleCode == 'user')
|
|
||||||
if (isUser != -1) {
|
|
||||||
Taro.setStorageSync('RoleName', '注册用户')
|
|
||||||
Taro.setStorageSync('RoleCode', 'user')
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
// 认证信息
|
|
||||||
myUserVerify({status: 1}).then(data => {
|
|
||||||
if (data?.realName) {
|
|
||||||
Taro.setStorageSync('RealName', data.realName)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('获取用户信息失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 查找上级推荐人
|
|
||||||
if(Taro.getStorageSync('UserId')){
|
|
||||||
const dealer = await getShopDealerRefereeByUserId(Taro.getStorageSync('UserId'))
|
|
||||||
if(dealer){
|
|
||||||
Taro.setStorageSync('DealerId', dealer.dealerId)
|
|
||||||
Taro.setStorageSync('Dealer', dealer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 获取用户手机号 */
|
|
||||||
// const handleGetPhoneNumber = ({detail}: { detail: { code?: string, encryptedData?: string, iv?: string } }) => {
|
|
||||||
// const {code, encryptedData, iv} = detail
|
|
||||||
// Taro.login({
|
|
||||||
// success: (loginRes) => {
|
|
||||||
// if (code) {
|
|
||||||
// Taro.request({
|
|
||||||
// url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
|
||||||
// method: 'POST',
|
|
||||||
// data: {
|
|
||||||
// authCode: loginRes.code,
|
|
||||||
// code,
|
|
||||||
// encryptedData,
|
|
||||||
// iv,
|
|
||||||
// notVerifyPhone: true,
|
|
||||||
// refereeId: 0,
|
|
||||||
// sceneType: 'save_referee',
|
|
||||||
// tenantId: TenantId
|
|
||||||
// },
|
|
||||||
// header: {
|
|
||||||
// 'content-type': 'application/json',
|
|
||||||
// TenantId
|
|
||||||
// },
|
|
||||||
// success: async function (res) {
|
|
||||||
// if (res.data.code == 1) {
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: res.data.message,
|
|
||||||
// icon: 'error',
|
|
||||||
// duration: 2000
|
|
||||||
// })
|
|
||||||
// return false;
|
|
||||||
// }
|
|
||||||
// // 登录成功
|
|
||||||
// const token = res.data.data.access_token;
|
|
||||||
// const userData = res.data.data.user;
|
|
||||||
//
|
|
||||||
// // 使用useUser Hook的loginUser方法更新状态
|
|
||||||
// loginUser(token, userData);
|
|
||||||
//
|
|
||||||
// // 处理邀请关系
|
|
||||||
// if (userData?.userId) {
|
|
||||||
// try {
|
|
||||||
// const inviteSuccess = await handleInviteRelation(userData.userId)
|
|
||||||
// if (inviteSuccess) {
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: '邀请关系建立成功',
|
|
||||||
// icon: 'success',
|
|
||||||
// duration: 2000
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
// } catch (error) {
|
|
||||||
// console.error('处理邀请关系失败:', error)
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
//
|
|
||||||
// // 显示登录成功提示
|
|
||||||
// Taro.showToast({
|
|
||||||
// title: '登录成功',
|
|
||||||
// icon: 'success',
|
|
||||||
// duration: 1500
|
|
||||||
// })
|
|
||||||
//
|
|
||||||
// // 不需要重新启动小程序,状态已经通过useUser更新
|
|
||||||
// // 可以选择性地刷新当前页面数据
|
|
||||||
// reload();
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// } else {
|
|
||||||
// console.log('登录失败!')
|
|
||||||
// }
|
|
||||||
// }
|
|
||||||
// })
|
|
||||||
// }
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload().then()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 监听用户信息变化,当用户信息更新后重新检查
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoggedIn && user) {
|
|
||||||
console.log('用户信息已更新:', user);
|
|
||||||
// 检查是否设置头像和昵称
|
|
||||||
if (user.nickname === '微信用户') {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请设置头像和昵称',
|
|
||||||
icon: 'none'
|
|
||||||
})
|
|
||||||
setTimeout(() => {
|
|
||||||
Taro.navigateTo({
|
|
||||||
url: '/user/profile/profile'
|
|
||||||
});
|
|
||||||
}, 2000)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}, [user, isLoggedIn])
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<View className={'fixed top-0 header-bg'} style={{
|
|
||||||
height: !props.stickyStatus ? '180px' : '148px',
|
|
||||||
}}>
|
|
||||||
<MySearch/>
|
|
||||||
{/*{!props.stickyStatus && <MySearch done={reload}/>}*/}
|
|
||||||
</View>
|
|
||||||
<NavBar
|
|
||||||
style={{marginTop: `${statusBarHeight}px`, marginBottom: '0px', backgroundColor: 'transparent'}}
|
|
||||||
onBackClick={() => {
|
|
||||||
}}
|
|
||||||
left={
|
|
||||||
<Space>
|
|
||||||
{/*统一扫码入口 - 支持登录和核销*/}
|
|
||||||
<UnifiedQRButton
|
|
||||||
size="small"
|
|
||||||
onSuccess={(result) => {
|
|
||||||
console.log('统一扫码成功:', result);
|
|
||||||
// 根据扫码类型给出不同的提示
|
|
||||||
if (result.type === 'verification') {
|
|
||||||
// 核销成功,可以显示更多信息或跳转到详情页
|
|
||||||
Taro.showModal({
|
|
||||||
title: '核销成功',
|
|
||||||
content: `已成功核销的品类:${result.data.goodsName || '礼品卡'},面值¥${result.data.faceValue}`
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}}
|
|
||||||
onError={(error) => {
|
|
||||||
console.error('统一扫码失败:', error);
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Space>
|
|
||||||
}
|
|
||||||
>
|
|
||||||
{isLoggedIn ? (
|
|
||||||
<Space onClick={() => navTo(`/user/profile/profile`, true)}>
|
|
||||||
<Text className={'text-white'}>{getWebsiteName()}</Text>
|
|
||||||
</Space>
|
|
||||||
) : (
|
|
||||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={getWebsiteLogo()}
|
|
||||||
/>
|
|
||||||
<Text className={'text-xs'} style={{color: '#ffffff'}}>{getWebsiteName()}</Text>
|
|
||||||
<TriangleDown size={9} className={'text-white'}/>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
</NavBar>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
export default Header
|
|
||||||
@@ -1,221 +0,0 @@
|
|||||||
import {useEffect, useState} from "react";
|
|
||||||
import Taro from '@tarojs/taro';
|
|
||||||
import {Button, Space} from '@nutui/nutui-react-taro'
|
|
||||||
import {TriangleDown} from '@nutui/icons-react-taro'
|
|
||||||
import {Popup, Avatar, NavBar} from '@nutui/nutui-react-taro'
|
|
||||||
import {getWxOpenId} from "@/api/layout";
|
|
||||||
import {TenantId} from "@/config/app";
|
|
||||||
import {getOrganization} from "@/api/system/organization";
|
|
||||||
import {myUserVerify} from "@/api/system/userVerify";
|
|
||||||
import { useShopInfo } from '@/hooks/useShopInfo';
|
|
||||||
import { useUser } from '@/hooks/useUser';
|
|
||||||
import {handleInviteRelation} from "@/utils/invite";
|
|
||||||
import MySearch from "./MySearch";
|
|
||||||
import './Header.scss';
|
|
||||||
|
|
||||||
const Header = (props: any) => {
|
|
||||||
// 使用新的hooks
|
|
||||||
const {
|
|
||||||
loading: shopLoading,
|
|
||||||
getWebsiteName,
|
|
||||||
getWebsiteLogo
|
|
||||||
} = useShopInfo();
|
|
||||||
|
|
||||||
const {
|
|
||||||
user,
|
|
||||||
isLoggedIn,
|
|
||||||
loading: userLoading
|
|
||||||
} = useUser();
|
|
||||||
|
|
||||||
const [showBasic, setShowBasic] = useState(false)
|
|
||||||
const [statusBarHeight, setStatusBarHeight] = useState<number>()
|
|
||||||
|
|
||||||
const reload = async () => {
|
|
||||||
Taro.getSystemInfo({
|
|
||||||
success: (res) => {
|
|
||||||
setStatusBarHeight(res.statusBarHeight)
|
|
||||||
},
|
|
||||||
})
|
|
||||||
|
|
||||||
// 注意:商店信息现在通过useShopInfo自动管理,不需要手动获取
|
|
||||||
// 用户信息现在通过useUser自动管理,不需要手动获取
|
|
||||||
|
|
||||||
// 如果需要获取openId,可以在用户登录后处理
|
|
||||||
if (user && !user.openid) {
|
|
||||||
Taro.login({
|
|
||||||
success: (res) => {
|
|
||||||
getWxOpenId({code: res.code}).then(() => {
|
|
||||||
console.log('OpenId获取成功');
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户认证状态
|
|
||||||
if (user?.userId) {
|
|
||||||
// 获取组织信息
|
|
||||||
getOrganization(user.userId).then((data) => {
|
|
||||||
console.log('组织信息>>>', data)
|
|
||||||
}).catch(() => {
|
|
||||||
console.log('获取组织信息失败')
|
|
||||||
});
|
|
||||||
|
|
||||||
// 检查用户认证
|
|
||||||
myUserVerify({id: user.userId}).then((data) => {
|
|
||||||
console.log('认证信息>>>', data)
|
|
||||||
}).catch(() => {
|
|
||||||
console.log('获取认证信息失败')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取手机号授权
|
|
||||||
const handleGetPhoneNumber = ({detail}: {detail: {code?: string, encryptedData?: string, iv?: string}}) => {
|
|
||||||
const {code, encryptedData, iv} = detail
|
|
||||||
Taro.login({
|
|
||||||
success: function (loginRes) {
|
|
||||||
if (code) {
|
|
||||||
Taro.request({
|
|
||||||
url: 'https://server.websoft.top/api/wx-login/loginByMpWxPhone',
|
|
||||||
method: 'POST',
|
|
||||||
data: {
|
|
||||||
authCode: loginRes.code,
|
|
||||||
code,
|
|
||||||
encryptedData,
|
|
||||||
iv,
|
|
||||||
notVerifyPhone: true,
|
|
||||||
refereeId: 0,
|
|
||||||
sceneType: 'save_referee',
|
|
||||||
tenantId: TenantId
|
|
||||||
},
|
|
||||||
success: async 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)
|
|
||||||
|
|
||||||
// 处理邀请关系
|
|
||||||
if (res.data.data.user?.userId) {
|
|
||||||
try {
|
|
||||||
const inviteSuccess = await handleInviteRelation(res.data.data.user.userId)
|
|
||||||
if (inviteSuccess) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '邀请关系建立成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
})
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理邀请关系失败:', error)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 重新加载小程序
|
|
||||||
Taro.reLaunch({
|
|
||||||
url: '/pages/index/index'
|
|
||||||
})
|
|
||||||
}
|
|
||||||
})
|
|
||||||
} else {
|
|
||||||
console.log('登录失败!')
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
reload().then()
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
// 显示加载状态
|
|
||||||
if (shopLoading || userLoading) {
|
|
||||||
return (
|
|
||||||
<div className={'fixed top-0 header-bg'} style={{
|
|
||||||
height: !props.stickyStatus ? '180px' : '148px',
|
|
||||||
}}>
|
|
||||||
<div style={{padding: '20px', textAlign: 'center', color: '#fff'}}>
|
|
||||||
加载中...
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<div className={'fixed top-0 header-bg'} style={{
|
|
||||||
height: !props.stickyStatus ? '180px' : '148px',
|
|
||||||
}}>
|
|
||||||
<MySearch/>
|
|
||||||
</div>
|
|
||||||
<NavBar
|
|
||||||
style={{marginTop: `${statusBarHeight}px`, marginBottom: '0px', backgroundColor: 'transparent'}}
|
|
||||||
onBackClick={() => {
|
|
||||||
}}
|
|
||||||
left={
|
|
||||||
!isLoggedIn ? (
|
|
||||||
<div style={{display: 'flex', alignItems: 'center'}}>
|
|
||||||
<Button style={{color: '#000'}} open-type="getPhoneNumber" onGetPhoneNumber={handleGetPhoneNumber}>
|
|
||||||
<Space>
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={getWebsiteLogo()}
|
|
||||||
/>
|
|
||||||
<span style={{color: '#000'}}>{getWebsiteName()}</span>
|
|
||||||
</Space>
|
|
||||||
</Button>
|
|
||||||
<TriangleDown size={9}/>
|
|
||||||
</div>
|
|
||||||
) : (
|
|
||||||
<div style={{display: 'flex', alignItems: 'center', gap: '8px'}}>
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={getWebsiteLogo()}
|
|
||||||
/>
|
|
||||||
<span className={'text-white'}>{getWebsiteName()}</span>
|
|
||||||
<TriangleDown className={'text-white'} size={9}/>
|
|
||||||
</div>
|
|
||||||
)}>
|
|
||||||
</NavBar>
|
|
||||||
<Popup
|
|
||||||
visible={showBasic}
|
|
||||||
position="bottom"
|
|
||||||
style={{width: '100%', height: '100%'}}
|
|
||||||
onClose={() => {
|
|
||||||
setShowBasic(false)
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<div style={{padding: '20px'}}>
|
|
||||||
<h3>商店信息</h3>
|
|
||||||
<div>网站名称: {getWebsiteName()}</div>
|
|
||||||
<div>Logo: <img src={getWebsiteLogo()} alt="logo" style={{width: '50px', height: '50px'}} /></div>
|
|
||||||
|
|
||||||
<h3>用户信息</h3>
|
|
||||||
<div>登录状态: {isLoggedIn ? '已登录' : '未登录'}</div>
|
|
||||||
{user && (
|
|
||||||
<>
|
|
||||||
<div>用户ID: {user.userId}</div>
|
|
||||||
<div>手机号: {user.phone}</div>
|
|
||||||
<div>昵称: {user.nickname}</div>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<button
|
|
||||||
onClick={() => setShowBasic(false)}
|
|
||||||
style={{marginTop: '20px', padding: '10px 20px'}}
|
|
||||||
>
|
|
||||||
关闭
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</Popup>
|
|
||||||
</>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Header;
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user