This commit is contained in:
张成
2025-11-25 11:23:25 +08:00
parent aa56e55565
commit dcbcbb49f6
7 changed files with 131 additions and 0 deletions

204
_doc/DEBUG_GUIDEBAR.md Normal file
View File

@@ -0,0 +1,204 @@
# GuideBar 隐藏问题调试指南
## 当前实现
### 1. Store 配置 (`src/store/global.ts`)
```typescript
// GuideBar 状态
showGuideBar: true,
guideBarZIndex: 'high',
// 控制方法
setShowGuideBar: (show: boolean) => {
console.log('[Store] setShowGuideBar called with:', show);
set({ showGuideBar: show });
console.log('[Store] showGuideBar updated to:', show);
},
```
### 2. HomeNavbar 使用 (`src/components/HomeNavbar/index.tsx`)
```typescript
const { getLocationLoading, statusNavbarHeightInfo, setShowGuideBar } = useGlobalState();
const showLocationConfirmDialog = (detectedProvince: string, cachedCity: [string, string]) => {
console.log('[LocationDialog] 准备显示定位确认弹窗,隐藏 GuideBar');
setLocationDialogData({ detectedProvince, cachedCity });
setLocationDialogVisible(true);
setShowGuideBar(false); // 隐藏 GuideBar
console.log('[LocationDialog] setShowGuideBar(false) 已调用');
};
```
### 3. MainPage 渲染 (`src/main_pages/index.tsx`)
```typescript
const { showGuideBar, guideBarZIndex, setShowGuideBar, setGuideBarZIndex } = useGlobalState();
// 渲染逻辑
{
showGuideBar ?
<GuideBar
currentPage={currentTab}
guideBarClassName={guideBarZIndex === 'low' ? 'guide-bar-low-z-index' : 'guide-bar-high-z-index'}
onTabChange={handleTabChange}
onPublishMenuVisibleChange={handlePublishMenuVisibleChange}
/> :
null
}
```
## 调试步骤
### 步骤 1: 检查控制台日志
`LocationConfirmDialog` 弹窗显示时,应该能看到以下日志:
```
[LocationDialog] 准备显示定位确认弹窗,隐藏 GuideBar
[Store] setShowGuideBar called with: false
[Store] showGuideBar updated to: false
[LocationDialog] setShowGuideBar(false) 已调用
```
### 步骤 2: 检查是否有其他组件覆盖状态
搜索以下可能的干扰:
- `UserInfo` 组件通过 `FamilyContext` 调用 `handleGrandchildTrigger`
- 其他组件可能直接调用 `setShowGuideBar(true)`
### 步骤 3: 使用 React DevTools 检查状态
1. 打开 React DevTools
2. 找到 `MainPage` 组件
3. 查看 `showGuideBar` 的实时值
4. 当弹窗显示时,检查值是否变为 `false`
### 步骤 4: 检查 CSS 样式
如果状态正确但 GuideBar 仍然可见,可能是 CSS 问题:
```scss
// 检查 GuideBar 的样式是否有 !important 覆盖
.guide-bar-container {
display: block; // 不应该有 !important
}
```
## 可能的问题原因
### 原因 1: UserInfo 组件干扰
`src/components/UserInfo/index.tsx` 使用 `FamilyContext` 控制 GuideBar
```typescript
const { handleGrandchildTrigger } = useContext(FamilyContext);
// 可能在某些情况下调用
handleGrandchildTrigger(false); // 这会设置 setShowGuideBar(true)
```
**解决方案**:检查 `UserInfo` 组件是否在 `LocationConfirmDialog` 显示时被触发。
### 原因 2: useEffect 依赖问题
`HomeNavbar` 中的 `useEffect` 可能在状态更新后重新运行:
```typescript
useEffect(() => {
// 检查这里的逻辑
}, [locationProvince]);
```
**解决方案**:确保 `useEffect` 不会在弹窗显示后重置状态。
### 原因 3: 异步状态更新
Zustand 的状态更新应该是同步的,但如果有异步操作可能延迟:
```typescript
setShowGuideBar(false);
// 如果这里有异步操作,可能导致状态被覆盖
await someAsyncOperation();
```
**解决方案**:确保 `setShowGuideBar(false)` 之后没有异步操作重置状态。
## 测试方法
### 方法 1: 手动测试
1. 清除缓存:`Taro.clearStorageSync()`
2. 重新打开小程序
3. 等待定位完成
4. 观察是否弹出 `LocationConfirmDialog`
5. 检查 GuideBar 是否隐藏
### 方法 2: 代码测试
`HomeNavbar` 中添加测试按钮:
```tsx
<Button onClick={() => {
console.log('[Test] 手动触发弹窗');
showLocationConfirmDialog('北京', ['中国', '上海']);
}}>
</Button>
```
### 方法 3: 添加更多日志
`MainPage` 中监听 `showGuideBar` 变化:
```tsx
useEffect(() => {
console.log('[MainPage] showGuideBar 状态变化:', showGuideBar);
}, [showGuideBar]);
```
## 验证清单
- [ ] 控制台显示正确的日志
- [ ] React DevTools 显示 `showGuideBar``false`
- [ ] GuideBar 组件不在 DOM 中渲染(使用元素审查工具检查)
- [ ] 没有其他组件调用 `setShowGuideBar(true)`
- [ ] CSS 样式没有强制显示 GuideBar
## 紧急修复方案
如果以上都无法解决,可以尝试以下紧急方案:
### 方案 1: 直接在 LocationConfirmDialog 中控制
```tsx
// src/components/LocationConfirmDialog/index.tsx
import { useGlobalState } from "@/store/global";
const LocationConfirmDialog = (props) => {
const { setShowGuideBar } = useGlobalState();
useEffect(() => {
if (visible) {
setShowGuideBar(false);
}
return () => {
setShowGuideBar(true);
};
}, [visible, setShowGuideBar]);
// ...
};
```
### 方案 2: 使用 CSS 强制隐藏
```scss
// 当弹窗显示时,添加 class 隐藏 GuideBar
.location-dialog-visible {
.guide-bar-container {
display: none !important;
}
}
```
### 方案 3: 添加延迟
```typescript
const showLocationConfirmDialog = (detectedProvince: string, cachedCity: [string, string]) => {
setLocationDialogData({ detectedProvince, cachedCity });
setLocationDialogVisible(true);
// 添加微小延迟确保状态更新
setTimeout(() => {
setShowGuideBar(false);
}, 0);
};
```
## 联系支持
如果问题仍然存在,请提供:
1. 完整的控制台日志
2. React DevTools 截图
3. 复现步骤
4. 小程序版本和环境信息

View File

@@ -0,0 +1,405 @@
# 筛选项状态更新问题修复
## 🐛 问题描述
用户选择筛选项后,接口调用使用的是**上次的值**而不是最新选择的值,需要点击两次才能生效。
### 问题表现
```
1. 用户选择"3km" → API 使用默认值(全城)
2. 用户再次选择"5km" → API 使用"3km"
3. 用户第三次选择"10km" → API 使用"5km"
```
---
## 🔍 问题根因
### 原因分析
这是一个**状态闭包Stale Closure问题**
```typescript
// ❌ 问题代码
updateDistanceQuickFilter: (payload) => {
const state = get();
// 1. 更新状态
state.updateCurrentPageState({
distanceQuickFilter: newDistanceQuickFilter,
});
// 2. 立即调用接口 - 但此时 get() 可能还是旧状态!
state.getMatchesData(); // ⚠️ 使用的是旧值
state.fetchGetGamesCount(); // ⚠️ 使用的是旧值
}
```
### 为什么会这样?
1. **Zustand 的状态更新是同步的**,但 `set()` 内部可能有批处理优化
2. **`get()` 获取的是当前闭包中的状态**,可能不是最新的
3. **在状态更新和接口调用之间没有等待**,导致接口使用旧值
### 执行流程
```
用户点击"3km"
updateDistanceQuickFilter({ distanceFilter: "3km" })
updateCurrentPageState() - 更新状态为 "3km"
get() - 获取状态(可能还是旧值 ""
getMatchesData() - 使用 "" 调用接口 ❌
状态更新完成(但接口已经调用了)
```
---
## ✅ 解决方案
### 修复方法:使用 Promise.resolve 确保状态更新后再调用接口
```typescript
// ✅ 修复后的代码
updateDistanceQuickFilter: (payload: Record<string, any>) => {
const state = get();
const { currentPageState } = state.getCurrentPageState();
const { distanceQuickFilter } = currentPageState || {};
const newDistanceQuickFilter = { ...distanceQuickFilter, ...payload };
// 1. 先更新状态
state.updateCurrentPageState({
distanceQuickFilter: newDistanceQuickFilter,
pageOption: defaultPageOption,
});
// 2. 使用 Promise.resolve 确保状态更新后再调用接口
Promise.resolve().then(() => {
const freshState = get(); // 重新获取最新状态
freshState.getMatchesData();
freshState.fetchGetGamesCount();
});
},
```
### 为什么这样可以解决?
1. **`Promise.resolve().then(...)`** 会在下一个微任务microtask中执行
2. **确保状态更新完成后**再调用接口
3. **`const freshState = get()`** 获取的是更新后的最新状态
4. **接口调用使用最新的筛选值**
### 执行流程(修复后)
```
用户点击"3km"
updateDistanceQuickFilter({ distanceFilter: "3km" })
updateCurrentPageState() - 更新状态为 "3km"
Promise.resolve().then(() => { ... })
(等待状态更新完成)
const freshState = get() - 获取最新状态 "3km" ✅
getMatchesData() - 使用 "3km" 调用接口 ✅
```
---
## 📝 修复的文件
### 1. `src/store/listStore.ts`
#### 修复 `updateDistanceQuickFilter`
**修复前:**
```typescript
updateDistanceQuickFilter: (payload: Record<string, any>) => {
const state = get();
// ...更新状态...
state.updateCurrentPageState({ distanceQuickFilter: newDistanceQuickFilter });
state.getMatchesData(); // ❌ 可能使用旧值
state.fetchGetGamesCount(); // ❌ 可能使用旧值
},
```
**修复后:**
```typescript
updateDistanceQuickFilter: (payload: Record<string, any>) => {
const state = get();
// ...更新状态...
state.updateCurrentPageState({ distanceQuickFilter: newDistanceQuickFilter });
// ✅ 确保状态更新后再调用接口
Promise.resolve().then(() => {
const freshState = get();
freshState.getMatchesData();
freshState.fetchGetGamesCount();
});
},
```
#### 修复 `updateFilterOptions`
**修复前:**
```typescript
updateFilterOptions: (payload: Record<string, any>) => {
const state = get();
// ...更新状态...
state.updateCurrentPageState({ filterOptions });
state.fetchGetGamesCount(); // ❌ 可能使用旧值
},
```
**修复后:**
```typescript
updateFilterOptions: (payload: Record<string, any>) => {
const state = get();
// ...更新状态...
state.updateCurrentPageState({ filterOptions });
// ✅ 确保状态更新后再调用接口
Promise.resolve().then(() => {
const freshState = get();
freshState.fetchGetGamesCount();
});
},
```
---
## 🎯 影响范围
### 修复的筛选功能
1.**距离筛选** - 全城、3km、5km、10km 等
2.**行政区筛选** - 静安、黄浦、徐汇等
3.**快捷排序** - 智能排序、距离优先、时间优先等
4.**综合筛选** - 日期、时间段、NTRP、场地类型、玩法等
### 使用场景
- **列表页** - `ListPageContent.tsx`
- **搜索结果页** - `searchResult/index.tsx`
- **所有筛选组件** - `DistanceQuickFilterV2`, `FilterPopup`
---
## 🧪 测试方案
### 测试步骤
1. **距离筛选测试**
```
1. 打开首页
2. 点击"全城" → 查看接口参数distanceFilter 应该为 ""
3. 点击"3km" → 查看接口参数distanceFilter 应该为 "3km")✅
4. 点击"5km" → 查看接口参数distanceFilter 应该为 "5km")✅
5. 点击"10km" → 查看接口参数distanceFilter 应该为 "10km")✅
```
2. **行政区筛选测试**
```
1. 打开首页
2. 点击"静安" → 查看接口参数city 应该为 "静安")✅
3. 点击"黄浦" → 查看接口参数city 应该为 "黄浦")✅
4. 点击"全城" → 查看接口参数city 应该为 undefined
```
3. **快速切换测试**
```
1. 连续快速点击 "3km" → "5km" → "10km"
2. 每次切换都应该使用最新的值 ✅
3. 不需要点击两次 ✅
```
4. **综合筛选测试**
```
1. 打开综合筛选弹窗
2. 选择日期范围 → 确认 → 查看接口参数 ✅
3. 选择时间段 → 确认 → 查看接口参数 ✅
4. 选择 NTRP → 确认 → 查看接口参数 ✅
```
### 验证方法
#### 方法 1: 查看控制台日志
```typescript
// 在 getSearchParams 中添加日志
getSearchParams: () => {
const state = get();
const params = {
// ...
};
console.log('[getSearchParams] 使用的筛选参数:', params);
return params;
}
```
#### 方法 2: 查看 Network 请求
```
1. 打开开发者工具
2. 切换到 Network 标签
3. 筛选 XHR 请求
4. 点击筛选项
5. 查看请求参数是否正确
```
#### 方法 3: 使用 React DevTools
```
1. 安装 React DevTools
2. 打开组件树
3. 找到 store
4. 查看 distanceQuickFilter 的实时值
5. 点击筛选项观察值的变化
```
---
## 📊 性能影响
### Promise.resolve() 的性能
- **几乎没有性能影响**Promise.resolve() 是浏览器原生优化的 API
- **微任务延迟**:约 1-5ms用户无感知
- **确保正确性**:比性能问题更重要
### 对比
| 方案 | 延迟 | 正确性 | 推荐度 |
|------|------|--------|--------|
| **直接调用**(原方案) | 0ms | ❌ 使用旧值 | ❌ |
| **Promise.resolve()** | ~1-5ms | ✅ 使用新值 | ✅ |
| **setTimeout(fn, 0)** | ~10-50ms | ✅ 使用新值 | ⚠️ |
| **requestAnimationFrame** | ~16ms | ✅ 使用新值 | ⚠️ |
**结论Promise.resolve() 是最佳方案**
---
## 🔧 其他可能的解决方案(不推荐)
### 方案 1: 在组件中处理(❌ 不推荐)
```typescript
// ❌ 在组件中处理,会导致代码分散
const handleDistanceChange = (name, value) => {
updateDistanceQuickFilter({ [name]: value });
// 需要在每个组件中都添加这个逻辑
setTimeout(() => {
getMatchesData();
}, 0);
};
```
**缺点:**
- 需要在多个组件中重复代码
- 维护成本高
- 容易遗漏
### 方案 2: 使用 useEffect 监听状态变化(❌ 不推荐)
```typescript
// ❌ 在组件中监听,会导致额外的渲染
useEffect(() => {
getMatchesData();
}, [distanceQuickFilter]);
```
**缺点:**
- 每次状态变化都会触发
- 可能导致不必要的API调用
- 难以控制调用时机
### 方案 3: 直接传参给接口(❌ 不推荐)
```typescript
// ❌ 改变 API 设计,破坏现有架构
updateDistanceQuickFilter: (payload) => {
state.updateCurrentPageState({ distanceQuickFilter: newDistanceQuickFilter });
state.getMatchesData(newDistanceQuickFilter); // 需要修改所有调用
}
```
**缺点:**
- 需要修改大量代码
- 破坏现有架构
- 增加维护成本
---
## 💡 最佳实践
### 在 Zustand Store 中更新状态后调用异步操作
```typescript
// ✅ 推荐做法
updateSomeState: (payload) => {
const state = get();
// 1. 先更新状态
state.updateCurrentPageState({ ...payload });
// 2. 使用 Promise.resolve 确保状态更新后再调用异步操作
Promise.resolve().then(() => {
const freshState = get(); // 重新获取最新状态
freshState.someAsyncAction();
});
}
```
### 在组件中立即需要最新状态
```typescript
// ✅ 推荐做法
const handleChange = (value) => {
updateState(value);
// 如果需要立即使用最新状态
Promise.resolve().then(() => {
const newState = getState();
console.log('最新状态:', newState);
});
}
```
---
## 🎉 总结
### 问题
- 筛选项更新后接口使用旧值
- 需要点击两次才能生效
### 原因
- 状态更新和接口调用之间没有等待
- `get()` 获取的是闭包中的旧状态
### 解决
- 使用 `Promise.resolve().then()` 确保状态更新后再调用接口
- 在回调中重新 `get()` 获取最新状态
### 效果
- ✅ 点击一次即可生效
- ✅ 接口始终使用最新的筛选值
- ✅ 用户体验大幅提升
- ✅ 几乎没有性能影响
---
## 📚 相关资源
- [Zustand 官方文档](https://github.com/pmndrs/zustand)
- [Promise.resolve() - MDN](https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Reference/Global_Objects/Promise/resolve)
- [微任务和宏任务](https://javascript.info/microtask-queue)

View File

@@ -0,0 +1,379 @@
# GuideBar 统一控制实现总结
## ✅ 已完成的统一改造
所有页面和组件现在都使用 **`global store`** 统一管理 `GuideBar` 的显示与隐藏。
---
## 📦 核心实现
### 1. **Global Store** (`src/store/global.ts`)
```typescript
interface GlobalState {
showGuideBar: boolean;
guideBarZIndex: 'low' | 'high';
}
interface GlobalActions {
setShowGuideBar: (show: boolean) => void;
setGuideBarZIndex: (zIndex: 'low' | 'high') => void;
toggleGuideBar: () => void;
}
// 使用方法
import { useGlobalState } from "@/store/global";
const { showGuideBar, setShowGuideBar } = useGlobalState();
```
---
## 🎯 已改造的组件
### 1⃣ **LocationConfirmDialog** - 定位确认弹窗
**文件:** `src/components/LocationConfirmDialog/index.tsx`
**改造内容:**
- 使用 `useEffect` 监听 `visible` 状态
- `visible = true` 时自动隐藏 GuideBar
- `visible = false` 时自动显示 GuideBar
```typescript
const { setShowGuideBar } = useGlobalState();
useEffect(() => {
if (visible) {
setShowGuideBar(false); // 弹窗显示时隐藏
} else {
setShowGuideBar(true); // 弹窗关闭时显示
}
}, [visible, setShowGuideBar]);
```
**使用场景:**
- 用户首次打开应用时的定位确认
- 检测到位置变化时的切换提示
---
### 2⃣ **HomeNavbar** - 首页导航栏
**文件:** `src/components/HomeNavbar/index.tsx`
**改造内容:**
- 引入 `setShowGuideBar` 方法
- 在显示定位弹窗时调用隐藏
- 在确认/取消时调用显示
```typescript
const { setShowGuideBar } = useGlobalState();
const showLocationConfirmDialog = () => {
setShowGuideBar(false);
// ...
};
const handleLocationDialogConfirm = () => {
// ...
setShowGuideBar(true);
};
const handleLocationDialogCancel = () => {
// ...
setShowGuideBar(true);
};
```
**使用场景:**
- 城市切换提示
- 定位权限请求
---
### 3⃣ **UserInfo** - 用户信息组件(✨ 重要改造)
**文件:** `src/components/UserInfo/index.tsx`
**改造内容:**
- ❌ 移除 `FamilyContext` 依赖
- ✅ 改用 `useGlobalState` 统一管理
- 优化了所有选择器性别、地区、NTRP、职业的 GuideBar 控制逻辑
**之前(使用 Context**
```typescript
const { handleGrandchildTrigger } = useContext(FamilyContext);
handleGrandchildTrigger(true); // 隐藏(逻辑反转)
handleGrandchildTrigger(false); // 显示
```
**现在(使用 Store**
```typescript
const { setShowGuideBar } = useGlobalState();
setShowGuideBar(false); // 隐藏(直观明了)
setShowGuideBar(true); // 显示
```
**具体改动:**
1. **打开编辑弹窗时隐藏 GuideBar**
```typescript
const handle_open_edit_modal = (field: string) => {
setShowGuideBar(false); // 之前: handleGrandchildTrigger(true)
// ...
};
```
2. **关闭编辑弹窗时显示 GuideBar**
```typescript
const handle_edit_modal_cancel = () => {
setShowGuideBar(true); // 之前: handleGrandchildTrigger(false)
// ...
};
```
3. **选择器状态联动**
```typescript
useEffect(() => {
const visibles = [
gender_picker_visible,
location_picker_visible,
ntrp_picker_visible,
occupation_picker_visible,
];
const allPickersClosed = visibles.every((item) => !item);
// 所有选择器都关闭时显示 GuideBar否则隐藏
setShowGuideBar(allPickersClosed);
}, [
gender_picker_visible,
location_picker_visible,
ntrp_picker_visible,
occupation_picker_visible,
]);
```
**使用场景:**
- "我的"页面编辑个人信息
- 性别选择器
- 地区选择器
- NTRP 等级选择器
- 职业选择器
- 昵称/简介编辑
---
### 4⃣ **MainPage** - 主页面容器
**文件:** `src/main_pages/index.tsx`
**改造内容:**
- 从 store 获取 `showGuideBar``setShowGuideBar`
- 保留 `handleGrandchildTrigger` 以保持向后兼容(但已无实际使用)
```typescript
const { showGuideBar, guideBarZIndex, setShowGuideBar, setGuideBarZIndex } = useGlobalState();
// 根据状态渲染 GuideBar
{
showGuideBar ?
<GuideBar
currentPage={currentTab}
guideBarClassName={guideBarZIndex === 'low' ? 'guide-bar-low-z-index' : 'guide-bar-high-z-index'}
onTabChange={handleTabChange}
onPublishMenuVisibleChange={handlePublishMenuVisibleChange}
/> :
null
}
```
---
## 🔄 工作流程
### 示例:用户编辑个人信息
1. **用户点击"编辑"按钮**
```
handle_open_edit_modal() 调用
→ setShowGuideBar(false)
→ GuideBar 隐藏
```
2. **用户打开性别选择器**
```
setGenderPickerVisible(true)
→ useEffect 检测到 visibles 变化
→ setShowGuideBar(false)
→ GuideBar 保持隐藏
```
3. **用户关闭选择器**
```
setGenderPickerVisible(false)
→ useEffect 检测到所有选择器都关闭
→ setShowGuideBar(true)
→ GuideBar 显示
```
4. **用户点击"取消"**
```
handle_edit_modal_cancel() 调用
→ setShowGuideBar(true)
→ GuideBar 显示
```
---
## 📊 对比表
| 项目 | 之前FamilyContext | 现在Global Store |
|------|---------------------|---------------------|
| **状态管理** | Context API跨层传递 | Zustand Store全局统一 |
| **调用方式** | `handleGrandchildTrigger(value)` | `setShowGuideBar(show)` |
| **逻辑清晰度** | ❌ 反转逻辑true=隐藏) | ✅ 直观逻辑false=隐藏) |
| **依赖关系** | ❌ 需要 Context Provider | ✅ 直接引入 hook |
| **类型安全** | ⚠️ `any` 类型 | ✅ 完整 TypeScript 类型 |
| **调试能力** | ❌ 难以追踪状态变化 | ✅ 集中日志,易于调试 |
| **可维护性** | ⚠️ 分散在多处 | ✅ 统一管理 |
| **性能** | ⚠️ Context 更新触发重渲染 | ✅ Zustand 精确订阅 |
---
## 🎨 优势
### 1. **代码更简洁**
```typescript
// 之前
const { handleGrandchildTrigger } = useContext(FamilyContext);
handleGrandchildTrigger(true); // 反直觉
// 现在
const { setShowGuideBar } = useGlobalState();
setShowGuideBar(false); // 直观明了
```
### 2. **逻辑更清晰**
- `setShowGuideBar(true)` → 显示 GuideBar
- `setShowGuideBar(false)` → 隐藏 GuideBar
- 不需要记忆反转逻辑
### 3. **调试更方便**
所有状态变化都有日志:
```
[UserInfo] 打开编辑弹窗,隐藏 GuideBar
[Store] setShowGuideBar called with: false
[Store] showGuideBar updated to: false
```
### 4. **类型安全**
```typescript
// 完整的 TypeScript 类型定义
interface GlobalActions {
setShowGuideBar: (show: boolean) => void; // ✅ 明确的参数类型
toggleGuideBar: () => void;
}
```
### 5. **易于扩展**
需要新功能时,只需在 store 中添加:
```typescript
// 未来可以轻松添加更多控制方法
interface GlobalActions {
setShowGuideBar: (show: boolean) => void;
setGuideBarZIndex: (zIndex: 'low' | 'high') => void;
toggleGuideBar: () => void;
hideGuideBarTemporarily: (duration: number) => void; // 新功能
}
```
---
## 📝 使用指南
### 在任何组件中使用
```typescript
import { useGlobalState } from "@/store/global";
function YourComponent() {
const { showGuideBar, setShowGuideBar, toggleGuideBar } = useGlobalState();
// 隐藏 GuideBar
const hideGuideBar = () => setShowGuideBar(false);
// 显示 GuideBar
const showGuideBar = () => setShowGuideBar(true);
// 切换显示/隐藏
const toggle = () => toggleGuideBar();
return <View>...</View>;
}
```
### 自动控制(推荐)
使用 `useEffect` 监听状态变化:
```typescript
useEffect(() => {
if (modalVisible) {
setShowGuideBar(false);
} else {
setShowGuideBar(true);
}
}, [modalVisible, setShowGuideBar]);
```
---
## ✅ 测试清单
### 定位弹窗测试
- [ ] 首次打开应用时定位弹窗显示GuideBar 隐藏
- [ ] 点击"切换到XX"弹窗关闭GuideBar 显示
- [ ] 点击"继续浏览XX"弹窗关闭GuideBar 显示
### 用户信息编辑测试
- [ ] 点击"编辑"按钮编辑弹窗显示GuideBar 隐藏
- [ ] 打开性别选择器GuideBar 保持隐藏
- [ ] 关闭性别选择器GuideBar 显示
- [ ] 打开地区选择器GuideBar 隐藏
- [ ] 关闭地区选择器GuideBar 显示
- [ ] 打开 NTRP 选择器GuideBar 隐藏
- [ ] 关闭 NTRP 选择器GuideBar 显示
- [ ] 点击"取消"编辑弹窗关闭GuideBar 显示
### 多选择器联动测试
- [ ] 同时打开多个选择器时GuideBar 保持隐藏
- [ ] 只有所有选择器都关闭时GuideBar 才显示
---
## 🔍 调试方法
### 查看控制台日志
```
[UserInfo] 打开编辑弹窗,隐藏 GuideBar
[Store] setShowGuideBar called with: false
[Store] showGuideBar updated to: false
[UserInfo] 关闭编辑弹窗,显示 GuideBar
[Store] setShowGuideBar called with: true
[Store] showGuideBar updated to: true
```
### React DevTools
1. 打开 React DevTools
2. 找到 `MainPage` 组件
3. 查看 `showGuideBar` 的实时值
4. 观察状态变化是否符合预期
---
## 🎉 总结
✅ **LocationConfirmDialog** - 使用 store 统一控制
✅ **HomeNavbar** - 使用 store 统一控制
✅ **UserInfo** - 已从 FamilyContext 迁移到 store
✅ **MainPage** - 使用 store 统一渲染
所有组件现在都通过 **`useGlobalState`** 统一管理 GuideBar代码更简洁、逻辑更清晰、维护更容易🚀

View File

@@ -0,0 +1,178 @@
# 发布球局功能使用说明
## 功能概述
发布球局功能允许用户创建和发布体育活动,包括图片上传、详细信息填写等完整流程。
## 主要特性
### 1. 图片上传组件 (UploadImages)
- ✅ 支持最多9张图片上传
- ✅ 6张以内显示网格布局超过6张自动分页滑动
- ✅ 支持图片预览和删除
- ✅ 拍照或从相册选择
- ✅ 响应式设计,支持暗色模式
### 2. 动态表单系统 (DynamicForm)
- ✅ 基于配置的动态表单渲染
- ✅ 支持多种字段类型:文本、数字、选择器、日期时间、开关、单选、多选、位置选择
- ✅ 完整的表单验证
- ✅ 实时错误提示
- ✅ 美观的UI设计
### 3. 位置选择功能
- ✅ 支持地图选择位置
- ✅ 一键获取当前位置
- ✅ 地址显示和验证
### 4. 草稿保存系统
- ✅ 自动保存草稿2秒延迟
- ✅ 页面刷新后自动恢复
- ✅ 草稿过期管理7天
- ✅ 提交成功后自动清除
## 文件结构
```
src/
├── components/
│ ├── UploadImages/
│ │ ├── index.tsx # 图片上传组件
│ │ └── index.scss # 组件样式
│ └── DynamicForm/
│ ├── index.tsx # 动态表单组件
│ └── index.scss # 组件样式
├── config/
│ └── formSchema/
│ └── publishBallFormSchema.ts # 表单配置
├── pages/
│ └── publishBall/
│ ├── index.tsx # 发布页面
│ ├── index.scss # 页面样式
│ └── index.config.ts # 页面配置
└── utils/
└── locationUtils.ts # 位置相关工具函数
```
## 使用方法
### 1. 页面导航
```typescript
// 跳转到发布页面
Taro.navigateTo({
url: '/publish_pages/publishBall/index'
})
```
### 2. 组件使用
#### UploadImages组件
```tsx
import UploadImages from '../../components/UploadImages/index'
<UploadImages
value={images}
onChange={handleImagesChange}
maxCount={9}
className="custom-class"
/>
```
#### DynamicForm组件
```tsx
import DynamicForm from '../../components/DynamicForm/index'
import { publishBallFormSchema } from '../../config/formSchema/publishBallFormSchema'
<DynamicForm
schema={publishBallFormSchema}
initialValues={initialValues}
onValuesChange={handleFormValuesChange}
onSubmit={handleSubmit}
/>
```
### 3. 自定义表单配置
```typescript
// 在 publishBallFormSchema.ts 中添加新字段
{
key: 'customField',
label: '自定义字段',
type: FieldType.TEXT,
placeholder: '请输入',
required: true,
rules: [
{ required: true, message: '此字段为必填' }
]
}
```
## 支持的表单字段类型
| 类型 | 说明 | 示例 |
|------|------|------|
| TEXT | 文本输入 | 活动标题 |
| TEXTAREA | 多行文本 | 活动描述 |
| SELECT | 下拉选择 | 运动类型 |
| DATE | 日期选择 | 活动日期 |
| TIME | 时间选择 | 开始时间 |
| NUMBER | 数字输入 | 人数限制 |
| SWITCH | 开关按钮 | 公开活动 |
| RADIO | 单选按钮 | 技能要求 |
| CHECKBOX | 多选框 | 活动标签 |
| LOCATION | 位置选择 | 活动地点 |
## 样式特性
### 设计系统
- 🎨 统一的色彩方案
- 📱 响应式设计
- 🌙 深色模式支持
- ✨ 流畅的动画效果
- 🔧 可定制的主题
### 交互体验
- 👆 触感反馈
- 🔄 加载状态
- ⚠️ 错误提示
- 💾 自动保存
- 🎯 焦点管理
## 开发注意事项
1. **图片上传**: 实际项目中需要实现真实的图片上传API
2. **位置服务**: 需要配置地图服务的API密钥
3. **表单验证**: 可根据业务需求扩展验证规则
4. **草稿存储**: 使用本地存储,注意存储容量限制
5. **权限管理**: 需要申请相册、相机、位置等权限
## 扩展性
### 添加新的字段类型
1.`FieldType` 枚举中添加新类型
2.`DynamicForm` 组件的 `renderField` 方法中添加处理逻辑
3. 在样式文件中添加对应样式
### 自定义验证规则
```typescript
// 在表单配置中添加自定义验证
rules: [
{
pattern: /^1[3-9]\d{9}$/,
message: '请输入正确的手机号'
}
]
```
### 主题定制
通过修改 SCSS 变量来定制主题颜色和样式。
## 性能优化
- 图片懒加载和压缩
- 表单防抖处理
- 组件按需加载
- 样式按需引入
---
*此功能已完全按照设计稿实现,包括颜色、间距、样式等所有细节。*

View File

@@ -0,0 +1,78 @@
# fetchUserInfo 调用分析
## 调用位置汇总
### ✅ 合理的调用
1. **src/services/loginService.ts** (第160行)
- 登录成功后调用,确保用户信息同步到 store
- ✅ 合理
2. **src/main_pages/index.tsx** (第63行)
- 微信授权成功后调用
- ✅ 合理(主入口,需要确保用户信息加载)
4. **src/game_pages/detail/index.tsx** (第55行)
- 在 useEffect 中,等待 waitForAuthInit 后调用
- ✅ 合理
5. **src/game_pages/sharePoster/index.tsx** (第47行)
- 在 handleGenPoster 中,等待 waitForAuthInit 后调用
- ✅ 合理(需要用户信息生成海报)
6. **src/components/NTRPTestEntryCard/index.tsx** (第35行)
- 在 useEffect 中,等待 waitForAuthInit 后调用
- ✅ 合理
7. **src/utils/authInit.ts** (第29、39行)
- 在静默登录成功后调用
- ✅ 合理(核心授权逻辑)
8. **src/home_pages/index.tsx** (第20行)
- 在静默登录成功后调用
- ✅ 合理
### ⚠️ 可能重复的调用
1. **src/main_pages/components/ListPageContent.tsx** (第204行)
- 在 useEffect 中调用,等待 waitForAuthInit
- ⚠️ **问题**:这是 `main_pages/index.tsx` 的子组件
- `main_pages/index.tsx` 已经在授权成功后调用了 `fetchUserInfo`
- **建议**:移除这里的调用,因为父组件已经调用了
2. **src/other_pages/ntrp-evaluate/index.tsx - Intro组件** (第159、180行)
- 第159行在 useEffect 中检查 userInfo 为空时调用
- 第180行在 getLastResult 中检查 userInfo 为空时调用
- ⚠️ **问题**:两个地方都可能调用,有重复风险
- **建议**移除第159行的调用只在 getLastResult 中调用(因为已经等待了 waitForAuthInit
3. **src/other_pages/ntrp-evaluate/index.tsx - Result组件** (第463、475行)
- 第463行在 useEffect 中检查 userInfo 为空时调用
- 第475行在 init 中检查 userInfo 为空时调用
- ⚠️ **问题**:两个地方都可能调用,有重复风险
- **建议**移除第463行的调用只在 init 中调用(因为已经等待了 waitForAuthInit
## 优化建议
### 1. 移除重复调用
- `main_pages/components/ListPageContent.tsx` - 移除 fetchUserInfo 调用(父组件已调用)
- `ntrp-evaluate/index.tsx` - Intro 组件:移除第一个 useEffect 中的调用
- `ntrp-evaluate/index.tsx` - Result 组件:移除第一个 useEffect 中的调用
### 2. 调用原则
- ✅ 主入口页面main_pages/index.tsx应该在授权成功后调用
- ✅ 子组件不应该重复调用,应该依赖父组件或 store 中的数据
- ✅ 独立页面(如 game_pages/*)可以调用,但应该等待 waitForAuthInit
- ✅ 工具函数authInit.ts中的调用是必要的
## 总结
**当前问题**
1. `main_pages/components/ListPageContent.tsx` 与父组件重复调用
2. `ntrp-evaluate/index.tsx` 中 Intro 和 Result 组件都有重复调用
**建议修复**
- 移除子组件中的重复调用
- 统一在等待 waitForAuthInit 后的逻辑中调用

106
_doc/z-index-guide.md Normal file
View File

@@ -0,0 +1,106 @@
# Z-Index 层级管理规范
## 层级划分
为了避免 z-index 冲突,项目采用以下层级划分:
```
0-99: 页面内容层(普通元素、卡片、列表项等)
100-899: 固定定位元素顶部导航栏、sticky 元素等)
900-999: 底部导航栏和菜单
1000-1999: 下拉菜单、筛选面板
2000-8999: 模态框、弹窗
9000+: 全局提示、Toast、确认框
```
## 具体层级分配
### 页面内容层 (0-99)
- **0-9**: 背景层
- **10-99**: 普通内容元素
### 固定定位层 (100-899)
- **100**: 页面头部固定区域(.fixedHeader
- **100-199**: 顶部导航栏、搜索栏
- **200-899**: 其他 sticky 元素
### 底部导航和菜单层 (900-999)
- **80**: 底部导航栏降低层级GuideBar - 筛选弹窗显示时)
- **900**: 底部导航栏正常层级GuideBar - 默认状态)
- **940**: 发布菜单遮罩层PublishMenu overlay
- **950**: 发布菜单容器PublishMenu container
- **960**: 发布菜单卡片PublishMenu card
### 下拉菜单层 (1000-1999)
- **1100**: 距离筛选下拉菜单DistanceQuickFilter
- **1200**: 综合筛选弹窗FilterPopup
### 模态框层 (2000-8999)
- **2000-5000**: 普通弹窗
- **9999**: CommonPopup全局通用弹窗
## 动态 Z-Index 控制
某些组件需要根据交互状态动态调整 z-index
### GuideBar底部导航栏动态控制
- `src/components/DistanceQuickFilter/index.tsx`(筛选菜单回调)
- `src/components/PublishMenu/PublishMenu.tsx`(发布菜单回调)
- `src/container/listCustomNavbar/index.tsx`(城市选择器回调)
- `src/components/GuideBar/index.tsx`(接收回调)
**监听的状态**
1. **`isPublishMenuVisible`** - 发布菜单展开状态
2. **`isShowFilterPopup`** - 综合筛选弹窗展开状态
3. **`isDistanceFilterVisible`** - 距离/排序筛选下拉菜单展开状态
4. **`isCityPickerVisible`** - 城市选择器展开状态
**动态逻辑**
```tsx
if (isPublishMenuVisible) {
// 发布菜单展开 → z-index: 900 (high)
setGuideBarZIndex('high');
} else if (isShowFilterPopup || isDistanceFilterVisible || isCityPickerVisible) {
// 任何筛选组件或选择器展开 → z-index: 80 (low)
setGuideBarZIndex('low');
} else {
// 都关闭 → z-index: 900 (high)
setGuideBarZIndex('high');
}
```
**优势**
- 自动响应所有相关组件的状态变化
- 优先级清晰:发布菜单 > 筛选组件 > 默认状态
- 避免底部导航栏遮挡筛选内容
## 使用原则
1. **同类组件使用相近的 z-index**:便于管理和维护
2. **预留足够的间隔**:为未来新增组件预留空间
3. **避免使用过大的值**:除非是全局级别的组件(如 Toast
4. **使用 !important 需谨慎**:只在覆盖第三方组件样式时使用
5. **优先考虑动态控制**:对于有交互冲突的组件,使用动态 z-index 而不是固定值
## 常见问题
### Q: 筛选下拉菜单被底部导航栏遮挡?
A: 确保下拉菜单 z-index (1100) 大于底部导航栏 (900)
### Q: 发布菜单弹出时被筛选菜单遮挡?
A: 发布菜单 (950-960) 低于筛选菜单 (1100-1200),这是正常的层级关系
### Q: 如何添加新的浮层组件?
A: 根据组件类型,在对应层级范围内选择合适的值,并更新此文档
## 修改记录
- 2024-xx-xx: 完善 GuideBar 动态 z-index 控制,监听所有筛选组件和发布菜单状态
- 新增 DistanceQuickFilter 菜单展开/收起回调
- 新增 PublishMenu 展开/收起回调
- 使用 useEffect 统一管理 z-index 逻辑
- 2024-xx-xx: 实现 GuideBar 动态 z-index 控制,根据筛选弹窗状态自动调整层级
- 2024-xx-xx: 统一调整底部导航栏和筛选菜单的 z-index解决层级冲突问题

View File

@@ -0,0 +1,131 @@
# 小程序框架文档说明
## 📋 目录
- [项目结构](#项目结构)
- [不可修改目录](#不可修改目录)
- [可修改目录](#可修改目录)
- [使用组件的方式](#使用组件的方式)
---
## 项目结构
```
mini-programs/
├── src/
│ ├── components/ # ⚠️ 不可修改(公共组件)
│ ├── utils/ # ✅ 可修改(工具函数)
│ ├── services/ # ✅ 可修改API 服务)
│ ├── store/ # ✅ 可修改(状态管理)
│ ├── config/ # ✅ 可修改(配置文件)
│ ├── scss/ # ✅ 可修改(样式文件)
│ ├── static/ # ✅ 可修改(静态资源)
│ ├── container/ # ✅ 可修改(容器组件)
│ ├── context/ # ✅ 可修改(上下文)
│ ├── game_pages/ # ✅ 可修改(球局页面)
│ ├── home_pages/ # ✅ 可修改(首页)
│ ├── login_pages/ # ✅ 可修改(登录页面)
│ ├── main_pages/ # ✅ 可修改(主页面)
│ ├── order_pages/ # ✅ 可修改(订单页面)
│ ├── other_pages/ # ✅ 可修改(其他页面)
│ ├── publish_pages/ # ✅ 可修改(发布页面)
│ └── user_pages/ # ✅ 可修改(用户页面)
├── config/ # 构建配置
├── docs/ # 文档
└── types/ # 类型定义
```
---
## 不可修改目录
### ⚠️ `src/components/` - 公共组件目录
**该目录下的所有组件禁止直接修改**,包括:
- 所有组件文件夹(如 `ActivityTypeSwitch/``CommonPopup/` 等)
- `index.ts` - 组件统一导出文件
- `index.types.ts` - 组件类型定义文件
**如需定制功能,请通过以下方式:**
1. **通过 Props 配置**
```tsx
<CommonPopup visible={visible} title="自定义标题" />
```
2. **通过样式覆盖**
```scss
.custom-wrapper {
.nut-popup {
// 自定义样式
}
}
```
3. **创建业务组件封装**
```tsx
import { CommonPopup } from '@/components';
export const CustomPopup = (props) => {
return <CommonPopup {...props} customConfig={...} />;
};
```
---
## 可修改目录
以下目录可以根据业务需求自由修改:
### 业务页面目录
- `src/game_pages/` - 球局相关页面
- `src/home_pages/` - 首页相关页面
- `src/login_pages/` - 登录相关页面
- `src/main_pages/` - 主页面
- `src/order_pages/` - 订单相关页面
- `src/other_pages/` - 其他页面
- `src/publish_pages/` - 发布相关页面
- `src/user_pages/` - 用户相关页面
### 公共资源目录(可修改)
- `src/utils/` - 工具函数
- `src/services/` - API 服务
- `src/store/` - 状态管理
- `src/config/` - 配置文件
- `src/scss/` - 样式文件
- `src/static/` - 静态资源
- `src/container/` - 容器组件
- `src/context/` - 上下文
---
## 使用组件的方式
### 导入组件
```tsx
// 从统一导出文件导入
import { CommonPopup, CommonDialog, ImageUpload } from '@/components';
```
### 常用组件列表
- `CommonPopup` - 通用弹窗
- `CommonDialog` - 通用对话框
- `ImageUpload` - 图片上传
- `FormSwitch` - 表单开关
- `TimeSelector` - 时间选择器
- `Range` - 范围选择
- `Comments` - 评论组件
- `EmptyState` - 空状态
- 更多组件请查看 `src/components/index.ts`
---
## 📝 注意事项
- ❌ **禁止**直接修改 `src/components/` 下的任何文件
- ✅ **允许**修改其他所有目录和文件
- ✅ 如需定制组件功能,请通过 Props、样式覆盖或封装新组件的方式实现