2012 lines
60 KiB
Markdown
2012 lines
60 KiB
Markdown
# 开发项目规范
|
||
|
||
## 通用开发规约
|
||
|
||
### 1. 文件头部注释规范(filekorolheader)
|
||
|
||
**规范要求:**
|
||
所有页面文件(page.tsx)必须在最上方添加filekorolheader注释,说明文件对应的页面功能、路径和用途。
|
||
|
||
**格式标准:**
|
||
```tsx
|
||
/**
|
||
* filekorolheader: [页面名称] - [功能描述]
|
||
* 功能:[主要功能列表]
|
||
* 路径:[页面路由路径]
|
||
* 规范:[遵循的特殊规范说明]
|
||
*/
|
||
```
|
||
|
||
**示例:**
|
||
```tsx
|
||
/**
|
||
* filekorolheader: 物联设备数据接入页面 - IoT设备数据管理中心
|
||
* 功能:设备列表管理、实时数据监控、数据对比分析、报告生成
|
||
* 路径:/ai-crop-model/data-sense-center/iot
|
||
* 规范:遵循crop-x-new/docs/开发项目规范.md,使用useReducer状态管理,shadcn语义化样式
|
||
*/
|
||
```
|
||
|
||
**实施要点:**
|
||
- 必须放在文件最顶部,在'use client'之前
|
||
- 页面名称要准确反映业务功能
|
||
- 功能描述要简明扼要,列出核心功能
|
||
- 路径必须是完整的路由路径
|
||
- 如有特殊规范遵循,需要在规范字段说明
|
||
|
||
---
|
||
|
||
## path:land-information/archive/statistics,name:统计分析页面开发经验
|
||
|
||
### 总体开发经验总结
|
||
|
||
在实现统计分析页面过程中,我们遵循了以下8条核心开发规范,确保代码质量、可维护性和用户体验的一致性。
|
||
|
||
### 1. shadcn 样式系统优先原则
|
||
|
||
**经验总结:**
|
||
- 优先使用shadcn的语义化颜色类(如 `bg-card`、`bg-background`)替代硬编码的Tailwind CSS颜色
|
||
- 避免使用 `bg-red-500` 等具体颜色值,改用 `bg-red-50 dark:bg-red-950` 等语义化类
|
||
- 统计卡片使用 `bg-green-50 dark:bg-green-950` 等主题感知的背景色
|
||
|
||
**最佳实践:**
|
||
```tsx
|
||
// ✅ 推荐写法
|
||
<Card className="p-4 bg-green-50 dark:bg-green-950">
|
||
<Card className="bg-card hover:bg-muted">
|
||
|
||
// ❌ 避免写法
|
||
<Card className="p-4 bg-green-100">
|
||
<Card className="bg-white hover:bg-gray-100">
|
||
```
|
||
|
||
### 2. 标签组件样式精确还原原则
|
||
|
||
**经验总结:**
|
||
- 严格按照参考文件实现标签的边框和颜色样式
|
||
- 使用 `style` 属性精确控制颜色,确保1:1还原视觉效果
|
||
- 不同类型的标签有特定的样式特征需要保持一致
|
||
|
||
**关键实现:**
|
||
```tsx
|
||
// 土壤类型标签:前缀彩色圆点
|
||
<div className="w-2 h-2 rounded-full mr-2" style={{ backgroundColor: type.color }} />
|
||
|
||
// 种植模式标签:前缀emoji + 固定绿色边框
|
||
<span className="mr-1">{mode.emoji}</span>
|
||
style={{
|
||
backgroundColor: filters.plantingModes.includes(mode.key) ? '#16a34a' : 'transparent',
|
||
borderColor: '#16a34a',
|
||
}}
|
||
|
||
// 自定义标签:纯色边框背景
|
||
style={{
|
||
backgroundColor: filters.tags.includes(tag.name) ? tag.color : 'transparent',
|
||
borderColor: tag.color,
|
||
}}
|
||
```
|
||
|
||
### 3. 最小化修改原则
|
||
|
||
**经验总结:**
|
||
- 严格遵循参考文件的功能边界,不添加多余功能
|
||
- 保持与原有系统的功能一致性
|
||
- 避免过度设计,专注核心功能实现
|
||
|
||
**实施要点:**
|
||
- 只实现参考文件中明确展示的功能
|
||
- 保持相同的用户交互流程
|
||
- 维持原有的数据结构和逻辑
|
||
|
||
### 4. 暗色主题全面支持原则
|
||
|
||
**经验总结:**
|
||
- 所有组件都必须支持暗色主题切换
|
||
- 使用 `dark:` 前缀提供暗色模式样式
|
||
- 确保在暗色主题下的可读性和视觉效果
|
||
|
||
**实现模式:**
|
||
```tsx
|
||
// 统计卡片暗色主题
|
||
<Card className="p-4 bg-green-50 dark:bg-green-950">
|
||
<div className="text-2xl text-green-600 dark:text-green-400">
|
||
|
||
// 背景和边框暗色主题
|
||
<Card className="bg-card hover:bg-muted">
|
||
<Card className="border-blue-200 dark:border-blue-800">
|
||
```
|
||
|
||
### 5. useReducer 状态管理架构原则
|
||
|
||
**经验总结:**
|
||
- 使用useReducer实现复杂状态管理,避免prop drilling
|
||
- 通过dispatch实现跨组件状态同步
|
||
- 集中化状态逻辑,提高代码可维护性
|
||
|
||
**架构模式:**
|
||
```tsx
|
||
// Reducer定义
|
||
export interface LandStatisticsState {
|
||
fields: Land[];
|
||
filters: FilterCondition;
|
||
statistics: StatisticsResult | null;
|
||
chartType: 'bar' | 'pie';
|
||
}
|
||
|
||
// Action类型定义
|
||
export type LandStatisticsAction =
|
||
| { type: 'SET_FIELDS'; payload: Land[] }
|
||
| { type: 'UPDATE_FILTER'; payload: { key: keyof FilterCondition; value: any } }
|
||
| { type: 'SET_STATISTICS'; payload: StatisticsResult | null };
|
||
|
||
// 状态同步使用
|
||
const handleFilterChange = (key: keyof FilterCondition, value: any) => {
|
||
dispatch({ type: 'UPDATE_FILTER', payload: { key, value } });
|
||
};
|
||
```
|
||
|
||
### 6. 模块化组件架构原则
|
||
|
||
**经验总结:**
|
||
- 每个页面建立独立的components文件夹
|
||
- 按功能职责拆分组件,确保单一职责
|
||
- 组件间通过props和回调函数通信
|
||
|
||
**目录结构示例:**
|
||
```
|
||
src/app/(app)/land-information/archive/statistics/
|
||
├── page.tsx # 主页面
|
||
└── components/
|
||
├── landStatisticsReducer.tsx # 状态管理
|
||
├── FilterPanel.tsx # 筛选面板
|
||
├── StatisticsResults.tsx # 统计结果
|
||
└── UsageExamples.tsx # 使用示例
|
||
```
|
||
|
||
### 7. 完整依赖引用实现原则
|
||
|
||
**经验总结:**
|
||
- 仔细分析参考文件的import依赖,确保完整实现
|
||
- 将所有引用的组件都实现在components目录中
|
||
- 保持与参考文件相同的组件结构和功能
|
||
|
||
**依赖检查清单:**
|
||
- UI组件:Card, Button, Badge, Input, Label等
|
||
- 图标组件:lucide-react图标
|
||
- 图表组件:recharts相关组件
|
||
- 工具函数:toast通知等
|
||
|
||
### 8. 1:1 功能还原实现原则
|
||
|
||
**经验总结:**
|
||
- 严格按照参考文件的功能逻辑实现
|
||
- 保持相同的用户交互体验
|
||
- 确保数据流和业务逻辑的一致性
|
||
|
||
**关键实现要点:**
|
||
- 筛选条件的多选逻辑
|
||
- 数据统计的计算方法
|
||
- 图表切换和显示逻辑
|
||
- 数据导出功能
|
||
|
||
## 开发工具和最佳实践
|
||
|
||
### 推荐工具链
|
||
- **状态管理**:React useReducer
|
||
- **UI组件库**:shadcn/ui
|
||
- **样式系统**:Tailwind CSS + 语义化颜色
|
||
- **图表库**:Recharts
|
||
- **图标库**:Lucide React
|
||
- **通知系统**:Sonner
|
||
|
||
### 代码质量保证
|
||
- TypeScript严格类型检查
|
||
- ESLint代码规范检查
|
||
- 组件props类型定义完整
|
||
- 状态管理逻辑清晰可维护
|
||
|
||
### 测试数据管理
|
||
- localStorage数据持久化
|
||
- 完整的测试数据覆盖所有业务场景
|
||
- 数据初始化和清理机制完善
|
||
|
||
通过遵循这些开发规范,我们可以确保代码的一致性、可维护性和用户体验的统一性。
|
||
|
||
---
|
||
### 9.注意乱码原则
|
||
生成的代码注意看看有没有乱码,必须遵守utf-8编码
|
||
### 10.接口调用原则。
|
||
接口必须调用 D:\code\repotest\smart-crop-ui\crop-x-new\src\lib\api\sdk.gen.ts 这个里面的接口
|
||
比如 /api/v1/departments/tree 这个路径,就是要调用export const getDepartmentTreeApiV1DepartmentsDepartmentsTreeGet = <ThrowOnError extends boolean = false>(options?: Options<GetDepartmentTreeApiV1DepartmentsDepartmentsTreeGetData, ThrowOnError>) => {
|
||
return (options?.client ?? client).get<GetDepartmentTreeApiV1DepartmentsDepartmentsTreeGetResponses, unknown, ThrowOnError>({
|
||
security: [
|
||
{
|
||
scheme: 'bearer',
|
||
type: 'http'
|
||
}
|
||
],
|
||
url: '/api/v1/departments/departments/tree',
|
||
...options
|
||
});
|
||
};
|
||
实际使用的时候,要参考D:\code\repotest\smart-crop-ui\crop-x-new\src\app\(app)\central-config\tenant\audit-history\components\auditHistoryApi.ts 里面
|
||
import {
|
||
getTenantAuditLogsApiV1TenantsAuditLogsGet,
|
||
} from "@/lib/api/sdk.gen"; 这个引入和用法。
|
||
|
||
### 11.Next.js 文件命名规范原则
|
||
|
||
**规范要求:**
|
||
所有文件名必须严格遵循 Next.js 的文件命名规范,确保路由系统和页面组件能够正确识别。
|
||
|
||
**规范标准:**
|
||
|
||
#### 页面文件命名规范
|
||
- **页面文件**:必须使用 `page.tsx` 作为页面文件名
|
||
- **布局文件**:必须使用 `layout.tsx` 作为布局文件名
|
||
- **加载状态**:必须使用 `loading.tsx` 作为加载状态文件名
|
||
- **错误处理**:必须使用 `error.tsx` 作为错误处理文件名
|
||
- **未找到页面**:必须使用 `not-found.tsx` 作为404页面文件名
|
||
|
||
#### 路由参数文件命名规范
|
||
- **动态路由**:使用 `[param].tsx` 格式,如 `[id].tsx`
|
||
- **可选参数**:使用 `[[param]].tsx` 格式
|
||
- **全部匹配**:使用 `[...param].tsx` 格式
|
||
- **可选全部匹配**:使用 `[[...param]].tsx` 格式
|
||
|
||
#### 组件和工具文件命名规范
|
||
- **React 组件**:使用 PascalCase 命名,如 `UserProfile.tsx`
|
||
- **工具函数**:使用 camelCase 命名,如 `dateUtils.ts`
|
||
- **类型定义**:使用 camelCase 命名,如 `userTypes.ts`
|
||
- **常量文件**:使用 UPPER_SNAKE_CASE 命名,如 `API_CONSTANTS.ts`
|
||
|
||
**示例目录结构:**
|
||
```
|
||
src/app/(app)/land-information/
|
||
├── layout.tsx # 布局文件
|
||
├── page.tsx # 主页面
|
||
├── loading.tsx # 加载状态
|
||
├── error.tsx # 错误处理
|
||
└── archive/
|
||
├── page.tsx # 归档页面
|
||
├── statistics/
|
||
│ ├── page.tsx # 统计页面
|
||
│ └── components/
|
||
│ ├── FilterPanel.tsx # 组件:PascalCase
|
||
│ └── statisticsReducer.tsx # 工具文件:camelCase
|
||
└── [id]/
|
||
└── page.tsx # 动态路由页面
|
||
```
|
||
|
||
**实施要点:**
|
||
- 严格遵循文件命名规范,不得随意修改文件名
|
||
- 页面文件必须使用 `page.tsx`,不能使用其他名称
|
||
- 动态路由参数必须使用方括号 `[param]` 格式
|
||
- 组件和工具文件遵循通用的 JavaScript/TypeScript 命名规范
|
||
|
||
## path:land-information/archive/statistics,name:统计分析页面开发经验与问题解决
|
||
|
||
### 问题1:图表横轴显示不完整
|
||
|
||
**问题描述:**
|
||
- 初始实现中,图表只显示有数据的土壤类型和种植模式
|
||
- 没有数据的项目在横轴上不显示,导致图表看起来不完整
|
||
|
||
**原始需求分析:**
|
||
- 土壤类型分布应显示所有定义的土壤类型,即使数量为0
|
||
- 种植模式分布应显示所有定义的种植模式,提供完整的分类视图
|
||
|
||
**解决方案:**
|
||
- 修改数据计算逻辑,从"基于筛选结果生成数据"改为"基于所有定义的分类生成数据"
|
||
- 使用 `state.soilTypes.map()` 和 `state.plantingModes.map()` 确保显示所有定义的分类
|
||
|
||
**代码改进对比:**
|
||
```tsx
|
||
// ❌ 初始实现(只显示有数据的分类)
|
||
const soilTypeMap = new Map<string, { count: number; area: number }>();
|
||
filteredFields.forEach(f => {
|
||
const current = soilTypeMap.get(f.soilType) || { count: 0, area: 0 };
|
||
soilTypeMap.set(f.soilType, {
|
||
count: current.count + 1,
|
||
area: current.area + f.area,
|
||
});
|
||
});
|
||
const soilTypeDistribution = Array.from(soilTypeMap.entries()).map(([key, value]) => ({
|
||
name: state.soilTypes.find(s => s.key === key)?.name || key,
|
||
count: value.count,
|
||
area: value.area,
|
||
color: state.soilTypes.find(s => s.key === key)?.color || '#6b7280',
|
||
}));
|
||
|
||
// ✅ 最终实现(显示所有定义的分类,包括数量为0的)
|
||
const soilTypeDistribution = state.soilTypes.map(soilType => {
|
||
const count = filteredFields.filter(f => f.soilType === soilType.key).length;
|
||
const area = filteredFields
|
||
.filter(f => f.soilType === soilType.key)
|
||
.reduce((sum, f) => sum + f.area, 0);
|
||
return {
|
||
name: soilType.name,
|
||
count,
|
||
area,
|
||
color: soilType.color,
|
||
};
|
||
});
|
||
```
|
||
|
||
### 问题2:标签字体粗细不符合视觉要求
|
||
|
||
**问题描述:**
|
||
- 筛选标签(土壤类型、种植模式、自定义标签)的字体过粗
|
||
- 用户反馈需要调整为细字体,以匹配参考文件的视觉效果
|
||
|
||
**原始需求分析:**
|
||
- 参考文件显示的是细字效果,需要精确还原视觉体验
|
||
- 字体粗细影响整体UI的美观和专业度
|
||
|
||
**解决方案:**
|
||
- 给所有Badge组件添加 `font-light` 类名
|
||
- 保持其他样式(颜色、边框、悬停效果)不变,只调整字体粗细
|
||
|
||
**代码改进:**
|
||
```tsx
|
||
// ❌ 初始实现
|
||
<Badge
|
||
key={type.id}
|
||
variant={filters.soilTypes.includes(type.key) ? 'default' : 'outline'}
|
||
className="cursor-pointer"
|
||
style={{ backgroundColor: filters.soilTypes.includes(type.key) ? type.color : 'transparent' }}
|
||
>
|
||
{type.name}
|
||
</Badge>
|
||
|
||
// ✅ 最终实现(添加字体细体)
|
||
<Badge
|
||
key={type.id}
|
||
variant={filters.soilTypes.includes(type.key) ? 'default' : 'outline'}
|
||
className="cursor-pointer font-light"
|
||
style={{ backgroundColor: filters.soilTypes.includes(type.key) ? type.color : 'transparent' }}
|
||
>
|
||
{type.name}
|
||
</Badge>
|
||
```
|
||
|
||
### 问题3:测试数据覆盖不完整影响演示效果
|
||
|
||
**问题描述:**
|
||
- localStorage中存在旧数据,导致某些土壤类型和种植模式没有对应的地块数据
|
||
- 部分图表项目显示为空或缺失,影响演示效果和用户体验
|
||
|
||
**原始需求分析:**
|
||
- 所有土壤类型和种植模式都应该有对应的测试数据
|
||
- 确保图表能完整展示所有分类的统计数据,即使是0也要显示
|
||
- 为用户提供完整的演示环境
|
||
|
||
**解决方案:**
|
||
- 创建完整的测试数据集,覆盖所有7种土壤类型和5种种植模式
|
||
- 在 `loadData` 函数中初始化这些测试数据,确保首次访问时有完整数据
|
||
- 通过localStorage持久化,确保数据在页面刷新后仍然存在
|
||
|
||
**测试数据设计原则:**
|
||
```tsx
|
||
const testFields = [
|
||
{
|
||
id: '1',
|
||
code: 'TD001',
|
||
name: '东区沙质土试验田',
|
||
area: 85.5,
|
||
location: '东区1号地块',
|
||
soilType: 'sandy', // 沙质土
|
||
plantingMode: 'conventional', // 传统种植
|
||
tags: ['有机种植', '高产示范', '滴灌设施'],
|
||
// ...其他完整字段
|
||
},
|
||
// 总计10个地块,确保每个土壤类型和种植模式都有覆盖
|
||
// 沙质土(2个)、黏质土(2个)、壤质土(2个)、泥炭土(1个)、石灰质土(1个)、粉质土(1个)、岩石土(1个)
|
||
// 传统种植(3个)、有机种植(3个)、温室种植(2个)、水培种植(1个)、气培种植(1个)
|
||
];
|
||
```
|
||
|
||
### 问题4:语义化颜色类使用存在不一致
|
||
|
||
**问题描述:**
|
||
- 部分组件仍使用硬编码的Tailwind颜色类
|
||
- 没有充分利用shadcn的语义化颜色系统
|
||
- 在暗色主题下可能存在兼容性问题
|
||
|
||
**原始需求分析:**
|
||
- 优先使用 `bg-gray` 等语义化颜色类
|
||
- 避免写死的Tailwind CSS样式,提高主题一致性
|
||
- 建立统一的颜色使用标准
|
||
|
||
**解决方案:**
|
||
- 全面检查并替换硬编码颜色为语义化颜色类
|
||
- 统计卡片使用 `bg-green-50 dark:bg-green-950` 等主题感知背景色
|
||
- 确保在暗色主题下的可读性和视觉效果一致性
|
||
|
||
**颜色使用改进:**
|
||
```tsx
|
||
// ❌ 不一致的硬编码实现
|
||
<Card className="p-4 bg-green-100">
|
||
<div className="text-green-600">
|
||
<Card className="bg-white hover:bg-gray-100">
|
||
|
||
// ✅ 统一的语义化颜色实现
|
||
<Card className="p-4 bg-green-50 dark:bg-green-950">
|
||
<div className="text-2xl text-green-600 dark:text-green-400">
|
||
<Card className="bg-card hover:bg-muted">
|
||
<Card className="border-blue-200 dark:border-blue-800">
|
||
```
|
||
|
||
### 问题5:多组件状态同步和数据管理复杂性
|
||
|
||
**问题描述:**
|
||
- 多个组件之间需要共享状态,使用prop传递会导致代码复杂且难以维护
|
||
- 数据更新时容易出现状态不一致的问题
|
||
- 缺乏集中化的状态管理机制
|
||
|
||
**原始需求分析:**
|
||
- 使用useReducer实现集中化状态管理
|
||
- 确保组件间数据同步的可靠性和性能
|
||
- 简化组件间的通信逻辑
|
||
|
||
**解决方案:**
|
||
- 设计完整的状态管理架构,包括状态接口、Action类型和Reducer函数
|
||
- 通过dispatch实现跨组件状态更新,避免prop drilling
|
||
- 使用localStorage进行数据持久化,确保页面刷新后状态保持
|
||
- 建立清晰的数据流和状态更新模式
|
||
|
||
**状态管理架构设计:**
|
||
```tsx
|
||
// 完整的状态接口定义
|
||
export interface LandStatisticsState {
|
||
fields: Land[];
|
||
tags: LandTag[];
|
||
soilTypes: SoilType[];
|
||
plantingModes: PlantingMode[];
|
||
filters: FilterCondition;
|
||
statistics: StatisticsResult | null;
|
||
chartType: 'bar' | 'pie';
|
||
}
|
||
|
||
// 细粒度的Action类型定义
|
||
export type LandStatisticsAction =
|
||
| { type: 'SET_FIELDS'; payload: Land[] }
|
||
| { type: 'SET_TAGS'; payload: LandTag[] }
|
||
| { type: 'SET_SOIL_TYPES'; payload: SoilType[] }
|
||
| { type: 'SET_PLANTING_MODES'; payload: PlantingMode[] }
|
||
| { type: 'SET_FILTERS'; payload: FilterCondition }
|
||
| { type: 'UPDATE_FILTER'; payload: { key: keyof FilterCondition; value: any } }
|
||
| { type: 'TOGGLE_ARRAY_FILTER'; payload: { key: 'soilTypes' | 'plantingModes' | 'tags'; value: string } }
|
||
| { type: 'CLEAR_FILTERS' }
|
||
| { type: 'SET_STATISTICS'; payload: StatisticsResult | null }
|
||
| { type: 'SET_CHART_TYPE'; payload: 'bar' | 'pie' };
|
||
|
||
// 跨组件状态同步实现
|
||
const handleFilterChange = (key: keyof FilterCondition, value: any) => {
|
||
dispatch({ type: 'UPDATE_FILTER', payload: { key, value } });
|
||
};
|
||
|
||
const handleToggleArrayFilter = (key: 'soilTypes' | 'plantingModes' | 'tags', value: string) => {
|
||
const currentArray = state.filters[key];
|
||
const newArray = currentArray.includes(value)
|
||
? currentArray.filter(v => v !== value)
|
||
: [...currentArray, value];
|
||
dispatch({ type: 'TOGGLE_ARRAY_FILTER', payload: { key, value } });
|
||
};
|
||
```
|
||
|
||
## 开发经验对比总结
|
||
|
||
### 与原始要求的差异分析
|
||
|
||
| 原始要求 | 实际实现 | 差异说明 | 解决过程 |
|
||
|---------|---------|---------|---------|
|
||
| 使用shadcn语义化样式 | 完全实现 + 统一规范 | 需要建立统一的使用标准 | 全面替换硬编码颜色,建立语义化颜色使用指南 |
|
||
| 1:1还原标签样式 | 精确还原 + 字体优化 | 字体粗细需要调整以匹配视觉 | 添加font-light类名,保持样式一致性 |
|
||
| 不动无关内容 | 完全遵循 | 严格保持功能边界,不添加多余功能 | 只实现参考文件中的明确功能 |
|
||
| 暗色主题支持 | 全面支持 | 需要系统化处理所有UI组件 | 使用dark:前缀系统化处理暗色主题 |
|
||
| useReducer状态管理 | 架构实现 + 最佳实践 | 需要设计状态同步机制和数据持久化 | 建立完整的状态管理架构和同步机制 |
|
||
| 模块化组件 | 完全拆分 | 需要合理的组件职责划分和通信机制 | 按功能领域拆分组件,通过props和回调通信 |
|
||
| 完整依赖引用 | 1:1还原 | 需要仔细分析引用关系和依赖完整性 | 建立依赖检查清单,确保所有引用组件完整实现 |
|
||
| 1:1功能还原 | 完整实现 | 需要精确还原业务逻辑和用户体验 | 严格对照参考文件实现所有功能 |
|
||
|
||
### 关键学习点和改进
|
||
|
||
1. **数据完整性思维**:不仅要实现功能,还要考虑数据的完整性和演示效果的完整性
|
||
- 学会了从用户体验角度思考数据展示的完整性
|
||
- 理解了即使count为0也应该显示的重要性
|
||
|
||
2. **细节精确把控**:字体粗细、颜色、边框等视觉细节需要精确还原
|
||
- 培养了对UI细节的敏感度
|
||
- 掌握了通过用户反馈快速迭代优化的方法
|
||
|
||
3. **状态管理设计**:useReducer不仅是技术选择,更是架构设计决策
|
||
- 深入理解了状态管理的架构设计原则
|
||
- 掌握了跨组件状态同步的最佳实践
|
||
|
||
4. **渐进式优化**:在开发过程中根据反馈不断调整和改进
|
||
- 学会了灵活应对开发过程中的需求变化
|
||
- 建立了基于反馈的快速响应机制
|
||
|
||
5. **文档化习惯**:将开发过程中的经验和教训记录下来,形成知识积累
|
||
- 认识到文档化对团队协作和知识传承的重要性
|
||
- 建立了完整的开发规范文档体系
|
||
|
||
---
|
||
|
||
## path:land-information/map/gis,name:GIS地图管理开发经验与问题解决
|
||
|
||
### 总体开发经验总结
|
||
|
||
GIS地图管理页面的开发过程是一个复杂的技术集成挑战,涉及到第三方地图库的集成、异步资源加载、多层级组件交互等多个技术难点。通过这次开发,我们建立了一套完整的GIS应用开发模式,特别是在处理真实地图数据源和优雅降级方面积累了宝贵经验。
|
||
|
||
### 问题1:地图组件初始化时缺少真实地图数据源
|
||
|
||
**问题描述:**
|
||
- 初始实现的BaseMap组件只是简单的模拟展示,无法加载真实的卫星图像
|
||
- 用户反馈参考文件可以看到真实的卫星图,但当前页面只显示占位符
|
||
- 缺乏对真实地图服务商的集成支持
|
||
|
||
**原始需求分析:**
|
||
- 需要支持真实的卫星影像显示,而不是简单的占位地图
|
||
- 必须支持多种地图图层切换(卫星、电子、地形、混合)
|
||
- 需要完整的地图交互功能,包括缩放、平移、全屏等
|
||
|
||
**解决方案:**
|
||
- 复制完整的GISMapEngine实现,支持多种地图提供商
|
||
- 实现leafletLoader动态加载器,支持异步加载地图库
|
||
- 建立真实地图瓦片数据源连接,包括ArcGIS卫星影像和OpenStreetMap
|
||
|
||
**代码实现对比:**
|
||
```tsx
|
||
// ❌ 初始简化实现
|
||
export class GISMapEngine {
|
||
constructor(map: any) {
|
||
this.map = map; // 只是一个模拟对象
|
||
}
|
||
addPolygon(polygon: MapPolygon): void {
|
||
this.polygons.set(polygon.id, polygon); // 没有真实渲染
|
||
}
|
||
}
|
||
|
||
// ✅ 最终完整实现
|
||
export class GISMapEngine {
|
||
constructor(config: MapConfig) {
|
||
this.provider = config.provider;
|
||
this.initialize(config); // 真实初始化流程
|
||
}
|
||
|
||
private async initLeaflet(config: MapConfig) {
|
||
// 动态加载Leaflet库
|
||
if (!window.L) {
|
||
await this.loadLeaflet();
|
||
}
|
||
// 创建真实地图实例
|
||
this.map = window.L.map(this.container).setView([center[1], center[0]], zoom);
|
||
// 设置真实瓦片图层
|
||
this.getLeafletLayer(layer).addTo(this.map);
|
||
}
|
||
}
|
||
```
|
||
|
||
### 问题2:地图库异步加载和依赖管理复杂性
|
||
|
||
**问题描述:**
|
||
- 地图库(Leaflet)需要从CDN异步加载,存在加载失败风险
|
||
- 地图组件需要在库加载完成后才能初始化,存在时序问题
|
||
- 多个地图组件可能重复加载同一资源,造成性能浪费
|
||
|
||
**原始需求分析:**
|
||
- 确保地图库能够可靠加载,提供良好的用户体验
|
||
- 处理加载失败的情况,提供优雅的降级方案
|
||
- 优化资源加载性能,避免重复加载
|
||
|
||
**解决方案:**
|
||
- 创建leafletLoader统一管理地图库的加载过程
|
||
- 实现加载状态管理和重试机制
|
||
- 建立全局加载状态缓存,避免重复加载
|
||
|
||
**关键实现代码:**
|
||
```tsx
|
||
// leafletLoader.ts - 统一加载管理
|
||
export const preloadLeaflet = (): Promise<boolean> => {
|
||
return new Promise((resolve) => {
|
||
if (leafletLoaded || window.L) {
|
||
resolve(true);
|
||
return;
|
||
}
|
||
|
||
if (leafletLoading) {
|
||
// 等待正在进行的加载完成
|
||
const checkInterval = setInterval(() => {
|
||
if (leafletLoaded || window.L) {
|
||
clearInterval(checkInterval);
|
||
resolve(true);
|
||
}
|
||
}, 100);
|
||
return;
|
||
}
|
||
|
||
// 执行实际加载过程
|
||
const script = document.createElement('script');
|
||
script.src = 'https://unpkg.com/leaflet@1.9.4/dist/leaflet.js';
|
||
script.onload = () => { leafletLoaded = true; resolve(true); };
|
||
script.onerror = () => { resolve(false); };
|
||
document.head.appendChild(script);
|
||
});
|
||
};
|
||
```
|
||
|
||
### 问题3:地图组件与状态管理的深度集成
|
||
|
||
**问题描述:**
|
||
- 地图组件需要与useReducer状态管理深度集成
|
||
- 地图的异步初始化过程与React生命周期存在冲突
|
||
- 地图事件回调与状态更新的时序同步问题
|
||
|
||
**原始需求分析:**
|
||
- 地图操作需要能够更新全局状态(如选中地块、图层切换)
|
||
- 状态变化需要能够反映到地图显示上
|
||
- 需要处理地图组件的清理和资源释放
|
||
|
||
**解决方案:**
|
||
- 使用useImperativeHandle暴露地图实例方法给父组件
|
||
- 实现地图引擎的引用管理,确保状态同步
|
||
- 建立完整的生命周期管理,包括组件卸载时的资源清理
|
||
|
||
**状态管理集成代码:**
|
||
```tsx
|
||
// BaseMap组件中的状态集成
|
||
useImperativeHandle(ref, () => ({
|
||
getMapEngine: () => mapEngineRef.current,
|
||
addMarker: (marker: Marker) => {
|
||
mapEngineRef.current?.addMarker(marker);
|
||
},
|
||
addPolygon: (polygon: Polygon) => {
|
||
mapEngineRef.current?.addPolygon(polygon);
|
||
},
|
||
setCenter: (position: MapPosition, zoom?: number) => {
|
||
mapEngineRef.current?.setCenter(position, zoom);
|
||
},
|
||
}));
|
||
|
||
// 组件卸载时的资源清理
|
||
useEffect(() => {
|
||
return () => {
|
||
if (mapEngineRef.current) {
|
||
mapEngineRef.current.destroy();
|
||
}
|
||
};
|
||
}, []);
|
||
```
|
||
|
||
### 问题4:真实地图数据源的集成和配置
|
||
|
||
**问题描述:**
|
||
- 需要集成多种真实的地图瓦片数据源
|
||
- 不同地图服务商的API格式和坐标系统存在差异
|
||
- 需要处理地图瓦片的加载性能和缓存策略
|
||
|
||
**原始需求分析:**
|
||
- 提供真实的卫星影像、电子地图、地形图等多种图层
|
||
- 确保地图数据的准确性和时效性
|
||
- 优化地图加载性能,提供流畅的用户体验
|
||
|
||
**解决方案:**
|
||
- 集成多个开源地图数据源,确保服务的可靠性
|
||
- 统一不同数据源的坐标系统和API格式
|
||
- 实现智能的图层切换和缓存机制
|
||
|
||
**地图数据源配置:**
|
||
```tsx
|
||
private getLeafletLayer(layer: MapLayer) {
|
||
const baseLayers: Record<MapLayer, string> = {
|
||
// ArcGIS卫星影像 - 真实卫星图
|
||
satellite: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||
// OpenStreetMap - 开源电子地图
|
||
street: 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png',
|
||
// OpenTopoMap - 开源地形图
|
||
terrain: 'https://{s}.tile.opentopomap.org/{z}/{x}/{y}.png',
|
||
// 混合图层使用卫星影像作为基础
|
||
hybrid: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
||
};
|
||
|
||
return window.L.tileLayer(baseLayers[layer], {
|
||
attribution: '© OpenStreetMap contributors',
|
||
maxZoom: 18,
|
||
});
|
||
}
|
||
```
|
||
|
||
### 问题5:地图交互功能的完整实现
|
||
|
||
**问题描述:**
|
||
- 需要实现地块多边形的渲染和交互
|
||
- 地图标记点的添加和点击事件处理
|
||
- 地图控件(缩放、图层切换、全屏等)的集成
|
||
|
||
**原始需求分析:**
|
||
- 地块需要在地图上以彩色多边形形式显示
|
||
- 点击地块需要触发选择事件和状态更新
|
||
- 提供完整的地图导航和操作功能
|
||
|
||
**解决方案:**
|
||
- 使用地图引擎的Polygon和Marker API实现地块渲染
|
||
- 建立事件处理机制,连接地图交互和状态管理
|
||
- 集成完整的地图控件套件,提供专业级用户体验
|
||
|
||
**交互功能实现:**
|
||
```tsx
|
||
// 地块多边形渲染
|
||
const polygon: Polygon = {
|
||
id: field.id,
|
||
path: field.coordinates,
|
||
fillColor: field.color,
|
||
strokeColor: field.color,
|
||
fillOpacity: 0.3,
|
||
strokeWeight: 2,
|
||
onClick: () => {
|
||
onFieldSelect(field); // 更新全局状态
|
||
toast.success(`已选择: ${field.name}`);
|
||
},
|
||
};
|
||
engine.addPolygon(polygon);
|
||
|
||
// 地块标记点渲染
|
||
const marker: Marker = {
|
||
id: `marker-${field.id}`,
|
||
position: { lat: centerLat, lng: centerLng },
|
||
title: field.name,
|
||
color: field.color,
|
||
onClick: () => {
|
||
onFieldSelect(field);
|
||
toast.success(`已选择: ${field.name}`);
|
||
},
|
||
};
|
||
engine.addMarker(marker);
|
||
```
|
||
|
||
## 开发经验对比总结
|
||
|
||
### 与原始要求的差异分析
|
||
|
||
| 原始要求 | 实际实现 | 差异说明 | 解决过程 |
|
||
|---------|---------|---------|---------|
|
||
| 1:1还原地图功能 | 完整实现 + 真实数据源 | 需要集成真实地图服务商和瓦片数据 | 建立完整的地图引擎架构,支持多种数据源 |
|
||
| 第三方库集成 | 专业级集成 | 需要处理异步加载、错误处理、性能优化 | 实现统一加载器和优雅降级机制 |
|
||
| 组件状态管理 | 深度集成 + 生命周期管理 | 地图组件与React状态系统需要深度集成 | 使用useImperativeHandle和引用管理 |
|
||
| 交互功能实现 | 完整交互套件 | 需要实现多边形、标记、控件等完整功能 | 集成地图引擎API,建立事件处理机制 |
|
||
|
||
### 关键学习点和改进
|
||
|
||
1. **第三方库集成思维**:学会了如何可靠地集成和管理复杂的第三方JavaScript库
|
||
- 掌握了异步加载、错误处理、优雅降级的完整流程
|
||
- 理解了库版本管理和兼容性处理的重要性
|
||
|
||
2. **地图API应用经验**:深入了解了Web地图开发的技术栈和最佳实践
|
||
- 学会了瓦片地图的原理和多种数据源的使用
|
||
- 掌握了地图交互事件的处理和状态同步机制
|
||
|
||
3. **React高级模式应用**:在复杂组件中应用了useImperativeHandle、useRef等高级React模式
|
||
- 深入理解了React组件的暴露方法和引用传递机制
|
||
- 掌握了复杂组件生命周期管理的最佳实践
|
||
|
||
4. **性能优化意识**:建立了地图应用的性能优化思维
|
||
- 学会了资源懒加载和缓存策略的设计
|
||
- 理解了大型第三方库对应用性能的影响和优化方法
|
||
|
||
5. **用户体验设计**:在技术实现中始终考虑用户体验
|
||
- 建立了加载状态和错误处理的设计模式
|
||
- 掌握了优雅降级和渐进增强的实现方法
|
||
|
||
6. **架构设计能力**:设计了可扩展的地图应用架构
|
||
- 建立了插件化的地图引擎设计
|
||
|
||
---
|
||
|
||
## path:src/app/(app)/land-information/map/draw,name:数字化绘制与编辑页面开发经验
|
||
|
||
### 1. **复杂状态管理设计**
|
||
- **useReducer 模式应用**:使用 useReducer 管理复杂的编辑状态,包含多个布尔状态、数组和对象
|
||
- **状态结构设计**:设计了包含高级编辑器状态、活动标签页、地块数据、保存对话框等的状态结构
|
||
- **Action 设计模式**:采用类型安全的 Action 设计,支持状态更新、字段管理、对话框控制等操作
|
||
|
||
### 2. **组件化架构设计**
|
||
- **模块化组件结构**:将复杂功能拆分为6个独立组件,每个组件负责单一职责
|
||
- `drawEditReducer.tsx`:状态管理核心
|
||
- `DrawingTools.tsx`:绘制工具组件
|
||
- `EditingTools.tsx`:编辑工具组件
|
||
- `FieldEntryDialog.tsx`:地块信息录入对话框
|
||
- `UsageGuide.tsx`:使用指南组件
|
||
- `AdvancedEditorPromo.tsx`:高级编辑器推广组件
|
||
- **组件通信设计**:通过 props 和回调函数实现组件间的数据传递和事件处理
|
||
|
||
### 3. **Canvas 绘图技术实现**
|
||
- **多种绘制模式**:实现点、线、多边形、矩形等多种绘制模式
|
||
- **实时交互反馈**:支持鼠标移动吸附、节点高亮、实时预览等功能
|
||
- **几何计算算法**:
|
||
- Shoelace 公式计算多边形面积
|
||
- 坐标距离计算周长
|
||
- 点在多边形内判断算法
|
||
- 自相交检测算法
|
||
|
||
### 4. **高级编辑功能实现**
|
||
- **节点编辑**:支持拖拽节点、添加节点、删除节点
|
||
- **地块分割**:绘制分割线将地块分成两部分,支持垂直和水平分割
|
||
- **地块合并**:多地块选择和凸包算法合并
|
||
- **历史记录管理**:实现撤销/重做功能,支持操作历史追踪
|
||
|
||
### 5. **用户体验设计**
|
||
- **分步骤操作引导**:为复杂操作提供详细的操作步骤说明
|
||
- **实时状态反馈**:Toast 通知、状态栏显示、操作确认等
|
||
- **键盘快捷键支持**:Ctrl+Z 撤销、Ctrl+S 保存、Delete 清除、Esc 取消
|
||
- **视觉状态管理**:选中高亮、禁用状态、加载状态等
|
||
|
||
### 6. **数据管理与持久化**
|
||
- **表单验证设计**:完整的表单验证逻辑,支持必填项检查和格式验证
|
||
- **本地存储集成**:与 localStorage 集成,支持地块数据的持久化
|
||
- **自动数据生成**:地块编号、名称的自动生成逻辑
|
||
- **标签管理功能**:支持标签的添加、删除和展示
|
||
|
||
### 7. **技术规范遵循**
|
||
- **shadcn/ui 语义样式**:使用 `bg-card`、`bg-muted`、`text-muted-foreground` 等语义化样式
|
||
- **暗色主题支持**:完整支持暗色主题,使用 `dark:` 前缀
|
||
- **TypeScript 类型安全**:完整的类型定义,确保类型安全
|
||
- **响应式设计**:支持不同屏幕尺寸的适配
|
||
|
||
### 8. **开发效率提升**
|
||
- **组件复用设计**:通用组件可在其他页面复用
|
||
- **配置化参数**:画布尺寸、吸附距离等参数可配置
|
||
- **错误处理机制**:完善的错误处理和用户提示
|
||
- **代码组织结构**:清晰的文件结构和命名规范
|
||
|
||
### 9. **性能优化考虑**
|
||
- **事件处理优化**:使用 useCallback 避免不必要的重渲染
|
||
- **状态更新策略**:合理的状态更新时机和批量处理
|
||
- **Canvas 渲染优化**:减少不必要的重绘和计算
|
||
|
||
### 10. **可扩展性设计**
|
||
- **插件化架构**:编辑工具采用插件化设计,易于扩展新功能
|
||
- **接口标准化**:统一的接口设计,便于功能模块替换
|
||
- **配置化开发**:支持通过配置文件调整功能和行为
|
||
- 理解了复杂应用中的组件分层和职责划分
|
||
|
||
---
|
||
|
||
## path:src/components/common/searchFormPagination,name:搜索、表格、分页三合一组件使用心得
|
||
|
||
### 组件概述
|
||
|
||
SearchFormPagination 是一个高度可配置的复合组件,集成了搜索表单、数据表格和分页功能。该组件采用了现代React开发模式,通过配置驱动的方式实现复杂数据展示页面的快速开发。
|
||
|
||
### 架构设计
|
||
|
||
#### 1. 组件层次结构
|
||
|
||
```
|
||
SearchFormPagination (主组件)
|
||
├── SearchFormComponent (搜索表单)
|
||
│ ├── Input (文本搜索框)
|
||
│ └── Select (下拉选择框)
|
||
├── Card (表格容器)
|
||
│ ├── Table (数据表格)
|
||
│ │ ├── TableHeader (表头)
|
||
│ │ └── TableBody (表体)
|
||
│ └── PaginationComponent (分页组件)
|
||
└── LoadingOverlay (加载遮罩)
|
||
```
|
||
|
||
#### 2. 核心文件结构
|
||
|
||
```
|
||
src/components/common/searchFormPagination/
|
||
├── index.ts # 主组件导出
|
||
├── page.tsx # SearchFormPagination主组件
|
||
├── components/
|
||
│ ├── SearchFormComponent.tsx # 搜索表单组件
|
||
│ ├── PaginationComponent.tsx # 分页组件
|
||
│ └── searchFormPaginationReducer.tsx # 状态管理(可选)
|
||
```
|
||
|
||
### 核心功能特性
|
||
|
||
#### 1. 搜索表单功能
|
||
|
||
**防抖搜索机制**:
|
||
```tsx
|
||
// 关键实现:300ms防抖,避免频繁API调用
|
||
useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
onFiltersChangeRef.current(localFilters);
|
||
}, 300);
|
||
|
||
return () => clearTimeout(timer);
|
||
}, [localFilters]);
|
||
```
|
||
|
||
**多字段配置支持**:
|
||
```tsx
|
||
const searchFields: SearchFieldConfig[] = [
|
||
{
|
||
key: 'search',
|
||
type: 'text',
|
||
placeholder: '搜索企业名称、编码...',
|
||
},
|
||
{
|
||
key: 'audit_status',
|
||
type: 'select',
|
||
defaultValue: 'all',
|
||
options: [
|
||
{ value: 'all', label: '全部状态' },
|
||
{ value: 'draft', label: '草稿' },
|
||
// ...更多选项
|
||
],
|
||
},
|
||
];
|
||
```
|
||
|
||
#### 2. 表格展示功能
|
||
|
||
**动态列配置**:
|
||
```tsx
|
||
const columns: TableColumnConfig[] = [
|
||
{
|
||
key: 'name',
|
||
label: '企业名称',
|
||
sortable: true, // 支持排序
|
||
render: (value, row) => (
|
||
<div className="font-medium">{value}</div>
|
||
),
|
||
},
|
||
{
|
||
key: 'status',
|
||
label: '状态',
|
||
render: (value) => getStatusBadge(value), // 自定义渲染
|
||
},
|
||
];
|
||
```
|
||
|
||
**加载状态处理**:
|
||
```tsx
|
||
// 表格加载遮罩 - 提升用户体验
|
||
{loading && (
|
||
<div className="absolute inset-0 bg-white/50 dark:bg-black/50 backdrop-blur-sm z-10">
|
||
<div className="flex items-center justify-center">
|
||
<RefreshCw className="w-6 h-6 animate-spin" />
|
||
<span>加载中...</span>
|
||
</div>
|
||
</div>
|
||
)}
|
||
```
|
||
|
||
#### 3. 分页功能
|
||
|
||
**完整分页配置**:
|
||
```tsx
|
||
interface PaginationConfig {
|
||
page: number;
|
||
size: number;
|
||
total: number;
|
||
totalPages: number;
|
||
hasNext: boolean;
|
||
hasPrev: boolean;
|
||
}
|
||
|
||
<PaginationComponent
|
||
pagination={pagination}
|
||
onPageChange={handlePageChange}
|
||
onSizeChange={handleSizeChange}
|
||
sizeOptions={[10, 30, 50, 100]} // 可配置每页条数
|
||
showSizeSelector={true}
|
||
showPageInfo={true}
|
||
/>
|
||
```
|
||
|
||
**智能分页逻辑**:
|
||
- 当只有一页数据时,分页按钮隐藏但每页条数选择器仍显示
|
||
- 支持页码跳转和快速导航
|
||
- 分页操作时保持当前搜索条件
|
||
|
||
### 使用示例
|
||
|
||
#### 完整调用示例
|
||
|
||
```tsx
|
||
import { SearchFormPagination } from '@/components/common/searchFormPagination';
|
||
|
||
export default function EnterpriseManagement() {
|
||
const [enterprises, setEnterprises] = useState([]);
|
||
const [loading, setLoading] = useState(false);
|
||
const [pagination, setPagination] = useState({
|
||
page: 1,
|
||
size: 10,
|
||
total: 0,
|
||
totalPages: 0,
|
||
hasNext: false,
|
||
hasPrev: false,
|
||
});
|
||
|
||
// 搜索字段配置
|
||
const searchFields = [
|
||
{
|
||
key: 'search',
|
||
label: '搜索',
|
||
type: 'text',
|
||
placeholder: '搜索企业名称、编码...',
|
||
},
|
||
{
|
||
key: 'audit_status',
|
||
label: '审核状态',
|
||
type: 'select',
|
||
defaultValue: 'all',
|
||
options: [
|
||
{ value: 'all', label: '全部状态' },
|
||
{ value: 'draft', label: '草稿' },
|
||
{ value: 'pending', label: '待审核' },
|
||
{ value: 'approved', label: '审核通过' },
|
||
],
|
||
},
|
||
];
|
||
|
||
// 表格列配置
|
||
const columns = [
|
||
{
|
||
key: 'name',
|
||
label: '企业名称',
|
||
sortable: true,
|
||
render: (value) => <div className="font-medium">{value}</div>,
|
||
},
|
||
{
|
||
key: 'auditStatus',
|
||
label: '审核状态',
|
||
render: (value) => getAuditStatusBadge(value),
|
||
},
|
||
{
|
||
key: 'actions',
|
||
label: '操作',
|
||
render: (_, row) => (
|
||
<div className="flex gap-2">
|
||
<Button size="sm" onClick={() => handleView(row)}>
|
||
查看
|
||
</Button>
|
||
<Button size="sm" variant="outline" onClick={() => handleEdit(row)}>
|
||
编辑
|
||
</Button>
|
||
</div>
|
||
),
|
||
},
|
||
];
|
||
|
||
// 数据加载函数
|
||
const loadEnterprises = useCallback(async (params) => {
|
||
try {
|
||
setLoading(true);
|
||
const response = await fetchTenants(params);
|
||
setEnterprises(response.data);
|
||
setPagination({
|
||
page: response.page,
|
||
size: response.size,
|
||
total: response.total,
|
||
totalPages: response.total_pages,
|
||
hasNext: response.has_next,
|
||
hasPrev: response.has_prev,
|
||
});
|
||
} catch (error) {
|
||
console.error('Failed to load enterprises:', error);
|
||
} finally {
|
||
setLoading(false);
|
||
}
|
||
}, []);
|
||
|
||
// 搜索处理
|
||
const handleSearch = useCallback((filters) => {
|
||
loadEnterprises({
|
||
filters,
|
||
pagination: { page: 1, size: pagination.size },
|
||
});
|
||
}, [loadEnterprises, pagination.size]);
|
||
|
||
// 分页处理
|
||
const handlePageChange = useCallback((page) => {
|
||
setPagination(prev => ({ ...prev, page }));
|
||
loadEnterprises({
|
||
pagination: { page, size: pagination.size },
|
||
filters: searchFilters,
|
||
});
|
||
}, [loadEnterprises, pagination.size]);
|
||
|
||
return (
|
||
<div className="space-y-6">
|
||
<SearchFormPagination
|
||
formTitle="企业列表"
|
||
formRightContent={<Button onClick={handleCreate}>新建企业</Button>}
|
||
searchFields={searchFields}
|
||
columns={columns}
|
||
data={enterprises}
|
||
loading={loading}
|
||
error={null}
|
||
pagination={pagination}
|
||
onPageChange={handlePageChange}
|
||
onSizeChange={handleSizeChange}
|
||
onSearch={handleSearch}
|
||
emptyIcon={<Building2 className="w-12 h-12" />}
|
||
emptyText="暂无企业数据"
|
||
/>
|
||
</div>
|
||
);
|
||
}
|
||
```
|
||
|
||
### 接口定义
|
||
|
||
#### SearchFieldConfig - 搜索字段配置
|
||
|
||
```tsx
|
||
interface SearchFieldConfig {
|
||
key: string; // 字段标识
|
||
label: string; // 显示标签
|
||
type: 'text' | 'select'; // 字段类型
|
||
placeholder?: string; // 占位符文本
|
||
options?: Array<{ value: string; label: string }>; // 下拉选项
|
||
defaultValue?: string; // 默认值
|
||
}
|
||
```
|
||
|
||
#### TableColumnConfig - 表格列配置
|
||
|
||
```tsx
|
||
interface TableColumnConfig {
|
||
key: string; // 数据字段名
|
||
label: string; // 表头显示文本
|
||
sortable?: boolean; // 是否支持排序
|
||
width?: string; // 列宽设置
|
||
render?: (value: any, row: any, index: number) => React.ReactNode; // 自定义渲染
|
||
}
|
||
```
|
||
|
||
#### SearchFormPaginationProps - 主组件属性
|
||
|
||
```tsx
|
||
interface SearchFormPaginationProps<T = any> {
|
||
// 搜索配置
|
||
searchFields: SearchFieldConfig[];
|
||
onSearch?: (filters: Record<string, string>) => void;
|
||
|
||
// 表格配置
|
||
columns: TableColumnConfig[];
|
||
data?: T[];
|
||
loading?: boolean;
|
||
error?: string | null;
|
||
|
||
// 分页配置
|
||
pagination?: PaginationConfig;
|
||
onPageChange?: (page: number) => void;
|
||
onSizeChange?: (size: number) => void;
|
||
onSort?: (sortBy: string, sortOrder: 'asc' | 'desc') => void;
|
||
|
||
// UI配置
|
||
formTitle?: string;
|
||
formRightContent?: React.ReactNode;
|
||
emptyIcon?: React.ReactNode;
|
||
emptyText?: string;
|
||
showSizeSelector?: boolean;
|
||
showPageInfo?: boolean;
|
||
}
|
||
```
|
||
|
||
### 最佳实践
|
||
|
||
#### 1. 性能优化
|
||
|
||
**使用 useCallback 优化函数引用**:
|
||
```tsx
|
||
// ✅ 正确做法:使用 useCallback 避免重复渲染
|
||
const handleSearch = useCallback((filters) => {
|
||
loadEnterprises({ filters });
|
||
}, [loadEnterprises]);
|
||
|
||
const handlePageChange = useCallback((page) => {
|
||
loadEnterprises({ page, size: pagination.size });
|
||
}, [loadEnterprises, pagination.size]);
|
||
|
||
// ❌ 错误做法:每次渲染都创建新函数
|
||
const handleSearch = (filters) => {
|
||
loadEnterprises({ filters });
|
||
};
|
||
```
|
||
|
||
**统一数据重载函数**:
|
||
```tsx
|
||
// ✅ 推荐:统一的数据重载逻辑,避免代码重复
|
||
const reloadData = useCallback(() => {
|
||
const reloadParams = {
|
||
filters: searchFilters,
|
||
pagination: {
|
||
page: pagination.page,
|
||
size: pagination.size
|
||
}
|
||
};
|
||
loadEnterprises(reloadParams);
|
||
}, [loadEnterprises, searchFilters, pagination]);
|
||
|
||
// 在多个地方使用
|
||
const handleCreateSuccess = () => reloadData();
|
||
const confirmStatusChange = async () => {
|
||
await enableTenant(tenantId);
|
||
reloadData();
|
||
};
|
||
```
|
||
|
||
#### 2. 状态管理
|
||
|
||
**合理的状态依赖**:
|
||
```tsx
|
||
// ✅ 正确:包含所有必要的依赖
|
||
const handlePageChange = useCallback((page: number) => {
|
||
loadEnterprises({
|
||
filters: searchFilters, // 确保搜索条件不会丢失
|
||
pagination: { page, size: pagination.size }
|
||
});
|
||
}, [loadEnterprises, searchFilters, pagination.size]);
|
||
```
|
||
|
||
#### 3. 错误处理
|
||
|
||
**完善的错误状态处理**:
|
||
```tsx
|
||
<SearchFormPagination
|
||
// ...
|
||
error={error}
|
||
// 组件会自动显示错误状态
|
||
/>
|
||
```
|
||
|
||
#### 4. 扩展性设计
|
||
|
||
**新增字段的简单步骤**:
|
||
1. 在 `searchFields` 数组中添加新配置
|
||
2. 确保后端API支持新的查询参数
|
||
3. 无需修改任何组件逻辑
|
||
|
||
```tsx
|
||
// 添加新的下拉框只需一行配置
|
||
{
|
||
key: 'enterprise_status',
|
||
label: '企业状态',
|
||
type: 'select',
|
||
defaultValue: 'all',
|
||
options: [
|
||
{ value: 'all', label: '全部状态' },
|
||
{ value: 'active', label: '启用' },
|
||
{ value: 'inactive', label: '禁用' },
|
||
],
|
||
}
|
||
```
|
||
|
||
### 技术特点
|
||
|
||
#### 1. 类型安全
|
||
- 完整的 TypeScript 类型定义
|
||
- 泛型支持,确保数据类型一致性
|
||
- 严格的接口约束
|
||
|
||
#### 2. 用户体验优化
|
||
- 防抖搜索,避免频繁请求
|
||
- 加载状态遮罩,提供视觉反馈
|
||
- 分页状态保持,避免搜索条件丢失
|
||
- 响应式设计,适配不同屏幕尺寸
|
||
|
||
#### 3. 可维护性
|
||
- 配置驱动,减少硬编码
|
||
- 组件化设计,职责单一
|
||
- 完善的错误处理机制
|
||
|
||
#### 4. 可扩展性
|
||
- 插件化的字段配置
|
||
- 自定义渲染函数支持
|
||
- 多种配置选项
|
||
|
||
### 常见问题解决
|
||
|
||
#### 1. 分页后搜索条件丢失
|
||
|
||
**问题**:切换页码或每页条数时,搜索条件被重置
|
||
|
||
**解决方案**:
|
||
```tsx
|
||
const handlePageChange = useCallback((page) => {
|
||
// 确保传递当前的搜索条件
|
||
loadEnterprises({
|
||
filters: searchFilters, // 关键:传递搜索条件
|
||
pagination: { page, size: pagination.size }
|
||
});
|
||
}, [loadEnterprises, searchFilters, pagination.size]);
|
||
```
|
||
|
||
#### 2. 频繁API调用问题
|
||
|
||
**问题**:用户快速输入时触发过多API请求
|
||
|
||
**解决方案**:组件内置300ms防抖机制,无需额外处理
|
||
|
||
#### 3. 加载状态处理
|
||
|
||
**问题**:数据加载时用户体验不佳
|
||
|
||
**解决方案**:
|
||
```tsx
|
||
<SearchFormPagination
|
||
loading={loading} // 自动显示加载遮罩和状态
|
||
// ...
|
||
/>
|
||
```
|
||
|
||
### 性能优化最佳实践
|
||
|
||
#### 1. 事件驱动模式
|
||
|
||
**原则**:避免使用setTimeout,尽可能减少useEffect,使用事件驱动来实现状态更新。
|
||
|
||
**最佳实践**:
|
||
```tsx
|
||
// ❌ 避免写法:使用setTimeout和过多useEffect
|
||
useEffect(() => {
|
||
const timer = setTimeout(() => {
|
||
loadData();
|
||
}, 300);
|
||
return () => clearTimeout(timer);
|
||
}, [filters]);
|
||
|
||
useEffect(() => {
|
||
if (page > 1) {
|
||
loadData();
|
||
}
|
||
}, [page]);
|
||
|
||
// ✅ 推荐写法:事件驱动,直接调用
|
||
const handleSearch = useCallback((filters) => {
|
||
setSearchFilters(filters);
|
||
loadData({ filters, pagination: { page: 1, size: pagination.size } });
|
||
}, [loadData, pagination.size]);
|
||
|
||
const handlePageChange = useCallback((page) => {
|
||
setPagination(prev => ({ ...prev, page }));
|
||
loadData({ filters: searchFilters, pagination: { page, size: pagination.size } });
|
||
}, [loadData, searchFilters, pagination.size]);
|
||
```
|
||
|
||
#### 2. 函数依赖优化
|
||
|
||
**原则**:减少useCallback和useMemo的依赖项,通过参数传递而非依赖外部状态。
|
||
|
||
**最佳实践**:
|
||
```tsx
|
||
// ❌ 避免写法:过多依赖项导致函数频繁重新创建
|
||
const loadData = useCallback(async () => {
|
||
// 依赖filters, pagination, sortBy等
|
||
}, [filters, pagination, sortBy]);
|
||
|
||
// ✅ 推荐写法:无依赖项,通过参数传递
|
||
const loadData = useCallback(async (params) => {
|
||
// 使用params.filters, params.pagination等
|
||
}, []); // 空依赖数组
|
||
```
|
||
|
||
#### 3. 搜索防抖优化
|
||
|
||
**原则**:下拉框选择立即触发,文本输入使用防抖,避免不必要的延迟。
|
||
|
||
**实现方式**:
|
||
```tsx
|
||
// SearchFormComponent中的优化实现
|
||
const handleInputChange = (key: string, value: string, fieldType: 'text' | 'select') => {
|
||
const newFilters = { ...localFilters, [key]: value };
|
||
setLocalFilters(newFilters);
|
||
|
||
// 下拉框选择立即触发查询,文本输入使用防抖
|
||
if (fieldType === 'select') {
|
||
onFiltersChangeRef.current(newFilters); // 立即执行
|
||
}
|
||
// 文本输入的防抖在useEffect中处理
|
||
};
|
||
```
|
||
|
||
### 组件设计原则
|
||
|
||
#### 1. 排序功能简化
|
||
|
||
**设计决策**:SearchFormPagination组件不再支持表头排序功能。
|
||
|
||
**原因**:
|
||
- 简化组件复杂度,提高性能
|
||
- 减少不必要的交互,专注核心功能(搜索、展示、分页)
|
||
- 避免排序逻辑与业务逻辑耦合
|
||
|
||
**替代方案**:
|
||
- 如需排序功能,在业务页面层面实现
|
||
- 通过下拉框或其他UI控件提供排序选项
|
||
|
||
#### 2. 接口简化
|
||
|
||
**删除的排序相关接口**:
|
||
```tsx
|
||
// ❌ 已删除的接口
|
||
interface TableColumnConfig {
|
||
sortable?: boolean; // 删除
|
||
// ...
|
||
}
|
||
|
||
interface SearchFormPaginationProps {
|
||
sortBy?: string; // 删除
|
||
sortOrder?: 'asc' | 'desc'; // 删除
|
||
onSort?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; // 删除
|
||
// ...
|
||
}
|
||
```
|
||
|
||
#### 3. 表头渲染简化
|
||
|
||
**删除的排序交互**:
|
||
- 删除了表头的点击事件处理
|
||
- 删除了排序箭头图标显示
|
||
- 删除了鼠标悬停样式效果
|
||
|
||
### 重构指南
|
||
|
||
如果要重构或基于此组件开发新功能,请遵循以下原则:
|
||
|
||
1. **保持接口兼容性**:不要破坏现有的props接口
|
||
2. **扩展而非修改**:通过新的配置项而非修改现有逻辑来添加功能
|
||
3. **类型安全**:确保所有新功能都有完整的TypeScript类型定义
|
||
4. **测试覆盖**:新功能应该有相应的测试用例
|
||
5. **文档更新**:及时更新使用文档和接口说明
|
||
6. **性能优先**:采用事件驱动模式,避免不必要的useEffect和setTimeout
|
||
7. **功能专注**:保持组件职责单一,避免功能过度复杂化
|
||
|
||
### 总结
|
||
|
||
SearchFormPagination 组件通过配置驱动的方式,极大地简化了复杂数据展示页面的开发工作。其核心优势在于:
|
||
|
||
- **高度可配置**:通过配置而非代码实现功能定制
|
||
- **性能优化**:事件驱动模式,无setTimeout依赖,最小化useEffect使用
|
||
- **用户体验**:下拉框立即响应,文本输入智能防抖
|
||
- **易于扩展**:新增功能只需要修改配置,无需修改组件逻辑
|
||
- **类型安全**:完整的 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` 等过于简化的参数名
|
||
|
||
该功能特别适用于数据展示、搜索、筛选等需要状态保持的场景,是提升用户体验的重要功能。 |