fix(dealer/customer): 修复客户添加表单手机号字段绑定错误- 修正了客户添加表单中手机号字段的绑定逻辑,确保使用表单值而非用户信息中的电话号码
- 在客户添加表单中新增了报备人(userId)字段的选择项 - 移除了二维码登录相关API和类型定义文件 - 移除了统一扫码处理组件及相关的Hook实现 - 优化了首页Header组件的登录状态显示逻辑- 移除了用户卡片组件中的冗余扫码功能 - 更新了门店核销页面的配置和实现,移除了扫码相关功能- 移除了多个文档文件,包括配置说明、登录状态更新测试指南、运行时错误解决方案和TypeScript修复说明
This commit is contained in:
@@ -1,59 +0,0 @@
|
|||||||
# 配置说明文档
|
|
||||||
|
|
||||||
## 环境配置
|
|
||||||
|
|
||||||
### 1. 复制环境变量文件
|
|
||||||
```bash
|
|
||||||
cp .env.example .env
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 修改配置文件
|
|
||||||
|
|
||||||
#### config/app.ts
|
|
||||||
```typescript
|
|
||||||
// 租户ID - 请根据实际情况修改
|
|
||||||
export const TenantId = 'YOUR_TENANT_ID';
|
|
||||||
// 接口地址 - 请根据实际情况修改
|
|
||||||
export const BaseUrl = 'https://your-api-domain.com/api';
|
|
||||||
```
|
|
||||||
|
|
||||||
#### src/utils/server.ts
|
|
||||||
```typescript
|
|
||||||
// 模版套餐ID - 请根据实际情况修改
|
|
||||||
export const TEMPLATE_ID = 'YOUR_TEMPLATE_ID';
|
|
||||||
// 服务接口 - 请根据实际情况修改
|
|
||||||
export const SERVER_API_URL = 'https://your-server-domain.com/api';
|
|
||||||
```
|
|
||||||
|
|
||||||
#### project.config.json
|
|
||||||
```json
|
|
||||||
{
|
|
||||||
"appid": "your_wechat_appid"
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 小程序配置
|
|
||||||
|
|
||||||
#### 微信小程序
|
|
||||||
1. 在微信公众平台申请小程序
|
|
||||||
2. 获取AppID并填入 `project.config.json`
|
|
||||||
3. 配置服务器域名白名单
|
|
||||||
|
|
||||||
#### 支付宝小程序
|
|
||||||
1. 在支付宝开放平台申请小程序
|
|
||||||
2. 获取AppID并配置相应文件
|
|
||||||
|
|
||||||
#### 字节跳动小程序
|
|
||||||
1. 在字节跳动开发者平台申请小程序
|
|
||||||
2. 获取AppID并填入 `project.tt.json`
|
|
||||||
|
|
||||||
### 4. API配置
|
|
||||||
|
|
||||||
确保后端API服务正常运行,并配置正确的域名和端口。
|
|
||||||
|
|
||||||
### 5. 安全注意事项
|
|
||||||
|
|
||||||
- 不要将真实的AppID、API密钥等敏感信息提交到公开仓库
|
|
||||||
- 使用环境变量管理敏感配置
|
|
||||||
- 定期更新密码和密钥
|
|
||||||
- 在生产环境中启用HTTPS
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
# 登录状态实时更新测试指南
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
之前的问题是:用户登录成功后,Header组件的登录状态没有实时更新,需要刷新页面才能看到登录状态的变化。
|
|
||||||
|
|
||||||
## 解决方案
|
|
||||||
修改了以下组件,使其使用 `useUser` Hook 来管理用户状态:
|
|
||||||
|
|
||||||
1. **Header组件** (`src/pages/index/Header.tsx`)
|
|
||||||
2. **UserCard组件** (`src/pages/user/components/UserCard.tsx`)
|
|
||||||
3. **Admin UserCard组件** (`src/admin/components/UserCard.tsx`)
|
|
||||||
|
|
||||||
## 额外优化:清理冗余代码
|
|
||||||
发现 UserCard 组件中存在冗余的数据获取逻辑:
|
|
||||||
- `useUserData` Hook 已经动态获取了优惠券、礼品卡、积分、余额等数据
|
|
||||||
- 删除了重复的本地状态:`couponCount`, `pointsCount`, `giftCount`
|
|
||||||
- 删除了重复的数据获取方法:`loadUserStats`
|
|
||||||
- 统一使用 `useUserData` 提供的数据
|
|
||||||
|
|
||||||
### 主要修改内容
|
|
||||||
|
|
||||||
#### 1. Header组件修改
|
|
||||||
- 移除了本地状态 `IsLogin` 和 `userInfo`
|
|
||||||
- 使用 `useUser` Hook 的 `user`, `isLoggedIn`, `loginUser`, `fetchUserInfo`
|
|
||||||
- 登录成功后调用 `loginUser(token, userData)` 而不是直接设置本地状态
|
|
||||||
- 移除了 `Taro.reLaunch` 重新启动小程序的逻辑
|
|
||||||
|
|
||||||
#### 2. UserCard组件修改
|
|
||||||
- 类似的修改,使用 `useUser` Hook 管理状态
|
|
||||||
- 登录成功后调用 `loginUser(token, userData)`
|
|
||||||
|
|
||||||
## 测试步骤
|
|
||||||
|
|
||||||
### 1. 基本登录状态更新测试
|
|
||||||
1. 打开小程序,确保处于未登录状态
|
|
||||||
2. 在首页Header区域,应该显示"未登录"状态
|
|
||||||
3. 点击登录按钮,完成手机号授权登录
|
|
||||||
4. **关键测试点**:登录成功后,Header应该立即显示用户信息,无需刷新页面
|
|
||||||
|
|
||||||
### 2. 跨页面状态同步测试
|
|
||||||
1. 在首页完成登录
|
|
||||||
2. 切换到用户中心页面
|
|
||||||
3. 检查UserCard组件是否正确显示登录状态
|
|
||||||
4. 返回首页,检查Header组件状态是否保持一致
|
|
||||||
|
|
||||||
### 3. 退出登录测试
|
|
||||||
1. 在用户中心点击"退出登录"
|
|
||||||
2. 返回首页,检查Header是否立即显示未登录状态
|
|
||||||
|
|
||||||
### 4. 页面刷新测试
|
|
||||||
1. 登录后刷新页面(或重新进入小程序)
|
|
||||||
2. 检查登录状态是否正确从本地存储恢复
|
|
||||||
|
|
||||||
## 预期结果
|
|
||||||
|
|
||||||
### 修改前的问题
|
|
||||||
- 登录成功后需要 `Taro.reLaunch` 重新启动小程序
|
|
||||||
- 状态更新不实时,用户体验差
|
|
||||||
|
|
||||||
### 修改后的预期效果
|
|
||||||
- 登录成功后立即更新UI状态
|
|
||||||
- 无需重新启动小程序
|
|
||||||
- 所有使用 `useUser` Hook 的组件都能实时同步状态
|
|
||||||
- 更好的用户体验
|
|
||||||
|
|
||||||
## 技术实现原理
|
|
||||||
|
|
||||||
### useUser Hook 的优势
|
|
||||||
1. **集中状态管理**:所有用户相关状态都在一个地方管理
|
|
||||||
2. **自动同步**:所有使用该Hook的组件都会自动同步状态变化
|
|
||||||
3. **持久化存储**:自动处理本地存储的读写
|
|
||||||
4. **错误处理**:统一的错误处理逻辑
|
|
||||||
|
|
||||||
### 状态更新流程
|
|
||||||
```
|
|
||||||
用户登录 → loginUser(token, userData) → 更新Hook状态 → 所有组件自动重新渲染
|
|
||||||
```
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. **确保所有相关组件都使用useUser Hook**:避免混用本地状态和全局状态
|
|
||||||
2. **测试边界情况**:网络错误、token过期等情况
|
|
||||||
3. **性能考虑**:useUser Hook 已经优化了不必要的重新渲染
|
|
||||||
|
|
||||||
## 如果测试失败
|
|
||||||
|
|
||||||
如果登录后状态仍然没有实时更新,请检查:
|
|
||||||
|
|
||||||
1. 组件是否正确导入和使用了 `useUser` Hook
|
|
||||||
2. 登录成功后是否调用了 `loginUser` 方法
|
|
||||||
3. 是否还有其他地方使用了本地状态而不是Hook状态
|
|
||||||
4. 控制台是否有错误信息
|
|
||||||
|
|
||||||
## 进一步优化建议
|
|
||||||
|
|
||||||
1. 可以考虑添加加载状态指示器
|
|
||||||
2. 可以添加登录状态变化的动画效果
|
|
||||||
3. 可以考虑使用 React Context 进一步优化性能
|
|
||||||
@@ -1,203 +0,0 @@
|
|||||||
# 运行时错误解决方案
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
遇到了运行时错误:`TypeError: Cannot read property 'mount' of null`
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
这个错误通常发生在 Taro 应用启动时,可能的原因包括:
|
|
||||||
1. 组件导入缺失
|
|
||||||
2. TypeScript 类型错误导致编译问题
|
|
||||||
3. 循环依赖
|
|
||||||
4. 组件初始化时的空引用
|
|
||||||
|
|
||||||
## 解决步骤
|
|
||||||
|
|
||||||
### 1. 修复 TypeScript 类型错误
|
|
||||||
在 `src/user/profile/profile.tsx` 中修复了类型定义:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 添加了明确的类型定义
|
|
||||||
interface ChooseAvatarEvent {
|
|
||||||
detail: {
|
|
||||||
avatarUrl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InputEvent {
|
|
||||||
detail: {
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 修复了函数参数类型
|
|
||||||
const uploadAvatar = ({detail}: ChooseAvatarEvent) => {
|
|
||||||
// 明确的类型定义
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitSucceed = (values: User) => {
|
|
||||||
// 使用具体的 User 类型
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitFailed = (error: unknown) => {
|
|
||||||
// 使用 unknown 类型替代 any
|
|
||||||
}
|
|
||||||
|
|
||||||
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
|
|
||||||
// 明确的事件类型
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 修复导入问题
|
|
||||||
在 `src/pages/index/Header.tsx` 中重新添加了缺失的导入:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {getUserInfo, getWxOpenId} from "@/api/layout";
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 重新编译
|
|
||||||
运行 `npm run dev:weapp` 重新编译项目。
|
|
||||||
|
|
||||||
## 编译结果
|
|
||||||
✅ **编译成功**
|
|
||||||
- 编译时间:10.28秒
|
|
||||||
- 发现了87个页面入口
|
|
||||||
- 所有TypeScript类型错误已修复
|
|
||||||
- 所有导入问题已解决
|
|
||||||
|
|
||||||
## 验证步骤
|
|
||||||
1. 编译成功完成
|
|
||||||
2. 开发服务器正常启动
|
|
||||||
3. 监听模式正常工作
|
|
||||||
|
|
||||||
## 根本原因
|
|
||||||
主要是由于之前的修改过程中:
|
|
||||||
1. 删除了必要的导入但没有完全清理相关引用
|
|
||||||
2. TypeScript 类型定义不完整导致编译错误
|
|
||||||
3. 这些编译错误可能导致运行时的初始化问题
|
|
||||||
|
|
||||||
## 预防措施
|
|
||||||
1. **渐进式修改**:一次只修改一个文件,确保每次修改后都能正常编译
|
|
||||||
2. **类型安全**:始终为函数参数和事件处理器提供明确的类型定义
|
|
||||||
3. **导入检查**:修改导入时要确保所有相关引用都正确更新
|
|
||||||
4. **编译验证**:每次修改后都要验证编译是否成功
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
- `src/user/profile/profile.tsx` - 修复了TypeScript类型错误
|
|
||||||
- `src/pages/index/Header.tsx` - 修复了导入问题
|
|
||||||
- `TYPESCRIPT_FIXES.md` - TypeScript类型修复详细说明
|
|
||||||
|
|
||||||
## 后续修复 - API调用优化
|
|
||||||
|
|
||||||
### 3. 修复未登录状态下的API调用
|
|
||||||
在 `src/hooks/useUserData.ts` 中添加了登录状态检查:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 获取用户数据
|
|
||||||
const fetchUserData = useCallback(async () => {
|
|
||||||
// 检查用户ID是否存在(更直接的登录状态检查)
|
|
||||||
const userId = Taro.getStorageSync('UserId')
|
|
||||||
if (!userId) {
|
|
||||||
setLoading(false)
|
|
||||||
setData(null)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
setLoading(true)
|
|
||||||
setError(null)
|
|
||||||
// ... 其余代码
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 实现完整的自动登录功能
|
|
||||||
在 `src/hooks/useUser.ts` 中添加了自动登录功能:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 自动登录(通过OpenID)
|
|
||||||
const autoLoginByOpenId = async () => {
|
|
||||||
try {
|
|
||||||
const res = await new Promise<any>((resolve, reject) => {
|
|
||||||
Taro.login({
|
|
||||||
success: (loginRes) => {
|
|
||||||
loginByOpenId({
|
|
||||||
code: loginRes.code,
|
|
||||||
tenantId: TenantId
|
|
||||||
}).then(async (data) => {
|
|
||||||
if (data) {
|
|
||||||
// 保存登录信息
|
|
||||||
saveUserToStorage(data.access_token, data.user);
|
|
||||||
setUser(data.user);
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
|
|
||||||
// 处理邀请关系
|
|
||||||
if (data.user?.userId) {
|
|
||||||
try {
|
|
||||||
const inviteSuccess = await handleInviteRelation(data.user.userId);
|
|
||||||
if (inviteSuccess) {
|
|
||||||
console.log('自动登录时邀请关系建立成功');
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('自动登录时处理邀请关系失败:', error);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
resolve(data.user);
|
|
||||||
} else {
|
|
||||||
reject(new Error('自动登录失败'));
|
|
||||||
}
|
|
||||||
}).catch(reject);
|
|
||||||
},
|
|
||||||
fail: reject
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return res;
|
|
||||||
} catch (error) {
|
|
||||||
console.error('自动登录失败:', error);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
并在 `loadUserFromStorage` 中集成自动登录:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 从本地存储加载用户数据
|
|
||||||
const loadUserFromStorage = async () => {
|
|
||||||
try {
|
|
||||||
const token = Taro.getStorageSync('access_token');
|
|
||||||
const userData = Taro.getStorageSync('User');
|
|
||||||
const userId = Taro.getStorageSync('UserId');
|
|
||||||
const tenantId = Taro.getStorageSync('TenantId');
|
|
||||||
|
|
||||||
if (token && userData) {
|
|
||||||
const userInfo = typeof userData === 'string' ? JSON.parse(userData) : userData;
|
|
||||||
setUser(userInfo);
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
} else if (token && userId) {
|
|
||||||
// 如果有token和userId但没有完整用户信息,标记为已登录但需要获取用户信息
|
|
||||||
setIsLoggedIn(true);
|
|
||||||
setUser({ userId, tenantId } as User);
|
|
||||||
} else {
|
|
||||||
// 没有本地登录信息,尝试自动登录
|
|
||||||
console.log('没有本地登录信息,尝试自动登录...');
|
|
||||||
const autoLoginResult = await autoLoginByOpenId();
|
|
||||||
if (!autoLoginResult) {
|
|
||||||
setUser(null);
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('加载用户数据失败:', error);
|
|
||||||
setUser(null);
|
|
||||||
setIsLoggedIn(false);
|
|
||||||
} finally {
|
|
||||||
setLoading(false);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 清理重复代码
|
|
||||||
移除了 `src/app.ts` 中的重复自动登录逻辑,避免代码冗余和潜在冲突。
|
|
||||||
|
|
||||||
## 状态
|
|
||||||
🟢 **已完全解决** - 应用现在可以正常编译和运行,具备完整的自动登录功能
|
|
||||||
@@ -1,95 +0,0 @@
|
|||||||
# TypeScript 类型错误修复
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
在 `src/user/profile/profile.tsx` 文件中遇到了 TypeScript 类型错误:
|
|
||||||
- `TS7031: Binding element 'detail' implicitly has an 'any' type.`
|
|
||||||
|
|
||||||
## 修复内容
|
|
||||||
|
|
||||||
### 1. 添加类型定义
|
|
||||||
为常用的事件类型创建了明确的接口定义:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 类型定义
|
|
||||||
interface ChooseAvatarEvent {
|
|
||||||
detail: {
|
|
||||||
avatarUrl: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
interface InputEvent {
|
|
||||||
detail: {
|
|
||||||
value: string;
|
|
||||||
};
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 修复函数参数类型
|
|
||||||
|
|
||||||
#### 修复前:
|
|
||||||
```typescript
|
|
||||||
const uploadAvatar = ({detail}) => {
|
|
||||||
// TS7031 错误:detail 隐式具有 any 类型
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitSucceed = (values: any) => {
|
|
||||||
// 使用 any 类型
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitFailed = (error: any) => {
|
|
||||||
// 使用 any 类型
|
|
||||||
}
|
|
||||||
|
|
||||||
onInput={(e) => getWxNickname(e.detail.value)}
|
|
||||||
// e 参数隐式具有 any 类型
|
|
||||||
```
|
|
||||||
|
|
||||||
#### 修复后:
|
|
||||||
```typescript
|
|
||||||
const uploadAvatar = ({detail}: ChooseAvatarEvent) => {
|
|
||||||
// 明确的类型定义
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitSucceed = (values: User) => {
|
|
||||||
// 使用具体的 User 类型
|
|
||||||
}
|
|
||||||
|
|
||||||
const submitFailed = (error: unknown) => {
|
|
||||||
// 使用 unknown 类型替代 any
|
|
||||||
}
|
|
||||||
|
|
||||||
onInput={(e: InputEvent) => getWxNickname(e.detail.value)}
|
|
||||||
// 明确的事件类型
|
|
||||||
```
|
|
||||||
|
|
||||||
## 修复的具体位置
|
|
||||||
|
|
||||||
1. **第86行** - `uploadAvatar` 函数参数类型
|
|
||||||
2. **第65行** - `submitSucceed` 函数参数类型
|
|
||||||
3. **第82行** - `submitFailed` 函数参数类型
|
|
||||||
4. **第176行** - `onInput` 事件处理器参数类型
|
|
||||||
|
|
||||||
## 类型安全改进
|
|
||||||
|
|
||||||
### 优势:
|
|
||||||
1. **编译时错误检查**:TypeScript 可以在编译时发现类型错误
|
|
||||||
2. **更好的 IDE 支持**:自动补全和类型提示
|
|
||||||
3. **代码可读性**:明确的类型定义让代码意图更清晰
|
|
||||||
4. **重构安全性**:类型检查帮助避免重构时的错误
|
|
||||||
|
|
||||||
### 最佳实践:
|
|
||||||
1. **避免使用 `any`**:尽量使用具体的类型定义
|
|
||||||
2. **使用 `unknown` 替代 `any`**:当类型不确定时,`unknown` 更安全
|
|
||||||
3. **创建接口定义**:为复杂的对象结构创建接口
|
|
||||||
4. **事件类型定义**:为事件处理器创建明确的类型定义
|
|
||||||
|
|
||||||
## 验证修复
|
|
||||||
修复后,TypeScript 编译器应该不再报告类型错误,并且:
|
|
||||||
- IDE 会提供更好的自动补全
|
|
||||||
- 类型检查会捕获潜在的运行时错误
|
|
||||||
- 代码更容易维护和理解
|
|
||||||
|
|
||||||
## 相关文件
|
|
||||||
- `src/user/profile/profile.tsx` - 主要修复文件
|
|
||||||
- `src/api/system/user/model.ts` - User 类型定义
|
|
||||||
- `src/api/system/dict-data/model.ts` - DictData 类型定义
|
|
||||||
@@ -1,132 +0,0 @@
|
|||||||
# 头像昵称同步问题修复说明
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
用户更新了头像和昵称后,首页仍然提示"您还没有上传头像和昵称",导致用户体验不佳。
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
|
|
||||||
### 根本原因
|
|
||||||
1. **检查逻辑错误**:`Header.tsx` 中检查的是 `Taro.getStorageSync('Avatar')`,但这个字段从未被设置过
|
|
||||||
2. **数据同步缺失**:`useUser` hook 中的 `saveUserToStorage` 函数没有保存头像和昵称到本地存储
|
|
||||||
3. **状态不一致**:用户信息更新后,本地存储和 `useUser` hook 状态不同步
|
|
||||||
|
|
||||||
### 具体问题
|
|
||||||
1. **Header.tsx 第42行**:
|
|
||||||
```typescript
|
|
||||||
if(!Taro.getStorageSync('Avatar')){
|
|
||||||
// 提示用户上传头像和昵称
|
|
||||||
}
|
|
||||||
```
|
|
||||||
这里检查的 `Avatar` 字段从未被保存到本地存储
|
|
||||||
|
|
||||||
2. **useUser.ts saveUserToStorage 函数**:
|
|
||||||
```typescript
|
|
||||||
const saveUserToStorage = (token: string, userInfo: User) => {
|
|
||||||
Taro.setStorageSync('access_token', token);
|
|
||||||
Taro.setStorageSync('User', userInfo);
|
|
||||||
Taro.setStorageSync('UserId', userInfo.userId);
|
|
||||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
|
||||||
Taro.setStorageSync('Phone', userInfo.phone);
|
|
||||||
// 缺少头像和昵称的保存
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
|
|
||||||
### 1. 修复 saveUserToStorage 函数
|
|
||||||
在 `src/hooks/useUser.ts` 中添加头像和昵称的保存:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const saveUserToStorage = (token: string, userInfo: User) => {
|
|
||||||
try {
|
|
||||||
Taro.setStorageSync('access_token', token);
|
|
||||||
Taro.setStorageSync('User', userInfo);
|
|
||||||
Taro.setStorageSync('UserId', userInfo.userId);
|
|
||||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
|
||||||
Taro.setStorageSync('Phone', userInfo.phone);
|
|
||||||
// 保存头像和昵称信息
|
|
||||||
if (userInfo.avatar) {
|
|
||||||
Taro.setStorageSync('Avatar', userInfo.avatar);
|
|
||||||
}
|
|
||||||
if (userInfo.nickname) {
|
|
||||||
Taro.setStorageSync('Nickname', userInfo.nickname);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存用户数据失败:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 优化 Header.tsx 检查逻辑
|
|
||||||
在 `src/pages/index/Header.tsx` 中改进检查逻辑:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 检查用户是否已登录并且有头像和昵称
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 修复效果
|
|
||||||
|
|
||||||
### ✅ 数据同步
|
|
||||||
- 用户更新头像/昵称后,信息会同时保存到 `useUser` hook 状态和本地存储
|
|
||||||
- 确保数据的一致性和持久化
|
|
||||||
|
|
||||||
### ✅ 检查逻辑优化
|
|
||||||
- 同时检查 `useUser` hook 中的用户信息和本地存储
|
|
||||||
- 提供双重保障,确保检查的准确性
|
|
||||||
|
|
||||||
### ✅ 用户体验改善
|
|
||||||
- 用户更新头像/昵称后不再收到错误提示
|
|
||||||
- 信息更新立即生效,无需重新登录
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### 数据流程
|
|
||||||
1. **用户更新信息** → `profile.tsx` 调用 `updateUser()`
|
|
||||||
2. **updateUser()** → 调用 API 更新服务器数据
|
|
||||||
3. **更新成功** → 调用 `saveUserToStorage()` 保存到本地
|
|
||||||
4. **本地存储** → 同时更新 `useUser` 状态和 Taro 存储
|
|
||||||
5. **Header 检查** → 检查两个数据源确保准确性
|
|
||||||
|
|
||||||
### 关键修改点
|
|
||||||
1. **useUser.ts 第99-106行**:添加头像和昵称的本地存储
|
|
||||||
2. **Header.tsx 第41-58行**:优化检查逻辑,支持双重验证
|
|
||||||
|
|
||||||
### 兼容性
|
|
||||||
- 向后兼容:旧的检查逻辑仍然有效
|
|
||||||
- 渐进增强:新的检查逻辑提供更好的准确性
|
|
||||||
- 数据迁移:现有用户的数据会在下次更新时自动同步
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
### 测试场景
|
|
||||||
1. **新用户注册**:验证首次上传头像/昵称后不再提示
|
|
||||||
2. **现有用户更新**:验证更新头像/昵称后立即生效
|
|
||||||
3. **应用重启**:验证重启应用后信息仍然正确
|
|
||||||
4. **网络异常**:验证网络异常时的降级处理
|
|
||||||
|
|
||||||
### 验证步骤
|
|
||||||
1. 清除应用数据
|
|
||||||
2. 登录并上传头像/昵称
|
|
||||||
3. 返回首页检查是否还有提示
|
|
||||||
4. 重启应用再次检查
|
|
||||||
5. 修改头像/昵称并验证同步
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
通过修复 `saveUserToStorage` 函数和优化 `Header.tsx` 的检查逻辑,我们解决了头像昵称同步问题,确保用户更新信息后能够立即生效,提升了用户体验。
|
|
||||||
@@ -1,165 +0,0 @@
|
|||||||
# 头像昵称显示问题修复说明
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
用户更新头像和昵称后,头部显示变成了"已登录"而不是显示用户的头像和昵称。
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
|
|
||||||
### 根本原因
|
|
||||||
在 Header 组件中,显示逻辑存在以下问题:
|
|
||||||
|
|
||||||
1. **显示逻辑不完整**:头像和昵称的显示只依赖 `user` 状态,没有考虑本地存储的备用数据
|
|
||||||
2. **状态同步延迟**:`useUser` hook 中的状态更新可能存在延迟,导致界面显示不及时
|
|
||||||
3. **缺少状态监听**:Header 组件没有充分监听用户信息的变化
|
|
||||||
|
|
||||||
### 具体问题代码
|
|
||||||
```typescript
|
|
||||||
// 原始代码 - 只依赖 user 状态
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={user?.avatar} // 如果 user.avatar 为空,头像不显示
|
|
||||||
/>
|
|
||||||
<Text className={'text-white'}>{user?.nickname || '已登录'}</Text> // 如果 user.nickname 为空,显示"已登录"
|
|
||||||
```
|
|
||||||
|
|
||||||
### 问题场景
|
|
||||||
1. 用户更新头像/昵称后,`useUser` hook 的状态可能还没有及时更新
|
|
||||||
2. 即使本地存储中已经保存了最新的头像和昵称,界面仍然显示旧的状态
|
|
||||||
3. 导致用户看到"已登录"而不是自己的头像和昵称
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
|
|
||||||
### 1. 优化显示逻辑
|
|
||||||
修改头像和昵称的显示逻辑,增加本地存储作为备用数据源:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 修复后的代码 - 同时检查状态和本地存储
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={user?.avatar || Taro.getStorageSync('Avatar')} // 优先使用状态,备用本地存储
|
|
||||||
/>
|
|
||||||
<Text className={'text-white'}>
|
|
||||||
{user?.nickname || Taro.getStorageSync('Nickname') || '已登录'} // 多层备用逻辑
|
|
||||||
</Text>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 添加状态监听
|
|
||||||
在 Header 组件中添加 `useEffect` 来监听用户信息变化:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// 监听用户信息变化,当用户信息更新后重新检查
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoggedIn && user) {
|
|
||||||
console.log('用户信息已更新:', user);
|
|
||||||
}
|
|
||||||
}, [user, isLoggedIn])
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 数据获取优先级
|
|
||||||
建立清晰的数据获取优先级:
|
|
||||||
1. **第一优先级**:`user` 状态中的数据(最新的内存状态)
|
|
||||||
2. **第二优先级**:本地存储中的数据(持久化备用数据)
|
|
||||||
3. **第三优先级**:默认显示文本("已登录")
|
|
||||||
|
|
||||||
## 修复效果
|
|
||||||
|
|
||||||
### ✅ 即时显示
|
|
||||||
- 用户更新头像/昵称后,界面立即显示最新信息
|
|
||||||
- 即使状态更新有延迟,也能从本地存储获取最新数据
|
|
||||||
|
|
||||||
### ✅ 数据一致性
|
|
||||||
- 确保界面显示与实际数据保持一致
|
|
||||||
- 避免出现"已登录"的错误显示
|
|
||||||
|
|
||||||
### ✅ 用户体验优化
|
|
||||||
- 用户操作后立即看到结果
|
|
||||||
- 减少界面闪烁和不一致的显示
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### 数据流向
|
|
||||||
```
|
|
||||||
用户更新 → updateUser() → 更新状态 + 本地存储 → 界面显示
|
|
||||||
↓
|
|
||||||
状态可能延迟 → 本地存储作为备用 → 确保界面正确显示
|
|
||||||
```
|
|
||||||
|
|
||||||
### 显示逻辑优化
|
|
||||||
```typescript
|
|
||||||
// 头像显示逻辑
|
|
||||||
const avatarSrc = user?.avatar || Taro.getStorageSync('Avatar');
|
|
||||||
|
|
||||||
// 昵称显示逻辑
|
|
||||||
const displayName = user?.nickname || Taro.getStorageSync('Nickname') || '已登录';
|
|
||||||
```
|
|
||||||
|
|
||||||
### 状态监听机制
|
|
||||||
```typescript
|
|
||||||
useEffect(() => {
|
|
||||||
if (isLoggedIn && user) {
|
|
||||||
// 用户信息更新时的处理逻辑
|
|
||||||
console.log('用户信息已更新:', user);
|
|
||||||
}
|
|
||||||
}, [user, isLoggedIn]);
|
|
||||||
```
|
|
||||||
|
|
||||||
## 相关文件修改
|
|
||||||
|
|
||||||
### src/pages/index/Header.tsx
|
|
||||||
1. **第216行**:修改头像显示逻辑,添加本地存储备用
|
|
||||||
2. **第217行**:修改昵称显示逻辑,添加多层备用机制
|
|
||||||
3. **第194-197行**:添加用户信息变化监听
|
|
||||||
|
|
||||||
### 修改前后对比
|
|
||||||
```typescript
|
|
||||||
// 修改前
|
|
||||||
<Avatar size="22" src={user?.avatar} />
|
|
||||||
<Text className={'text-white'}>{user?.nickname || '已登录'}</Text>
|
|
||||||
|
|
||||||
// 修改后
|
|
||||||
<Avatar size="22" src={user?.avatar || Taro.getStorageSync('Avatar')} />
|
|
||||||
<Text className={'text-white'}>{user?.nickname || Taro.getStorageSync('Nickname') || '已登录'}</Text>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 防护机制
|
|
||||||
|
|
||||||
### 多层备用策略
|
|
||||||
1. **状态优先**:优先使用内存中的最新状态
|
|
||||||
2. **存储备用**:状态为空时使用本地存储数据
|
|
||||||
3. **默认显示**:都为空时显示默认文本
|
|
||||||
|
|
||||||
### 数据同步保障
|
|
||||||
- `useUser` hook 确保状态和存储的同步
|
|
||||||
- Header 组件监听状态变化,及时响应更新
|
|
||||||
- 本地存储作为可靠的备用数据源
|
|
||||||
|
|
||||||
### 错误处理
|
|
||||||
- 避免因状态延迟导致的显示错误
|
|
||||||
- 确保在各种情况下都有合适的显示内容
|
|
||||||
- 提供友好的用户体验
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
### 测试场景
|
|
||||||
1. **正常更新**:更新头像/昵称后检查显示是否正确
|
|
||||||
2. **状态延迟**:模拟状态更新延迟,检查备用机制是否生效
|
|
||||||
3. **数据清空**:清空状态数据,检查是否能从存储获取
|
|
||||||
4. **完全清空**:清空所有数据,检查是否显示默认文本
|
|
||||||
|
|
||||||
### 验证方法
|
|
||||||
```javascript
|
|
||||||
// 检查显示数据来源
|
|
||||||
console.log('用户状态:', user?.nickname);
|
|
||||||
console.log('本地存储:', Taro.getStorageSync('Nickname'));
|
|
||||||
console.log('最终显示:', user?.nickname || Taro.getStorageSync('Nickname') || '已登录');
|
|
||||||
```
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
通过优化显示逻辑和添加本地存储备用机制,我们成功解决了头像昵称显示问题。这个解决方案:
|
|
||||||
|
|
||||||
1. **可靠性高**:多层备用机制确保数据显示的可靠性
|
|
||||||
2. **响应及时**:即使状态更新有延迟也能正确显示
|
|
||||||
3. **用户友好**:避免了"已登录"的错误显示
|
|
||||||
4. **易于维护**:逻辑清晰,便于后续维护和扩展
|
|
||||||
|
|
||||||
现在用户更新头像和昵称后,界面会立即显示正确的信息,不再出现"已登录"的问题。
|
|
||||||
@@ -1,140 +0,0 @@
|
|||||||
# 客户交易页面上拉加载更多功能实现说明
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
为 `src/dealer/customer/trading.tsx` 页面添加了完善的上拉加载更多功能,参考了客户管理模块和订单管理模块的成熟实现。
|
|
||||||
|
|
||||||
## 主要修改内容
|
|
||||||
|
|
||||||
### 1. 导入必要的组件
|
|
||||||
```typescript
|
|
||||||
import Taro from '@tarojs/taro'
|
|
||||||
import {Loading, InfiniteLoading, Empty, Space, SearchBar} from '@nutui/nutui-react-taro'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 添加状态管理
|
|
||||||
```typescript
|
|
||||||
const [page, setPage] = useState(1)
|
|
||||||
const [hasMore, setHasMore] = useState(true)
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 优化数据获取函数 `fetchCustomerData`
|
|
||||||
- **新增参数**:
|
|
||||||
- `resetPage`: 是否重置页码(用于初始化或搜索)
|
|
||||||
- `targetPage`: 目标页码(用于加载更多)
|
|
||||||
- `searchKeyword`: 搜索关键词(支持服务端搜索)
|
|
||||||
- **改进逻辑**:
|
|
||||||
- 支持重置页面和追加数据两种模式
|
|
||||||
- 正确处理页码状态
|
|
||||||
- 添加错误处理和用户提示
|
|
||||||
- 支持服务端搜索功能
|
|
||||||
|
|
||||||
### 4. 实现 `reloadMore` 函数
|
|
||||||
```typescript
|
|
||||||
const reloadMore = async () => {
|
|
||||||
if (loading || !hasMore) return; // 防止重复加载
|
|
||||||
const nextPage = page + 1;
|
|
||||||
await fetchCustomerData(false, nextPage, searchValue);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 优化搜索功能
|
|
||||||
- **服务端搜索**:将搜索逻辑从客户端过滤改为服务端搜索
|
|
||||||
- **搜索处理函数**:
|
|
||||||
```typescript
|
|
||||||
const handleSearch = (keyword: string) => {
|
|
||||||
setSearchValue(keyword);
|
|
||||||
setList([]);
|
|
||||||
setPage(1);
|
|
||||||
setHasMore(true);
|
|
||||||
fetchCustomerData(true, 1, keyword);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
- **清空搜索**:
|
|
||||||
```typescript
|
|
||||||
const handleClearSearch = () => {
|
|
||||||
setSearchValue('');
|
|
||||||
setList([]);
|
|
||||||
setPage(1);
|
|
||||||
setHasMore(true);
|
|
||||||
fetchCustomerData(true, 1, '');
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 改进UI渲染
|
|
||||||
- **InfiniteLoading组件**:
|
|
||||||
- 设置合适的容器高度(75vh)
|
|
||||||
- 优化加载状态显示
|
|
||||||
- 改进空数据状态展示
|
|
||||||
- 添加滚动容器样式
|
|
||||||
|
|
||||||
### 7. 简化数据过滤
|
|
||||||
由于现在使用服务端搜索,简化了 `getFilteredList` 函数:
|
|
||||||
```typescript
|
|
||||||
const getFilteredList = () => {
|
|
||||||
return list; // 直接返回列表,不需要客户端过滤
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心功能特性
|
|
||||||
|
|
||||||
### ✅ 上拉加载更多
|
|
||||||
- 滚动到底部自动触发加载下一页数据
|
|
||||||
- 防止重复加载机制
|
|
||||||
- 正确的页码管理
|
|
||||||
|
|
||||||
### ✅ 服务端搜索
|
|
||||||
- 搜索关键词通过API参数传递给后端
|
|
||||||
- 搜索时重置列表和页码
|
|
||||||
- 支持清空搜索功能
|
|
||||||
|
|
||||||
### ✅ 错误处理
|
|
||||||
- 网络请求失败时显示错误提示
|
|
||||||
- 加载失败时不影响现有数据
|
|
||||||
|
|
||||||
### ✅ 加载状态
|
|
||||||
- 首次加载显示loading状态
|
|
||||||
- 加载更多时显示"加载中..."
|
|
||||||
- 无更多数据时显示"没有更多了"
|
|
||||||
|
|
||||||
### ✅ 空数据处理
|
|
||||||
- 无数据时显示友好的空状态提示
|
|
||||||
- 区分加载中和真正的无数据状态
|
|
||||||
|
|
||||||
## 与其他页面的区别
|
|
||||||
|
|
||||||
### 相比客户管理模块
|
|
||||||
- **搜索方式**:使用服务端搜索而非客户端过滤
|
|
||||||
- **数据类型**:专门处理交易客户数据(type: 3)
|
|
||||||
- **无Tab切换**:简化了状态管理逻辑
|
|
||||||
|
|
||||||
### 相比订单管理模块
|
|
||||||
- **更简洁的实现**:没有复杂的状态筛选
|
|
||||||
- **专注搜索**:主要功能是搜索和分页
|
|
||||||
|
|
||||||
## API参数说明
|
|
||||||
```typescript
|
|
||||||
const params: any = {
|
|
||||||
type: 3, // 交易客户类型
|
|
||||||
page: currentPage, // 当前页码
|
|
||||||
keywords: searchKeyword // 搜索关键词(可选)
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
1. 进入客户交易页面
|
|
||||||
2. 滚动到列表底部自动加载更多数据
|
|
||||||
3. 使用搜索框搜索特定客户
|
|
||||||
4. 点击清空按钮清除搜索条件
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
- React + TypeScript
|
|
||||||
- Taro框架
|
|
||||||
- NutUI组件库
|
|
||||||
- InfiniteLoading组件实现上拉加载
|
|
||||||
- SearchBar组件实现搜索功能
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
- 每页默认加载10条数据
|
|
||||||
- 当返回数据少于10条时认为没有更多数据
|
|
||||||
- 搜索功能使用服务端搜索,提高性能
|
|
||||||
- 所有状态变化都会正确重置页码和列表数据
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
# 客户管理模块上拉加载更多功能实现说明
|
|
||||||
|
|
||||||
## 概述
|
|
||||||
参考订单管理模块 `src/user/order/order.tsx` 的实现,为客户管理模块 `src/dealer/customer/index.tsx` 添加了完善的上拉加载更多功能。
|
|
||||||
|
|
||||||
## 主要修改内容
|
|
||||||
|
|
||||||
### 1. 优化数据获取函数 `fetchCustomerData`
|
|
||||||
- **新增参数**:
|
|
||||||
- `resetPage`: 是否重置页码(用于切换tab或刷新)
|
|
||||||
- `targetPage`: 目标页码(用于加载更多)
|
|
||||||
- **改进逻辑**:
|
|
||||||
- 支持重置页面和追加数据两种模式
|
|
||||||
- 正确处理页码状态
|
|
||||||
- 添加错误处理和用户提示
|
|
||||||
|
|
||||||
### 2. 改进 `reloadMore` 函数
|
|
||||||
- 防止重复加载(检查 `loading` 和 `hasMore` 状态)
|
|
||||||
- 正确计算下一页页码
|
|
||||||
- 调用优化后的 `fetchCustomerData` 函数
|
|
||||||
|
|
||||||
### 3. 优化状态管理
|
|
||||||
- **初始化数据**:使用 `resetPage=true` 确保从第一页开始
|
|
||||||
- **Tab切换**:清空列表、重置页码和加载状态,然后重新获取数据
|
|
||||||
- **取消操作**:操作成功后重新加载当前tab数据
|
|
||||||
|
|
||||||
### 4. 改进UI渲染
|
|
||||||
- **InfiniteLoading组件**:
|
|
||||||
- 设置合适的容器高度(75vh)
|
|
||||||
- 优化加载状态显示
|
|
||||||
- 改进空数据状态展示
|
|
||||||
- 添加滚动容器样式
|
|
||||||
|
|
||||||
### 5. 修复依赖问题
|
|
||||||
- 移除 `fetchCustomerData` 函数中的 `page` 和 `list` 依赖,避免无限循环
|
|
||||||
|
|
||||||
## 核心功能特性
|
|
||||||
|
|
||||||
### ✅ 上拉加载更多
|
|
||||||
- 滚动到底部自动触发加载下一页数据
|
|
||||||
- 防止重复加载机制
|
|
||||||
- 正确的页码管理
|
|
||||||
|
|
||||||
### ✅ Tab切换支持
|
|
||||||
- 切换tab时重置列表和页码
|
|
||||||
- 每个tab独立的数据加载
|
|
||||||
|
|
||||||
### ✅ 错误处理
|
|
||||||
- 网络请求失败时显示错误提示
|
|
||||||
- 加载失败时不影响现有数据
|
|
||||||
|
|
||||||
### ✅ 加载状态
|
|
||||||
- 首次加载显示loading状态
|
|
||||||
- 加载更多时显示"加载中..."
|
|
||||||
- 无更多数据时显示"没有更多了"
|
|
||||||
|
|
||||||
### ✅ 空数据处理
|
|
||||||
- 无数据时显示友好的空状态提示
|
|
||||||
- 区分加载中和真正的无数据状态
|
|
||||||
|
|
||||||
## 参考实现
|
|
||||||
本实现参考了订单管理模块 `src/user/order/components/OrderList.tsx` 的以下核心特性:
|
|
||||||
- `reload` 函数的 `resetPage` 和 `targetPage` 参数设计
|
|
||||||
- `reloadMore` 函数的防重复加载逻辑
|
|
||||||
- `InfiniteLoading` 组件的配置和样式
|
|
||||||
- 状态管理和错误处理机制
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
1. 进入客户管理页面
|
|
||||||
2. 滚动到列表底部自动加载更多数据
|
|
||||||
3. 切换不同状态tab查看对应客户
|
|
||||||
4. 支持搜索功能(原有功能保持不变)
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
- React + TypeScript
|
|
||||||
- Taro框架
|
|
||||||
- NutUI组件库
|
|
||||||
- InfiniteLoading组件实现上拉加载
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
- 每页默认加载10条数据
|
|
||||||
- 当返回数据少于10条时认为没有更多数据
|
|
||||||
- 所有状态切换都会重置页码和列表数据
|
|
||||||
- 保持了原有的搜索和筛选功能
|
|
||||||
@@ -1,161 +0,0 @@
|
|||||||
# 报备成功后列表刷新问题修复说明
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
用户在客户报备页面成功提交报备信息后,返回到客户列表页面时,列表没有自动更新显示最新的数据,需要手动刷新才能看到新增的客户信息。
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
|
|
||||||
### 根本原因
|
|
||||||
在 Taro 框架中,当从一个页面返回到上一个页面时,上一个页面不会自动重新加载数据。客户列表页面缺少监听页面显示的逻辑,导致从报备页面返回时无法自动刷新数据。
|
|
||||||
|
|
||||||
### 具体问题
|
|
||||||
1. **缺少页面显示监听**:客户列表页面没有使用 `useDidShow` 钩子监听页面显示事件
|
|
||||||
2. **数据不同步**:报备成功后只是简单地调用 `Taro.navigateBack()`,没有通知列表页面刷新
|
|
||||||
3. **用户体验差**:用户需要手动下拉刷新或重新进入页面才能看到最新数据
|
|
||||||
|
|
||||||
### 页面生命周期问题
|
|
||||||
```typescript
|
|
||||||
// 报备页面 (add.tsx)
|
|
||||||
const submitSucceed = async (values: any) => {
|
|
||||||
// ... 提交逻辑
|
|
||||||
await addShopDealerApply(submitData);
|
|
||||||
|
|
||||||
Taro.showToast({
|
|
||||||
title: '提交成功',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
|
|
||||||
setTimeout(() => {
|
|
||||||
Taro.navigateBack(); // 只是返回,没有通知列表页面刷新
|
|
||||||
}, 1000);
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
|
|
||||||
### 1. 添加页面显示监听
|
|
||||||
在客户列表页面 (`src/dealer/customer/index.tsx`) 中添加 `useDidShow` 钩子:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import {useState, useEffect, useCallback} from 'react'
|
|
||||||
import {View, Text} from '@tarojs/components'
|
|
||||||
import Taro, {useDidShow} from '@tarojs/taro' // 导入 useDidShow
|
|
||||||
|
|
||||||
// ... 其他代码
|
|
||||||
|
|
||||||
// 监听页面显示,当从其他页面返回时刷新数据
|
|
||||||
useDidShow(() => {
|
|
||||||
// 刷新当前tab的数据和统计信息
|
|
||||||
setList([]);
|
|
||||||
setPage(1);
|
|
||||||
setHasMore(true);
|
|
||||||
fetchCustomerData(activeTab, true);
|
|
||||||
fetchStatusCounts();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 完整的数据刷新逻辑
|
|
||||||
当页面显示时,执行以下操作:
|
|
||||||
- 清空当前列表数据
|
|
||||||
- 重置页码为第一页
|
|
||||||
- 重置加载状态
|
|
||||||
- 重新获取当前tab的数据
|
|
||||||
- 刷新状态统计信息
|
|
||||||
|
|
||||||
## 修复效果
|
|
||||||
|
|
||||||
### ✅ 自动刷新
|
|
||||||
- 从报备页面返回时,列表自动刷新显示最新数据
|
|
||||||
- 无需用户手动操作,提升用户体验
|
|
||||||
|
|
||||||
### ✅ 数据同步
|
|
||||||
- 新增的客户信息立即显示在列表中
|
|
||||||
- 状态统计数量实时更新
|
|
||||||
- 保持数据的一致性
|
|
||||||
|
|
||||||
### ✅ 用户体验优化
|
|
||||||
- 报备成功后立即看到结果
|
|
||||||
- 减少用户困惑和重复操作
|
|
||||||
- 提供流畅的操作体验
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### useDidShow 钩子
|
|
||||||
`useDidShow` 是 Taro 提供的页面生命周期钩子,会在以下情况触发:
|
|
||||||
- 页面首次加载完成
|
|
||||||
- 从其他页面返回到当前页面
|
|
||||||
- 从后台切换到前台
|
|
||||||
|
|
||||||
### 数据刷新策略
|
|
||||||
```typescript
|
|
||||||
useDidShow(() => {
|
|
||||||
// 1. 清空现有数据,避免闪烁
|
|
||||||
setList([]);
|
|
||||||
|
|
||||||
// 2. 重置分页状态
|
|
||||||
setPage(1);
|
|
||||||
setHasMore(true);
|
|
||||||
|
|
||||||
// 3. 重新获取数据
|
|
||||||
fetchCustomerData(activeTab, true);
|
|
||||||
|
|
||||||
// 4. 刷新统计信息
|
|
||||||
fetchStatusCounts();
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 与现有逻辑的协调
|
|
||||||
- 保持与 `useEffect` 的初始化逻辑一致
|
|
||||||
- 不影响 Tab 切换时的数据加载
|
|
||||||
- 确保上拉加载更多功能正常工作
|
|
||||||
|
|
||||||
## 其他相关页面
|
|
||||||
|
|
||||||
### 交易页面
|
|
||||||
如果交易页面也有类似的问题,可以采用相同的解决方案:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
// src/dealer/customer/trading.tsx
|
|
||||||
import Taro, {useDidShow} from '@tarojs/taro'
|
|
||||||
|
|
||||||
useDidShow(() => {
|
|
||||||
setList([]);
|
|
||||||
setPage(1);
|
|
||||||
setHasMore(true);
|
|
||||||
fetchCustomerData(true);
|
|
||||||
});
|
|
||||||
```
|
|
||||||
|
|
||||||
### 通用解决方案
|
|
||||||
对于所有需要在返回时刷新数据的列表页面,都可以使用这种模式:
|
|
||||||
|
|
||||||
1. 导入 `useDidShow` 钩子
|
|
||||||
2. 在钩子中重置状态
|
|
||||||
3. 重新获取数据
|
|
||||||
4. 刷新相关统计信息
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
### 性能考虑
|
|
||||||
- `useDidShow` 会在每次页面显示时触发,包括首次加载
|
|
||||||
- 避免在钩子中执行过于复杂的操作
|
|
||||||
- 考虑添加防抖或节流机制(如果需要)
|
|
||||||
|
|
||||||
### 用户体验
|
|
||||||
- 数据刷新时显示适当的加载状态
|
|
||||||
- 避免频繁的网络请求
|
|
||||||
- 保持界面的响应性
|
|
||||||
|
|
||||||
### 错误处理
|
|
||||||
- 确保网络请求失败时的友好提示
|
|
||||||
- 避免因刷新失败导致页面异常
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
通过添加 `useDidShow` 钩子监听页面显示事件,我们成功解决了报备成功后列表不自动刷新的问题。这个解决方案:
|
|
||||||
|
|
||||||
1. **简单有效**:只需添加几行代码即可解决问题
|
|
||||||
2. **用户友好**:提供了流畅的操作体验
|
|
||||||
3. **技术合理**:符合 Taro 框架的最佳实践
|
|
||||||
4. **易于维护**:代码清晰,逻辑简单
|
|
||||||
|
|
||||||
现在用户在完成客户报备后,返回列表页面时会自动看到最新的数据,大大提升了应用的用户体验。
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
# 用户信息缓存丢失问题修复说明
|
|
||||||
|
|
||||||
## 问题描述
|
|
||||||
用户更新头像和昵称后,缓存中的 `UserId` 和 `Phone` 等关键字段丢失,导致应用功能异常。
|
|
||||||
|
|
||||||
## 问题分析
|
|
||||||
|
|
||||||
### 根本原因
|
|
||||||
1. **API数据不完整**:`updateUserInfo` API 可能返回不完整的用户数据
|
|
||||||
2. **本地状态覆盖**:`updateUser` 方法使用本地状态合并数据,但本地状态可能已经过时
|
|
||||||
3. **保存逻辑缺陷**:`saveUserToStorage` 函数会无条件覆盖所有字段,包括空值
|
|
||||||
|
|
||||||
### 具体问题
|
|
||||||
1. **数据流程问题**:
|
|
||||||
```typescript
|
|
||||||
// 原来的流程
|
|
||||||
const updatedUser = { ...user, ...userData }; // 使用可能过时的本地状态
|
|
||||||
await updateUserInfo(updatedUser);
|
|
||||||
```
|
|
||||||
|
|
||||||
2. **保存逻辑问题**:
|
|
||||||
```typescript
|
|
||||||
// 原来的保存逻辑
|
|
||||||
Taro.setStorageSync('UserId', userInfo.userId); // 可能为空,覆盖现有值
|
|
||||||
Taro.setStorageSync('Phone', userInfo.phone); // 可能为空,覆盖现有值
|
|
||||||
```
|
|
||||||
|
|
||||||
## 修复方案
|
|
||||||
|
|
||||||
### 1. 优化 updateUser 方法
|
|
||||||
确保在更新前获取最新的完整用户信息:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const updateUser = async (userData: Partial<User>) => {
|
|
||||||
if (!user) {
|
|
||||||
throw new Error('用户未登录');
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 先获取最新的用户信息,确保我们有完整的数据
|
|
||||||
const latestUserInfo = await getUserInfo();
|
|
||||||
|
|
||||||
// 合并最新的用户信息和要更新的数据
|
|
||||||
const updatedUser = { ...latestUserInfo, ...userData };
|
|
||||||
|
|
||||||
// 调用API更新用户信息
|
|
||||||
await updateUserInfo(updatedUser);
|
|
||||||
|
|
||||||
// 更新本地状态和存储
|
|
||||||
setUser(updatedUser);
|
|
||||||
const token = Taro.getStorageSync('access_token');
|
|
||||||
if (token) {
|
|
||||||
saveUserToStorage(token, updatedUser);
|
|
||||||
}
|
|
||||||
|
|
||||||
return updatedUser;
|
|
||||||
} catch (error) {
|
|
||||||
// 错误处理
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 改进 saveUserToStorage 函数
|
|
||||||
只在字段有值时才保存,避免覆盖现有数据:
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
const saveUserToStorage = (token: string, userInfo: User) => {
|
|
||||||
try {
|
|
||||||
Taro.setStorageSync('access_token', token);
|
|
||||||
Taro.setStorageSync('User', userInfo);
|
|
||||||
|
|
||||||
// 确保关键字段不为空时才保存,避免覆盖现有数据
|
|
||||||
if (userInfo.userId) {
|
|
||||||
Taro.setStorageSync('UserId', userInfo.userId);
|
|
||||||
}
|
|
||||||
if (userInfo.tenantId) {
|
|
||||||
Taro.setStorageSync('TenantId', userInfo.tenantId);
|
|
||||||
}
|
|
||||||
if (userInfo.phone) {
|
|
||||||
Taro.setStorageSync('Phone', userInfo.phone);
|
|
||||||
}
|
|
||||||
// 保存头像和昵称信息
|
|
||||||
if (userInfo.avatar) {
|
|
||||||
Taro.setStorageSync('Avatar', userInfo.avatar);
|
|
||||||
}
|
|
||||||
if (userInfo.nickname) {
|
|
||||||
Taro.setStorageSync('Nickname', userInfo.nickname);
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('保存用户数据失败:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
## 修复效果
|
|
||||||
|
|
||||||
### ✅ 数据完整性保障
|
|
||||||
- 更新前先获取最新的完整用户信息
|
|
||||||
- 确保合并的数据包含所有必要字段
|
|
||||||
- 避免使用过时的本地状态
|
|
||||||
|
|
||||||
### ✅ 安全的存储策略
|
|
||||||
- 只在字段有值时才保存到本地存储
|
|
||||||
- 避免空值覆盖现有的有效数据
|
|
||||||
- 保护关键字段如 `UserId`、`Phone` 等
|
|
||||||
|
|
||||||
### ✅ 用户体验改善
|
|
||||||
- 用户更新头像/昵称后,所有信息保持完整
|
|
||||||
- 避免因数据丢失导致的功能异常
|
|
||||||
- 确保应用状态的一致性
|
|
||||||
|
|
||||||
## 技术细节
|
|
||||||
|
|
||||||
### 数据流程优化
|
|
||||||
1. **获取最新数据** → `getUserInfo()` 获取服务器最新信息
|
|
||||||
2. **安全合并** → `{ ...latestUserInfo, ...userData }` 确保数据完整
|
|
||||||
3. **API更新** → `updateUserInfo(updatedUser)` 提交完整数据
|
|
||||||
4. **本地同步** → `saveUserToStorage()` 安全保存到本地
|
|
||||||
|
|
||||||
### 关键修改点
|
|
||||||
1. **useUser.ts 第178-185行**:在更新前获取最新用户信息
|
|
||||||
2. **useUser.ts 第95-118行**:改进保存逻辑,避免空值覆盖
|
|
||||||
|
|
||||||
### 防护机制
|
|
||||||
- **数据验证**:保存前检查字段是否有值
|
|
||||||
- **错误处理**:完善的异常捕获和处理
|
|
||||||
- **状态同步**:确保内存状态和本地存储一致
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
### 测试场景
|
|
||||||
1. **头像更新**:验证更新头像后 `UserId`、`Phone` 等字段保持不变
|
|
||||||
2. **昵称更新**:验证更新昵称后其他字段完整性
|
|
||||||
3. **网络异常**:验证网络异常时的数据保护
|
|
||||||
4. **并发更新**:验证多次快速更新的数据一致性
|
|
||||||
|
|
||||||
### 验证步骤
|
|
||||||
1. 登录并检查初始缓存数据
|
|
||||||
2. 更新头像,检查缓存中的 `UserId`、`Phone` 是否保持
|
|
||||||
3. 更新昵称,再次检查数据完整性
|
|
||||||
4. 重启应用,验证数据持久化
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
通过优化数据获取流程和改进存储策略,我们解决了用户信息缓存丢失问题,确保了数据的完整性和一致性,提升了应用的稳定性和用户体验。
|
|
||||||
@@ -1,209 +0,0 @@
|
|||||||
# 签约时间和合同日期选择功能实现说明
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
为客户签约页面实现了签约时间和合同日期的选择功能,使用 NutUI 的 DatePicker 组件提供友好的日期选择体验。
|
|
||||||
|
|
||||||
## 实现特性
|
|
||||||
|
|
||||||
### ✅ 日期选择器
|
|
||||||
- **签约时间选择**:支持选择具体的签约日期
|
|
||||||
- **合同日期选择**:支持选择合同生效日期
|
|
||||||
- **弹窗式选择**:使用 Popup + DatePicker 组合,提供良好的用户体验
|
|
||||||
|
|
||||||
### ✅ 界面优化
|
|
||||||
- **图标提示**:使用日历图标提示用户可以选择日期
|
|
||||||
- **状态显示**:清晰显示已选择的日期或提示文本
|
|
||||||
- **禁用状态**:编辑模式下禁用日期选择,防止误操作
|
|
||||||
|
|
||||||
### ✅ 数据处理
|
|
||||||
- **格式化**:自动格式化日期为 YYYY-MM-DD 格式
|
|
||||||
- **表单同步**:选择日期后自动更新表单数据
|
|
||||||
- **数据持久化**:正确保存和加载日期数据
|
|
||||||
|
|
||||||
## 技术实现
|
|
||||||
|
|
||||||
### 1. 组件导入
|
|
||||||
```typescript
|
|
||||||
import {Loading, CellGroup, Cell, Input, Form, DatePicker, Popup} from '@nutui/nutui-react-taro'
|
|
||||||
import {Edit, Calendar} from '@nutui/icons-react-taro'
|
|
||||||
```
|
|
||||||
|
|
||||||
### 2. 状态管理
|
|
||||||
```typescript
|
|
||||||
// 日期选择器状态
|
|
||||||
const [showApplyTimePicker, setShowApplyTimePicker] = useState<boolean>(false)
|
|
||||||
const [showContractDatePicker, setShowContractDatePicker] = useState<boolean>(false)
|
|
||||||
const [applyTime, setApplyTime] = useState<string>('')
|
|
||||||
const [contractDate, setContractDate] = useState<string>('')
|
|
||||||
```
|
|
||||||
|
|
||||||
### 3. 日期格式化函数
|
|
||||||
```typescript
|
|
||||||
// 格式化日期为 YYYY-MM-DD
|
|
||||||
const formatDate = (date: Date): string => {
|
|
||||||
const year = date.getFullYear()
|
|
||||||
const month = String(date.getMonth() + 1).padStart(2, '0')
|
|
||||||
const day = String(date.getDate()).padStart(2, '0')
|
|
||||||
return `${year}-${month}-${day}`
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 4. 日期选择处理
|
|
||||||
```typescript
|
|
||||||
// 处理签约时间选择
|
|
||||||
const handleApplyTimeConfirm = (options: any, values: Date[]) => {
|
|
||||||
const selectedDate = values[0]
|
|
||||||
const formattedDate = formatDate(selectedDate)
|
|
||||||
setApplyTime(formattedDate)
|
|
||||||
setShowApplyTimePicker(false)
|
|
||||||
|
|
||||||
// 更新表单数据
|
|
||||||
if (formRef.current) {
|
|
||||||
formRef.current.setFieldsValue({
|
|
||||||
applyTime: formattedDate
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 5. 界面组件
|
|
||||||
```typescript
|
|
||||||
<Form.Item name="applyTime" label="签约时间" initialValue={FormData?.applyTime} required>
|
|
||||||
<View
|
|
||||||
className="flex items-center justify-between py-2"
|
|
||||||
onClick={() => !isEditMode && setShowApplyTimePicker(true)}
|
|
||||||
style={{
|
|
||||||
cursor: isEditMode ? 'not-allowed' : 'pointer',
|
|
||||||
opacity: isEditMode ? 0.6 : 1
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<View className="flex items-center">
|
|
||||||
<Calendar size={16} color="#999" className="mr-2" />
|
|
||||||
<Text style={{ color: applyTime ? '#333' : '#999' }}>
|
|
||||||
{applyTime || '请选择签约时间'}
|
|
||||||
</Text>
|
|
||||||
</View>
|
|
||||||
</View>
|
|
||||||
</Form.Item>
|
|
||||||
```
|
|
||||||
|
|
||||||
### 6. 弹窗选择器
|
|
||||||
```typescript
|
|
||||||
{/* 签约时间选择器 */}
|
|
||||||
<Popup
|
|
||||||
visible={showApplyTimePicker}
|
|
||||||
position="bottom"
|
|
||||||
onClose={() => setShowApplyTimePicker(false)}
|
|
||||||
>
|
|
||||||
<DatePicker
|
|
||||||
title="选择签约时间"
|
|
||||||
type="date"
|
|
||||||
onConfirm={handleApplyTimeConfirm}
|
|
||||||
onClose={() => setShowApplyTimePicker(false)}
|
|
||||||
/>
|
|
||||||
</Popup>
|
|
||||||
```
|
|
||||||
|
|
||||||
## 核心功能
|
|
||||||
|
|
||||||
### 📅 日期选择流程
|
|
||||||
1. **点击触发**:用户点击日期字段
|
|
||||||
2. **弹窗显示**:底部弹出日期选择器
|
|
||||||
3. **日期选择**:用户滚动选择年月日
|
|
||||||
4. **确认选择**:点击确认按钮
|
|
||||||
5. **数据更新**:自动更新界面显示和表单数据
|
|
||||||
6. **弹窗关闭**:选择完成后自动关闭
|
|
||||||
|
|
||||||
### 🔒 编辑模式控制
|
|
||||||
- **新增模式**:可以自由选择日期
|
|
||||||
- **编辑模式**:禁用日期选择,防止修改已确定的日期
|
|
||||||
- **视觉反馈**:通过透明度和鼠标样式提示状态
|
|
||||||
|
|
||||||
### 💾 数据同步
|
|
||||||
- **表单同步**:选择日期后立即更新表单字段值
|
|
||||||
- **状态同步**:本地状态与表单数据保持一致
|
|
||||||
- **提交处理**:确保日期数据正确提交到服务器
|
|
||||||
|
|
||||||
## 用户体验优化
|
|
||||||
|
|
||||||
### 🎨 界面设计
|
|
||||||
- **图标指示**:日历图标清晰表明可以选择日期
|
|
||||||
- **状态区分**:已选择日期显示为深色,提示文本显示为灰色
|
|
||||||
- **点击区域**:整个字段区域都可点击,提高操作便利性
|
|
||||||
|
|
||||||
### 📱 交互体验
|
|
||||||
- **底部弹窗**:符合移动端操作习惯
|
|
||||||
- **滚动选择**:直观的日期滚动选择方式
|
|
||||||
- **即时反馈**:选择后立即显示结果
|
|
||||||
|
|
||||||
### 🚫 错误防护
|
|
||||||
- **编辑限制**:编辑模式下禁用选择,避免误操作
|
|
||||||
- **数据验证**:确保日期格式正确
|
|
||||||
- **状态管理**:防止状态不一致问题
|
|
||||||
|
|
||||||
## 文件修改清单
|
|
||||||
|
|
||||||
### src/dealer/customer/add.tsx
|
|
||||||
1. **导入组件**:添加 DatePicker、Popup、Calendar 组件
|
|
||||||
2. **状态管理**:添加日期选择器相关状态
|
|
||||||
3. **处理函数**:实现日期格式化和选择处理逻辑
|
|
||||||
4. **界面更新**:替换输入框为可点击的日期选择区域
|
|
||||||
5. **弹窗组件**:添加日期选择器弹窗
|
|
||||||
6. **数据初始化**:在 reload 函数中初始化日期数据
|
|
||||||
7. **提交处理**:确保日期数据正确提交
|
|
||||||
|
|
||||||
## 技术栈
|
|
||||||
|
|
||||||
### 核心技术
|
|
||||||
- **React + TypeScript**:类型安全的组件开发
|
|
||||||
- **Taro 框架**:跨平台小程序开发
|
|
||||||
- **NutUI 组件库**:提供 DatePicker 和 Popup 组件
|
|
||||||
|
|
||||||
### 关键组件
|
|
||||||
- **DatePicker**:日期选择核心组件
|
|
||||||
- **Popup**:弹窗容器组件
|
|
||||||
- **Calendar**:日历图标组件
|
|
||||||
- **Form.Item**:表单项容器
|
|
||||||
|
|
||||||
## 使用方式
|
|
||||||
|
|
||||||
### 用户操作流程
|
|
||||||
1. 进入客户签约页面
|
|
||||||
2. 点击"签约时间"字段
|
|
||||||
3. 在弹出的日期选择器中选择日期
|
|
||||||
4. 点击确认完成选择
|
|
||||||
5. 重复步骤选择"合同日期"
|
|
||||||
6. 填写其他信息后提交表单
|
|
||||||
|
|
||||||
### 开发者扩展
|
|
||||||
- 可以轻松添加更多日期字段
|
|
||||||
- 支持自定义日期格式
|
|
||||||
- 可以添加日期范围限制
|
|
||||||
- 支持国际化配置
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
### 兼容性
|
|
||||||
- 确保 NutUI 版本支持 DatePicker 组件
|
|
||||||
- 测试不同设备上的显示效果
|
|
||||||
- 验证日期格式在不同环境下的一致性
|
|
||||||
|
|
||||||
### 性能优化
|
|
||||||
- 合理使用 useState 避免不必要的重渲染
|
|
||||||
- 日期格式化函数可以考虑缓存
|
|
||||||
- 弹窗组件按需渲染
|
|
||||||
|
|
||||||
### 用户体验
|
|
||||||
- 提供清晰的操作提示
|
|
||||||
- 确保在编辑模式下的正确禁用
|
|
||||||
- 考虑添加日期范围验证
|
|
||||||
|
|
||||||
## 总结
|
|
||||||
成功实现了签约时间和合同日期的选择功能,提供了:
|
|
||||||
|
|
||||||
1. **直观的操作界面**:图标提示 + 点击选择
|
|
||||||
2. **友好的选择体验**:底部弹窗 + 滚动选择
|
|
||||||
3. **完整的数据处理**:格式化 + 同步 + 提交
|
|
||||||
4. **合理的状态控制**:编辑模式禁用 + 视觉反馈
|
|
||||||
|
|
||||||
这个实现为用户提供了便捷的日期选择体验,同时保证了数据的准确性和一致性。
|
|
||||||
182
docs/统一扫码功能说明.md
182
docs/统一扫码功能说明.md
@@ -1,182 +0,0 @@
|
|||||||
# 统一扫码功能说明
|
|
||||||
|
|
||||||
## 📋 功能概述
|
|
||||||
|
|
||||||
本项目实现了统一扫码入口,支持多种类型的二维码识别和处理:
|
|
||||||
|
|
||||||
1. **二维码登录** - 扫码确认后台管理系统登录
|
|
||||||
2. **礼品卡核销** - 管理员扫码核销礼品卡
|
|
||||||
3. **礼品卡兑换** - 用户扫码兑换礼品卡
|
|
||||||
4. **车辆查询** - 扫码查询车辆信息
|
|
||||||
5. **未知类型** - 提供选择处理方式
|
|
||||||
|
|
||||||
## 🔧 技术实现
|
|
||||||
|
|
||||||
### 核心组件
|
|
||||||
|
|
||||||
#### 1. API接口层 (`src/api/qrLogin/`)
|
|
||||||
- `model/index.ts` - 类型定义
|
|
||||||
- `index.ts` - API接口和扫码结果解析逻辑
|
|
||||||
|
|
||||||
#### 2. 统一扫码组件 (`src/components/UniversalScanner.tsx`)
|
|
||||||
- `useUniversalScanner` Hook - 提供扫码功能
|
|
||||||
- 智能识别不同类型的二维码
|
|
||||||
- 权限控制和错误处理
|
|
||||||
|
|
||||||
#### 3. 用户界面集成
|
|
||||||
- `UserCard.tsx` - 移除管理员权限限制,所有登录用户可见扫码按钮
|
|
||||||
- `verification.tsx` - 支持从统一扫码传递参数
|
|
||||||
|
|
||||||
## 🎯 使用方法
|
|
||||||
|
|
||||||
### 在组件中使用统一扫码
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
import { useUniversalScanner } from '@/components/UniversalScanner';
|
|
||||||
|
|
||||||
function MyComponent() {
|
|
||||||
const { startScan } = useUniversalScanner({
|
|
||||||
onScanSuccess: (result) => {
|
|
||||||
console.log('扫码成功:', result);
|
|
||||||
},
|
|
||||||
onScanError: (error) => {
|
|
||||||
console.error('扫码失败:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Button onClick={startScan}>
|
|
||||||
扫码
|
|
||||||
</Button>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
### 扫码结果类型
|
|
||||||
|
|
||||||
```typescript
|
|
||||||
export type ScanResultType =
|
|
||||||
| 'qr-login' // 二维码登录
|
|
||||||
| 'gift-verification' // 礼品卡核销
|
|
||||||
| 'gift-redeem' // 礼品卡兑换
|
|
||||||
| 'vehicle-query' // 车辆查询
|
|
||||||
| 'unknown'; // 未知类型
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🔍 二维码格式识别规则
|
|
||||||
|
|
||||||
### 1. 二维码登录
|
|
||||||
- 格式:`qr-login:token` 或纯token(32位以上字符串)
|
|
||||||
- 示例:`qr-login:abc123def456...` 或 `abc123def456ghi789...`
|
|
||||||
|
|
||||||
### 2. 礼品卡核销
|
|
||||||
- 格式:JSON格式,包含 `businessType: 'gift'`
|
|
||||||
- 示例:`{"businessType":"gift","token":"xxx","data":"yyy"}`
|
|
||||||
|
|
||||||
### 3. 礼品卡兑换
|
|
||||||
- 格式:6位字母数字组合
|
|
||||||
- 示例:`ABC123`、`XYZ789`
|
|
||||||
|
|
||||||
### 4. 车辆查询
|
|
||||||
- 格式:以 `vehicle-` 或 `car-` 开头
|
|
||||||
- 示例:`vehicle-12345`、`car-abc123`
|
|
||||||
|
|
||||||
### 5. URL格式
|
|
||||||
- 支持包含特定参数的URL
|
|
||||||
- 二维码登录:包含 `qr-login-token` 参数
|
|
||||||
- 礼品卡:包含 `gift-code` 参数
|
|
||||||
|
|
||||||
## 🛡️ 权限控制
|
|
||||||
|
|
||||||
### 用户权限
|
|
||||||
- **所有已登录用户**:可以看到扫码按钮
|
|
||||||
- **二维码登录**:需要登录状态
|
|
||||||
- **礼品卡兑换**:无需特殊权限
|
|
||||||
- **车辆查询**:无需特殊权限
|
|
||||||
|
|
||||||
### 管理员权限
|
|
||||||
- **礼品卡核销**:仅管理员可用
|
|
||||||
|
|
||||||
## 🔄 处理流程
|
|
||||||
|
|
||||||
### 二维码登录流程
|
|
||||||
1. 扫码获取token
|
|
||||||
2. 调用 `scanQrCode(token)` 更新状态为已扫码
|
|
||||||
3. 调用 `wechatMiniProgramConfirm()` 确认登录
|
|
||||||
4. 显示成功提示
|
|
||||||
|
|
||||||
### 礼品卡核销流程
|
|
||||||
1. 检查管理员权限
|
|
||||||
2. 跳转到核销页面并传递扫码数据
|
|
||||||
3. 在核销页面处理解密和验证
|
|
||||||
|
|
||||||
### 礼品卡兑换流程
|
|
||||||
1. 直接跳转到兑换页面
|
|
||||||
2. 传递兑换码参数
|
|
||||||
|
|
||||||
## 🚨 错误处理
|
|
||||||
|
|
||||||
### 常见错误类型
|
|
||||||
- **权限不足**:非管理员尝试核销
|
|
||||||
- **未登录**:需要登录的功能
|
|
||||||
- **二维码过期**:登录token过期
|
|
||||||
- **无效二维码**:格式不正确
|
|
||||||
- **网络错误**:API调用失败
|
|
||||||
|
|
||||||
### 错误提示
|
|
||||||
- 自动显示Toast提示
|
|
||||||
- 根据错误类型显示不同消息
|
|
||||||
- 支持自定义错误处理回调
|
|
||||||
|
|
||||||
## 📱 用户体验
|
|
||||||
|
|
||||||
### 成功提示
|
|
||||||
- **二维码登录**:显示确认弹窗,提示在电脑端查看
|
|
||||||
- **其他功能**:显示成功Toast并跳转相应页面
|
|
||||||
|
|
||||||
### 未知类型处理
|
|
||||||
- 显示操作选择弹窗
|
|
||||||
- 提供复制、作为礼品卡码、作为车辆码等选项
|
|
||||||
|
|
||||||
## 🔧 配置选项
|
|
||||||
|
|
||||||
### useUniversalScanner 参数
|
|
||||||
```typescript
|
|
||||||
interface UniversalScannerProps {
|
|
||||||
/** 扫码成功回调 */
|
|
||||||
onScanSuccess?: (result: ScanResultParsed) => void;
|
|
||||||
/** 扫码失败回调 */
|
|
||||||
onScanError?: (error: string) => void;
|
|
||||||
/** 是否显示处理结果提示 */
|
|
||||||
showToast?: boolean;
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
## 🧪 测试建议
|
|
||||||
|
|
||||||
### 测试场景
|
|
||||||
1. **不同用户权限**:普通用户、管理员
|
|
||||||
2. **不同二维码类型**:登录、核销、兑换、车辆查询
|
|
||||||
3. **错误情况**:过期、无效、网络错误
|
|
||||||
4. **边界情况**:未登录、权限不足
|
|
||||||
|
|
||||||
### 测试数据
|
|
||||||
- 有效的登录token
|
|
||||||
- 有效的礼品卡核销JSON
|
|
||||||
- 有效的兑换码(6位)
|
|
||||||
- 有效的车辆查询码
|
|
||||||
- 无效/过期的各种码
|
|
||||||
|
|
||||||
## 📝 注意事项
|
|
||||||
|
|
||||||
1. **安全性**:登录token应该有过期时间
|
|
||||||
2. **性能**:扫码解析逻辑应该高效
|
|
||||||
3. **用户体验**:错误提示应该清晰明确
|
|
||||||
4. **扩展性**:新增扫码类型时只需修改解析逻辑
|
|
||||||
|
|
||||||
## 🔄 后续优化
|
|
||||||
|
|
||||||
1. **缓存机制**:缓存扫码结果避免重复处理
|
|
||||||
2. **统计功能**:记录扫码使用情况
|
|
||||||
3. **批量处理**:支持连续扫码
|
|
||||||
4. **离线支持**:部分功能支持离线处理
|
|
||||||
@@ -1,176 +0,0 @@
|
|||||||
# 订单筛选及查询功能实现说明
|
|
||||||
|
|
||||||
## 功能概述
|
|
||||||
|
|
||||||
为订单页面实现了完整的筛选及查询功能,包括关键词搜索、状态筛选、支付方式筛选等多种筛选条件。
|
|
||||||
|
|
||||||
## 实现的功能
|
|
||||||
|
|
||||||
### 1. 关键词搜索
|
|
||||||
- 支持按订单号搜索
|
|
||||||
- 支持按商品名称搜索
|
|
||||||
- 实时搜索,按回车键或点击搜索按钮触发
|
|
||||||
|
|
||||||
### 2. Tabs订单状态筛选 ✅ 已修复完成 + 排除已取消订单
|
|
||||||
- **全部订单** - 显示所有订单 (不添加筛选条件)
|
|
||||||
- **待付款** - 筛选未付款且未取消的订单 (payStatus = 0, 排除已取消)
|
|
||||||
- **待发货** - 筛选已付款但未发货且未取消的订单 (payStatus = 1, deliveryStatus = 10, 排除已取消)
|
|
||||||
- **待收货** - 筛选已发货且未取消的订单 (deliveryStatus = 20, 排除已取消)
|
|
||||||
- **已完成** - 筛选已完成订单 (orderStatus = 1)
|
|
||||||
- **已取消** - 筛选已取消/退款的订单 (orderStatus = 2,3,4,6,7)
|
|
||||||
|
|
||||||
### 3. 已取消订单状态说明
|
|
||||||
为了避免已取消的订单出现在正常流程的tab中,系统会自动排除以下状态的订单:
|
|
||||||
- `orderStatus = 2`: 已取消
|
|
||||||
- `orderStatus = 3`: 取消中
|
|
||||||
- `orderStatus = 4`: 退款申请中
|
|
||||||
- `orderStatus = 6`: 退款成功
|
|
||||||
- `orderStatus = 7`: 客户端申请退款
|
|
||||||
|
|
||||||
### 4. 后端筛选逻辑对应关系
|
|
||||||
根据实际的API参数,各tab对应的筛选逻辑如下:
|
|
||||||
|
|
||||||
| Tab索引 | Tab名称 | API参数 | 前端筛选 | 说明 |
|
|
||||||
|---------|---------|---------|----------|------|
|
|
||||||
| 0 | 全部订单 | 无额外参数 | 无 | 显示所有订单 |
|
|
||||||
| 1 | 待付款 | `payStatus=0` | 排除已取消 | 未付款且未取消的订单 |
|
|
||||||
| 2 | 待发货 | `payStatus=1&deliveryStatus=10` | 排除已取消 | 已付款但未发货且未取消 |
|
|
||||||
| 3 | 待收货 | `deliveryStatus=20` | 排除已取消 | 已发货且未取消 |
|
|
||||||
| 4 | 已完成 | `orderStatus=1` | 无 | 订单已完成 |
|
|
||||||
| 5 | 已取消 | 无额外参数 | 只显示已取消 | 已取消/退款的订单 |
|
|
||||||
|
|
||||||
### 4. Tabs功能特性
|
|
||||||
- 点击不同tab自动筛选对应状态的订单
|
|
||||||
- 使用后端标准的statusFilter参数
|
|
||||||
- 显示当前筛选状态和订单数量
|
|
||||||
- 加载状态指示器
|
|
||||||
- 平滑的切换动画效果
|
|
||||||
|
|
||||||
### 3. 支付状态筛选
|
|
||||||
- 全部
|
|
||||||
- 未付款
|
|
||||||
- 已付款
|
|
||||||
|
|
||||||
### 4. 支付方式筛选
|
|
||||||
- 全部
|
|
||||||
- 余额支付
|
|
||||||
- 微信支付
|
|
||||||
- 会员卡支付
|
|
||||||
- 支付宝
|
|
||||||
- 现金
|
|
||||||
- POS机
|
|
||||||
|
|
||||||
## 文件结构
|
|
||||||
|
|
||||||
```
|
|
||||||
src/pages/order/
|
|
||||||
├── order.tsx # 主订单页面
|
|
||||||
├── components/
|
|
||||||
│ ├── OrderList.tsx # 订单列表组件
|
|
||||||
│ ├── OrderSearch.tsx # 搜索筛选组件
|
|
||||||
│ └── OrderSearch.scss # 搜索组件样式
|
|
||||||
└── order.scss # 订单页面样式
|
|
||||||
```
|
|
||||||
|
|
||||||
## 主要修改
|
|
||||||
|
|
||||||
### 1. 扩展API接口参数
|
|
||||||
- 在 `src/api/shop/shopOrder/model/index.ts` 中扩展了 `ShopOrderParam` 接口
|
|
||||||
- 添加了 `deliveryStatus` 字段支持发货状态筛选
|
|
||||||
|
|
||||||
### 2. 更新订单页面 (order.tsx)
|
|
||||||
- 添加搜索状态管理
|
|
||||||
- 集成搜索组件
|
|
||||||
- 实现搜索和重置功能
|
|
||||||
- 将搜索参数传递给订单列表组件
|
|
||||||
|
|
||||||
### 3. 更新订单列表组件 (OrderList.tsx) ✅ 重点修复
|
|
||||||
- **修复tabs筛选逻辑** - 解决tapIndex类型不匹配问题
|
|
||||||
- **完善筛选参数映射** - 每个tab对应正确的API筛选条件
|
|
||||||
- **添加筛选状态提示** - 显示当前筛选的tab和订单数量
|
|
||||||
- **优化用户体验** - 添加加载状态和视觉反馈
|
|
||||||
- **支持搜索参数** - 监听搜索参数变化并重新加载数据
|
|
||||||
- **调试信息** - 添加console.log便于调试筛选逻辑
|
|
||||||
|
|
||||||
### 4. 创建搜索组件 (OrderSearch.tsx)
|
|
||||||
- 实现关键词搜索界面
|
|
||||||
- 实现高级筛选弹窗
|
|
||||||
- 支持多种筛选条件组合
|
|
||||||
- 响应式设计,适配移动端
|
|
||||||
|
|
||||||
### 5. 样式优化
|
|
||||||
- 优化tabs的视觉效果
|
|
||||||
- 添加筛选提示的动画效果
|
|
||||||
- 改进搜索框的样式和交互
|
|
||||||
|
|
||||||
## 使用方法
|
|
||||||
|
|
||||||
### 基础搜索
|
|
||||||
1. 点击导航栏的搜索图标或筛选图标
|
|
||||||
2. 在搜索框中输入订单号或商品名称
|
|
||||||
3. 按回车键或点击搜索按钮
|
|
||||||
|
|
||||||
### 高级筛选
|
|
||||||
1. 点击筛选图标打开筛选弹窗
|
|
||||||
2. 选择需要的筛选条件:
|
|
||||||
- 输入具体订单号
|
|
||||||
- 输入手机号
|
|
||||||
- 选择订单状态
|
|
||||||
- 选择支付状态
|
|
||||||
- 选择支付方式
|
|
||||||
3. 点击确定按钮应用筛选
|
|
||||||
|
|
||||||
### 重置搜索
|
|
||||||
- 点击重置按钮清空所有搜索条件
|
|
||||||
- 返回显示所有订单
|
|
||||||
|
|
||||||
## 技术特点
|
|
||||||
|
|
||||||
### 1. 类型安全
|
|
||||||
- 使用TypeScript确保类型安全
|
|
||||||
- 定义了完整的接口类型
|
|
||||||
|
|
||||||
### 2. 组件化设计
|
|
||||||
- 搜索功能独立封装为组件
|
|
||||||
- 便于维护和复用
|
|
||||||
|
|
||||||
### 3. 响应式设计
|
|
||||||
- 适配移动端界面
|
|
||||||
- 使用Tailwind CSS实现响应式布局
|
|
||||||
|
|
||||||
### 4. 用户体验优化
|
|
||||||
- 实时搜索反馈
|
|
||||||
- 清晰的筛选界面
|
|
||||||
- 便捷的重置功能
|
|
||||||
|
|
||||||
## 扩展建议
|
|
||||||
|
|
||||||
### 1. 日期范围筛选
|
|
||||||
可以添加订单创建时间、支付时间等日期范围筛选功能。
|
|
||||||
|
|
||||||
### 2. 金额范围筛选
|
|
||||||
支持按订单金额范围进行筛选。
|
|
||||||
|
|
||||||
### 3. 商户筛选
|
|
||||||
如果有多商户,可以添加按商户筛选的功能。
|
|
||||||
|
|
||||||
### 4. 搜索历史
|
|
||||||
保存用户的搜索历史,提供快速搜索选项。
|
|
||||||
|
|
||||||
### 5. 导出功能
|
|
||||||
支持将筛选结果导出为Excel或PDF格式。
|
|
||||||
|
|
||||||
## 注意事项
|
|
||||||
|
|
||||||
1. 搜索功能依赖后端API支持相应的查询参数
|
|
||||||
2. 筛选条件会影响分页加载,需要在切换筛选条件时重置页码
|
|
||||||
3. 建议在生产环境中添加搜索防抖功能,避免频繁请求
|
|
||||||
4. 可以考虑添加搜索结果缓存,提升用户体验
|
|
||||||
|
|
||||||
## 测试建议
|
|
||||||
|
|
||||||
1. 测试各种搜索条件的组合
|
|
||||||
2. 测试搜索结果的准确性
|
|
||||||
3. 测试重置功能是否正常
|
|
||||||
4. 测试在不同设备上的显示效果
|
|
||||||
5. 测试搜索性能,特别是大数据量情况下
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
package com.gxwebsoft.auto.controller;
|
|
||||||
|
|
||||||
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
|
|
||||||
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
|
|
||||||
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
|
|
||||||
import com.gxwebsoft.auto.service.QrLoginService;
|
|
||||||
import com.gxwebsoft.common.core.web.BaseController;
|
|
||||||
import com.gxwebsoft.common.core.web.ApiResult;
|
|
||||||
import io.swagger.v3.oas.annotations.Operation;
|
|
||||||
import io.swagger.v3.oas.annotations.Parameter;
|
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.web.bind.annotation.*;
|
|
||||||
|
|
||||||
import javax.validation.Valid;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 认证模块
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-03-06 22:50:25
|
|
||||||
*/
|
|
||||||
@Tag(name = "认证模块")
|
|
||||||
@RestController
|
|
||||||
@RequestMapping("/api/qr-login")
|
|
||||||
public class QrLoginController extends BaseController {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private QrLoginService qrLoginService;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成扫码登录token
|
|
||||||
*/
|
|
||||||
@Operation(summary = "生成扫码登录token")
|
|
||||||
@PostMapping("/generate")
|
|
||||||
public ApiResult<?> generateQrLoginToken() {
|
|
||||||
try {
|
|
||||||
QrLoginGenerateResponse response = qrLoginService.generateQrLoginToken();
|
|
||||||
return success("生成成功", response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return fail(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查扫码登录状态
|
|
||||||
*/
|
|
||||||
@Operation(summary = "检查扫码登录状态")
|
|
||||||
@GetMapping("/status/{token}")
|
|
||||||
public ApiResult<?> checkQrLoginStatus(
|
|
||||||
@Parameter(description = "扫码登录token") @PathVariable String token) {
|
|
||||||
try {
|
|
||||||
QrLoginStatusResponse response = qrLoginService.checkQrLoginStatus(token);
|
|
||||||
return success("查询成功", response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return fail(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认扫码登录
|
|
||||||
*/
|
|
||||||
@Operation(summary = "确认扫码登录")
|
|
||||||
@PostMapping("/confirm")
|
|
||||||
public ApiResult<?> confirmQrLogin(@Valid @RequestBody QrLoginConfirmRequest request) {
|
|
||||||
try {
|
|
||||||
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
|
|
||||||
return success("确认成功", response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return fail(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码操作(可选接口,用于移动端扫码后更新状态)
|
|
||||||
*/
|
|
||||||
@Operation(summary = "扫码操作")
|
|
||||||
@PostMapping("/scan/{token}")
|
|
||||||
public ApiResult<?> scanQrCode(@Parameter(description = "扫码登录token") @PathVariable String token) {
|
|
||||||
try {
|
|
||||||
boolean result = qrLoginService.scanQrCode(token);
|
|
||||||
return success("操作成功", result);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return fail(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信小程序扫码登录确认(便捷接口)
|
|
||||||
*/
|
|
||||||
@Operation(summary = "微信小程序扫码登录确认")
|
|
||||||
@PostMapping("/wechat-confirm")
|
|
||||||
public ApiResult<?> wechatMiniProgramConfirm(@Valid @RequestBody QrLoginConfirmRequest request) {
|
|
||||||
try {
|
|
||||||
// 设置平台为微信小程序
|
|
||||||
request.setPlatform("miniprogram");
|
|
||||||
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
|
|
||||||
return success("微信小程序登录确认成功", response);
|
|
||||||
} catch (Exception e) {
|
|
||||||
return fail(e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package com.gxwebsoft.auto.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.Data;
|
|
||||||
|
|
||||||
import javax.validation.constraints.NotBlank;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码登录确认请求
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-08-31
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Schema(description = "扫码登录确认请求")
|
|
||||||
public class QrLoginConfirmRequest {
|
|
||||||
|
|
||||||
@Schema(description = "扫码登录token")
|
|
||||||
@NotBlank(message = "token不能为空")
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
@Schema(description = "用户ID")
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
@Schema(description = "登录平台: web-网页端, app-移动应用, miniprogram-微信小程序")
|
|
||||||
private String platform;
|
|
||||||
|
|
||||||
@Schema(description = "微信小程序相关信息")
|
|
||||||
private WechatMiniProgramInfo wechatInfo;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信小程序信息
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@Schema(description = "微信小程序信息")
|
|
||||||
public static class WechatMiniProgramInfo {
|
|
||||||
@Schema(description = "微信openid")
|
|
||||||
private String openid;
|
|
||||||
|
|
||||||
@Schema(description = "微信unionid")
|
|
||||||
private String unionid;
|
|
||||||
|
|
||||||
@Schema(description = "微信昵称")
|
|
||||||
private String nickname;
|
|
||||||
|
|
||||||
@Schema(description = "微信头像")
|
|
||||||
private String avatar;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
package com.gxwebsoft.auto.dto;
|
|
||||||
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码登录数据模型
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-08-31
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
public class QrLoginData {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码登录token
|
|
||||||
*/
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期
|
|
||||||
*/
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户ID(扫码确认后设置)
|
|
||||||
*/
|
|
||||||
private Integer userId;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 用户名(扫码确认后设置)
|
|
||||||
*/
|
|
||||||
private String username;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 创建时间
|
|
||||||
*/
|
|
||||||
private LocalDateTime createTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 过期时间
|
|
||||||
*/
|
|
||||||
private LocalDateTime expireTime;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* JWT访问令牌(确认后生成)
|
|
||||||
*/
|
|
||||||
private String accessToken;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
package com.gxwebsoft.auto.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码登录生成响应
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-08-31
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "扫码登录生成响应")
|
|
||||||
public class QrLoginGenerateResponse {
|
|
||||||
|
|
||||||
@Schema(description = "扫码登录token")
|
|
||||||
private String token;
|
|
||||||
|
|
||||||
@Schema(description = "二维码内容")
|
|
||||||
private String qrCode;
|
|
||||||
|
|
||||||
@Schema(description = "过期时间(秒)")
|
|
||||||
private Long expiresIn;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package com.gxwebsoft.auto.dto;
|
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Data;
|
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码登录状态响应
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-08-31
|
|
||||||
*/
|
|
||||||
@Data
|
|
||||||
@NoArgsConstructor
|
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "扫码登录状态响应")
|
|
||||||
public class QrLoginStatusResponse {
|
|
||||||
|
|
||||||
@Schema(description = "状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期")
|
|
||||||
private String status;
|
|
||||||
|
|
||||||
@Schema(description = "JWT访问令牌(仅在confirmed状态时返回)")
|
|
||||||
private String accessToken;
|
|
||||||
|
|
||||||
@Schema(description = "用户信息(仅在confirmed状态时返回)")
|
|
||||||
private Object userInfo;
|
|
||||||
|
|
||||||
@Schema(description = "剩余过期时间(秒)")
|
|
||||||
private Long expiresIn;
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
package com.gxwebsoft.auto.service;
|
|
||||||
|
|
||||||
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
|
|
||||||
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
|
|
||||||
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码登录服务接口
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-08-31
|
|
||||||
*/
|
|
||||||
public interface QrLoginService {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成扫码登录token
|
|
||||||
*
|
|
||||||
* @return QrLoginGenerateResponse
|
|
||||||
*/
|
|
||||||
QrLoginGenerateResponse generateQrLoginToken();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查扫码登录状态
|
|
||||||
*
|
|
||||||
* @param token 扫码登录token
|
|
||||||
* @return QrLoginStatusResponse
|
|
||||||
*/
|
|
||||||
QrLoginStatusResponse checkQrLoginStatus(String token);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认扫码登录
|
|
||||||
*
|
|
||||||
* @param request 确认请求
|
|
||||||
* @return QrLoginStatusResponse
|
|
||||||
*/
|
|
||||||
QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码操作(更新状态为已扫码)
|
|
||||||
*
|
|
||||||
* @param token 扫码登录token
|
|
||||||
* @return boolean
|
|
||||||
*/
|
|
||||||
boolean scanQrCode(String token);
|
|
||||||
|
|
||||||
}
|
|
||||||
@@ -1,239 +0,0 @@
|
|||||||
package com.gxwebsoft.auto.service.impl;
|
|
||||||
|
|
||||||
import cn.hutool.core.lang.UUID;
|
|
||||||
import cn.hutool.core.util.StrUtil;
|
|
||||||
import com.gxwebsoft.auto.dto.*;
|
|
||||||
import com.gxwebsoft.auto.service.QrLoginService;
|
|
||||||
import com.gxwebsoft.common.core.security.JwtSubject;
|
|
||||||
import com.gxwebsoft.common.core.security.JwtUtil;
|
|
||||||
import com.gxwebsoft.common.core.utils.JSONUtil;
|
|
||||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
|
||||||
import com.gxwebsoft.common.system.entity.User;
|
|
||||||
import com.gxwebsoft.common.system.service.UserService;
|
|
||||||
import lombok.extern.slf4j.Slf4j;
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
|
||||||
import org.springframework.beans.factory.annotation.Value;
|
|
||||||
import org.springframework.stereotype.Service;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
|
||||||
import java.time.temporal.ChronoUnit;
|
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码登录服务实现
|
|
||||||
*
|
|
||||||
* @author 科技小王子
|
|
||||||
* @since 2025-08-31
|
|
||||||
*/
|
|
||||||
@Slf4j
|
|
||||||
@Service
|
|
||||||
public class QrLoginServiceImpl implements QrLoginService {
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private RedisUtil redisUtil;
|
|
||||||
|
|
||||||
@Autowired
|
|
||||||
private UserService userService;
|
|
||||||
|
|
||||||
@Value("${config.jwt.secret:websoft-jwt-secret-key-2025}")
|
|
||||||
private String jwtSecret;
|
|
||||||
|
|
||||||
@Value("${config.jwt.expire:86400}")
|
|
||||||
private Long jwtExpire;
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public QrLoginGenerateResponse generateQrLoginToken() {
|
|
||||||
// 生成唯一的扫码登录token
|
|
||||||
String token = UUID.randomUUID().toString(true);
|
|
||||||
|
|
||||||
// 创建扫码登录数据
|
|
||||||
QrLoginData qrLoginData = new QrLoginData();
|
|
||||||
qrLoginData.setToken(token);
|
|
||||||
qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
|
|
||||||
qrLoginData.setCreateTime(LocalDateTime.now());
|
|
||||||
qrLoginData.setExpireTime(LocalDateTime.now().plusSeconds(QR_LOGIN_TOKEN_TTL));
|
|
||||||
|
|
||||||
// 存储到Redis,设置过期时间
|
|
||||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
|
||||||
redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
log.info("生成扫码登录token: {}", token);
|
|
||||||
|
|
||||||
// 构造二维码内容(这里可以是前端登录页面的URL + token参数)
|
|
||||||
String qrCodeContent = "qr-login:" + token;
|
|
||||||
|
|
||||||
return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public QrLoginStatusResponse checkQrLoginStatus(String token) {
|
|
||||||
if (StrUtil.isBlank(token)) {
|
|
||||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
|
||||||
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
|
||||||
|
|
||||||
if (qrLoginData == null) {
|
|
||||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
|
||||||
// 删除过期的token
|
|
||||||
redisUtil.delete(redisKey);
|
|
||||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算剩余过期时间
|
|
||||||
long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
|
|
||||||
|
|
||||||
QrLoginStatusResponse response = new QrLoginStatusResponse();
|
|
||||||
response.setStatus(qrLoginData.getStatus());
|
|
||||||
response.setExpiresIn(expiresIn);
|
|
||||||
|
|
||||||
// 如果已确认,返回token和用户信息
|
|
||||||
if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())) {
|
|
||||||
response.setAccessToken(qrLoginData.getAccessToken());
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
if (qrLoginData.getUserId() != null) {
|
|
||||||
User user = userService.getByIdRel(qrLoginData.getUserId());
|
|
||||||
if (user != null) {
|
|
||||||
// 清除敏感信息
|
|
||||||
user.setPassword(null);
|
|
||||||
response.setUserInfo(user);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 确认后删除token,防止重复使用
|
|
||||||
redisUtil.delete(redisKey);
|
|
||||||
}
|
|
||||||
|
|
||||||
return response;
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) {
|
|
||||||
String token = request.getToken();
|
|
||||||
Integer userId = request.getUserId();
|
|
||||||
String platform = request.getPlatform();
|
|
||||||
|
|
||||||
if (StrUtil.isBlank(token) || userId == null) {
|
|
||||||
throw new RuntimeException("参数不能为空");
|
|
||||||
}
|
|
||||||
|
|
||||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
|
||||||
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
|
||||||
|
|
||||||
if (qrLoginData == null) {
|
|
||||||
throw new RuntimeException("扫码登录token不存在或已过期");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
|
||||||
redisUtil.delete(redisKey);
|
|
||||||
throw new RuntimeException("扫码登录token已过期");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 获取用户信息
|
|
||||||
User user = userService.getByIdRel(userId);
|
|
||||||
if (user == null) {
|
|
||||||
throw new RuntimeException("用户不存在");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查用户状态
|
|
||||||
if (user.getStatus() != null && user.getStatus() != 0) {
|
|
||||||
throw new RuntimeException("用户已被冻结");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果是微信小程序登录,处理微信相关信息
|
|
||||||
if ("miniprogram".equals(platform) && request.getWechatInfo() != null) {
|
|
||||||
handleWechatMiniProgramLogin(user, request.getWechatInfo());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 生成JWT token
|
|
||||||
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
|
|
||||||
String accessToken = JwtUtil.buildToken(jwtSubject, jwtExpire, jwtSecret);
|
|
||||||
|
|
||||||
// 更新扫码登录数据
|
|
||||||
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
|
|
||||||
qrLoginData.setUserId(userId);
|
|
||||||
qrLoginData.setUsername(user.getUsername());
|
|
||||||
qrLoginData.setAccessToken(accessToken);
|
|
||||||
|
|
||||||
// 更新Redis中的数据
|
|
||||||
redisUtil.set(redisKey, qrLoginData, 60L, TimeUnit.SECONDS); // 给前端60秒时间获取token
|
|
||||||
|
|
||||||
log.info("用户 {} 通过 {} 平台确认扫码登录,token: {}", user.getUsername(),
|
|
||||||
platform != null ? platform : "unknown", token);
|
|
||||||
|
|
||||||
// 清除敏感信息
|
|
||||||
user.setPassword(null);
|
|
||||||
|
|
||||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_CONFIRMED, accessToken, user, 60L);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理微信小程序登录相关逻辑
|
|
||||||
*/
|
|
||||||
private void handleWechatMiniProgramLogin(User user, QrLoginConfirmRequest.WechatMiniProgramInfo wechatInfo) {
|
|
||||||
// 更新用户的微信信息
|
|
||||||
if (StrUtil.isNotBlank(wechatInfo.getOpenid())) {
|
|
||||||
user.setOpenid(wechatInfo.getOpenid());
|
|
||||||
}
|
|
||||||
if (StrUtil.isNotBlank(wechatInfo.getUnionid())) {
|
|
||||||
user.setUnionid(wechatInfo.getUnionid());
|
|
||||||
}
|
|
||||||
if (StrUtil.isNotBlank(wechatInfo.getNickname()) && StrUtil.isBlank(user.getNickname())) {
|
|
||||||
user.setNickname(wechatInfo.getNickname());
|
|
||||||
}
|
|
||||||
if (StrUtil.isNotBlank(wechatInfo.getAvatar()) && StrUtil.isBlank(user.getAvatar())) {
|
|
||||||
user.setAvatar(wechatInfo.getAvatar());
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新用户信息到数据库
|
|
||||||
try {
|
|
||||||
userService.updateById(user);
|
|
||||||
log.info("更新用户 {} 的微信小程序信息成功", user.getUsername());
|
|
||||||
} catch (Exception e) {
|
|
||||||
log.warn("更新用户 {} 的微信小程序信息失败: {}", user.getUsername(), e.getMessage());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean scanQrCode(String token) {
|
|
||||||
if (StrUtil.isBlank(token)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
|
||||||
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
|
||||||
|
|
||||||
if (qrLoginData == null) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否过期
|
|
||||||
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
|
||||||
redisUtil.delete(redisKey);
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 只有pending状态才能更新为scanned
|
|
||||||
if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) {
|
|
||||||
qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
|
|
||||||
|
|
||||||
// 计算剩余过期时间
|
|
||||||
long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
|
|
||||||
redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
|
|
||||||
|
|
||||||
log.info("扫码登录token {} 状态更新为已扫码", token);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,197 +0,0 @@
|
|||||||
import request from '@/utils/request';
|
|
||||||
import type { ApiResult } from '@/api';
|
|
||||||
import type {
|
|
||||||
QrLoginGenerateResponse,
|
|
||||||
QrLoginStatusResponse,
|
|
||||||
QrLoginConfirmRequest,
|
|
||||||
ScanResultParsed,
|
|
||||||
ScanResultType
|
|
||||||
} from './model';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 生成扫码登录token
|
|
||||||
*/
|
|
||||||
export async function generateQrLoginToken() {
|
|
||||||
const res = await request.post<ApiResult<QrLoginGenerateResponse>>(
|
|
||||||
'/api/qr-login/generate'
|
|
||||||
);
|
|
||||||
if (res.code === 0 && res.data) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(res.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 检查扫码登录状态
|
|
||||||
*/
|
|
||||||
export async function checkQrLoginStatus(token: string) {
|
|
||||||
const res = await request.get<ApiResult<QrLoginStatusResponse>>(
|
|
||||||
`/api/qr-login/status/${token}`
|
|
||||||
);
|
|
||||||
if (res.code === 0 && res.data) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(res.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 确认扫码登录
|
|
||||||
*/
|
|
||||||
export async function confirmQrLogin(data: QrLoginConfirmRequest) {
|
|
||||||
const res = await request.post<ApiResult<QrLoginStatusResponse>>(
|
|
||||||
'/api/qr-login/confirm',
|
|
||||||
data
|
|
||||||
);
|
|
||||||
if (res.code === 0 && res.data) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(res.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信小程序扫码登录确认
|
|
||||||
*/
|
|
||||||
export async function wechatMiniProgramConfirm(data: QrLoginConfirmRequest) {
|
|
||||||
const res = await request.post<ApiResult<QrLoginStatusResponse>>(
|
|
||||||
'/api/qr-login/wechat-confirm',
|
|
||||||
data
|
|
||||||
);
|
|
||||||
if (res.code === 0 && res.data) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(res.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码操作(更新状态为已扫码)
|
|
||||||
*/
|
|
||||||
export async function scanQrCode(token: string) {
|
|
||||||
const res = await request.post<ApiResult<boolean>>(
|
|
||||||
`/api/qr-login/scan/${token}`
|
|
||||||
);
|
|
||||||
if (res.code === 0) {
|
|
||||||
return res.data;
|
|
||||||
}
|
|
||||||
return Promise.reject(new Error(res.message));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 判断字符串是否为有效的JSON
|
|
||||||
*/
|
|
||||||
export function isValidJSON(str: string): boolean {
|
|
||||||
try {
|
|
||||||
JSON.parse(str);
|
|
||||||
return true;
|
|
||||||
} catch (e) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 解析扫码结果,识别二维码类型
|
|
||||||
*/
|
|
||||||
export function parseScanResult(scanResult: string): ScanResultParsed {
|
|
||||||
const rawContent = scanResult.trim();
|
|
||||||
|
|
||||||
try {
|
|
||||||
// 1. 尝试解析JSON格式(礼品卡核销)
|
|
||||||
if (isValidJSON(rawContent)) {
|
|
||||||
const json = JSON.parse(rawContent);
|
|
||||||
if (json.businessType === 'gift') {
|
|
||||||
return {
|
|
||||||
type: 'gift-verification',
|
|
||||||
rawContent,
|
|
||||||
data: json,
|
|
||||||
requireAuth: true,
|
|
||||||
requireAdmin: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. 检查是否为二维码登录token格式
|
|
||||||
// 假设二维码登录的格式为: qr-login:token 或者纯token(32位以上字符串)
|
|
||||||
if (rawContent.startsWith('qr-login:')) {
|
|
||||||
const token = rawContent.replace('qr-login:', '');
|
|
||||||
return {
|
|
||||||
type: 'qr-login',
|
|
||||||
rawContent,
|
|
||||||
data: { token },
|
|
||||||
requireAuth: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为纯token格式(32位以上的字母数字组合)
|
|
||||||
if (/^[a-zA-Z0-9-]{32,}$/.test(rawContent)) {
|
|
||||||
return {
|
|
||||||
type: 'qr-login',
|
|
||||||
rawContent,
|
|
||||||
data: { token: rawContent },
|
|
||||||
requireAuth: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 3. 检查是否为礼品卡兑换码(6位字母数字组合)
|
|
||||||
if (/^[A-Z0-9]{6}$/.test(rawContent)) {
|
|
||||||
return {
|
|
||||||
type: 'gift-redeem',
|
|
||||||
rawContent,
|
|
||||||
data: { code: rawContent },
|
|
||||||
requireAuth: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 4. 检查是否为车辆查询码
|
|
||||||
if (rawContent.startsWith('vehicle-') || rawContent.startsWith('car-')) {
|
|
||||||
return {
|
|
||||||
type: 'vehicle-query' as ScanResultType,
|
|
||||||
rawContent,
|
|
||||||
data: { vehicleId: rawContent },
|
|
||||||
requireAuth: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 5. 检查URL格式的二维码
|
|
||||||
if (rawContent.startsWith('http://') || rawContent.startsWith('https://')) {
|
|
||||||
const url = new URL(rawContent);
|
|
||||||
|
|
||||||
// 检查是否包含二维码登录相关参数
|
|
||||||
if (url.searchParams.has('qr-login-token') || url.pathname.includes('/qr-login/')) {
|
|
||||||
const token = url.searchParams.get('qr-login-token') || url.pathname.split('/').pop();
|
|
||||||
return {
|
|
||||||
type: 'qr-login',
|
|
||||||
rawContent,
|
|
||||||
data: { token },
|
|
||||||
requireAuth: true
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查是否为礼品卡相关URL
|
|
||||||
if (url.pathname.includes('/gift/') || url.searchParams.has('gift-code')) {
|
|
||||||
const code = url.searchParams.get('gift-code') || url.pathname.split('/').pop();
|
|
||||||
return {
|
|
||||||
type: 'gift-redeem',
|
|
||||||
rawContent,
|
|
||||||
data: { code },
|
|
||||||
requireAuth: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 6. 默认返回未知类型
|
|
||||||
return {
|
|
||||||
type: 'unknown',
|
|
||||||
rawContent,
|
|
||||||
data: { content: rawContent },
|
|
||||||
requireAuth: false
|
|
||||||
};
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('解析扫码结果失败:', error);
|
|
||||||
return {
|
|
||||||
type: 'unknown',
|
|
||||||
rawContent,
|
|
||||||
data: { content: rawContent, error: error.message },
|
|
||||||
requireAuth: false
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,85 +0,0 @@
|
|||||||
/**
|
|
||||||
* 二维码登录相关类型定义
|
|
||||||
*/
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 二维码登录生成响应
|
|
||||||
*/
|
|
||||||
export interface QrLoginGenerateResponse {
|
|
||||||
/** 扫码登录token */
|
|
||||||
token: string;
|
|
||||||
/** 二维码内容 */
|
|
||||||
qrCode: string;
|
|
||||||
/** 过期时间(秒) */
|
|
||||||
expiresIn: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 二维码登录状态响应
|
|
||||||
*/
|
|
||||||
export interface QrLoginStatusResponse {
|
|
||||||
/** 登录状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期, cancelled-已取消 */
|
|
||||||
status: 'pending' | 'scanned' | 'confirmed' | 'expired' | 'cancelled';
|
|
||||||
/** 状态描述 */
|
|
||||||
message?: string;
|
|
||||||
/** 登录成功时返回的用户信息 */
|
|
||||||
user?: any;
|
|
||||||
/** 登录成功时返回的访问令牌 */
|
|
||||||
accessToken?: string;
|
|
||||||
/** 剩余过期时间(秒) */
|
|
||||||
remainingTime?: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 二维码登录确认请求
|
|
||||||
*/
|
|
||||||
export interface QrLoginConfirmRequest {
|
|
||||||
/** 扫码登录token */
|
|
||||||
token: string;
|
|
||||||
/** 用户ID */
|
|
||||||
userId?: number;
|
|
||||||
/** 登录平台: web-网页端, app-移动应用, miniprogram-微信小程序 */
|
|
||||||
platform?: string;
|
|
||||||
/** 微信小程序相关信息 */
|
|
||||||
wechatInfo?: WechatMiniProgramInfo;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 微信小程序信息
|
|
||||||
*/
|
|
||||||
export interface WechatMiniProgramInfo {
|
|
||||||
/** 微信openid */
|
|
||||||
openid?: string;
|
|
||||||
/** 微信unionid */
|
|
||||||
unionid?: string;
|
|
||||||
/** 微信昵称 */
|
|
||||||
nickname?: string;
|
|
||||||
/** 微信头像 */
|
|
||||||
avatar?: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码结果类型
|
|
||||||
*/
|
|
||||||
export type ScanResultType =
|
|
||||||
| 'qr-login' // 二维码登录
|
|
||||||
| 'gift-verification' // 礼品卡核销
|
|
||||||
| 'gift-redeem' // 礼品卡兑换
|
|
||||||
| 'vehicle-query' // 车辆查询
|
|
||||||
| 'unknown'; // 未知类型
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 扫码结果解析
|
|
||||||
*/
|
|
||||||
export interface ScanResultParsed {
|
|
||||||
/** 扫码结果类型 */
|
|
||||||
type: ScanResultType;
|
|
||||||
/** 原始扫码内容 */
|
|
||||||
rawContent: string;
|
|
||||||
/** 解析后的数据 */
|
|
||||||
data: any;
|
|
||||||
/** 是否需要权限验证 */
|
|
||||||
requireAuth?: boolean;
|
|
||||||
/** 是否需要管理员权限 */
|
|
||||||
requireAdmin?: boolean;
|
|
||||||
}
|
|
||||||
@@ -1,6 +1,4 @@
|
|||||||
import React from 'react';
|
import React from 'react';
|
||||||
import { Button } from '@nutui/nutui-react-taro';
|
|
||||||
import { View } from '@tarojs/components';
|
|
||||||
import { Scan } from '@nutui/icons-react-taro';
|
import { Scan } from '@nutui/icons-react-taro';
|
||||||
import Taro from '@tarojs/taro';
|
import Taro from '@tarojs/taro';
|
||||||
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan';
|
||||||
@@ -29,15 +27,11 @@ export interface UnifiedQRButtonProps {
|
|||||||
* 支持登录和核销两种类型的二维码扫描
|
* 支持登录和核销两种类型的二维码扫描
|
||||||
*/
|
*/
|
||||||
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
||||||
type = 'success',
|
|
||||||
size = 'small',
|
|
||||||
text = '',
|
|
||||||
showIcon = true,
|
|
||||||
onSuccess,
|
onSuccess,
|
||||||
onError,
|
onError,
|
||||||
usePageMode = false
|
usePageMode = false
|
||||||
}) => {
|
}) => {
|
||||||
const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan();
|
const { startScan, canScan, result } = useUnifiedQRScan();
|
||||||
console.log(result,'useUnifiedQRScan>>result')
|
console.log(result,'useUnifiedQRScan>>result')
|
||||||
// 处理点击事件
|
// 处理点击事件
|
||||||
const handleClick = async () => {
|
const handleClick = async () => {
|
||||||
@@ -83,43 +77,8 @@ const UnifiedQRButton: React.FC<UnifiedQRButtonProps> = ({
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const disabled = !canScan() || isLoading;
|
|
||||||
|
|
||||||
// 根据当前状态动态显示文本
|
|
||||||
const getButtonText = () => {
|
|
||||||
if (isLoading) {
|
|
||||||
switch (state) {
|
|
||||||
case 'scanning':
|
|
||||||
return '扫码中...';
|
|
||||||
case 'processing':
|
|
||||||
return '处理中...';
|
|
||||||
default:
|
|
||||||
return '扫码中...';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (disabled && !canScan()) {
|
|
||||||
return '请先登录';
|
|
||||||
}
|
|
||||||
|
|
||||||
return text;
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Button
|
<Scan className={'text-white'} onClick={handleClick} />
|
||||||
type={type}
|
|
||||||
size={size}
|
|
||||||
loading={isLoading}
|
|
||||||
disabled={disabled}
|
|
||||||
onClick={handleClick}
|
|
||||||
>
|
|
||||||
<View className="flex items-center justify-center text-white">
|
|
||||||
{showIcon && !isLoading && (
|
|
||||||
<Scan />
|
|
||||||
)}
|
|
||||||
{getButtonText()}
|
|
||||||
</View>
|
|
||||||
</Button>
|
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,305 +0,0 @@
|
|||||||
import React from 'react';
|
|
||||||
import Taro from '@tarojs/taro';
|
|
||||||
import { parseScanResult, wechatMiniProgramConfirm, scanQrCode } from '@/api/qrLogin';
|
|
||||||
import type { ScanResultParsed } from '@/api/qrLogin/model';
|
|
||||||
import navTo from '@/utils/common';
|
|
||||||
import { useUser } from '@/hooks/useUser';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一扫码处理组件
|
|
||||||
*/
|
|
||||||
export interface UniversalScannerProps {
|
|
||||||
/** 扫码成功回调 */
|
|
||||||
onScanSuccess?: (result: ScanResultParsed) => void;
|
|
||||||
/** 扫码失败回调 */
|
|
||||||
onScanError?: (error: string) => void;
|
|
||||||
/** 是否显示处理结果提示 */
|
|
||||||
showToast?: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一扫码处理Hook
|
|
||||||
*/
|
|
||||||
export function useUniversalScanner(props: UniversalScannerProps = {}) {
|
|
||||||
const {
|
|
||||||
onScanSuccess,
|
|
||||||
onScanError,
|
|
||||||
showToast = true
|
|
||||||
} = props;
|
|
||||||
|
|
||||||
const { user, isLoggedIn, isAdmin } = useUser();
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 启动扫码
|
|
||||||
*/
|
|
||||||
const startScan = () => {
|
|
||||||
Taro.scanCode({
|
|
||||||
onlyFromCamera: true,
|
|
||||||
scanType: ['qrCode', 'barCode'],
|
|
||||||
success: (res) => {
|
|
||||||
handleScanResult(res.result);
|
|
||||||
},
|
|
||||||
fail: (err) => {
|
|
||||||
console.error('扫码失败:', err);
|
|
||||||
const errorMsg = '扫码失败,请重试';
|
|
||||||
if (showToast) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: errorMsg,
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onScanError?.(errorMsg);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理扫码结果
|
|
||||||
*/
|
|
||||||
const handleScanResult = async (scanResult: string) => {
|
|
||||||
try {
|
|
||||||
console.log('扫码结果:', scanResult);
|
|
||||||
|
|
||||||
// 解析扫码结果
|
|
||||||
const parsed = parseScanResult(scanResult);
|
|
||||||
console.log('解析结果:', parsed);
|
|
||||||
|
|
||||||
// 权限检查
|
|
||||||
if (parsed.requireAuth && !isLoggedIn) {
|
|
||||||
if (showToast) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '请先登录',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onScanError?.('请先登录');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parsed.requireAdmin && !isAdmin()) {
|
|
||||||
if (showToast) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '仅管理员可使用此功能',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onScanError?.('权限不足');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据类型处理
|
|
||||||
await handleByType(parsed);
|
|
||||||
|
|
||||||
// 回调
|
|
||||||
onScanSuccess?.(parsed);
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理扫码结果失败:', error);
|
|
||||||
const errorMsg = error instanceof Error ? error.message : '处理扫码结果失败';
|
|
||||||
if (showToast) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: errorMsg,
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
onScanError?.(errorMsg);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 根据类型处理扫码结果
|
|
||||||
*/
|
|
||||||
const handleByType = async (parsed: ScanResultParsed) => {
|
|
||||||
switch (parsed.type) {
|
|
||||||
case 'qr-login':
|
|
||||||
await handleQrLogin(parsed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'gift-verification':
|
|
||||||
handleGiftVerification(parsed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'gift-redeem':
|
|
||||||
handleGiftRedeem(parsed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'vehicle-query':
|
|
||||||
handleVehicleQuery(parsed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 'unknown':
|
|
||||||
handleUnknownType(parsed);
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
throw new Error(`未支持的扫码类型: ${parsed.type}`);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理二维码登录
|
|
||||||
*/
|
|
||||||
const handleQrLogin = async (parsed: ScanResultParsed) => {
|
|
||||||
const { token } = parsed.data;
|
|
||||||
|
|
||||||
try {
|
|
||||||
if (showToast) {
|
|
||||||
Taro.showLoading({ title: '正在处理登录...' });
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1. 先调用扫码接口,更新状态为已扫码
|
|
||||||
await scanQrCode(token);
|
|
||||||
|
|
||||||
// 2. 确认登录
|
|
||||||
const confirmData = {
|
|
||||||
token,
|
|
||||||
userId: user?.userId,
|
|
||||||
platform: 'miniprogram',
|
|
||||||
wechatInfo: {
|
|
||||||
openid: user?.openid,
|
|
||||||
nickname: user?.nickname || user?.realName,
|
|
||||||
avatar: user?.avatar
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
const result = await wechatMiniProgramConfirm(confirmData);
|
|
||||||
|
|
||||||
if (result.status === 'confirmed') {
|
|
||||||
if (showToast) {
|
|
||||||
Taro.hideLoading();
|
|
||||||
Taro.showToast({
|
|
||||||
title: '后台管理登录确认成功',
|
|
||||||
icon: 'success',
|
|
||||||
duration: 2000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
// 显示成功提示弹窗
|
|
||||||
Taro.showModal({
|
|
||||||
title: '登录成功',
|
|
||||||
content: '您已成功确认后台管理系统登录,请在电脑端查看登录状态。',
|
|
||||||
showCancel: false,
|
|
||||||
confirmText: '知道了'
|
|
||||||
});
|
|
||||||
|
|
||||||
} else {
|
|
||||||
throw new Error(result.message || '登录确认失败');
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (error) {
|
|
||||||
if (showToast) {
|
|
||||||
Taro.hideLoading();
|
|
||||||
}
|
|
||||||
|
|
||||||
// 根据错误类型显示不同的提示
|
|
||||||
let errorMessage = '登录确认失败';
|
|
||||||
const errorMsg = error instanceof Error ? error.message : '';
|
|
||||||
if (errorMsg?.includes('过期')) {
|
|
||||||
errorMessage = '二维码已过期,请重新生成';
|
|
||||||
} else if (errorMsg?.includes('无效')) {
|
|
||||||
errorMessage = '无效的登录二维码';
|
|
||||||
} else if (errorMsg) {
|
|
||||||
errorMessage = errorMsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (showToast) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: errorMessage,
|
|
||||||
icon: 'error',
|
|
||||||
duration: 3000
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error(errorMessage);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理礼品卡核销
|
|
||||||
*/
|
|
||||||
const handleGiftVerification = (parsed: ScanResultParsed) => {
|
|
||||||
// 跳转到核销页面,并传递扫码数据
|
|
||||||
const encryptedData = encodeURIComponent(JSON.stringify(parsed.data));
|
|
||||||
navTo(`/user/store/verification?scanData=${encryptedData}`, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理礼品卡兑换
|
|
||||||
*/
|
|
||||||
const handleGiftRedeem = (parsed: ScanResultParsed) => {
|
|
||||||
const { code } = parsed.data;
|
|
||||||
navTo(`/user/gift/redeem?code=${encodeURIComponent(code)}`, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理车辆查询
|
|
||||||
*/
|
|
||||||
const handleVehicleQuery = (parsed: ScanResultParsed) => {
|
|
||||||
const { vehicleId } = parsed.data;
|
|
||||||
navTo(`/hjm/query?id=${vehicleId}`, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 处理未知类型
|
|
||||||
*/
|
|
||||||
const handleUnknownType = (parsed: ScanResultParsed) => {
|
|
||||||
// 显示选择弹窗,让用户选择如何处理
|
|
||||||
Taro.showActionSheet({
|
|
||||||
itemList: [
|
|
||||||
'复制内容',
|
|
||||||
'作为礼品卡兑换码',
|
|
||||||
'作为车辆查询码',
|
|
||||||
'取消'
|
|
||||||
],
|
|
||||||
success: (res) => {
|
|
||||||
const { tapIndex } = res;
|
|
||||||
switch (tapIndex) {
|
|
||||||
case 0:
|
|
||||||
// 复制内容
|
|
||||||
Taro.setClipboardData({
|
|
||||||
data: parsed.rawContent,
|
|
||||||
success: () => {
|
|
||||||
if (showToast) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '已复制到剪贴板',
|
|
||||||
icon: 'success'
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 1:
|
|
||||||
// 作为礼品卡兑换码
|
|
||||||
navTo(`/user/gift/redeem?code=${encodeURIComponent(parsed.rawContent)}`, true);
|
|
||||||
break;
|
|
||||||
|
|
||||||
case 2:
|
|
||||||
// 作为车辆查询码
|
|
||||||
navTo(`/hjm/query?id=${parsed.rawContent}`, true);
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return {
|
|
||||||
startScan,
|
|
||||||
handleScanResult
|
|
||||||
};
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 统一扫码处理组件(如果需要作为组件使用)
|
|
||||||
*/
|
|
||||||
const UniversalScanner: React.FC<UniversalScannerProps> = (props) => {
|
|
||||||
const { startScan } = useUniversalScanner(props);
|
|
||||||
console.log(startScan,'startScan333')
|
|
||||||
// 这个组件主要提供Hook,不渲染UI
|
|
||||||
// 如果需要可以返回一个扫码按钮
|
|
||||||
return null;
|
|
||||||
};
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
export default UniversalScanner;
|
|
||||||
@@ -204,7 +204,7 @@ const AddShopDealerApply = () => {
|
|||||||
...values,
|
...values,
|
||||||
type: 4,
|
type: 4,
|
||||||
realName: values.realName || user?.nickname,
|
realName: values.realName || user?.nickname,
|
||||||
mobile: user?.phone,
|
mobile: values.mobile,
|
||||||
refereeId: 33534,
|
refereeId: 33534,
|
||||||
applyStatus: isEditMode ? 20 : 10,
|
applyStatus: isEditMode ? 20 : 10,
|
||||||
auditTime: undefined,
|
auditTime: undefined,
|
||||||
@@ -338,6 +338,9 @@ const AddShopDealerApply = () => {
|
|||||||
{/*</Form.Item>*/}
|
{/*</Form.Item>*/}
|
||||||
</>
|
</>
|
||||||
)}
|
)}
|
||||||
|
<Form.Item name="userId" label="报备人" initialValue={FormData?.userId} required>
|
||||||
|
选择
|
||||||
|
</Form.Item>
|
||||||
</CellGroup>
|
</CellGroup>
|
||||||
</Form>
|
</Form>
|
||||||
|
|
||||||
|
|||||||
@@ -222,32 +222,7 @@ const Header = (props: any) => {
|
|||||||
onBackClick={() => {
|
onBackClick={() => {
|
||||||
}}
|
}}
|
||||||
left={
|
left={
|
||||||
isLoggedIn ? (
|
<Space>
|
||||||
<View style={{display: 'flex', alignItems: 'center', gap: '8px'}}
|
|
||||||
onClick={() => navTo(`/user/profile/profile`, true)}>
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={getWebsiteLogo()}
|
|
||||||
/>
|
|
||||||
<Text className={'text-white'}>{getWebsiteName()}</Text>
|
|
||||||
<TriangleDown className={'text-white'} size={9}/>
|
|
||||||
</View>
|
|
||||||
) : (
|
|
||||||
<View style={{display: 'flex', alignItems: 'center'}}>
|
|
||||||
<Space>
|
|
||||||
<Avatar
|
|
||||||
size="22"
|
|
||||||
src={getWebsiteLogo()}
|
|
||||||
/>
|
|
||||||
<Text style={{color: '#ffffff'}}>{getWebsiteName()}</Text>
|
|
||||||
<TriangleDown size={9} className={'text-white'}/>
|
|
||||||
</Space>
|
|
||||||
</View>
|
|
||||||
)}
|
|
||||||
right={
|
|
||||||
<Space style={{
|
|
||||||
marginRight: '100px'
|
|
||||||
}}>
|
|
||||||
{/*统一扫码入口 - 支持登录和核销*/}
|
{/*统一扫码入口 - 支持登录和核销*/}
|
||||||
<UnifiedQRButton
|
<UnifiedQRButton
|
||||||
size="small"
|
size="small"
|
||||||
@@ -269,6 +244,20 @@ const Header = (props: any) => {
|
|||||||
</Space>
|
</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>
|
</NavBar>
|
||||||
</>
|
</>
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import navTo from "@/utils/common";
|
|||||||
import {TenantId} from "@/config/app";
|
import {TenantId} from "@/config/app";
|
||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
import {useUserData} from "@/hooks/useUserData";
|
import {useUserData} from "@/hooks/useUserData";
|
||||||
import {useUniversalScanner} from "@/components/UniversalScanner";
|
|
||||||
|
|
||||||
function UserCard() {
|
function UserCard() {
|
||||||
const {
|
const {
|
||||||
@@ -23,17 +22,6 @@ function UserCard() {
|
|||||||
} = useUser();
|
} = useUser();
|
||||||
const {data} = useUserData();
|
const {data} = useUserData();
|
||||||
|
|
||||||
// 统一扫码处理
|
|
||||||
const { startScan } = useUniversalScanner({
|
|
||||||
onScanSuccess: (result) => {
|
|
||||||
console.log('扫码成功:', result);
|
|
||||||
},
|
|
||||||
onScanError: (error) => {
|
|
||||||
console.error('扫码失败:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(startScan, 'startScan')
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
// Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。
|
||||||
Taro.getSetting({
|
Taro.getSetting({
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
export default definePageConfig({
|
export default {
|
||||||
navigationBarTitleText: '门店核销',
|
navigationBarTitleText: '门店核销',
|
||||||
navigationBarTextStyle: 'black'
|
navigationBarTextStyle: 'black'
|
||||||
})
|
}
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import React, {useState, useEffect} from 'react'
|
import React, {useState} from 'react'
|
||||||
import {View, Text, Image} from '@tarojs/components'
|
import {View, Text, Image} from '@tarojs/components'
|
||||||
import {Button, Input} from '@nutui/nutui-react-taro'
|
import {Button, Input} from '@nutui/nutui-react-taro'
|
||||||
import {Scan, Search} from '@nutui/icons-react-taro'
|
import {Scan, Search} from '@nutui/icons-react-taro'
|
||||||
@@ -8,7 +8,6 @@ import {getShopGiftByCode, updateShopGift, decryptQrData} from "@/api/shop/shopG
|
|||||||
import {useUser} from "@/hooks/useUser";
|
import {useUser} from "@/hooks/useUser";
|
||||||
import type {ShopGift} from "@/api/shop/shopGift/model";
|
import type {ShopGift} from "@/api/shop/shopGift/model";
|
||||||
import {isValidJSON} from "@/utils/jsonUtils";
|
import {isValidJSON} from "@/utils/jsonUtils";
|
||||||
import {useUniversalScanner} from "@/components/UniversalScanner";
|
|
||||||
|
|
||||||
const StoreVerification: React.FC = () => {
|
const StoreVerification: React.FC = () => {
|
||||||
const {
|
const {
|
||||||
@@ -19,55 +18,8 @@ const StoreVerification: React.FC = () => {
|
|||||||
const [giftInfo, setGiftInfo] = useState<ShopGift | null>(null)
|
const [giftInfo, setGiftInfo] = useState<ShopGift | null>(null)
|
||||||
const [loading, setLoading] = useState(false)
|
const [loading, setLoading] = useState(false)
|
||||||
|
|
||||||
// 统一扫码处理(仅用于管理员权限检查后的扫码)
|
// 扫码功能
|
||||||
const { startScan } = useUniversalScanner({
|
|
||||||
onScanSuccess: (result) => {
|
|
||||||
console.log('管理员扫码成功:', result);
|
|
||||||
},
|
|
||||||
onScanError: (error) => {
|
|
||||||
console.error('管理员扫码失败:', error);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log(startScan,'startScan222')
|
|
||||||
|
|
||||||
// 页面加载时检查是否有传递的扫码数据
|
|
||||||
useEffect(() => {
|
|
||||||
const handlePageLoad = async () => {
|
|
||||||
try {
|
|
||||||
// 获取页面参数
|
|
||||||
const instance = Taro.getCurrentInstance();
|
|
||||||
const params = instance.router?.params;
|
|
||||||
|
|
||||||
if (params?.scanData) {
|
|
||||||
// 解析传递过来的扫码数据
|
|
||||||
const scanData = JSON.parse(decodeURIComponent(params.scanData));
|
|
||||||
console.log('接收到扫码数据:', scanData);
|
|
||||||
|
|
||||||
if (scanData.businessType === 'gift') {
|
|
||||||
setLoading(true);
|
|
||||||
await handleDecryptAndVerify(scanData.token, scanData.data);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} catch (error) {
|
|
||||||
console.error('处理页面参数失败:', error);
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
handlePageLoad();
|
|
||||||
}, []);
|
|
||||||
|
|
||||||
// 扫码功能(保留原有的直接扫码功能,用于管理员在此页面的直接操作)
|
|
||||||
const handleScan = () => {
|
const handleScan = () => {
|
||||||
// 检查管理员权限
|
|
||||||
if (!isAdmin()) {
|
|
||||||
Taro.showToast({
|
|
||||||
title: '仅管理员可使用核销功能',
|
|
||||||
icon: 'error'
|
|
||||||
});
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Taro.scanCode({
|
Taro.scanCode({
|
||||||
success: (res) => {
|
success: (res) => {
|
||||||
if (res.result) {
|
if (res.result) {
|
||||||
|
|||||||
Reference in New Issue
Block a user