From b1be380334b9bc4c7f8b8ff58f91b4ff85c14002 Mon Sep 17 00:00:00 2001 From: peng Date: Tue, 11 Nov 2025 10:28:02 +0800 Subject: [PATCH] =?UTF-8?q?=E7=94=9F=E4=BA=A7=E7=AE=A1=E7=90=86=E7=B3=BB?= =?UTF-8?q?=E7=BB=9F=20-=20=E9=A1=B5=E9=9D=A2=E4=B8=8A=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E5=8F=82=E6=95=B0=E7=BC=93=E5=AD=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crop-x-new/docs/开发项目规范.md | 537 +++++++++++++++++- .../tenant/audit-history/page.tsx | 203 +++++-- .../tenant/enterprise-audit/page.tsx | 156 +++-- .../tenant/enterprise-management/page.tsx | 193 +++++-- .../tenant/user-management/page.tsx | 57 +- .../components/SearchFormComponent.tsx | 38 +- .../common/searchFormPagination/page.tsx | 383 ++++++++++++- 7 files changed, 1413 insertions(+), 154 deletions(-) diff --git a/crop-x-new/docs/开发项目规范.md b/crop-x-new/docs/开发项目规范.md index 838acb9..1ce73bf 100644 --- a/crop-x-new/docs/开发项目规范.md +++ b/crop-x-new/docs/开发项目规范.md @@ -1474,4 +1474,539 @@ SearchFormPagination 组件通过配置驱动的方式,极大地简化了复 - **类型安全**:完整的 TypeScript 支持 - **功能专注**:专注搜索、展示、分页核心功能,避免过度设计 -该组件可以作为项目中所有数据展示页面的标准解决方案,显著提升开发效率和代码质量。 \ No newline at end of file +该组件可以作为项目中所有数据展示页面的标准解决方案,显著提升开发效率和代码质量。 + +--- + +## 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 同步(默认行为) + + +// 启用 URL 同步 - 极简配置 + +``` + +### 接口定义 + +#### 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 void; +``` + +### 使用方式 + +#### 1. 基础使用(推荐) + +```tsx + +``` + +#### 2. 高级配置(自定义参数名) + +```tsx + { + console.log('URL 状态变化:', urlState); + }} +/> +``` + +#### 3. 父组件统一查询处理 + +```tsx +// 统一查询处理函数 +const handleQueryChange = useCallback((query: { + filters: Record; + 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 +// 现有代码无需修改,继续正常工作 + + +// 极简启用 - 只需添加一行 + +``` + +### 配置简化对比 + +#### 优化前(复杂配置) +```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` 等过于简化的参数名 + +该功能特别适用于数据展示、搜索、筛选等需要状态保持的场景,是提升用户体验的重要功能。 \ No newline at end of file diff --git a/crop-x-new/src/app/(app)/central-config/tenant/audit-history/page.tsx b/crop-x-new/src/app/(app)/central-config/tenant/audit-history/page.tsx index f848ce9..b5e8b11 100644 --- a/crop-x-new/src/app/(app)/central-config/tenant/audit-history/page.tsx +++ b/crop-x-new/src/app/(app)/central-config/tenant/audit-history/page.tsx @@ -242,43 +242,103 @@ export default function AuditHistoryPage() { date_range: 'all' }); - // 数据加载函数 - 移除不必要的依赖避免重复调用 - const loadAuditHistory = useCallback(async (params?: { + // 数据加载函数 - 优先从浏览器URL参数读取 + const loadAuditHistory = useCallback(async (options: { + resetPage?: boolean; filters?: Record; - pagination?: { page: number; size: number }; - sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' }; - }) => { + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + page?: number; + size?: number; + } = {}) => { 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); setError(null); - const finalParams: AuditLogsQueryParams = { - search_keyword: (params?.filters?.search ?? searchFilters.search) || undefined, - action: params?.filters?.action ?? searchFilters.action, - audit_status: params?.filters?.audit_status ?? searchFilters.audit_status, - date_range: params?.filters?.date_range ?? searchFilters.date_range, - page: params?.pagination?.page || pagination.page, - size: params?.pagination?.size || pagination.size, - order_by: params?.sort?.sortBy, - sort_order: params?.sort?.sortOrder, + // 解构选项参数,提供默认值 + const { + resetPage = false, + filters, + sortBy, + sortOrder, + page, + size + } = 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'则不传该参数 - if (finalParams.action === 'all') { - finalParams.action = undefined; - } - if (finalParams.audit_status === 'all') { - finalParams.audit_status = undefined; - } - if (finalParams.date_range === 'all') { - finalParams.date_range = undefined; + // 使用正确的优先级:URL参数 > 传入参数 > 父组件状态 + const currentFilters = { + search: urlParams.search || (filters?.search) || searchFilters.search, + action: urlParams.action || (filters?.action) || searchFilters.action, + audit_status: urlParams.audit_status || (filters?.audit_status) || searchFilters.audit_status, + date_range: urlParams.date_range || (filters?.date_range) || searchFilters.date_range + }; + const currentSortBy = sortBy || 'created_at'; + 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); 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) { const errorMessage = err instanceof Error ? err.message : '加载审核历史失败'; setError(errorMessage); @@ -286,7 +346,7 @@ export default function AuditHistoryPage() { } finally { setLoading(false); } - }, [searchFilters, pagination]); // 添加依赖以保持函数引用最新 + }, []); // 移除依赖项,通过参数传递 const didFetchRef = useRef(false) @@ -295,38 +355,94 @@ useEffect(() => { didFetchRef.current = true loadAuditHistory() }, []) - // 事件处理器 + // 搜索处理 - 保持传统的简洁方式 const handleSearch = useCallback((filters: Record) => { - setSearchFilters(filters); - // 搜索时重置到第一页 - loadAuditHistory({ - filters, - pagination: { page: 1, size: pagination.size } - }); - }, [loadAuditHistory, pagination.size]); + console.log('审核历史页面 - 收到搜索条件:', filters); + // 更新过滤器状态 + 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') => { // 排序时重置到第一页 + setPagination(prev => ({ ...prev, page: 1 })); loadAuditHistory({ - pagination: { page: 1, size: pagination.size }, - sort: { sortBy, sortOrder } + resetPage: true, + page: 1, + filters: searchFilters, + sortBy, + sortOrder, + size: pagination.size }); - }, [loadAuditHistory, pagination.size]); + }, [searchFilters, pagination.size, loadAuditHistory]); + // 分页处理 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 })); loadAuditHistory({ + page, 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) => { setPagination(prev => ({ ...prev, size, page: 1 })); loadAuditHistory({ - filters: searchFilters, - pagination: { page: 1, size } + resetPage: true, + page: 1, + size, + filters: searchFilters }); - }, [loadAuditHistory, searchFilters]); + }, [searchFilters, loadAuditHistory]); + + // URL状态变化处理 - 处理浏览器前进后退时的参数恢复 + const handleUrlStateChange = useCallback((urlState: { + filters: Record; + 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) => { @@ -381,7 +497,8 @@ useEffect(() => { emptyIcon={} emptyText="暂无审核记录" sizeOptions={[10, 20, 50, 100]} - /> + + /> {/* View Audit Record Details Dialog */} dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}> diff --git a/crop-x-new/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx b/crop-x-new/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx index a1d2c10..390de75 100644 --- a/crop-x-new/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx +++ b/crop-x-new/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx @@ -165,12 +165,13 @@ export default function EnterpriseAuditPage() { sortable: false, // 禁用排序 render: (value: string) => { 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-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 ( {config.label} @@ -227,31 +228,53 @@ export default function EnterpriseAuditPage() { didFetchRef.current = true loadEnterprises() }, []) - // 加载企业数据 - 移除依赖项,通过参数传递状态 - const loadEnterprises = useCallback(async (params?: { - filters?: Record; - pagination?: { page: number; size: number }; - sort?: { sortBy?: string; sortOrder: 'asc' | 'desc' }; + // 加载企业数据 - 统一参数结构 + const loadEnterprises = useCallback(async (options: { resetPage?: boolean; - }) => { + filters?: Record; + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + page?: number; + size?: number; + } = {}) => { try { dispatch({ type: 'SET_LOADING', payload: true }); - const finalParams: TenantsQueryParams = { - search: (params?.filters?.search ?? state.filters.search) || undefined, - audit_status: params?.filters?.audit_status ?? state.filters.audit_status, - page: params?.resetPage ? 1 : (params?.pagination?.page || state.pagination.page), - size: params?.pagination?.size || state.pagination.size, - order_by: params?.sort?.sortBy, - sort_order: params?.sort?.sortOrder, + // 解构选项参数,提供默认值 + const { + resetPage = false, + filters, + sortBy, + 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') { - finalParams.audit_status = undefined; + // 使用传入的过滤器参数,如果没有传入则使用当前状态 + const currentFilters = filters || state.filters; + 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); dispatch({ @@ -284,24 +307,49 @@ export default function EnterpriseAuditPage() { rejected: state.enterprises.filter(e => e.auditStatus === '已驳回').length, }), [state.enterprises, state.pagination.total]); - // 事件处理器 + // 搜索处理 - 保持传统的简洁方式 const handleSearch = useCallback((filters: Record) => { + console.log('企业审核页面 - 收到搜索条件:', 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') => { + // 排序时重置到第1页 dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } }); - loadEnterprises({ - filters: state.filters, - sort: { sortBy, sortOrder }, - resetPage: true - }); - }, [loadEnterprises, state.filters]); + dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } }); + 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) => { // 边界检查,确保页码在有效范围内 if (page < 1) { @@ -309,20 +357,47 @@ export default function EnterpriseAuditPage() { } else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) { page = state.pagination.totalPages; } + dispatch({ type: 'SET_PAGINATION', payload: { page } }); loadEnterprises({ + page, 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) => { dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } }); loadEnterprises({ - filters: state.filters, - pagination: { page: 1, size } + resetPage: true, + page: 1, + size, + filters: state.filters }); - }, [loadEnterprises, state.filters]); + }, [state.filters, loadEnterprises]); + + // URL状态变化处理 - 处理浏览器前进后退时的参数恢复 + const handleUrlStateChange = useCallback((urlState: { + filters: Record; + 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(() => { dispatch({ type: 'REFRESH_DATA' }); @@ -435,12 +510,6 @@ export default function EnterpriseAuditPage() { {/* 搜索、表格和分页 - 使用重构后的组件 */} - - 刷新 - - } searchFields={searchFields} columns={columns} data={state.enterprises} @@ -458,7 +527,8 @@ export default function EnterpriseAuditPage() { showSizeSelector={true} showPageInfo={true} sizeOptions={[10, 20, 50, 100]} - /> + + /> {/* 企业详情对话框 - 保留原有功能 */} ; - pagination?: { page: number; size: number }; - sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' }; - }) => { + sortBy?: string; + sortOrder?: 'asc' | 'desc'; + page?: number; + size?: number; + } = {}) => { 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); 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 = { - search: (params?.filters?.search ?? searchFilters.search) || undefined, - audit_status: params?.filters?.audit_status ?? searchFilters.audit_status, - page: params?.pagination?.page || pagination.page, - size: params?.pagination?.size || pagination.size, - order_by: params?.sort?.sortBy, - sort_order: params?.sort?.sortOrder, + page: finalPage, + size: finalSize, }; - // 处理audit_status,如果为'all'则不传该参数 - if (finalParams.audit_status === 'all') { - finalParams.audit_status = undefined; + // 使用正确的优先级:URL参数 > 传入参数 > 父组件状态 + const currentFilters = { + 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 transformedData = response.data.map(transformTenantData); @@ -291,51 +346,105 @@ export default function EnterpriseManagement() { } }, []); // 移除所有依赖,使用参数传递状态变化 - // 事件处理器 + // 搜索处理 - 参考audit-history页面的统一方式 const handleSearch = useCallback((filters: Record) => { - setSearchFilters(filters); - // 搜索时重置到第一页 - loadEnterprises({ - filters, - pagination: { page: 1, size: pagination.size } - }); - }, [loadEnterprises, pagination.size]); + console.log('企业管理页面 - 收到搜索条件:', filters); - const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { - // 排序时重置到第一页 + // 更新过滤器状态 + setSearchFilters(filters); + + // 搜索时重置到第1页 + setPagination(prev => ({ ...prev, page: 1 })); + + // 执行查询 loadEnterprises({ - pagination: { page: 1, size: pagination.size }, - sort: { sortBy, sortOrder } + resetPage: true, + 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 reloadParams = { - filters: searchFilters, - pagination: { - page: pagination.page, - size: pagination.size - } - }; - loadEnterprises(reloadParams); + loadEnterprises({ + page: pagination.page, + size: pagination.size, + filters: searchFilters + }); }, [loadEnterprises, searchFilters, pagination]); + // 分页处理 - 参考audit-history页面的统一方式 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 })); loadEnterprises({ + page, 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) => { setPagination(prev => ({ ...prev, size, page: 1 })); loadEnterprises({ - filters: searchFilters, - pagination: { page: 1, size } + resetPage: true, + page: 1, + size, + filters: searchFilters }); - }, [loadEnterprises, searchFilters]); + }, [searchFilters, loadEnterprises]); + + // URL状态变化处理 - 处理浏览器前进后退时的参数恢复 + const handleUrlStateChange = useCallback((urlState: { + filters: Record; + 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(() => { @@ -496,7 +605,7 @@ export default function EnterpriseManagement() { onSort={handleSort} emptyIcon={} emptyText="暂无企业数据" - /> + /> {/* View Enterprise Details Dialog */} dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}> diff --git a/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx b/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx index 6401581..a612d6d 100644 --- a/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx +++ b/crop-x-new/src/app/(app)/central-config/tenant/user-management/page.tsx @@ -282,6 +282,20 @@ export default function TenantUserManagementPage() { size?: number; } = {}) => { 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 }); // 解构选项参数,提供默认值 @@ -294,14 +308,22 @@ export default function TenantUserManagementPage() { size } = options; + // 优先级:URL参数 > 传入参数 > 父组件状态 + const finalPage = resetPage ? 1 : (urlParams.page || page || state.pagination.page); + const finalSize = urlParams.size || size || state.pagination.size; + const params: UsersQueryParams = { - page: resetPage ? 1 : (page || state.pagination.page), - size: size || state.pagination.size, + page: finalPage, + size: finalSize, is_active: true, }; - // 使用传入的过滤器参数,如果没有传入则使用当前状态 - const currentFilters = filters || state.filters; + // 使用正确的优先级:URL参数 > 传入参数 > 父组件状态 + 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 currentSortOrder = sortOrder || state.sortOrder; @@ -350,24 +372,41 @@ export default function TenantUserManagementPage() { } }, []); - // 搜索处理 + // 搜索处理 - 保持传统的简洁方式 const handleSearch = useCallback((filters: Record) => { + console.log('用户管理页面 - 收到搜索条件:', filters); + const mappedFilters = { searchKeyword: filters.search || '', statusFilter: filters.status || 'all', typeFilter: filters.type || 'all' }; + + // 更新过滤器状态 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({ resetPage: true, + page: 1, filters: mappedFilters, sortBy: state.sortBy, sortOrder: state.sortOrder, 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') => { @@ -504,7 +543,7 @@ export default function TenantUserManagementPage() { onSort={handleSort} emptyText="暂无用户数据" sizeOptions={[10, 20, 50, 100]} - /> + /> {/* 用户详情对话框 */} (); + const lastTextChangeRef = useRef(''); + useEffect(() => { // 只有当最后变化的是 text 字段时才进行防抖,排除初始化和 select 字段 if (localFilters._lastChangedFieldType === 'text') { - const timer = setTimeout(() => { - // 移除标记字段后再调用 - const { _lastChangedFieldType, _lastChangedFieldKey, ...cleanFilters } = localFilters; - // 使用ref引用最新的onFiltersChange函数,避免依赖变化导致重复触发 - onFiltersChangeRef.current(cleanFilters); - }, 300); // 300ms 防抖延迟 + // 提取当前文本字段的值 + const textValue = localFilters[localFilters._lastChangedFieldKey] || ''; - 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 + // 组件卸载时清理防抖定时器 + useEffect(() => { + return () => { + if (debounceTimeoutRef.current) { + clearTimeout(debounceTimeoutRef.current); + } + }; + }, []); + // 计算显示的字段 const visibleFields = showAllFields ? fields diff --git a/crop-x-new/src/components/common/searchFormPagination/page.tsx b/crop-x-new/src/components/common/searchFormPagination/page.tsx index f015b21..7e06d17 100644 --- a/crop-x-new/src/components/common/searchFormPagination/page.tsx +++ b/crop-x-new/src/components/common/searchFormPagination/page.tsx @@ -6,7 +6,7 @@ */ 'use client'; -import { useState, useEffect, useMemo, useCallback } from 'react'; +import { useState, useEffect, useMemo, useCallback, useRef } from 'react'; import { Card } from '@/components/ui/card'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; @@ -47,7 +47,28 @@ export interface PaginationConfig { 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 { // 搜索表单配置 formTitle?: string; @@ -81,6 +102,9 @@ export interface SearchFormPaginationProps { // 自定义样式 className?: string; + // URL 参数同步配置 + urlSync?: UrlSyncConfig; + // 数据更新回调 - 用于父组件获取搜索条件变化 onDataUpdate?: (data: { items: T[]; @@ -88,6 +112,15 @@ export interface SearchFormPaginationProps { loading: boolean; error: string | null; }) => void; + + // URL 状态变化回调 - 用于父组件监听 URL 参数变化(可选) + onUrlStateChange?: (urlState: { + filters: Record; + pagination: { + page: number; + size: number; + }; + }) => void; } export function SearchFormPagination({ @@ -111,16 +144,313 @@ export function SearchFormPagination({ sizeOptions = [10, 30, 50, 100], maxVisiblePages = 7, className = '', + urlSync, onDataUpdate, + onUrlStateChange, }: SearchFormPaginationProps) { + // 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(''); + + // 更新来源跟踪 - 用于区分不同类型的更新 + const updateSourceRef = useRef<'user' | 'parent' | 'url' | 'init'>('init'); + // 简化的内部状态 - 只管理搜索条件 - const [filters, setFilters] = useState>( - searchFields.reduce((acc, field) => { + const [filters, setFilters] = useState>(() => { + // 初始化时从 URL 读取参数(如果启用 URL 同步) + if (urlConfig.enabled && typeof window !== 'undefined') { + const urlParams = new URLSearchParams(window.location.search); + const initialFilters: Record = {}; + + // 从 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 || ''; return acc; - }, {} as Record) + }, {} as Record); + }); + + // URL 更新函数 - 事件驱动模型,移除防抖 + const updateUrl = useCallback(( + newFilters: Record, + 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 = {}; + 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 = {}; + + // 从 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(() => { onDataUpdate?.({ @@ -138,19 +468,54 @@ export function SearchFormPagination({ }); }, [data, pagination, loading, error, onDataUpdate]); - // 简化的事件处理器 - 纯粹的状态通知 + // 简化的事件处理器 - 事件驱动模型 const handleSearch = useCallback((newFilters: Record) => { + console.log('用户搜索操作:', newFilters); + + // 更新内部状态 setFilters(newFilters); + + // 搜索时重置到第1页,保持当前每页大小 + const newPagination = { page: 1, size: internalPagination.size }; + setInternalPagination(newPagination); + + // 通知父组件搜索条件变化 onSearch?.(newFilters); - }, [onSearch]); + + // 同步到URL(标记为用户操作) + updateUrl(newFilters, newPagination, 'user'); + }, [internalPagination.size, onSearch, updateUrl]); const handlePageChange = useCallback((page: number) => { + console.log('用户分页操作:', page); + + const newPagination = { ...internalPagination, page }; + + // 更新内部状态 + setInternalPagination(newPagination); + + // 通知父组件分页变化 onPageChange?.(page); - }, [onPageChange]); + + // 同步到URL(标记为用户操作) + updateUrl(filters, newPagination, 'user'); + }, [internalPagination, filters, onPageChange, updateUrl]); const handleSizeChange = useCallback((size: number) => { + console.log('用户修改每页大小:', size); + + // 修改每页大小时重置到第1页 + const newPagination = { page: 1, size }; + + // 更新内部状态 + setInternalPagination(newPagination); + + // 通知父组件每页大小变化 onSizeChange?.(size); - }, [onSizeChange]); + + // 同步到URL(标记为用户操作) + updateUrl(filters, newPagination, 'user'); + }, [filters, onSizeChange, updateUrl]); // 稳定的filters引用 const stableFilters = useMemo(() => filters, [filters]);