diff --git a/docs/开发项目规范.md b/docs/开发项目规范.md
index 838acb9..1ce73bf 100644
--- a/docs/开发项目规范.md
+++ b/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/src/app/(app)/central-config/tenant/audit-history/page.tsx b/src/app/(app)/central-config/tenant/audit-history/page.tsx
index f848ce9..b5e8b11 100644
--- a/src/app/(app)/central-config/tenant/audit-history/page.tsx
+++ b/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 */}