diff --git a/docs/CONFIG.md b/docs/CONFIG.md deleted file mode 100644 index d274f55..0000000 --- a/docs/CONFIG.md +++ /dev/null @@ -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 diff --git a/docs/LOGIN_STATUS_UPDATE_TEST.md b/docs/LOGIN_STATUS_UPDATE_TEST.md deleted file mode 100644 index 1d6a338..0000000 --- a/docs/LOGIN_STATUS_UPDATE_TEST.md +++ /dev/null @@ -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 进一步优化性能 diff --git a/docs/RUNTIME_ERROR_RESOLUTION.md b/docs/RUNTIME_ERROR_RESOLUTION.md deleted file mode 100644 index ca051a3..0000000 --- a/docs/RUNTIME_ERROR_RESOLUTION.md +++ /dev/null @@ -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((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` 中的重复自动登录逻辑,避免代码冗余和潜在冲突。 - -## 状态 -🟢 **已完全解决** - 应用现在可以正常编译和运行,具备完整的自动登录功能 diff --git a/docs/TYPESCRIPT_FIXES.md b/docs/TYPESCRIPT_FIXES.md deleted file mode 100644 index 1072407..0000000 --- a/docs/TYPESCRIPT_FIXES.md +++ /dev/null @@ -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 类型定义 diff --git a/docs/头像昵称同步问题修复说明.md b/docs/头像昵称同步问题修复说明.md deleted file mode 100644 index 0b2d7a6..0000000 --- a/docs/头像昵称同步问题修复说明.md +++ /dev/null @@ -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` 的检查逻辑,我们解决了头像昵称同步问题,确保用户更新信息后能够立即生效,提升了用户体验。 diff --git a/docs/头像昵称显示问题修复说明.md b/docs/头像昵称显示问题修复说明.md deleted file mode 100644 index 1c674a6..0000000 --- a/docs/头像昵称显示问题修复说明.md +++ /dev/null @@ -1,165 +0,0 @@ -# 头像昵称显示问题修复说明 - -## 问题描述 -用户更新头像和昵称后,头部显示变成了"已登录"而不是显示用户的头像和昵称。 - -## 问题分析 - -### 根本原因 -在 Header 组件中,显示逻辑存在以下问题: - -1. **显示逻辑不完整**:头像和昵称的显示只依赖 `user` 状态,没有考虑本地存储的备用数据 -2. **状态同步延迟**:`useUser` hook 中的状态更新可能存在延迟,导致界面显示不及时 -3. **缺少状态监听**:Header 组件没有充分监听用户信息的变化 - -### 具体问题代码 -```typescript -// 原始代码 - 只依赖 user 状态 - -{user?.nickname || '已登录'} // 如果 user.nickname 为空,显示"已登录" -``` - -### 问题场景 -1. 用户更新头像/昵称后,`useUser` hook 的状态可能还没有及时更新 -2. 即使本地存储中已经保存了最新的头像和昵称,界面仍然显示旧的状态 -3. 导致用户看到"已登录"而不是自己的头像和昵称 - -## 修复方案 - -### 1. 优化显示逻辑 -修改头像和昵称的显示逻辑,增加本地存储作为备用数据源: - -```typescript -// 修复后的代码 - 同时检查状态和本地存储 - - - {user?.nickname || Taro.getStorageSync('Nickname') || '已登录'} // 多层备用逻辑 - -``` - -### 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 -// 修改前 - -{user?.nickname || '已登录'} - -// 修改后 - -{user?.nickname || Taro.getStorageSync('Nickname') || '已登录'} -``` - -## 防护机制 - -### 多层备用策略 -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. **易于维护**:逻辑清晰,便于后续维护和扩展 - -现在用户更新头像和昵称后,界面会立即显示正确的信息,不再出现"已登录"的问题。 diff --git a/docs/客户交易页面上拉加载功能实现说明.md b/docs/客户交易页面上拉加载功能实现说明.md deleted file mode 100644 index 1594b87..0000000 --- a/docs/客户交易页面上拉加载功能实现说明.md +++ /dev/null @@ -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条时认为没有更多数据 -- 搜索功能使用服务端搜索,提高性能 -- 所有状态变化都会正确重置页码和列表数据 diff --git a/docs/客户管理上拉加载功能实现说明.md b/docs/客户管理上拉加载功能实现说明.md deleted file mode 100644 index c64ba30..0000000 --- a/docs/客户管理上拉加载功能实现说明.md +++ /dev/null @@ -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条时认为没有更多数据 -- 所有状态切换都会重置页码和列表数据 -- 保持了原有的搜索和筛选功能 diff --git a/docs/报备成功后列表刷新问题修复说明.md b/docs/报备成功后列表刷新问题修复说明.md deleted file mode 100644 index 58d9499..0000000 --- a/docs/报备成功后列表刷新问题修复说明.md +++ /dev/null @@ -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. **易于维护**:代码清晰,逻辑简单 - -现在用户在完成客户报备后,返回列表页面时会自动看到最新的数据,大大提升了应用的用户体验。 diff --git a/docs/用户信息缓存丢失问题修复说明.md b/docs/用户信息缓存丢失问题修复说明.md deleted file mode 100644 index 214855b..0000000 --- a/docs/用户信息缓存丢失问题修复说明.md +++ /dev/null @@ -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) => { - 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. 重启应用,验证数据持久化 - -## 总结 -通过优化数据获取流程和改进存储策略,我们解决了用户信息缓存丢失问题,确保了数据的完整性和一致性,提升了应用的稳定性和用户体验。 diff --git a/docs/签约时间和合同日期选择功能实现说明.md b/docs/签约时间和合同日期选择功能实现说明.md deleted file mode 100644 index fb7c071..0000000 --- a/docs/签约时间和合同日期选择功能实现说明.md +++ /dev/null @@ -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(false) -const [showContractDatePicker, setShowContractDatePicker] = useState(false) -const [applyTime, setApplyTime] = useState('') -const [contractDate, setContractDate] = useState('') -``` - -### 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 - - !isEditMode && setShowApplyTimePicker(true)} - style={{ - cursor: isEditMode ? 'not-allowed' : 'pointer', - opacity: isEditMode ? 0.6 : 1 - }} - > - - - - {applyTime || '请选择签约时间'} - - - - -``` - -### 6. 弹窗选择器 -```typescript -{/* 签约时间选择器 */} - setShowApplyTimePicker(false)} -> - setShowApplyTimePicker(false)} - /> - -``` - -## 核心功能 - -### 📅 日期选择流程 -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. **合理的状态控制**:编辑模式禁用 + 视觉反馈 - -这个实现为用户提供了便捷的日期选择体验,同时保证了数据的准确性和一致性。 diff --git a/docs/统一扫码功能说明.md b/docs/统一扫码功能说明.md deleted file mode 100644 index c4036e5..0000000 --- a/docs/统一扫码功能说明.md +++ /dev/null @@ -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 ( - - ); -} -``` - -### 扫码结果类型 - -```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. **离线支持**:部分功能支持离线处理 diff --git a/docs/订单筛选查询功能说明.md b/docs/订单筛选查询功能说明.md deleted file mode 100644 index 9665730..0000000 --- a/docs/订单筛选查询功能说明.md +++ /dev/null @@ -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. 测试搜索性能,特别是大数据量情况下 diff --git a/java/auto/controller/QrLoginController.java b/java/auto/controller/QrLoginController.java deleted file mode 100644 index d296d82..0000000 --- a/java/auto/controller/QrLoginController.java +++ /dev/null @@ -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()); - } - } - -} diff --git a/java/auto/dto/QrLoginConfirmRequest.java b/java/auto/dto/QrLoginConfirmRequest.java deleted file mode 100644 index f3b423e..0000000 --- a/java/auto/dto/QrLoginConfirmRequest.java +++ /dev/null @@ -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; - } - -} diff --git a/java/auto/dto/QrLoginData.java b/java/auto/dto/QrLoginData.java deleted file mode 100644 index 563bf1d..0000000 --- a/java/auto/dto/QrLoginData.java +++ /dev/null @@ -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; - -} diff --git a/java/auto/dto/QrLoginGenerateResponse.java b/java/auto/dto/QrLoginGenerateResponse.java deleted file mode 100644 index f0b69e5..0000000 --- a/java/auto/dto/QrLoginGenerateResponse.java +++ /dev/null @@ -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; - -} diff --git a/java/auto/dto/QrLoginStatusResponse.java b/java/auto/dto/QrLoginStatusResponse.java deleted file mode 100644 index 1eb0d4a..0000000 --- a/java/auto/dto/QrLoginStatusResponse.java +++ /dev/null @@ -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; - -} diff --git a/java/auto/service/QrLoginService.java b/java/auto/service/QrLoginService.java deleted file mode 100644 index 85ed28f..0000000 --- a/java/auto/service/QrLoginService.java +++ /dev/null @@ -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); - -} diff --git a/java/auto/service/impl/QrLoginServiceImpl.java b/java/auto/service/impl/QrLoginServiceImpl.java deleted file mode 100644 index 34658e8..0000000 --- a/java/auto/service/impl/QrLoginServiceImpl.java +++ /dev/null @@ -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; - } -} diff --git a/src/api/qrLogin/index.ts b/src/api/qrLogin/index.ts deleted file mode 100644 index a2807bb..0000000 --- a/src/api/qrLogin/index.ts +++ /dev/null @@ -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>( - '/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>( - `/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>( - '/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>( - '/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>( - `/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 - }; - } -} diff --git a/src/api/qrLogin/model/index.ts b/src/api/qrLogin/model/index.ts deleted file mode 100644 index c831119..0000000 --- a/src/api/qrLogin/model/index.ts +++ /dev/null @@ -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; -} diff --git a/src/components/UnifiedQRButton.tsx b/src/components/UnifiedQRButton.tsx index 52ba516..7d33fd7 100644 --- a/src/components/UnifiedQRButton.tsx +++ b/src/components/UnifiedQRButton.tsx @@ -1,6 +1,4 @@ import React from 'react'; -import { Button } from '@nutui/nutui-react-taro'; -import { View } from '@tarojs/components'; import { Scan } from '@nutui/icons-react-taro'; import Taro from '@tarojs/taro'; import { useUnifiedQRScan, ScanType, type UnifiedScanResult } from '@/hooks/useUnifiedQRScan'; @@ -29,15 +27,11 @@ export interface UnifiedQRButtonProps { * 支持登录和核销两种类型的二维码扫描 */ const UnifiedQRButton: React.FC = ({ - type = 'success', - size = 'small', - text = '', - showIcon = true, onSuccess, onError, usePageMode = false }) => { - const { startScan, isLoading, canScan, state, result } = useUnifiedQRScan(); + const { startScan, canScan, result } = useUnifiedQRScan(); console.log(result,'useUnifiedQRScan>>result') // 处理点击事件 const handleClick = async () => { @@ -83,43 +77,8 @@ const UnifiedQRButton: React.FC = ({ } }; - 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 ( - + ); }; diff --git a/src/components/UniversalScanner.tsx b/src/components/UniversalScanner.tsx deleted file mode 100644 index 9f01c24..0000000 --- a/src/components/UniversalScanner.tsx +++ /dev/null @@ -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 = (props) => { - const { startScan } = useUniversalScanner(props); - console.log(startScan,'startScan333') - // 这个组件主要提供Hook,不渲染UI - // 如果需要可以返回一个扫码按钮 - return null; -}; - - - -export default UniversalScanner; diff --git a/src/dealer/customer/add.tsx b/src/dealer/customer/add.tsx index 3fc1509..d91c3b0 100644 --- a/src/dealer/customer/add.tsx +++ b/src/dealer/customer/add.tsx @@ -204,7 +204,7 @@ const AddShopDealerApply = () => { ...values, type: 4, realName: values.realName || user?.nickname, - mobile: user?.phone, + mobile: values.mobile, refereeId: 33534, applyStatus: isEditMode ? 20 : 10, auditTime: undefined, @@ -338,6 +338,9 @@ const AddShopDealerApply = () => { {/**/} )} + +选择 + diff --git a/src/pages/index/Header.tsx b/src/pages/index/Header.tsx index 66053b0..04e4472 100644 --- a/src/pages/index/Header.tsx +++ b/src/pages/index/Header.tsx @@ -222,32 +222,7 @@ const Header = (props: any) => { onBackClick={() => { }} left={ - isLoggedIn ? ( - navTo(`/user/profile/profile`, true)}> - - {getWebsiteName()} - - - ) : ( - - - - {getWebsiteName()} - - - - )} - right={ - + {/*统一扫码入口 - 支持登录和核销*/} { } > + {isLoggedIn ? ( + navTo(`/user/profile/profile`, true)}> + {getWebsiteName()} + + ) : ( + + + {getWebsiteName()} + + + )} ) diff --git a/src/pages/user/components/UserCard.tsx b/src/pages/user/components/UserCard.tsx index 9b9bd21..5fefc6e 100644 --- a/src/pages/user/components/UserCard.tsx +++ b/src/pages/user/components/UserCard.tsx @@ -9,7 +9,6 @@ import navTo from "@/utils/common"; import {TenantId} from "@/config/app"; import {useUser} from "@/hooks/useUser"; import {useUserData} from "@/hooks/useUserData"; -import {useUniversalScanner} from "@/components/UniversalScanner"; function UserCard() { const { @@ -23,17 +22,6 @@ function UserCard() { } = useUser(); const {data} = useUserData(); - // 统一扫码处理 - const { startScan } = useUniversalScanner({ - onScanSuccess: (result) => { - console.log('扫码成功:', result); - }, - onScanError: (error) => { - console.error('扫码失败:', error); - } - }); - - console.log(startScan, 'startScan') useEffect(() => { // Taro.getSetting:获取用户的当前设置。返回值中只会出现小程序已经向用户请求过的权限。 Taro.getSetting({ diff --git a/src/user/store/verification.config.ts b/src/user/store/verification.config.ts index 77afd68..729a6f6 100644 --- a/src/user/store/verification.config.ts +++ b/src/user/store/verification.config.ts @@ -1,4 +1,4 @@ -export default definePageConfig({ +export default { navigationBarTitleText: '门店核销', navigationBarTextStyle: 'black' -}) +} diff --git a/src/user/store/verification.tsx b/src/user/store/verification.tsx index f6ce353..cb2cdb6 100644 --- a/src/user/store/verification.tsx +++ b/src/user/store/verification.tsx @@ -1,4 +1,4 @@ -import React, {useState, useEffect} from 'react' +import React, {useState} from 'react' import {View, Text, Image} from '@tarojs/components' import {Button, Input} from '@nutui/nutui-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 type {ShopGift} from "@/api/shop/shopGift/model"; import {isValidJSON} from "@/utils/jsonUtils"; -import {useUniversalScanner} from "@/components/UniversalScanner"; const StoreVerification: React.FC = () => { const { @@ -19,55 +18,8 @@ const StoreVerification: React.FC = () => { const [giftInfo, setGiftInfo] = useState(null) 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 = () => { - // 检查管理员权限 - if (!isAdmin()) { - Taro.showToast({ - title: '仅管理员可使用核销功能', - icon: 'error' - }); - return; - } - Taro.scanCode({ success: (res) => { if (res.result) {