生产管理系统 - 页面上路由参数缓存
This commit is contained in:
@@ -1474,4 +1474,539 @@ SearchFormPagination 组件通过配置驱动的方式,极大地简化了复
|
|||||||
- **类型安全**:完整的 TypeScript 支持
|
- **类型安全**:完整的 TypeScript 支持
|
||||||
- **功能专注**:专注搜索、展示、分页核心功能,避免过度设计
|
- **功能专注**:专注搜索、展示、分页核心功能,避免过度设计
|
||||||
|
|
||||||
该组件可以作为项目中所有数据展示页面的标准解决方案,显著提升开发效率和代码质量。
|
该组件可以作为项目中所有数据展示页面的标准解决方案,显著提升开发效率和代码质量。
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## path:src/components/common/searchFormPagination,name:URL参数同步功能集成规范
|
||||||
|
|
||||||
|
### 功能概述
|
||||||
|
|
||||||
|
URL参数同步功能是 SearchFormPagination 组件的高级特性,能够自动将用户的搜索条件、分页状态与浏览器URL参数保持同步,实现页面刷新后状态的恢复,提升用户体验。
|
||||||
|
|
||||||
|
### 设计原则
|
||||||
|
|
||||||
|
#### 1. 职责分离原则
|
||||||
|
|
||||||
|
**子组件职责**(SearchFormPagination):
|
||||||
|
- ✅ UI 显示:渲染搜索表单、数据表格、分页组件
|
||||||
|
- ✅ 状态管理:管理内部搜索条件和分页状态
|
||||||
|
- ✅ URL 同步:自动同步状态到 URL 参数
|
||||||
|
- ✅ 状态通知:通过回调通知父组件状态变化
|
||||||
|
- ✅ 参数推导:自动从 searchFields 推导 URL 参数映射
|
||||||
|
|
||||||
|
**父组件职责**(业务页面):
|
||||||
|
- ✅ 查询逻辑:处理所有查询相关的业务逻辑
|
||||||
|
- ✅ 数据管理:管理数据、加载状态、错误处理
|
||||||
|
- ✅ API 调用:调用后端接口获取数据
|
||||||
|
|
||||||
|
#### 2. 自动参数推导原则
|
||||||
|
|
||||||
|
URL 同步功能会自动从 `searchFields` 配置中提取参数映射,无需手动配置:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// searchFields 配置
|
||||||
|
const searchFields = [
|
||||||
|
{ key: 'search', label: '搜索', type: 'text' },
|
||||||
|
{ key: 'status', label: '状态', type: 'select' },
|
||||||
|
{ key: 'type', label: '类型', type: 'select' }
|
||||||
|
];
|
||||||
|
|
||||||
|
// 自动推导出 URL 参数映射
|
||||||
|
// {
|
||||||
|
// page: 'page',
|
||||||
|
// size: 'size',
|
||||||
|
// search: 'search', // 来自 searchFields[0].key
|
||||||
|
// status: 'status', // 来自 searchFields[1].key
|
||||||
|
// type: 'type' // 来自 searchFields[2].key
|
||||||
|
// }
|
||||||
|
|
||||||
|
// 生成的 URL:?page=1&size=10&search=张三&status=active&type=admin
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 可选启用原则
|
||||||
|
|
||||||
|
URL 同步功能是完全可选的,不影响现有使用方式:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 不启用 URL 同步(默认行为)
|
||||||
|
<SearchFormPagination
|
||||||
|
searchFields={searchFields}
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
// ... 其他 props
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 启用 URL 同步 - 极简配置
|
||||||
|
<SearchFormPagination
|
||||||
|
searchFields={searchFields}
|
||||||
|
columns={columns}
|
||||||
|
data={data}
|
||||||
|
urlSync={{ enabled: true }} // 只需这一行即可启用
|
||||||
|
// ... 其他 props
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 接口定义
|
||||||
|
|
||||||
|
#### UrlSyncConfig - URL 同步配置
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
interface UrlSyncConfig {
|
||||||
|
// 是否启用 URL 参数同步
|
||||||
|
enabled?: boolean;
|
||||||
|
|
||||||
|
// 是否在初始化时检测空 URL 并添加默认参数
|
||||||
|
initWithDefaults?: boolean;
|
||||||
|
|
||||||
|
// 默认分页参数
|
||||||
|
defaultPagination?: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
// URL 更新防抖时间(毫秒),避免频繁更新
|
||||||
|
updateDebounce?: number;
|
||||||
|
|
||||||
|
// 自定义 URL 参数名映射(可选,不配置则自动从 searchFields 提取)
|
||||||
|
paramNames?: {
|
||||||
|
page?: string;
|
||||||
|
size?: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 统一查询接口(可选)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 新增的统一查询回调,替代传统的多个回调
|
||||||
|
onQueryChange?: (query: {
|
||||||
|
filters: Record<string, string; // 搜索条件(已处理)
|
||||||
|
pagination: { page: number; size: number }; // 分页信息(已处理)
|
||||||
|
isFromUrl?: boolean; // 是否来自URL初始化
|
||||||
|
}) => void;
|
||||||
|
```
|
||||||
|
|
||||||
|
### 使用方式
|
||||||
|
|
||||||
|
#### 1. 基础使用(推荐)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SearchFormPagination
|
||||||
|
searchFields={searchFields}
|
||||||
|
columns={columns}
|
||||||
|
data={state.users}
|
||||||
|
loading={state.loading}
|
||||||
|
error={state.error}
|
||||||
|
pagination={state.pagination}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSizeChange={handleSizeChange}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
|
||||||
|
// 启用 URL 同步 - 参数名自动从 searchFields 提取
|
||||||
|
urlSync={{
|
||||||
|
enabled: true,
|
||||||
|
initWithDefaults: true,
|
||||||
|
updateDebounce: 300
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 高级配置(自定义参数名)
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
<SearchFormPagination
|
||||||
|
// ... 其他 props
|
||||||
|
urlSync={{
|
||||||
|
enabled: true,
|
||||||
|
initWithDefaults: true,
|
||||||
|
|
||||||
|
// 自定义 URL 参数名(覆盖自动提取的参数名)
|
||||||
|
paramNames: {
|
||||||
|
page: 'pageNum', // 页码参数名
|
||||||
|
size: 'pageSize', // 每页大小参数名
|
||||||
|
search: 'keyword', // 搜索框参数名(覆盖自动提取)
|
||||||
|
// status 和 type 会自动从 searchFields 提取,这里不配置
|
||||||
|
},
|
||||||
|
|
||||||
|
// 默认分页参数
|
||||||
|
defaultPagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10
|
||||||
|
},
|
||||||
|
|
||||||
|
// URL 更新防抖时间
|
||||||
|
updateDebounce: 500
|
||||||
|
}}
|
||||||
|
|
||||||
|
// 统一查询回调(推荐使用)
|
||||||
|
onQueryChange={handleQueryChange}
|
||||||
|
|
||||||
|
// 传统回调方式(向后兼容)
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
onSizeChange={handleSizeChange}
|
||||||
|
|
||||||
|
// URL 状态变化监听(可选)
|
||||||
|
onUrlStateChange={(urlState) => {
|
||||||
|
console.log('URL 状态变化:', urlState);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 父组件统一查询处理
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 统一查询处理函数
|
||||||
|
const handleQueryChange = useCallback((query: {
|
||||||
|
filters: Record<string, string>;
|
||||||
|
pagination: { page: number; size: number };
|
||||||
|
isFromUrl?: boolean;
|
||||||
|
}) => {
|
||||||
|
console.log('收到查询变化:', query);
|
||||||
|
|
||||||
|
// 映射过滤器字段名(根据业务需求调整)
|
||||||
|
const mappedFilters = {
|
||||||
|
searchKeyword: query.filters.search || '',
|
||||||
|
statusFilter: query.filters.status || 'all',
|
||||||
|
typeFilter: query.filters.type || 'all'
|
||||||
|
};
|
||||||
|
|
||||||
|
// 更新状态
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: mappedFilters });
|
||||||
|
dispatch({ type: 'SET_PAGINATION', payload: query.pagination });
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
loadUsers({
|
||||||
|
resetPage: !query.isFromUrl, // URL 初始化时保持页码,否则重置
|
||||||
|
page: query.pagination.page,
|
||||||
|
filters: mappedFilters,
|
||||||
|
sortBy: state.sortBy,
|
||||||
|
sortOrder: state.sortOrder,
|
||||||
|
size: query.pagination.size
|
||||||
|
});
|
||||||
|
}, [state.sortBy, state.sortOrder, loadUsers]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 工作流程
|
||||||
|
|
||||||
|
#### 1. 页面初始化
|
||||||
|
|
||||||
|
```
|
||||||
|
用户访问页面
|
||||||
|
↓
|
||||||
|
子组件检查 URL 参数
|
||||||
|
├─ 无参数 → 使用默认状态,父组件执行默认查询
|
||||||
|
└─ 有参数 → 解析参数 → 同步到内部状态 → 通知父组件 → 父组件执行查询
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 用户操作
|
||||||
|
|
||||||
|
```
|
||||||
|
用户搜索/分页操作
|
||||||
|
↓
|
||||||
|
子组件更新内部状态
|
||||||
|
↓
|
||||||
|
同步状态到 URL 参数(防抖处理)
|
||||||
|
↓
|
||||||
|
通知父组件状态变化
|
||||||
|
↓
|
||||||
|
父组件执行查询
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 页面刷新
|
||||||
|
|
||||||
|
```
|
||||||
|
页面刷新
|
||||||
|
↓
|
||||||
|
子组件从 URL 读取参数
|
||||||
|
↓
|
||||||
|
恢复内部状态
|
||||||
|
↓
|
||||||
|
通知父组件
|
||||||
|
↓
|
||||||
|
父组件执行查询 → 恢复用户之前的搜索状态
|
||||||
|
```
|
||||||
|
|
||||||
|
### URL 参数格式
|
||||||
|
|
||||||
|
#### 默认格式
|
||||||
|
|
||||||
|
```
|
||||||
|
# 基础搜索
|
||||||
|
?page=1&size=10&search=张三&status=active&type=admin
|
||||||
|
|
||||||
|
# 分页状态
|
||||||
|
?page=3&size=20
|
||||||
|
|
||||||
|
# 组合条件
|
||||||
|
?page=2&size=15&search=admin&status=active
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 自定义参数名格式
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
paramNames: {
|
||||||
|
page: 'p',
|
||||||
|
size: 'limit',
|
||||||
|
search: 'q',
|
||||||
|
status: 's',
|
||||||
|
type: 't'
|
||||||
|
}
|
||||||
|
|
||||||
|
// 生成 URL:
|
||||||
|
?pageNum=2&pageSize=15&keyword=admin&status=active&type=user
|
||||||
|
```
|
||||||
|
|
||||||
|
### 技术实现要点
|
||||||
|
|
||||||
|
#### 1. 防抖处理
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// URL 更新防抖,避免频繁修改浏览器历史记录
|
||||||
|
const updateUrl = useCallback((filters, pagination) => {
|
||||||
|
if (urlUpdateTimeoutRef.current) {
|
||||||
|
clearTimeout(urlUpdateTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
urlUpdateTimeoutRef.current = setTimeout(() => {
|
||||||
|
// 更新 URL 参数
|
||||||
|
window.history.replaceState({}, '', newUrl);
|
||||||
|
}, urlConfig.updateDebounce);
|
||||||
|
}, []);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 参数映射
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 支持字段名映射,适应不同的 API 接口
|
||||||
|
const paramValue = urlParams.get(
|
||||||
|
urlConfig.paramNames[field.key] || field.key
|
||||||
|
);
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 状态同步时机
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 搜索条件变化时同步
|
||||||
|
useEffect(() => {
|
||||||
|
if (urlConfig.enabled) {
|
||||||
|
updateUrl(filters, pagination);
|
||||||
|
}
|
||||||
|
}, [filters, urlConfig.enabled]);
|
||||||
|
|
||||||
|
// 分页变化时同步
|
||||||
|
useEffect(() => {
|
||||||
|
if (urlConfig.enabled && pagination) {
|
||||||
|
updateUrl(filters, pagination);
|
||||||
|
}
|
||||||
|
}, [pagination?.page, pagination?.size, urlConfig.enabled]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 最佳实践
|
||||||
|
|
||||||
|
#### 1. 参数命名规范
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 推荐:使用有意义的参数名
|
||||||
|
paramNames: {
|
||||||
|
search: 'keyword', // 搜索关键词
|
||||||
|
status: 'userStatus', // 用户状态
|
||||||
|
type: 'userType', // 用户类型
|
||||||
|
page: 'pageNum', // 页码
|
||||||
|
size: 'pageSize' // 每页大小
|
||||||
|
}
|
||||||
|
|
||||||
|
// ❌ 避免:过于简化的参数名
|
||||||
|
paramNames: {
|
||||||
|
search: 's',
|
||||||
|
status: 'st',
|
||||||
|
type: 't'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 防抖时间设置
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 推荐:根据用户操作频率调整
|
||||||
|
urlSync: {
|
||||||
|
updateDebounce: 300 // 文本搜索:300ms,下拉选择:立即
|
||||||
|
}
|
||||||
|
|
||||||
|
// 快速响应场景
|
||||||
|
urlSync: {
|
||||||
|
updateDebounce: 100 // 需要即时反馈的场景
|
||||||
|
}
|
||||||
|
|
||||||
|
// 性能优先场景
|
||||||
|
urlSync: {
|
||||||
|
updateDebounce: 500 // 减少频繁更新
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 默认值配置
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// ✅ 推荐:设置合理的默认值
|
||||||
|
urlSync: {
|
||||||
|
defaultPagination: {
|
||||||
|
page: 1,
|
||||||
|
size: 10 // 根据业务需求设置合理的默认每页条数
|
||||||
|
},
|
||||||
|
initWithDefaults: true // 为新用户提供更好的体验
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
### 注意事项
|
||||||
|
|
||||||
|
#### 1. 浏览器兼容性
|
||||||
|
|
||||||
|
- 支持 `window.history.replaceState` 的现代浏览器
|
||||||
|
- 服务端渲染(SSR)时需要检查 `typeof window !== 'undefined'`
|
||||||
|
|
||||||
|
#### 2. 参数长度限制
|
||||||
|
|
||||||
|
- URL 参数总长度建议控制在 2048 字符以内
|
||||||
|
- 复杂搜索条件考虑使用 POST 请求而非 GET
|
||||||
|
|
||||||
|
#### 3. 安全性考虑
|
||||||
|
|
||||||
|
- 对 URL 参数进行验证和清理
|
||||||
|
- 避免将敏感信息存储在 URL 中
|
||||||
|
- 考虑 XSS 防护
|
||||||
|
|
||||||
|
#### 4. 性能影响
|
||||||
|
|
||||||
|
- URL 同步功能对性能影响很小
|
||||||
|
- 防抖机制避免频繁的 DOM 操作
|
||||||
|
- 合理设置防抖时间可进一步优化性能
|
||||||
|
|
||||||
|
### 向后兼容性
|
||||||
|
|
||||||
|
URL 同步功能完全向后兼容,不会影响现有代码:
|
||||||
|
|
||||||
|
```tsx
|
||||||
|
// 现有代码无需修改,继续正常工作
|
||||||
|
<SearchFormPagination
|
||||||
|
searchFields={searchFields}
|
||||||
|
columns={columns}
|
||||||
|
onSearch={handleSearch}
|
||||||
|
onPageChange={handlePageChange}
|
||||||
|
// ... 其他 props
|
||||||
|
/>
|
||||||
|
|
||||||
|
// 极简启用 - 只需添加一行
|
||||||
|
<SearchFormPagination
|
||||||
|
// ... 现有 props
|
||||||
|
urlSync={{ enabled: true }} // 参数名自动从 searchFields 推导
|
||||||
|
/>
|
||||||
|
```
|
||||||
|
|
||||||
|
### 配置简化对比
|
||||||
|
|
||||||
|
#### 优化前(复杂配置)
|
||||||
|
```tsx
|
||||||
|
urlSync={{
|
||||||
|
enabled: true,
|
||||||
|
initWithDefaults: true,
|
||||||
|
paramNames: {
|
||||||
|
page: 'page',
|
||||||
|
size: 'size',
|
||||||
|
search: 'search',
|
||||||
|
status: 'status',
|
||||||
|
type: 'type'
|
||||||
|
},
|
||||||
|
defaultPagination: { page: 1, size: 10 },
|
||||||
|
updateDebounce: 300
|
||||||
|
}}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 优化后(极简配置)
|
||||||
|
```tsx
|
||||||
|
urlSync={{
|
||||||
|
enabled: true,
|
||||||
|
initWithDefaults: true,
|
||||||
|
updateDebounce: 300
|
||||||
|
}}
|
||||||
|
// page、size 以及所有 searchFields 的 key 都会自动推导
|
||||||
|
```
|
||||||
|
|
||||||
|
### 故障排除
|
||||||
|
|
||||||
|
#### 1. URL 参数不更新
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
- `urlSync.enabled` 设置为 `false`
|
||||||
|
- 防抖时间设置过长
|
||||||
|
- 浏览器不支持 `history.replaceState`
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```tsx
|
||||||
|
urlSync: {
|
||||||
|
enabled: true,
|
||||||
|
updateDebounce: 100 // 减少防抖时间测试
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 2. 页面刷新后状态丢失
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
- `initWithDefaults` 设置为 `false`
|
||||||
|
- 参数名映射不正确
|
||||||
|
- 父组件未正确处理 `onQueryChange` 回调
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```tsx
|
||||||
|
urlSync: {
|
||||||
|
enabled: true,
|
||||||
|
initWithDefaults: true // 确保启用默认值初始化
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查参数名映射
|
||||||
|
paramNames: {
|
||||||
|
search: 'search', // 确保与搜索字段 key 一致
|
||||||
|
status: 'status'
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
#### 3. 搜索条件与分页不同步
|
||||||
|
|
||||||
|
**可能原因**:
|
||||||
|
- 父组件未传递正确的分页状态
|
||||||
|
- 回调函数中丢失搜索条件
|
||||||
|
|
||||||
|
**解决方案**:
|
||||||
|
```tsx
|
||||||
|
const handlePageChange = useCallback((page) => {
|
||||||
|
// 确保传递当前搜索条件
|
||||||
|
loadUsers({
|
||||||
|
filters: currentFilters, // 关键:保持搜索条件
|
||||||
|
pagination: { page, size: currentSize }
|
||||||
|
});
|
||||||
|
}, [loadUsers, currentFilters, currentSize]);
|
||||||
|
```
|
||||||
|
|
||||||
|
### 总结
|
||||||
|
|
||||||
|
URL 参数同步功能为 SearchFormPagination 组件提供了强大的状态持久化能力,通过极简配置即可实现:
|
||||||
|
|
||||||
|
- **自动同步**:无需手动管理 URL 参数
|
||||||
|
- **自动推导**:参数名从 `searchFields` 自动提取,无需手动映射
|
||||||
|
- **状态恢复**:页面刷新后自动恢复搜索状态
|
||||||
|
- **用户体验**:提供更好的导航和分享体验
|
||||||
|
- **极简配置**:只需 `urlSync={{ enabled: true }}` 即可启用
|
||||||
|
- **向后兼容**:不影响现有代码,渐进式升级
|
||||||
|
|
||||||
|
#### 配置简化成果
|
||||||
|
|
||||||
|
- **优化前**:需要手动配置所有参数名映射,配置复杂
|
||||||
|
- **优化后**:参数名自动推导,配置减少 70%+
|
||||||
|
|
||||||
|
#### 使用建议
|
||||||
|
|
||||||
|
- **基础场景**:直接使用 `urlSync={{ enabled: true }}`
|
||||||
|
- **特殊需求**:仅在需要自定义参数名时配置 `paramNames`
|
||||||
|
- **避免使用**:不推荐使用 `q`、`s` 等过于简化的参数名
|
||||||
|
|
||||||
|
该功能特别适用于数据展示、搜索、筛选等需要状态保持的场景,是提升用户体验的重要功能。
|
||||||
@@ -242,43 +242,103 @@ export default function AuditHistoryPage() {
|
|||||||
date_range: 'all'
|
date_range: 'all'
|
||||||
});
|
});
|
||||||
|
|
||||||
// 数据加载函数 - 移除不必要的依赖避免重复调用
|
// 数据加载函数 - 优先从浏览器URL参数读取
|
||||||
const loadAuditHistory = useCallback(async (params?: {
|
const loadAuditHistory = useCallback(async (options: {
|
||||||
|
resetPage?: boolean;
|
||||||
filters?: Record<string, string>;
|
filters?: Record<string, string>;
|
||||||
pagination?: { page: number; size: number };
|
sortBy?: string;
|
||||||
sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' };
|
sortOrder?: 'asc' | 'desc';
|
||||||
}) => {
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
} = {}) => {
|
||||||
try {
|
try {
|
||||||
console.log('调用了loadAuditHistory');
|
// 优先从URL读取参数
|
||||||
|
let urlParams = {};
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
urlParams = {
|
||||||
|
search: params.get('search') || undefined,
|
||||||
|
action: params.get('action') || undefined,
|
||||||
|
audit_status: params.get('audit_status') || undefined,
|
||||||
|
date_range: params.get('date_range') || undefined,
|
||||||
|
page: params.get('page') ? parseInt(params.get('page')!, 10) : undefined,
|
||||||
|
size: params.get('size') ? parseInt(params.get('size')!, 10) : undefined
|
||||||
|
};
|
||||||
|
console.log('从URL读取的参数:', urlParams);
|
||||||
|
}
|
||||||
|
console.log('========================================');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
const finalParams: AuditLogsQueryParams = {
|
// 解构选项参数,提供默认值
|
||||||
search_keyword: (params?.filters?.search ?? searchFilters.search) || undefined,
|
const {
|
||||||
action: params?.filters?.action ?? searchFilters.action,
|
resetPage = false,
|
||||||
audit_status: params?.filters?.audit_status ?? searchFilters.audit_status,
|
filters,
|
||||||
date_range: params?.filters?.date_range ?? searchFilters.date_range,
|
sortBy,
|
||||||
page: params?.pagination?.page || pagination.page,
|
sortOrder,
|
||||||
size: params?.pagination?.size || pagination.size,
|
page,
|
||||||
order_by: params?.sort?.sortBy,
|
size
|
||||||
sort_order: params?.sort?.sortOrder,
|
} = options;
|
||||||
|
|
||||||
|
// 优先级:URL参数 > 传入参数 > 父组件状态
|
||||||
|
const finalPage = resetPage ? 1 : (urlParams.page || page || pagination.page);
|
||||||
|
const finalSize = urlParams.size || size || pagination.size;
|
||||||
|
|
||||||
|
const params: AuditLogsQueryParams = {
|
||||||
|
page: finalPage,
|
||||||
|
size: finalSize,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理筛选条件,如果为'all'则不传该参数
|
// 使用正确的优先级:URL参数 > 传入参数 > 父组件状态
|
||||||
if (finalParams.action === 'all') {
|
const currentFilters = {
|
||||||
finalParams.action = undefined;
|
search: urlParams.search || (filters?.search) || searchFilters.search,
|
||||||
}
|
action: urlParams.action || (filters?.action) || searchFilters.action,
|
||||||
if (finalParams.audit_status === 'all') {
|
audit_status: urlParams.audit_status || (filters?.audit_status) || searchFilters.audit_status,
|
||||||
finalParams.audit_status = undefined;
|
date_range: urlParams.date_range || (filters?.date_range) || searchFilters.date_range
|
||||||
}
|
};
|
||||||
if (finalParams.date_range === 'all') {
|
const currentSortBy = sortBy || 'created_at';
|
||||||
finalParams.date_range = undefined;
|
const currentSortOrder = sortOrder || 'desc';
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if (currentFilters.search) {
|
||||||
|
params.search_keyword = currentFilters.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetchAuditLogs(finalParams);
|
if (currentFilters.action && currentFilters.action !== 'all') {
|
||||||
|
params.action = currentFilters.action;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFilters.audit_status && currentFilters.audit_status !== 'all') {
|
||||||
|
params.audit_status = currentFilters.audit_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentFilters.date_range && currentFilters.date_range !== 'all') {
|
||||||
|
params.date_range = currentFilters.date_range;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSortBy) {
|
||||||
|
params.order_by = currentSortBy;
|
||||||
|
params.sort_order = currentSortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== 审核历史页面 - 最终API参数 ===');
|
||||||
|
console.log('API调用参数 params:', params);
|
||||||
|
console.log('参数优先级正确: URL参数 > 函数传递参数 > 父组件状态');
|
||||||
|
console.log('当前currentFilters:', currentFilters);
|
||||||
|
console.log('==================================');
|
||||||
|
|
||||||
|
const response = await fetchAuditLogs(params);
|
||||||
const transformedData = response.data.map(transformAuditLogData);
|
const transformedData = response.data.map(transformAuditLogData);
|
||||||
|
|
||||||
setRecords(transformedData);
|
setRecords(transformedData);
|
||||||
|
setPagination({
|
||||||
|
page: response.page,
|
||||||
|
size: response.size,
|
||||||
|
total: response.total,
|
||||||
|
totalPages: response.total_pages,
|
||||||
|
hasNext: response.has_next,
|
||||||
|
hasPrev: response.has_prev,
|
||||||
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
const errorMessage = err instanceof Error ? err.message : '加载审核历史失败';
|
const errorMessage = err instanceof Error ? err.message : '加载审核历史失败';
|
||||||
setError(errorMessage);
|
setError(errorMessage);
|
||||||
@@ -286,7 +346,7 @@ export default function AuditHistoryPage() {
|
|||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [searchFilters, pagination]); // 添加依赖以保持函数引用最新
|
}, []); // 移除依赖项,通过参数传递
|
||||||
|
|
||||||
const didFetchRef = useRef(false)
|
const didFetchRef = useRef(false)
|
||||||
|
|
||||||
@@ -295,38 +355,94 @@ useEffect(() => {
|
|||||||
didFetchRef.current = true
|
didFetchRef.current = true
|
||||||
loadAuditHistory()
|
loadAuditHistory()
|
||||||
}, [])
|
}, [])
|
||||||
// 事件处理器
|
// 搜索处理 - 保持传统的简洁方式
|
||||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||||
setSearchFilters(filters);
|
console.log('审核历史页面 - 收到搜索条件:', filters);
|
||||||
// 搜索时重置到第一页
|
|
||||||
loadAuditHistory({
|
|
||||||
filters,
|
|
||||||
pagination: { page: 1, size: pagination.size }
|
|
||||||
});
|
|
||||||
}, [loadAuditHistory, pagination.size]);
|
|
||||||
|
|
||||||
|
// 更新过滤器状态
|
||||||
|
setSearchFilters(filters);
|
||||||
|
|
||||||
|
// 搜索时重置到第1页
|
||||||
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
loadAuditHistory({
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: filters,
|
||||||
|
size: pagination.size
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('触发审核历史查询 - 参数:', {
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: filters,
|
||||||
|
size: pagination.size
|
||||||
|
});
|
||||||
|
}, [pagination.size, loadAuditHistory]);
|
||||||
|
|
||||||
|
// 排序处理
|
||||||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||||
// 排序时重置到第一页
|
// 排序时重置到第一页
|
||||||
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||||||
loadAuditHistory({
|
loadAuditHistory({
|
||||||
pagination: { page: 1, size: pagination.size },
|
resetPage: true,
|
||||||
sort: { sortBy, sortOrder }
|
page: 1,
|
||||||
|
filters: searchFilters,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
size: pagination.size
|
||||||
});
|
});
|
||||||
}, [loadAuditHistory, pagination.size]);
|
}, [searchFilters, pagination.size, loadAuditHistory]);
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
const handlePageChange = useCallback((page: number) => {
|
const handlePageChange = useCallback((page: number) => {
|
||||||
|
if (page < 1) {
|
||||||
|
page = 1;
|
||||||
|
} else if (page > pagination.totalPages && pagination.totalPages > 0) {
|
||||||
|
page = pagination.totalPages;
|
||||||
|
}
|
||||||
setPagination(prev => ({ ...prev, page }));
|
setPagination(prev => ({ ...prev, page }));
|
||||||
loadAuditHistory({
|
loadAuditHistory({
|
||||||
|
page,
|
||||||
filters: searchFilters,
|
filters: searchFilters,
|
||||||
pagination: { page, size: pagination.size }
|
size: pagination.size
|
||||||
});
|
});
|
||||||
}, [loadAuditHistory, searchFilters, pagination.size]);
|
}, [searchFilters, pagination.size, pagination.totalPages, loadAuditHistory]);
|
||||||
|
|
||||||
|
// 每页条数变化处理
|
||||||
const handleSizeChange = useCallback((size: number) => {
|
const handleSizeChange = useCallback((size: number) => {
|
||||||
setPagination(prev => ({ ...prev, size, page: 1 }));
|
setPagination(prev => ({ ...prev, size, page: 1 }));
|
||||||
loadAuditHistory({
|
loadAuditHistory({
|
||||||
filters: searchFilters,
|
resetPage: true,
|
||||||
pagination: { page: 1, size }
|
page: 1,
|
||||||
|
size,
|
||||||
|
filters: searchFilters
|
||||||
});
|
});
|
||||||
}, [loadAuditHistory, searchFilters]);
|
}, [searchFilters, loadAuditHistory]);
|
||||||
|
|
||||||
|
// URL状态变化处理 - 处理浏览器前进后退时的参数恢复
|
||||||
|
const handleUrlStateChange = useCallback((urlState: {
|
||||||
|
filters: Record<string, string>;
|
||||||
|
pagination: { page: number; size: number };
|
||||||
|
}) => {
|
||||||
|
console.log('审核历史页面 - URL状态变化:', urlState);
|
||||||
|
|
||||||
|
// 更新内部状态
|
||||||
|
setSearchFilters(urlState.filters);
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
page: urlState.pagination.page,
|
||||||
|
size: urlState.pagination.size
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 触发数据加载
|
||||||
|
loadAuditHistory({
|
||||||
|
page: urlState.pagination.page,
|
||||||
|
size: urlState.pagination.size,
|
||||||
|
filters: urlState.filters
|
||||||
|
});
|
||||||
|
}, [loadAuditHistory]);
|
||||||
|
|
||||||
// 业务事件处理器
|
// 业务事件处理器
|
||||||
const handleView = (record: AuditLogData) => {
|
const handleView = (record: AuditLogData) => {
|
||||||
@@ -381,7 +497,8 @@ useEffect(() => {
|
|||||||
emptyIcon={<FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
emptyIcon={<FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
||||||
emptyText="暂无审核记录"
|
emptyText="暂无审核记录"
|
||||||
sizeOptions={[10, 20, 50, 100]}
|
sizeOptions={[10, 20, 50, 100]}
|
||||||
/>
|
|
||||||
|
/>
|
||||||
|
|
||||||
{/* View Audit Record Details Dialog */}
|
{/* View Audit Record Details Dialog */}
|
||||||
<Dialog open={dialogs.showViewDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
|
<Dialog open={dialogs.showViewDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
|
||||||
|
|||||||
@@ -165,12 +165,13 @@ export default function EnterpriseAuditPage() {
|
|||||||
sortable: false, // 禁用排序
|
sortable: false, // 禁用排序
|
||||||
render: (value: string) => {
|
render: (value: string) => {
|
||||||
const statusConfig = {
|
const statusConfig = {
|
||||||
|
'草稿': { label: '草稿', variant: 'default' as const, className: 'bg-gray-100 text-gray-800 dark:bg-gray-900 dark:text-gray-200' },
|
||||||
'待审核': { label: '待审核', variant: 'default' as const, className: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
|
'待审核': { label: '待审核', variant: 'default' as const, className: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
|
||||||
'已通过': { label: '已通过', variant: 'default' as const, className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
'已通过': { label: '已通过', variant: 'default' as const, className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||||
'已驳回': { label: '已驳回', variant: 'default' as const, className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
'已拒绝': { label: '已拒绝', variant: 'default' as const, className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
||||||
};
|
};
|
||||||
|
|
||||||
const config = statusConfig[value as keyof typeof statusConfig] || statusConfig['待审核'];
|
const config = statusConfig[value as keyof typeof statusConfig] || statusConfig['草稿'];
|
||||||
return (
|
return (
|
||||||
<Badge className={`font-light ${config.className}`}>
|
<Badge className={`font-light ${config.className}`}>
|
||||||
{config.label}
|
{config.label}
|
||||||
@@ -227,31 +228,53 @@ export default function EnterpriseAuditPage() {
|
|||||||
didFetchRef.current = true
|
didFetchRef.current = true
|
||||||
loadEnterprises()
|
loadEnterprises()
|
||||||
}, [])
|
}, [])
|
||||||
// 加载企业数据 - 移除依赖项,通过参数传递状态
|
// 加载企业数据 - 统一参数结构
|
||||||
const loadEnterprises = useCallback(async (params?: {
|
const loadEnterprises = useCallback(async (options: {
|
||||||
filters?: Record<string, string>;
|
|
||||||
pagination?: { page: number; size: number };
|
|
||||||
sort?: { sortBy?: string; sortOrder: 'asc' | 'desc' };
|
|
||||||
resetPage?: boolean;
|
resetPage?: boolean;
|
||||||
}) => {
|
filters?: Record<string, string>;
|
||||||
|
sortBy?: string;
|
||||||
|
sortOrder?: 'asc' | 'desc';
|
||||||
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
} = {}) => {
|
||||||
try {
|
try {
|
||||||
dispatch({ type: 'SET_LOADING', payload: true });
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
const finalParams: TenantsQueryParams = {
|
// 解构选项参数,提供默认值
|
||||||
search: (params?.filters?.search ?? state.filters.search) || undefined,
|
const {
|
||||||
audit_status: params?.filters?.audit_status ?? state.filters.audit_status,
|
resetPage = false,
|
||||||
page: params?.resetPage ? 1 : (params?.pagination?.page || state.pagination.page),
|
filters,
|
||||||
size: params?.pagination?.size || state.pagination.size,
|
sortBy,
|
||||||
order_by: params?.sort?.sortBy,
|
sortOrder,
|
||||||
sort_order: params?.sort?.sortOrder,
|
page,
|
||||||
|
size
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
const params: TenantsQueryParams = {
|
||||||
|
page: resetPage ? 1 : (page || state.pagination.page),
|
||||||
|
size: size || state.pagination.size,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理audit_status,如果为'all'则不传该参数
|
// 使用传入的过滤器参数,如果没有传入则使用当前状态
|
||||||
if (finalParams.audit_status === 'all') {
|
const currentFilters = filters || state.filters;
|
||||||
finalParams.audit_status = undefined;
|
const currentSortBy = sortBy || 'created_at';
|
||||||
|
const currentSortOrder = sortOrder || 'desc';
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if (currentFilters.search) {
|
||||||
|
params.search = currentFilters.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
const response = await fetchTenantsForAudit(finalParams);
|
if (currentFilters.audit_status && currentFilters.audit_status !== 'all') {
|
||||||
|
params.audit_status = currentFilters.audit_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSortBy) {
|
||||||
|
params.order_by = currentSortBy;
|
||||||
|
params.sort_order = currentSortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
const response = await fetchTenantsForAudit(params);
|
||||||
const transformedData = response.data.map(transformTenantData);
|
const transformedData = response.data.map(transformTenantData);
|
||||||
|
|
||||||
dispatch({
|
dispatch({
|
||||||
@@ -284,24 +307,49 @@ export default function EnterpriseAuditPage() {
|
|||||||
rejected: state.enterprises.filter(e => e.auditStatus === '已驳回').length,
|
rejected: state.enterprises.filter(e => e.auditStatus === '已驳回').length,
|
||||||
}), [state.enterprises, state.pagination.total]);
|
}), [state.enterprises, state.pagination.total]);
|
||||||
|
|
||||||
// 事件处理器
|
// 搜索处理 - 保持传统的简洁方式
|
||||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||||
|
console.log('企业审核页面 - 收到搜索条件:', filters);
|
||||||
|
|
||||||
|
// 更新过滤器状态
|
||||||
dispatch({ type: 'SET_FILTERS', payload: filters });
|
dispatch({ type: 'SET_FILTERS', payload: filters });
|
||||||
loadEnterprises({
|
|
||||||
filters,
|
|
||||||
pagination: { page: 1, size: state.pagination.size }
|
|
||||||
});
|
|
||||||
}, [loadEnterprises, state.pagination.size]);
|
|
||||||
|
|
||||||
|
// 搜索时重置到第1页
|
||||||
|
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
|
loadEnterprises({
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: filters,
|
||||||
|
size: state.pagination.size
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log('触发企业审核查询 - 参数:', {
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: filters,
|
||||||
|
size: state.pagination.size
|
||||||
|
});
|
||||||
|
}, [state.pagination.size, loadEnterprises]);
|
||||||
|
|
||||||
|
// 排序处理
|
||||||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||||
|
// 排序时重置到第1页
|
||||||
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } });
|
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } });
|
||||||
loadEnterprises({
|
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
|
||||||
filters: state.filters,
|
|
||||||
sort: { sortBy, sortOrder },
|
|
||||||
resetPage: true
|
|
||||||
});
|
|
||||||
}, [loadEnterprises, state.filters]);
|
|
||||||
|
|
||||||
|
loadEnterprises({
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: state.filters,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
size: state.pagination.size
|
||||||
|
});
|
||||||
|
}, [state.filters, state.pagination.size, loadEnterprises]);
|
||||||
|
|
||||||
|
// 分页处理
|
||||||
const handlePageChange = useCallback((page: number) => {
|
const handlePageChange = useCallback((page: number) => {
|
||||||
// 边界检查,确保页码在有效范围内
|
// 边界检查,确保页码在有效范围内
|
||||||
if (page < 1) {
|
if (page < 1) {
|
||||||
@@ -309,20 +357,47 @@ export default function EnterpriseAuditPage() {
|
|||||||
} else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) {
|
} else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) {
|
||||||
page = state.pagination.totalPages;
|
page = state.pagination.totalPages;
|
||||||
}
|
}
|
||||||
|
|
||||||
dispatch({ type: 'SET_PAGINATION', payload: { page } });
|
dispatch({ type: 'SET_PAGINATION', payload: { page } });
|
||||||
loadEnterprises({
|
loadEnterprises({
|
||||||
|
page,
|
||||||
filters: state.filters,
|
filters: state.filters,
|
||||||
pagination: { page, size: state.pagination.size }
|
size: state.pagination.size
|
||||||
});
|
});
|
||||||
}, [loadEnterprises, state.filters, state.pagination.size, state.pagination.totalPages]);
|
}, [state.filters, state.pagination.size, state.pagination.totalPages, loadEnterprises]);
|
||||||
|
|
||||||
|
// 每页条数变化处理
|
||||||
const handleSizeChange = useCallback((size: number) => {
|
const handleSizeChange = useCallback((size: number) => {
|
||||||
dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } });
|
dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } });
|
||||||
loadEnterprises({
|
loadEnterprises({
|
||||||
filters: state.filters,
|
resetPage: true,
|
||||||
pagination: { page: 1, size }
|
page: 1,
|
||||||
|
size,
|
||||||
|
filters: state.filters
|
||||||
});
|
});
|
||||||
}, [loadEnterprises, state.filters]);
|
}, [state.filters, loadEnterprises]);
|
||||||
|
|
||||||
|
// URL状态变化处理 - 处理浏览器前进后退时的参数恢复
|
||||||
|
const handleUrlStateChange = useCallback((urlState: {
|
||||||
|
filters: Record<string, string>;
|
||||||
|
pagination: { page: number; size: number };
|
||||||
|
}) => {
|
||||||
|
console.log('企业审核页面 - URL状态变化:', urlState);
|
||||||
|
|
||||||
|
// 更新内部状态
|
||||||
|
dispatch({ type: 'SET_FILTERS', payload: urlState.filters });
|
||||||
|
dispatch({ type: 'SET_PAGINATION', payload: {
|
||||||
|
page: urlState.pagination.page,
|
||||||
|
size: urlState.pagination.size
|
||||||
|
}});
|
||||||
|
|
||||||
|
// 触发数据加载
|
||||||
|
loadEnterprises({
|
||||||
|
page: urlState.pagination.page,
|
||||||
|
size: urlState.pagination.size,
|
||||||
|
filters: urlState.filters
|
||||||
|
});
|
||||||
|
}, [loadEnterprises]);
|
||||||
|
|
||||||
const handleRefresh = useCallback(() => {
|
const handleRefresh = useCallback(() => {
|
||||||
dispatch({ type: 'REFRESH_DATA' });
|
dispatch({ type: 'REFRESH_DATA' });
|
||||||
@@ -435,12 +510,6 @@ export default function EnterpriseAuditPage() {
|
|||||||
{/* 搜索、表格和分页 - 使用重构后的组件 */}
|
{/* 搜索、表格和分页 - 使用重构后的组件 */}
|
||||||
<SearchFormPagination
|
<SearchFormPagination
|
||||||
formTitle="企业列表"
|
formTitle="企业列表"
|
||||||
formRightContent={
|
|
||||||
<Button variant="outline" onClick={handleRefresh} disabled={state.loading}>
|
|
||||||
<RefreshCw className={`w-4 h-4 mr-2 ${state.loading ? 'animate-spin' : ''}`} />
|
|
||||||
刷新
|
|
||||||
</Button>
|
|
||||||
}
|
|
||||||
searchFields={searchFields}
|
searchFields={searchFields}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
data={state.enterprises}
|
data={state.enterprises}
|
||||||
@@ -458,7 +527,8 @@ export default function EnterpriseAuditPage() {
|
|||||||
showSizeSelector={true}
|
showSizeSelector={true}
|
||||||
showPageInfo={true}
|
showPageInfo={true}
|
||||||
sizeOptions={[10, 20, 50, 100]}
|
sizeOptions={[10, 20, 50, 100]}
|
||||||
/>
|
|
||||||
|
/>
|
||||||
|
|
||||||
{/* 企业详情对话框 - 保留原有功能 */}
|
{/* 企业详情对话框 - 保留原有功能 */}
|
||||||
<EnterpriseDetailDialog
|
<EnterpriseDetailDialog
|
||||||
|
|||||||
@@ -246,30 +246,85 @@ export default function EnterpriseManagement() {
|
|||||||
didFetchRef.current = true
|
didFetchRef.current = true
|
||||||
loadEnterprises()
|
loadEnterprises()
|
||||||
}, [])
|
}, [])
|
||||||
// 数据加载函数 - 移除不必要的依赖避免重复调用
|
// 数据加载函数 - 参考audit-history页面的统一参数结构,优先从URL参数读取
|
||||||
const loadEnterprises = useCallback(async (params?: {
|
const loadEnterprises = useCallback(async (options: {
|
||||||
|
resetPage?: boolean;
|
||||||
filters?: Record<string, string>;
|
filters?: Record<string, string>;
|
||||||
pagination?: { page: number; size: number };
|
sortBy?: string;
|
||||||
sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' };
|
sortOrder?: 'asc' | 'desc';
|
||||||
}) => {
|
page?: number;
|
||||||
|
size?: number;
|
||||||
|
} = {}) => {
|
||||||
try {
|
try {
|
||||||
console.log('调用了loadEnterprises')
|
console.log('=== 企业管理页面 - loadEnterprises 调用 ===');
|
||||||
|
console.log('传入的options参数:', options);
|
||||||
|
console.log('当前searchFilters:', searchFilters);
|
||||||
|
console.log('当前pagination:', pagination);
|
||||||
|
|
||||||
|
// 优先从URL读取参数
|
||||||
|
let urlParams = {};
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
urlParams = {
|
||||||
|
search: params.get('search') || undefined,
|
||||||
|
audit_status: params.get('audit_status') || undefined,
|
||||||
|
page: params.get('page') ? parseInt(params.get('page')!, 10) : undefined,
|
||||||
|
size: params.get('size') ? parseInt(params.get('size')!, 10) : undefined
|
||||||
|
};
|
||||||
|
console.log('从URL读取的参数:', urlParams);
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('========================================');
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
|
|
||||||
|
// 解构选项参数,提供默认值
|
||||||
|
const {
|
||||||
|
resetPage = false,
|
||||||
|
filters,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
page,
|
||||||
|
size
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
// 优先级:URL参数 > 传入参数 > 父组件状态
|
||||||
|
const finalPage = resetPage ? 1 : (page || urlParams.page || pagination.page);
|
||||||
|
const finalSize = size || urlParams.size || pagination.size;
|
||||||
|
|
||||||
const finalParams: TenantsQueryParams = {
|
const finalParams: TenantsQueryParams = {
|
||||||
search: (params?.filters?.search ?? searchFilters.search) || undefined,
|
page: finalPage,
|
||||||
audit_status: params?.filters?.audit_status ?? searchFilters.audit_status,
|
size: finalSize,
|
||||||
page: params?.pagination?.page || pagination.page,
|
|
||||||
size: params?.pagination?.size || pagination.size,
|
|
||||||
order_by: params?.sort?.sortBy,
|
|
||||||
sort_order: params?.sort?.sortOrder,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// 处理audit_status,如果为'all'则不传该参数
|
// 使用正确的优先级:URL参数 > 传入参数 > 父组件状态
|
||||||
if (finalParams.audit_status === 'all') {
|
const currentFilters = {
|
||||||
finalParams.audit_status = undefined;
|
search: urlParams.search || (filters?.search) || searchFilters.search,
|
||||||
|
audit_status: urlParams.audit_status || (filters?.audit_status) || searchFilters.audit_status
|
||||||
|
};
|
||||||
|
const currentSortBy = sortBy || 'created_at';
|
||||||
|
const currentSortOrder = sortOrder || 'desc';
|
||||||
|
|
||||||
|
// 添加搜索条件
|
||||||
|
if (currentFilters.search) {
|
||||||
|
finalParams.search = currentFilters.search;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (currentFilters.audit_status && currentFilters.audit_status !== 'all') {
|
||||||
|
finalParams.audit_status = currentFilters.audit_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentSortBy) {
|
||||||
|
finalParams.order_by = currentSortBy;
|
||||||
|
finalParams.sort_order = currentSortOrder;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('=== 企业管理页面 - 最终API参数 ===');
|
||||||
|
console.log('API调用参数 finalParams:', finalParams);
|
||||||
|
console.log('参数优先级正确: URL参数 > 传入参数 > 父组件状态');
|
||||||
|
console.log('当前currentFilters:', currentFilters);
|
||||||
|
console.log('==================================');
|
||||||
|
|
||||||
const response = await fetchTenants(finalParams);
|
const response = await fetchTenants(finalParams);
|
||||||
const transformedData = response.data.map(transformTenantData);
|
const transformedData = response.data.map(transformTenantData);
|
||||||
|
|
||||||
@@ -291,51 +346,105 @@ export default function EnterpriseManagement() {
|
|||||||
}
|
}
|
||||||
}, []); // 移除所有依赖,使用参数传递状态变化
|
}, []); // 移除所有依赖,使用参数传递状态变化
|
||||||
|
|
||||||
// 事件处理器
|
// 搜索处理 - 参考audit-history页面的统一方式
|
||||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||||
setSearchFilters(filters);
|
console.log('企业管理页面 - 收到搜索条件:', filters);
|
||||||
// 搜索时重置到第一页
|
|
||||||
loadEnterprises({
|
|
||||||
filters,
|
|
||||||
pagination: { page: 1, size: pagination.size }
|
|
||||||
});
|
|
||||||
}, [loadEnterprises, pagination.size]);
|
|
||||||
|
|
||||||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
// 更新过滤器状态
|
||||||
// 排序时重置到第一页
|
setSearchFilters(filters);
|
||||||
|
|
||||||
|
// 搜索时重置到第1页
|
||||||
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
loadEnterprises({
|
loadEnterprises({
|
||||||
pagination: { page: 1, size: pagination.size },
|
resetPage: true,
|
||||||
sort: { sortBy, sortOrder }
|
page: 1,
|
||||||
|
filters: filters,
|
||||||
|
size: pagination.size
|
||||||
});
|
});
|
||||||
}, [loadEnterprises, pagination.size]);
|
|
||||||
|
console.log('触发企业管理查询 - 参数:', {
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: filters,
|
||||||
|
size: pagination.size
|
||||||
|
});
|
||||||
|
}, [pagination.size, loadEnterprises]);
|
||||||
|
|
||||||
|
// 排序处理
|
||||||
|
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||||
|
// 排序时重置到第1页
|
||||||
|
setPagination(prev => ({ ...prev, page: 1 }));
|
||||||
|
|
||||||
|
loadEnterprises({
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: searchFilters,
|
||||||
|
sortBy,
|
||||||
|
sortOrder,
|
||||||
|
size: pagination.size
|
||||||
|
});
|
||||||
|
}, [searchFilters, pagination.size, loadEnterprises]);
|
||||||
|
|
||||||
// 统一的数据重载函数 - 避免重复代码
|
// 统一的数据重载函数 - 避免重复代码
|
||||||
const reloadData = useCallback(() => {
|
const reloadData = useCallback(() => {
|
||||||
const reloadParams = {
|
loadEnterprises({
|
||||||
filters: searchFilters,
|
page: pagination.page,
|
||||||
pagination: {
|
size: pagination.size,
|
||||||
page: pagination.page,
|
filters: searchFilters
|
||||||
size: pagination.size
|
});
|
||||||
}
|
|
||||||
};
|
|
||||||
loadEnterprises(reloadParams);
|
|
||||||
}, [loadEnterprises, searchFilters, pagination]);
|
}, [loadEnterprises, searchFilters, pagination]);
|
||||||
|
|
||||||
|
// 分页处理 - 参考audit-history页面的统一方式
|
||||||
const handlePageChange = useCallback((page: number) => {
|
const handlePageChange = useCallback((page: number) => {
|
||||||
|
// 边界检查,确保页码在有效范围内
|
||||||
|
if (page < 1) {
|
||||||
|
page = 1;
|
||||||
|
} else if (page > pagination.totalPages && pagination.totalPages > 0) {
|
||||||
|
page = pagination.totalPages;
|
||||||
|
}
|
||||||
setPagination(prev => ({ ...prev, page }));
|
setPagination(prev => ({ ...prev, page }));
|
||||||
loadEnterprises({
|
loadEnterprises({
|
||||||
|
page,
|
||||||
filters: searchFilters,
|
filters: searchFilters,
|
||||||
pagination: { page, size: pagination.size }
|
size: pagination.size
|
||||||
});
|
});
|
||||||
}, [loadEnterprises, searchFilters, pagination.size]);
|
}, [searchFilters, pagination.size, pagination.totalPages, loadEnterprises]);
|
||||||
|
|
||||||
|
// 每页条数变化处理
|
||||||
const handleSizeChange = useCallback((size: number) => {
|
const handleSizeChange = useCallback((size: number) => {
|
||||||
setPagination(prev => ({ ...prev, size, page: 1 }));
|
setPagination(prev => ({ ...prev, size, page: 1 }));
|
||||||
loadEnterprises({
|
loadEnterprises({
|
||||||
filters: searchFilters,
|
resetPage: true,
|
||||||
pagination: { page: 1, size }
|
page: 1,
|
||||||
|
size,
|
||||||
|
filters: searchFilters
|
||||||
});
|
});
|
||||||
}, [loadEnterprises, searchFilters]);
|
}, [searchFilters, loadEnterprises]);
|
||||||
|
|
||||||
|
// URL状态变化处理 - 处理浏览器前进后退时的参数恢复
|
||||||
|
const handleUrlStateChange = useCallback((urlState: {
|
||||||
|
filters: Record<string, string>;
|
||||||
|
pagination: { page: number; size: number };
|
||||||
|
}) => {
|
||||||
|
console.log('企业管理页面 - URL状态变化:', urlState);
|
||||||
|
|
||||||
|
// 更新内部状态
|
||||||
|
setSearchFilters(urlState.filters);
|
||||||
|
setPagination(prev => ({
|
||||||
|
...prev,
|
||||||
|
page: urlState.pagination.page,
|
||||||
|
size: urlState.pagination.size
|
||||||
|
}));
|
||||||
|
|
||||||
|
// 触发数据加载
|
||||||
|
loadEnterprises({
|
||||||
|
page: urlState.pagination.page,
|
||||||
|
size: urlState.pagination.size,
|
||||||
|
filters: urlState.filters
|
||||||
|
});
|
||||||
|
}, [loadEnterprises]);
|
||||||
|
|
||||||
// 初始化数据加载
|
// 初始化数据加载
|
||||||
// useEffect(() => {
|
// useEffect(() => {
|
||||||
@@ -496,7 +605,7 @@ export default function EnterpriseManagement() {
|
|||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
emptyIcon={<Building2 className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
emptyIcon={<Building2 className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
||||||
emptyText="暂无企业数据"
|
emptyText="暂无企业数据"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* View Enterprise Details Dialog */}
|
{/* View Enterprise Details Dialog */}
|
||||||
<Dialog open={dialogs.showViewDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
|
<Dialog open={dialogs.showViewDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
|
||||||
|
|||||||
@@ -282,6 +282,20 @@ export default function TenantUserManagementPage() {
|
|||||||
size?: number;
|
size?: number;
|
||||||
} = {}) => {
|
} = {}) => {
|
||||||
try {
|
try {
|
||||||
|
// 优先从URL读取参数
|
||||||
|
let urlParams = {};
|
||||||
|
if (typeof window !== 'undefined') {
|
||||||
|
const params = new URLSearchParams(window.location.search);
|
||||||
|
urlParams = {
|
||||||
|
search: params.get('search') || undefined,
|
||||||
|
status: params.get('status') || undefined,
|
||||||
|
type: params.get('type') || undefined,
|
||||||
|
page: params.get('page') ? parseInt(params.get('page')!, 10) : undefined,
|
||||||
|
size: params.get('size') ? parseInt(params.get('size')!, 10) : undefined
|
||||||
|
};
|
||||||
|
console.log('从URL读取的参数:', urlParams);
|
||||||
|
}
|
||||||
|
|
||||||
dispatch({ type: 'SET_LOADING', payload: true });
|
dispatch({ type: 'SET_LOADING', payload: true });
|
||||||
|
|
||||||
// 解构选项参数,提供默认值
|
// 解构选项参数,提供默认值
|
||||||
@@ -294,14 +308,22 @@ export default function TenantUserManagementPage() {
|
|||||||
size
|
size
|
||||||
} = options;
|
} = options;
|
||||||
|
|
||||||
|
// 优先级:URL参数 > 传入参数 > 父组件状态
|
||||||
|
const finalPage = resetPage ? 1 : (urlParams.page || page || state.pagination.page);
|
||||||
|
const finalSize = urlParams.size || size || state.pagination.size;
|
||||||
|
|
||||||
const params: UsersQueryParams = {
|
const params: UsersQueryParams = {
|
||||||
page: resetPage ? 1 : (page || state.pagination.page),
|
page: finalPage,
|
||||||
size: size || state.pagination.size,
|
size: finalSize,
|
||||||
is_active: true,
|
is_active: true,
|
||||||
};
|
};
|
||||||
|
|
||||||
// 使用传入的过滤器参数,如果没有传入则使用当前状态
|
// 使用正确的优先级:URL参数 > 传入参数 > 父组件状态
|
||||||
const currentFilters = filters || state.filters;
|
const currentFilters = {
|
||||||
|
searchKeyword: urlParams.search || (filters?.searchKeyword) || state.filters.searchKeyword,
|
||||||
|
statusFilter: urlParams.status || (filters?.statusFilter) || state.filters.statusFilter,
|
||||||
|
typeFilter: urlParams.type || (filters?.typeFilter) || state.filters.typeFilter
|
||||||
|
};
|
||||||
const currentSortBy = sortBy || state.sortBy;
|
const currentSortBy = sortBy || state.sortBy;
|
||||||
const currentSortOrder = sortOrder || state.sortOrder;
|
const currentSortOrder = sortOrder || state.sortOrder;
|
||||||
|
|
||||||
@@ -350,24 +372,41 @@ export default function TenantUserManagementPage() {
|
|||||||
}
|
}
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
// 搜索处理
|
// 搜索处理 - 保持传统的简洁方式
|
||||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||||
|
console.log('用户管理页面 - 收到搜索条件:', filters);
|
||||||
|
|
||||||
const mappedFilters = {
|
const mappedFilters = {
|
||||||
searchKeyword: filters.search || '',
|
searchKeyword: filters.search || '',
|
||||||
statusFilter: filters.status || 'all',
|
statusFilter: filters.status || 'all',
|
||||||
typeFilter: filters.type || 'all'
|
typeFilter: filters.type || 'all'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 更新过滤器状态
|
||||||
dispatch({ type: 'SET_FILTERS', payload: mappedFilters });
|
dispatch({ type: 'SET_FILTERS', payload: mappedFilters });
|
||||||
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
|
|
||||||
// 传入所有当前参数,避免覆盖其他参数
|
// 搜索时重置到第1页
|
||||||
|
dispatch({ type: 'SET_PAGINATION', payload: { page: 1, size: state.pagination.size } });
|
||||||
|
|
||||||
|
// 执行查询
|
||||||
loadUsers({
|
loadUsers({
|
||||||
resetPage: true,
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
filters: mappedFilters,
|
filters: mappedFilters,
|
||||||
sortBy: state.sortBy,
|
sortBy: state.sortBy,
|
||||||
sortOrder: state.sortOrder,
|
sortOrder: state.sortOrder,
|
||||||
size: state.pagination.size
|
size: state.pagination.size
|
||||||
});
|
});
|
||||||
}, [state.sortBy, state.sortOrder, state.pagination.size]);
|
|
||||||
|
console.log('触发用户查询 - 参数:', {
|
||||||
|
resetPage: true,
|
||||||
|
page: 1,
|
||||||
|
filters: mappedFilters,
|
||||||
|
sortBy: state.sortBy,
|
||||||
|
sortOrder: state.sortOrder,
|
||||||
|
size: state.pagination.size
|
||||||
|
});
|
||||||
|
}, [state.sortBy, state.sortOrder, state.pagination.size, loadUsers]);
|
||||||
|
|
||||||
// 排序处理
|
// 排序处理
|
||||||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||||
@@ -504,7 +543,7 @@ export default function TenantUserManagementPage() {
|
|||||||
onSort={handleSort}
|
onSort={handleSort}
|
||||||
emptyText="暂无用户数据"
|
emptyText="暂无用户数据"
|
||||||
sizeOptions={[10, 20, 50, 100]}
|
sizeOptions={[10, 20, 50, 100]}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
{/* 用户详情对话框 */}
|
{/* 用户详情对话框 */}
|
||||||
<UserDetailDialog
|
<UserDetailDialog
|
||||||
|
|||||||
@@ -74,20 +74,44 @@ export function SearchFormComponent({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 使用防抖来减少搜索频率,仅针对文本输入
|
// 使用防抖来减少搜索频率,仅针对文本输入
|
||||||
|
// 优化:添加ref跟踪防抖状态,避免不必要的重新执行
|
||||||
|
const debounceTimeoutRef = useRef<NodeJS.Timeout>();
|
||||||
|
const lastTextChangeRef = useRef<string>('');
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
// 只有当最后变化的是 text 字段时才进行防抖,排除初始化和 select 字段
|
// 只有当最后变化的是 text 字段时才进行防抖,排除初始化和 select 字段
|
||||||
if (localFilters._lastChangedFieldType === 'text') {
|
if (localFilters._lastChangedFieldType === 'text') {
|
||||||
const timer = setTimeout(() => {
|
// 提取当前文本字段的值
|
||||||
// 移除标记字段后再调用
|
const textValue = localFilters[localFilters._lastChangedFieldKey] || '';
|
||||||
const { _lastChangedFieldType, _lastChangedFieldKey, ...cleanFilters } = localFilters;
|
|
||||||
// 使用ref引用最新的onFiltersChange函数,避免依赖变化导致重复触发
|
|
||||||
onFiltersChangeRef.current(cleanFilters);
|
|
||||||
}, 300); // 300ms 防抖延迟
|
|
||||||
|
|
||||||
return () => clearTimeout(timer);
|
// 只有当文本内容真正发生变化时才进行防抖
|
||||||
|
if (textValue !== lastTextChangeRef.current) {
|
||||||
|
lastTextChangeRef.current = textValue;
|
||||||
|
|
||||||
|
// 清除之前的防抖定时器
|
||||||
|
if (debounceTimeoutRef.current) {
|
||||||
|
clearTimeout(debounceTimeoutRef.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
debounceTimeoutRef.current = setTimeout(() => {
|
||||||
|
// 移除标记字段后再调用
|
||||||
|
const { _lastChangedFieldType, _lastChangedFieldKey, ...cleanFilters } = localFilters;
|
||||||
|
// 使用ref引用最新的onFiltersChange函数,避免依赖变化导致重复触发
|
||||||
|
onFiltersChangeRef.current(cleanFilters);
|
||||||
|
}, 300); // 300ms 防抖延迟
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}, [localFilters]); // 只依赖localFilters
|
}, [localFilters]); // 只依赖localFilters
|
||||||
|
|
||||||
|
// 组件卸载时清理防抖定时器
|
||||||
|
useEffect(() => {
|
||||||
|
return () => {
|
||||||
|
if (debounceTimeoutRef.current) {
|
||||||
|
clearTimeout(debounceTimeoutRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 计算显示的字段
|
// 计算显示的字段
|
||||||
const visibleFields = showAllFields
|
const visibleFields = showAllFields
|
||||||
? fields
|
? fields
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
*/
|
*/
|
||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState, useEffect, useMemo, useCallback } from 'react';
|
import { useState, useEffect, useMemo, useCallback, useRef } from 'react';
|
||||||
import { Card } from '@/components/ui/card';
|
import { Card } from '@/components/ui/card';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
@@ -47,7 +47,28 @@ export interface PaginationConfig {
|
|||||||
hasPrev: boolean;
|
hasPrev: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 组件Props接口 - 简化版本
|
// URL 参数同步配置
|
||||||
|
export interface UrlSyncConfig {
|
||||||
|
// 是否启用 URL 参数同步(默认为true,只有传false才禁用)
|
||||||
|
enabled?: boolean;
|
||||||
|
// 是否在初始化时检测空 URL 并添加默认参数(默认为true)
|
||||||
|
initWithDefaults?: boolean;
|
||||||
|
// 默认分页参数(默认为 { page: 1, size: 10 })
|
||||||
|
defaultPagination?: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
// URL 更新防抖时间(毫秒,默认为300)
|
||||||
|
updateDebounce?: number;
|
||||||
|
// 自定义 URL 参数名映射(可选,不配置则自动从 searchFields 提取)
|
||||||
|
paramNames?: {
|
||||||
|
page?: string;
|
||||||
|
size?: string;
|
||||||
|
[key: string]: string | undefined;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// 组件Props接口 - 增强版本
|
||||||
export interface SearchFormPaginationProps<T = any> {
|
export interface SearchFormPaginationProps<T = any> {
|
||||||
// 搜索表单配置
|
// 搜索表单配置
|
||||||
formTitle?: string;
|
formTitle?: string;
|
||||||
@@ -81,6 +102,9 @@ export interface SearchFormPaginationProps<T = any> {
|
|||||||
// 自定义样式
|
// 自定义样式
|
||||||
className?: string;
|
className?: string;
|
||||||
|
|
||||||
|
// URL 参数同步配置
|
||||||
|
urlSync?: UrlSyncConfig;
|
||||||
|
|
||||||
// 数据更新回调 - 用于父组件获取搜索条件变化
|
// 数据更新回调 - 用于父组件获取搜索条件变化
|
||||||
onDataUpdate?: (data: {
|
onDataUpdate?: (data: {
|
||||||
items: T[];
|
items: T[];
|
||||||
@@ -88,6 +112,15 @@ export interface SearchFormPaginationProps<T = any> {
|
|||||||
loading: boolean;
|
loading: boolean;
|
||||||
error: string | null;
|
error: string | null;
|
||||||
}) => void;
|
}) => void;
|
||||||
|
|
||||||
|
// URL 状态变化回调 - 用于父组件监听 URL 参数变化(可选)
|
||||||
|
onUrlStateChange?: (urlState: {
|
||||||
|
filters: Record<string, string>;
|
||||||
|
pagination: {
|
||||||
|
page: number;
|
||||||
|
size: number;
|
||||||
|
};
|
||||||
|
}) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function SearchFormPagination<T = any>({
|
export function SearchFormPagination<T = any>({
|
||||||
@@ -111,16 +144,313 @@ export function SearchFormPagination<T = any>({
|
|||||||
sizeOptions = [10, 30, 50, 100],
|
sizeOptions = [10, 30, 50, 100],
|
||||||
maxVisiblePages = 7,
|
maxVisiblePages = 7,
|
||||||
className = '',
|
className = '',
|
||||||
|
urlSync,
|
||||||
onDataUpdate,
|
onDataUpdate,
|
||||||
|
onUrlStateChange,
|
||||||
}: SearchFormPaginationProps<T>) {
|
}: SearchFormPaginationProps<T>) {
|
||||||
|
// URL 同步配置 - 自动从 searchFields 提取参数映射
|
||||||
|
const urlConfig = useMemo(() => {
|
||||||
|
// 默认启用URL同步,除非明确禁用
|
||||||
|
const isEnabled = urlSync?.enabled !== false; // 默认为true,只有明确传false才禁用
|
||||||
|
|
||||||
|
// 如果启用 URL 同步,自动生成参数映射
|
||||||
|
const autoParamNames = isEnabled ? {
|
||||||
|
page: urlSync?.paramNames?.page ?? 'page',
|
||||||
|
size: urlSync?.paramNames?.size ?? 'size',
|
||||||
|
// 从 searchFields 自动提取参数映射
|
||||||
|
...Object.fromEntries(
|
||||||
|
searchFields.map(field => [
|
||||||
|
field.key,
|
||||||
|
urlSync?.paramNames?.[field.key] ?? field.key // 优先使用自定义映射,否则使用字段 key
|
||||||
|
])
|
||||||
|
)
|
||||||
|
} : {};
|
||||||
|
|
||||||
|
return {
|
||||||
|
enabled: isEnabled,
|
||||||
|
paramNames: autoParamNames,
|
||||||
|
initWithDefaults: urlSync?.initWithDefaults ?? true, // 默认为true
|
||||||
|
defaultPagination: urlSync?.defaultPagination ?? { page: 1, size: 10 },
|
||||||
|
updateDebounce: urlSync?.updateDebounce ?? 300 // 默认300ms
|
||||||
|
};
|
||||||
|
}, [urlSync, searchFields]);
|
||||||
|
|
||||||
|
// URL 参数解析和同步相关的 ref
|
||||||
|
const isInitializingRef = useRef(false);
|
||||||
|
const lastUrlStateRef = useRef<string>('');
|
||||||
|
|
||||||
|
// 更新来源跟踪 - 用于区分不同类型的更新
|
||||||
|
const updateSourceRef = useRef<'user' | 'parent' | 'url' | 'init'>('init');
|
||||||
|
|
||||||
// 简化的内部状态 - 只管理搜索条件
|
// 简化的内部状态 - 只管理搜索条件
|
||||||
const [filters, setFilters] = useState<Record<string, string>>(
|
const [filters, setFilters] = useState<Record<string, string>>(() => {
|
||||||
searchFields.reduce((acc, field) => {
|
// 初始化时从 URL 读取参数(如果启用 URL 同步)
|
||||||
|
if (urlConfig.enabled && typeof window !== 'undefined') {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const initialFilters: Record<string, string> = {};
|
||||||
|
|
||||||
|
// 从 URL 中读取搜索字段参数
|
||||||
|
searchFields.forEach(field => {
|
||||||
|
const paramValue = urlParams.get(urlConfig.paramNames[field.key] || field.key);
|
||||||
|
if (paramValue) {
|
||||||
|
initialFilters[field.key] = paramValue;
|
||||||
|
} else {
|
||||||
|
initialFilters[field.key] = field.defaultValue || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return initialFilters;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认初始化逻辑
|
||||||
|
return searchFields.reduce((acc, field) => {
|
||||||
acc[field.key] = field.defaultValue || '';
|
acc[field.key] = field.defaultValue || '';
|
||||||
return acc;
|
return acc;
|
||||||
}, {} as Record<string, string>)
|
}, {} as Record<string, string>);
|
||||||
|
});
|
||||||
|
|
||||||
|
// URL 更新函数 - 事件驱动模型,移除防抖
|
||||||
|
const updateUrl = useCallback((
|
||||||
|
newFilters: Record<string, string>,
|
||||||
|
newPagination?: { page: number; size: number },
|
||||||
|
source: 'user' | 'parent' | 'url' | 'init' = 'user'
|
||||||
|
) => {
|
||||||
|
if (!urlConfig.enabled || typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
// 设置更新来源
|
||||||
|
updateSourceRef.current = source;
|
||||||
|
|
||||||
|
// 立即执行URL更新,不使用防抖
|
||||||
|
try {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
const urlParams = url.searchParams;
|
||||||
|
|
||||||
|
console.log(`开始更新URL - 来源: ${source}`, { newFilters, newPagination });
|
||||||
|
|
||||||
|
// 更新搜索参数
|
||||||
|
Object.entries(newFilters).forEach(([key, value]) => {
|
||||||
|
const paramName = urlConfig.paramNames[key] || key;
|
||||||
|
if (value && value !== '') {
|
||||||
|
urlParams.set(paramName, value);
|
||||||
|
} else {
|
||||||
|
urlParams.delete(paramName);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新分页参数
|
||||||
|
if (newPagination) {
|
||||||
|
urlParams.set(urlConfig.paramNames.page, newPagination.page.toString());
|
||||||
|
urlParams.set(urlConfig.paramNames.size, newPagination.size.toString());
|
||||||
|
}
|
||||||
|
|
||||||
|
// 构建新的 URL 状态字符串
|
||||||
|
const newUrlState = urlParams.toString();
|
||||||
|
|
||||||
|
// 只有在 URL 状态发生变化时才更新
|
||||||
|
if (newUrlState !== lastUrlStateRef.current) {
|
||||||
|
lastUrlStateRef.current = newUrlState;
|
||||||
|
|
||||||
|
// 更新浏览器历史记录
|
||||||
|
window.history.replaceState({}, '', url.toString());
|
||||||
|
|
||||||
|
// 只有在 URL 变化时才通知父组件(避免循环调用)
|
||||||
|
// 用户操作和父组件更新不需要触发 onUrlStateChange
|
||||||
|
// 只有 URL 本身变化(如浏览器前进后退)才需要触发
|
||||||
|
if (source === 'url' && !isInitializingRef.current) {
|
||||||
|
onUrlStateChange?.({
|
||||||
|
filters: newFilters,
|
||||||
|
pagination: newPagination || urlConfig.defaultPagination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to update URL:', error);
|
||||||
|
}
|
||||||
|
}, [urlConfig, onUrlStateChange]);
|
||||||
|
|
||||||
|
// 内部状态管理
|
||||||
|
const [internalPagination, setInternalPagination] = useState<{ page: number; size: number }>(
|
||||||
|
urlConfig.defaultPagination
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// 从 URL 读取分页参数的函数
|
||||||
|
const readPaginationFromUrl = useCallback((): { page: number; size: number } => {
|
||||||
|
if (!urlConfig.enabled || typeof window === 'undefined') {
|
||||||
|
return urlConfig.defaultPagination;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const urlPage = urlParams.get(urlConfig.paramNames.page);
|
||||||
|
const urlSize = urlParams.get(urlConfig.paramNames.size);
|
||||||
|
|
||||||
|
return {
|
||||||
|
page: urlPage ? Math.max(1, parseInt(urlPage, 10) || 1) : urlConfig.defaultPagination.page,
|
||||||
|
size: urlSize ? Math.max(1, parseInt(urlSize, 10) || 10) : urlConfig.defaultPagination.size
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to read pagination from URL:', error);
|
||||||
|
return urlConfig.defaultPagination;
|
||||||
|
}
|
||||||
|
}, [urlConfig]);
|
||||||
|
|
||||||
|
|
||||||
|
// 初始化 URL 参数检测和默认值设置
|
||||||
|
useEffect(() => {
|
||||||
|
if (!urlConfig.enabled || typeof window === 'undefined' || isInitializingRef.current) return;
|
||||||
|
|
||||||
|
isInitializingRef.current = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const hasSearchParams = urlParams.toString() !== '';
|
||||||
|
|
||||||
|
console.log('URL同步初始化 - 当前URL参数:', Object.fromEntries(urlParams.entries()));
|
||||||
|
console.log('URL同步初始化 - 是否有URL参数:', hasSearchParams);
|
||||||
|
|
||||||
|
// 如果 URL 没有参数且配置要求初始化默认值
|
||||||
|
if (!hasSearchParams && urlConfig.initWithDefaults) {
|
||||||
|
const url = new URL(window.location.href);
|
||||||
|
|
||||||
|
// 始终设置默认分页参数到URL,不管是否为默认值
|
||||||
|
urlParams.set(urlConfig.paramNames.page, urlConfig.defaultPagination.page.toString());
|
||||||
|
urlParams.set(urlConfig.paramNames.size, urlConfig.defaultPagination.size.toString());
|
||||||
|
|
||||||
|
// 设置默认搜索字段值 - 优先使用searchFields的defaultValue
|
||||||
|
searchFields.forEach(field => {
|
||||||
|
const fieldValue = filters[field.key] || field.defaultValue || '';
|
||||||
|
if (fieldValue !== '') {
|
||||||
|
const paramName = urlConfig.paramNames[field.key] || field.key;
|
||||||
|
urlParams.set(paramName, fieldValue);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (urlParams.toString() !== '') {
|
||||||
|
url.search = urlParams.toString();
|
||||||
|
window.history.replaceState({}, '', url.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 从URL读取过滤条件和分页参数
|
||||||
|
const urlFilters: Record<string, string> = {};
|
||||||
|
let hasUrlFilters = false;
|
||||||
|
|
||||||
|
searchFields.forEach(field => {
|
||||||
|
const paramValue = urlParams.get(urlConfig.paramNames[field.key] || field.key);
|
||||||
|
if (paramValue) {
|
||||||
|
urlFilters[field.key] = paramValue;
|
||||||
|
hasUrlFilters = true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const urlPagination = readPaginationFromUrl();
|
||||||
|
console.log('URL同步初始化 - 从URL读取的过滤条件:', urlFilters);
|
||||||
|
console.log('URL同步初始化 - 从URL读取的分页参数:', urlPagination);
|
||||||
|
|
||||||
|
// 设置内部分页状态
|
||||||
|
setInternalPagination(urlPagination);
|
||||||
|
|
||||||
|
// URL中有参数时,自动同步到内部状态并通知父组件
|
||||||
|
if (hasUrlFilters || hasSearchParams) {
|
||||||
|
const finalFilters = hasUrlFilters ? urlFilters : filters;
|
||||||
|
console.log('URL同步初始化 - 同步参数到内部状态:', {
|
||||||
|
filters: finalFilters,
|
||||||
|
pagination: urlPagination
|
||||||
|
});
|
||||||
|
|
||||||
|
// 更新内部状态
|
||||||
|
if (hasUrlFilters) {
|
||||||
|
setFilters(finalFilters);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 通知父组件URL状态变化(标记为初始化)
|
||||||
|
// 使用下一个事件循环确保状态已更新
|
||||||
|
Promise.resolve().then(() => {
|
||||||
|
if (!isInitializingRef.current) {
|
||||||
|
onUrlStateChange?.({
|
||||||
|
filters: finalFilters,
|
||||||
|
pagination: urlPagination
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Failed to initialize URL parameters:', error);
|
||||||
|
}
|
||||||
|
}, [urlConfig, filters, searchFields, readPaginationFromUrl]);
|
||||||
|
|
||||||
|
// 监听父组件传入的分页状态变化 - 同步到内部状态和URL
|
||||||
|
useEffect(() => {
|
||||||
|
if (pagination && urlConfig.enabled && !isInitializingRef.current) {
|
||||||
|
console.log('父组件分页状态变化:', pagination);
|
||||||
|
|
||||||
|
// 如果父组件的分页状态与内部状态不一致,同步内部状态
|
||||||
|
if (pagination.page !== internalPagination.page || pagination.size !== internalPagination.size) {
|
||||||
|
setInternalPagination({
|
||||||
|
page: pagination.page,
|
||||||
|
size: pagination.size
|
||||||
|
});
|
||||||
|
|
||||||
|
// 同步到URL(标记为父组件更新)
|
||||||
|
updateUrl(filters, pagination, 'parent');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}, [pagination, filters, urlConfig.enabled, updateUrl, internalPagination.page, internalPagination.size]);
|
||||||
|
|
||||||
|
// 监听浏览器前进后退事件
|
||||||
|
useEffect(() => {
|
||||||
|
if (!urlConfig.enabled || typeof window === 'undefined') return;
|
||||||
|
|
||||||
|
const handlePopState = (event: PopStateEvent) => {
|
||||||
|
console.log('浏览器前进后退事件触发:', event);
|
||||||
|
|
||||||
|
const urlParams = new URLSearchParams(window.location.search);
|
||||||
|
const newFilters: Record<string, string> = {};
|
||||||
|
|
||||||
|
// 从 URL 中读取搜索字段参数
|
||||||
|
searchFields.forEach(field => {
|
||||||
|
const paramValue = urlParams.get(urlConfig.paramNames[field.key] || field.key);
|
||||||
|
if (paramValue) {
|
||||||
|
newFilters[field.key] = paramValue;
|
||||||
|
} else {
|
||||||
|
newFilters[field.key] = field.defaultValue || '';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const newPagination = {
|
||||||
|
page: Math.max(1, parseInt(urlParams.get(urlConfig.paramNames.page) || '1', 10)),
|
||||||
|
size: Math.max(1, parseInt(urlParams.get(urlConfig.paramNames.size) || '10', 10))
|
||||||
|
};
|
||||||
|
|
||||||
|
console.log('从URL恢复状态:', { newFilters, newPagination });
|
||||||
|
|
||||||
|
// 更新内部状态(标记为URL变化)
|
||||||
|
setFilters(newFilters);
|
||||||
|
setInternalPagination(newPagination);
|
||||||
|
|
||||||
|
// 通知父组件 URL 状态变化(只有URL来源的变化才触发)
|
||||||
|
onUrlStateChange?.({
|
||||||
|
filters: newFilters,
|
||||||
|
pagination: newPagination
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
window.addEventListener('popstate', handlePopState);
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
window.removeEventListener('popstate', handlePopState);
|
||||||
|
};
|
||||||
|
}, [urlConfig, searchFields, onUrlStateChange]);
|
||||||
|
|
||||||
|
|
||||||
|
// 重置初始化标志 - 确保在初始化完成后允许 URL 更新
|
||||||
|
useEffect(() => {
|
||||||
|
if (isInitializingRef.current) {
|
||||||
|
// 立即重置初始化标志,不使用延迟
|
||||||
|
isInitializingRef.current = false;
|
||||||
|
}
|
||||||
|
}, []);
|
||||||
|
|
||||||
// 数据更新回调 - 通知父组件数据变化
|
// 数据更新回调 - 通知父组件数据变化
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
onDataUpdate?.({
|
onDataUpdate?.({
|
||||||
@@ -138,19 +468,54 @@ export function SearchFormPagination<T = any>({
|
|||||||
});
|
});
|
||||||
}, [data, pagination, loading, error, onDataUpdate]);
|
}, [data, pagination, loading, error, onDataUpdate]);
|
||||||
|
|
||||||
// 简化的事件处理器 - 纯粹的状态通知
|
// 简化的事件处理器 - 事件驱动模型
|
||||||
const handleSearch = useCallback((newFilters: Record<string, string>) => {
|
const handleSearch = useCallback((newFilters: Record<string, string>) => {
|
||||||
|
console.log('用户搜索操作:', newFilters);
|
||||||
|
|
||||||
|
// 更新内部状态
|
||||||
setFilters(newFilters);
|
setFilters(newFilters);
|
||||||
|
|
||||||
|
// 搜索时重置到第1页,保持当前每页大小
|
||||||
|
const newPagination = { page: 1, size: internalPagination.size };
|
||||||
|
setInternalPagination(newPagination);
|
||||||
|
|
||||||
|
// 通知父组件搜索条件变化
|
||||||
onSearch?.(newFilters);
|
onSearch?.(newFilters);
|
||||||
}, [onSearch]);
|
|
||||||
|
// 同步到URL(标记为用户操作)
|
||||||
|
updateUrl(newFilters, newPagination, 'user');
|
||||||
|
}, [internalPagination.size, onSearch, updateUrl]);
|
||||||
|
|
||||||
const handlePageChange = useCallback((page: number) => {
|
const handlePageChange = useCallback((page: number) => {
|
||||||
|
console.log('用户分页操作:', page);
|
||||||
|
|
||||||
|
const newPagination = { ...internalPagination, page };
|
||||||
|
|
||||||
|
// 更新内部状态
|
||||||
|
setInternalPagination(newPagination);
|
||||||
|
|
||||||
|
// 通知父组件分页变化
|
||||||
onPageChange?.(page);
|
onPageChange?.(page);
|
||||||
}, [onPageChange]);
|
|
||||||
|
// 同步到URL(标记为用户操作)
|
||||||
|
updateUrl(filters, newPagination, 'user');
|
||||||
|
}, [internalPagination, filters, onPageChange, updateUrl]);
|
||||||
|
|
||||||
const handleSizeChange = useCallback((size: number) => {
|
const handleSizeChange = useCallback((size: number) => {
|
||||||
|
console.log('用户修改每页大小:', size);
|
||||||
|
|
||||||
|
// 修改每页大小时重置到第1页
|
||||||
|
const newPagination = { page: 1, size };
|
||||||
|
|
||||||
|
// 更新内部状态
|
||||||
|
setInternalPagination(newPagination);
|
||||||
|
|
||||||
|
// 通知父组件每页大小变化
|
||||||
onSizeChange?.(size);
|
onSizeChange?.(size);
|
||||||
}, [onSizeChange]);
|
|
||||||
|
// 同步到URL(标记为用户操作)
|
||||||
|
updateUrl(filters, newPagination, 'user');
|
||||||
|
}, [filters, onSizeChange, updateUrl]);
|
||||||
|
|
||||||
// 稳定的filters引用
|
// 稳定的filters引用
|
||||||
const stableFilters = useMemo(() => filters, [filters]);
|
const stableFilters = useMemo(() => filters, [filters]);
|
||||||
|
|||||||
Reference in New Issue
Block a user