From 008fc12db9a2f886c99e9323d442347020b0ca1a Mon Sep 17 00:00:00 2001 From: peng Date: Thu, 6 Nov 2025 17:47:14 +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=E4=BC=81=E4=B8=9A=E7=AE=A1=E7=90=86=E5=92=8C?= =?UTF-8?q?=E7=94=A8=E6=88=B7=E7=AE=A1=E7=90=86=20=E5=88=97=E8=A1=A8?= =?UTF-8?q?=E9=87=8D=E6=9E=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- AGENTS.md | 19 + crop-x/docs/开发项目规范.md | 657 +++++++++++++++++- .../tenant/enterprise-audit/page.tsx | 292 +++++--- .../tenant/enterprise-management/page.tsx | 36 +- .../tenant/user-management/page.tsx | 346 ++++++--- .../components/SearchFormComponent.tsx | 18 +- .../components/example.tsx | 248 ------- .../common/searchFormPagination/page.tsx | 37 - 8 files changed, 1154 insertions(+), 499 deletions(-) create mode 100644 AGENTS.md delete mode 100644 crop-x/src/components/common/searchFormPagination/components/example.tsx diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..d24e008 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,19 @@ +# Repository Guidelines + +## Project Structure & Module Organization +The primary Vite application lives in `src/`, with feature folders such as `components/ai`, `components/auth`, and `components/dashboard` grouping screens, while shared primitives stay in `components/ui` alongside the `cn` helper. Domain utilities and integrations sit in `lib/`, typed contracts in `types/`, and global styles in `styles/`. Reference notes, migration guides, and UX briefs belong in `docs/`. Build artifacts are staged in `build/` and `bundles/` - regenerate them instead of editing in place. The `crop-x/` workspace hosts the API-driven Next.js toolchain, and `nextjs-frontend/` contains a Jest-enabled prototype; treat each as an isolated package with its own dependencies. + +## Build, Test, and Development Commands +Install dependencies with `npm install` at the repository root before any work. Launch the Vite dev server via `npm run dev` and build production bundles with `npm run build`. When touching the Next.js workspace, switch into `crop-x/` (or `nextjs-frontend/`) and run the same `npm run dev` / `npm run build` loop; lint with `npm run lint` and regenerate OpenAPI clients through `npm run generate-client`. The Next.js prototype keeps Jest wired - execute `npm test` or `npm run coverage` from `nextjs-frontend/` to validate UI contracts. + +## Coding Style & Naming Conventions +Author components as TypeScript function components, export them with PascalCase names, and keep props camelCase. Align layout and spacing with Tailwind classes; reach for `components/ui` primitives before adding bespoke markup. Follow the established single-quote, semicolon-terminated formatting visible in `src/App.tsx`. In the Next.js packages, run Prettier (`npm run prettier`) when formatting cross-file changes and keep ESLint green before opening a review. + +## Testing Guidelines +Automated coverage is concentrated in `nextjs-frontend/__tests__` using Jest and Testing Library; mirror existing file naming (`*.test.tsx`) when adding scenarios. For the Vite app, smoke the key dashboards after significant UI changes: authentication, machinery, fields, operations, assets, AI models, and irrigation. Document manual steps or screenshots in the PR when you touch flows without automated tests. + +## Commit & Pull Request Guidelines +Commit messages currently follow the "ϵͳ - " prefix; keep that Chinese tag and supply a short, imperative summary. Group related changes per commit so reviewers can bisect easily. Pull requests should describe the user-visible impact, list any config or schema updates, and attach before/after captures for UI adjustments. Link tracking tickets where possible and flag required environment variables (`VITE_API_BASE_URL`, etc.) if they change. + +## Security & Configuration Tips +Never commit secrets - use `.env.local` entries for API hosts and credentials, and document new keys in `docs/` instead. When adding third-party scripts, load them through vetted helpers like `lib/mapLoader` to keep CSP compliance intact. Review generated OpenAPI clients before shipping to ensure endpoints line up with the deployed backend. diff --git a/crop-x/docs/开发项目规范.md b/crop-x/docs/开发项目规范.md index e23fbd4..069d686 100644 --- a/crop-x/docs/开发项目规范.md +++ b/crop-x/docs/开发项目规范.md @@ -234,6 +234,56 @@ 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:图表横轴显示不完整 @@ -819,4 +869,609 @@ engine.addMarker(marker); - **插件化架构**:编辑工具采用插件化设计,易于扩展新功能 - **接口标准化**:统一的接口设计,便于功能模块替换 - **配置化开发**:支持通过配置文件调整功能和行为 - - 理解了复杂应用中的组件分层和职责划分 \ No newline at end of file + - 理解了复杂应用中的组件分层和职责划分 + +--- + +## 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) => ( +
{value}
+ ), + }, + { + key: 'status', + label: '状态', + render: (value) => getStatusBadge(value), // 自定义渲染 + }, +]; +``` + +**加载状态处理**: +```tsx +// 表格加载遮罩 - 提升用户体验 +{loading && ( +
+
+ + 加载中... +
+
+)} +``` + +#### 3. 分页功能 + +**完整分页配置**: +```tsx +interface PaginationConfig { + page: number; + size: number; + total: number; + totalPages: number; + hasNext: boolean; + hasPrev: boolean; +} + + +``` + +**智能分页逻辑**: +- 当只有一页数据时,分页按钮隐藏但每页条数选择器仍显示 +- 支持页码跳转和快速导航 +- 分页操作时保持当前搜索条件 + +### 使用示例 + +#### 完整调用示例 + +```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) =>
{value}
, + }, + { + key: 'auditStatus', + label: '审核状态', + render: (value) => getAuditStatusBadge(value), + }, + { + key: 'actions', + label: '操作', + render: (_, row) => ( +
+ + +
+ ), + }, + ]; + + // 数据加载函数 + 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 ( +
+ 新建企业} + searchFields={searchFields} + columns={columns} + data={enterprises} + loading={loading} + error={null} + pagination={pagination} + onPageChange={handlePageChange} + onSizeChange={handleSizeChange} + onSearch={handleSearch} + emptyIcon={} + emptyText="暂无企业数据" + /> +
+ ); +} +``` + +### 接口定义 + +#### 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 { + // 搜索配置 + searchFields: SearchFieldConfig[]; + onSearch?: (filters: Record) => 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 + +``` + +#### 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 + +``` + +### 性能优化最佳实践 + +#### 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 支持 +- **功能专注**:专注搜索、展示、分页核心功能,避免过度设计 + +该组件可以作为项目中所有数据展示页面的标准解决方案,显著提升开发效率和代码质量。 \ No newline at end of file diff --git a/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx b/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx index 4cacc3e..5238bf9 100644 --- a/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx +++ b/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx @@ -2,20 +2,20 @@ * filekorolheader: 企业审核页面 - 企业注册审核管理页面 * 功能:企业审核列表、搜索筛选、审核操作、详情查看 * 路径:/central-config/tenant/enterprise-audit - * 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,模块化组件 + * 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,模块化组件,SearchFormPagination重构 */ 'use client'; -import { useReducer, useEffect, useMemo, useRef } from 'react'; +import { useReducer, useEffect, useMemo, useRef, useCallback } from 'react'; import { toast } from 'sonner'; -import { Building2, RefreshCw } from 'lucide-react'; +import { Building2, RefreshCw, Eye, Check, X } from 'lucide-react'; +import { Button } from '@/components/ui/button'; +import { Badge } from '@/components/ui/badge'; +import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination'; import { fetchTenantsForAudit, auditTenant, transformTenantData, TenantsQueryParams, Enterprise } from './components/enterpriseAuditApi'; import { AuditStatsCards } from './components/AuditStatsCards'; -import { AuditSearchAndFilter } from './components/AuditSearchAndFilter'; -import { EnterpriseAuditTable } from './components/EnterpriseAuditTable'; import { EnterpriseDetailDialog } from './components/EnterpriseDetailDialog'; -import { AuditPagination } from './components/AuditPagination'; // 审核状态管理 interface AuditState { @@ -119,21 +119,133 @@ export default function EnterpriseAuditPage() { const [state, dispatch] = useReducer(auditReducer, initialState); const isFirstLoad = useRef(true); - // 加载企业数据 - const loadEnterprises = async (resetPage = false) => { + // 搜索字段配置 + const searchFields: SearchFieldConfig[] = [ + { + key: 'search', + label: '搜索', + type: 'text', + placeholder: '搜索企业名称、编码...', + }, + { + key: 'audit_status', + label: '审核状态', + type: 'select', + defaultValue: 'all', + options: [ + { value: 'all', label: '全部状态' }, + { value: '待审核', label: '待审核' }, + { value: '已通过', label: '已通过' }, + { value: '已驳回', label: '已驳回' }, + ], + }, + ]; + + // 表格列配置 + const columns: TableColumnConfig[] = [ + { + key: 'name', + label: '企业名称', + sortable: false, // 禁用排序 + render: (value: string) => ( +
{value}
+ ), + }, + { + key: 'code', + label: '企业编码', + sortable: false, // 禁用排序 + render: (value: string) => ( +
{value}
+ ), + }, + { + key: 'auditStatus', + label: '审核状态', + sortable: false, // 禁用排序 + render: (value: string) => { + const statusConfig = { + '待审核': { 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' }, + }; + + const config = statusConfig[value as keyof typeof statusConfig] || statusConfig['待审核']; + return ( + + {config.label} + + ); + }, + }, + { + key: 'contactPerson', + label: '联系人', + sortable: false, // 禁用排序 + }, + { + key: 'contactPhone', + label: '联系电话', + sortable: false, // 禁用排序 + render: (value: string) => ( +
{value || '-'}
+ ), + }, + { + key: 'createdAt', + label: '创建时间', + sortable: false, // 禁用排序 + render: (value: string) => ( +
+ {value ? new Date(value).toLocaleDateString('zh-CN') : '-'} +
+ ), + }, + { + key: 'actions', + label: '操作', + sortable: false, // 操作列不能排序 + render: (_: any, row: Enterprise) => ( +
+ +
+ ), + }, + ]; + + // 加载企业数据 - 移除依赖项,通过参数传递状态 + const loadEnterprises = useCallback(async (params?: { + filters?: Record; + pagination?: { page: number; size: number }; + sort?: { sortBy?: string; sortOrder: 'asc' | 'desc' }; + resetPage?: boolean; + }) => { try { dispatch({ type: 'SET_LOADING', payload: true }); - const params: TenantsQueryParams = { - search: state.filters.search || undefined, - audit_status: state.filters.audit_status === 'all' ? undefined : state.filters.audit_status, - page: resetPage ? 1 : state.pagination.page, - size: state.pagination.size, - order_by: state.sortBy, - sort_order: state.sortOrder, + 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 response = await fetchTenantsForAudit(params); + // 处理audit_status,如果为'all'则不传该参数 + if (finalParams.audit_status === 'all') { + finalParams.audit_status = undefined; + } + + const response = await fetchTenantsForAudit(finalParams); const transformedData = response.data.map(transformTenantData); dispatch({ @@ -156,56 +268,48 @@ export default function EnterpriseAuditPage() { dispatch({ type: 'SET_ERROR', payload: errorMessage }); toast.error(errorMessage); } - }; + }, []); // 移除所有依赖,使用参数传递状态变化 - // 首次加载数据 - useEffect(() => { + // 首次加载数据 - 使用事件驱动,避免useEffect + const initializeData = useCallback(() => { if (isFirstLoad.current) { isFirstLoad.current = false; - loadEnterprises(true); + loadEnterprises({ resetPage: true }); } - }, []); + }, [loadEnterprises]); - // 监听筛选和排序变化(排除首次加载) + // 页面加载时初始化数据 useEffect(() => { - if (!isFirstLoad.current) { - const timer = setTimeout(() => { - loadEnterprises(true); - }, 300); - return () => clearTimeout(timer); - } - }, [state.filters.search, state.filters.audit_status, state.sortBy, state.sortOrder]); - - // 分页加载 - useEffect(() => { - if (!isFirstLoad.current && state.pagination.page > 1) { - loadEnterprises(false); - } - }, [state.pagination.page]); + initializeData(); + }, []); // 只在组件挂载时执行一次 // 计算统计数据 const stats = useMemo(() => ({ total: state.pagination.total, - pending: state.enterprises.filter(e => e.auditStatus === 'pending').length, - approved: state.enterprises.filter(e => e.auditStatus === 'approved').length, - rejected: state.enterprises.filter(e => e.auditStatus === 'rejected').length, + pending: state.enterprises.filter(e => e.auditStatus === '待审核').length, + approved: state.enterprises.filter(e => e.auditStatus === '已通过').length, + rejected: state.enterprises.filter(e => e.auditStatus === '已驳回').length, }), [state.enterprises, state.pagination.total]); // 事件处理器 - const handleSearch = (value: string) => { - dispatch({ type: 'SET_FILTERS', payload: { search: value } }); - }; + const handleSearch = useCallback((filters: Record) => { + dispatch({ type: 'SET_FILTERS', payload: filters }); + loadEnterprises({ + filters, + pagination: { page: 1, size: state.pagination.size } + }); + }, [loadEnterprises, state.pagination.size]); - const handleAuditStatusFilter = (value: string) => { - dispatch({ type: 'SET_FILTERS', payload: { audit_status: value === 'all' ? 'all' : value } }); - }; + const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { + dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } }); + loadEnterprises({ + filters: state.filters, + sort: { sortBy, sortOrder }, + resetPage: true + }); + }, [loadEnterprises, state.filters]); - const handleSort = (sortBy?: string) => { - const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc'; - dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } }); - }; - - const handlePageChange = (page: number) => { + const handlePageChange = useCallback((page: number) => { // 边界检查,确保页码在有效范围内 if (page < 1) { page = 1; @@ -213,13 +317,25 @@ export default function EnterpriseAuditPage() { page = state.pagination.totalPages; } dispatch({ type: 'SET_PAGINATION', payload: { page } }); - }; + loadEnterprises({ + filters: state.filters, + pagination: { page, size: state.pagination.size } + }); + }, [loadEnterprises, state.filters, state.pagination.size, state.pagination.totalPages]); - const handleRefresh = () => { + const handleSizeChange = useCallback((size: number) => { + dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } }); + loadEnterprises({ + filters: state.filters, + pagination: { page: 1, size } + }); + }, [loadEnterprises, state.filters]); + + const handleRefresh = useCallback(() => { dispatch({ type: 'REFRESH_DATA' }); - loadEnterprises(true); + loadEnterprises({ resetPage: true }); toast.success('数据已刷新'); - }; + }, [loadEnterprises]); const handleViewDetail = (enterprise: Enterprise) => { dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise }); @@ -257,10 +373,8 @@ export default function EnterpriseAuditPage() { dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false }); toast.success('审核通过'); - // 1秒后刷新列表 - setTimeout(() => { - loadEnterprises(true); - }, 1000); + // 立即刷新列表,无需延迟 + loadEnterprises({ resetPage: true }); } catch (error) { console.error('Approve failed:', error); const errorMessage = error instanceof Error ? error.message : '审核通过失败'; @@ -300,10 +414,8 @@ export default function EnterpriseAuditPage() { dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false }); toast.success('已驳回'); - // 1秒后刷新列表 - setTimeout(() => { - loadEnterprises(true); - }, 1000); + // 立即刷新列表,无需延迟 + loadEnterprises({ resetPage: true }); } catch (error) { console.error('Reject failed:', error); const errorMessage = error instanceof Error ? error.message : '审核驳回失败'; @@ -321,45 +433,47 @@ export default function EnterpriseAuditPage() {

管理企业注册与变更审核流程

- {/* 统计卡片 */} + {/* 统计卡片 - 保留原有功能 */} - {/* 搜索和筛选 */} - dispatch({ type: 'SET_FILTERS', payload: { search: value } })} - statusFilter={state.filters.audit_status} - onStatusFilterChange={(value) => dispatch({ type: 'SET_FILTERS', payload: { audit_status: value } })} - onRefresh={handleRefresh} + {/* 搜索、表格和分页 - 使用重构后的组件 */} + + + 刷新 + + } + searchFields={searchFields} + columns={columns} + data={state.enterprises} loading={state.loading} + error={state.error} + pagination={state.pagination} + sortBy={state.sortBy} + sortOrder={state.sortOrder} + onPageChange={handlePageChange} + onSizeChange={handleSizeChange} + onSearch={handleSearch} + onSort={handleSort} + emptyIcon={} + emptyText="暂无企业审核数据" + showSizeSelector={true} + showPageInfo={true} + sizeOptions={[10, 20, 50, 100]} /> - {/* 企业列表 */} - - - {/* 分页 */} - {state.pagination.total > 0 && ( - - )} - - {/* 企业详情对话框 */} + {/* 企业详情对话框 - 保留原有功能 */} dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })} enterprise={state.selectedEnterprise} auditReason={state.auditReason} - onAuditReasonChange={(reason) => dispatch({ type: 'SET_AUDIT_REASON', payload: reason })} + onAuditReasonChange={handleAuditReasonChange} onApprove={handleApprove} onReject={handleReject} loading={state.actionLoading} diff --git a/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx b/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx index bd969f9..ba01c30 100644 --- a/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx +++ b/crop-x/src/app/(app)/central-config/tenant/enterprise-management/page.tsx @@ -304,19 +304,33 @@ export default function EnterpriseManagement() { }); }, [loadEnterprises, pagination.size]); + // 统一的数据重载函数 - 避免重复代码 + const reloadData = useCallback(() => { + const reloadParams = { + filters: searchFilters, + pagination: { + page: pagination.page, + size: pagination.size + } + }; + loadEnterprises(reloadParams); + }, [loadEnterprises, searchFilters, pagination]); + const handlePageChange = useCallback((page: number) => { setPagination(prev => ({ ...prev, page })); loadEnterprises({ + filters: searchFilters, pagination: { page, size: pagination.size } }); - }, [loadEnterprises, pagination.size]); + }, [loadEnterprises, searchFilters, pagination.size]); const handleSizeChange = useCallback((size: number) => { setPagination(prev => ({ ...prev, size, page: 1 })); loadEnterprises({ + filters: searchFilters, pagination: { page: 1, size } }); - }, [loadEnterprises]); + }, [loadEnterprises, searchFilters]); // 初始化数据加载 // useEffect(() => { @@ -366,15 +380,7 @@ export default function EnterpriseManagement() { dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: false }); // 重新加载数据来反映状态变化 - const reloadParams: any = { - filters: searchFilters, - pagination: { - page: pagination.page, - size: pagination.size - } - }; - - loadEnterprises(reloadParams); + reloadData(); } catch (error) { console.error('Status change failed:', error); const errorMessage = error instanceof Error ? error.message : '状态更新失败'; @@ -391,13 +397,7 @@ export default function EnterpriseManagement() { const handleCreateSuccess = () => { // 创建成功后重新加载数据,保持当前搜索条件和分页状态 - loadEnterprises({ - filters: searchFilters, - pagination: { - page: pagination.page, - size: pagination.size - } - }); + reloadData(); }; // 操作按钮配置 diff --git a/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx b/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx index ebfbe95..d745811 100644 --- a/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx +++ b/crop-x/src/app/(app)/central-config/tenant/user-management/page.tsx @@ -2,25 +2,27 @@ * filekorolheader: 用户管理页面 - 用户查询和管理页面 * 功能:用户列表查询、搜索筛选、详情查看、用户管理 * 路径:/central-config/tenant/user-management - * 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式 + * 规范:遵循crop-x/docs/开发项目规范.md,使用SearchFormPagination公共组件,shadcn语义化样式 */ 'use client'; -import { useReducer, useEffect } from 'react'; +import { useReducer, useEffect, useState, useCallback, useMemo } from 'react'; import { toast } from 'sonner'; +import { Button } from '@/components/ui/button'; +import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react'; +import { UserDetailDialog } from './components/UserDetailDialog'; +import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination'; import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi'; import { UserManagementHeader } from './components/UserManagementHeader'; import { UserManagementStatsCards } from './components/UserManagementStatsCards'; -import { UserManagementFilters } from './components/UserManagementFilters'; -import { UserList } from './components/UserList'; -import { UserDetailDialog } from './components/UserDetailDialog'; -import { Enterprise, UserFilters } from './types'; +import { UserFilters } from './types'; + +// 移除了Enterprise的引用,因为新实现中不再需要 // 用户管理状态管理 interface UserManagementState { users: User[]; - enterprises: Enterprise[]; loading: boolean; error: string | null; pagination: PaginationState; @@ -38,7 +40,6 @@ type UserManagementAction = | { type: 'SET_FILTERS'; payload: Partial } | { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } } | { type: 'SET_PAGINATION'; payload: Partial } - | { type: 'SET_ENTERPRISES'; payload: Enterprise[] } | { type: 'SET_SELECTED_USER'; payload: User | null } | { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean } | { type: 'REFRESH_DATA' }; @@ -63,8 +64,6 @@ const userManagementReducer = (state: UserManagementState, action: UserManagemen return { ...state, sortBy: action.payload.sortBy, sortOrder: action.payload.sortOrder }; case 'SET_PAGINATION': return { ...state, pagination: { ...state.pagination, ...action.payload } }; - case 'SET_ENTERPRISES': - return { ...state, enterprises: action.payload }; case 'SET_SELECTED_USER': return { ...state, selectedUser: action.payload }; case 'TOGGLE_DETAIL_DIALOG': @@ -78,7 +77,6 @@ const userManagementReducer = (state: UserManagementState, action: UserManagemen const initialState: UserManagementState = { users: [], - enterprises: [], loading: false, error: null, pagination: { @@ -102,11 +100,178 @@ const initialState: UserManagementState = { export default function TenantUserManagementPage() { const [state, dispatch] = useReducer(userManagementReducer, initialState); + const [searchFilters, setSearchFilters] = useState>({}); + + // 搜索字段配置 + const searchFields: SearchFieldConfig[] = useMemo(() => [ + { + key: 'search', + label: '搜索', + type: 'text', + placeholder: '搜索用户名、姓名、邮箱...', + }, + { + key: 'status', + label: '用户状态', + type: 'select', + defaultValue: 'all', + options: [ + { value: 'all', label: '全部状态' }, + { value: 'active', label: '活跃' }, + { value: 'inactive', label: '未激活' }, + ], + }, + { + key: 'type', + label: '用户类型', + type: 'select', + defaultValue: 'all', + options: [ + { value: 'all', label: '全部类型' }, + { value: 'admin', label: '管理员' }, + { value: 'user', label: '普通用户' }, + { value: 'staff', label: '员工' }, + ], + }, + ], []); + + // 表格列配置 + const columns: TableColumnConfig[] = useMemo(() => [ + { + key: 'username', + label: '用户名', + sortable: true, + render: (value: string, user: User) => ( +
{value}
+ ), + }, + { + key: 'fullName', + label: '姓名', + sortable: true, + render: (value: string) => value || '-', + }, + { + key: 'email', + label: '邮箱', + sortable: true, + render: (value: string) => value || '-', + }, + { + key: 'isActive', + label: '状态', + sortable: true, + render: (value: boolean) => ( +
+
+ {value ? '活跃' : '未激活'} +
+ ), + }, + { + key: 'isSuperuser', + label: '角色', + sortable: true, + render: (value: boolean, user: User) => { + if (value) { + return ( +
+
+ 超级管理员 +
+ ); + } + return ( +
+
+ 普通用户 +
+ ); + }, + }, + { + key: 'isVerified', + label: '验证', + sortable: true, + render: (value: boolean) => ( +
+ {value ? '已验证' : '未验证'} +
+ ), + }, + { + key: 'lastLoginAt', + label: '最后登录', + sortable: true, + render: (value: string) => { + if (!value) return '-'; + try { + const date = new Date(value); + return date.toLocaleDateString('zh-CN', { + year: 'numeric', + month: '2-digit', + day: '2-digit', + hour: '2-digit', + minute: '2-digit', + }); + } catch { + return value; + } + }, + }, + { + key: 'actions', + label: '操作', + render: (_, user: User) => ( +
+ + + + +
+ ), + }, + ], []); // 加载用户数据 - const loadUsers = async (resetPage = false) => { + const loadUsers = useCallback(async (resetPage = false) => { try { - dispatch({ type: 'SET_LOADING', payload: true }); const params: UsersQueryParams = { @@ -114,6 +279,26 @@ export default function TenantUserManagementPage() { size: state.pagination.size, is_active: true, }; + + // 添加搜索条件 + if (searchFilters.search) { + params.search = searchFilters.search; + } + + if (searchFilters.status && searchFilters.status !== 'all') { + params.is_active = searchFilters.status === 'active'; + } + + if (searchFilters.type && searchFilters.type !== 'all') { + // For user type filtering, we'll need to handle this differently based on the API + // For now, we'll filter on the client side if needed + } + + if (state.sortBy) { + params.order_by = state.sortBy; + params.sort_order = state.sortOrder; + } + const response: UsersApiResponse = await fetchUsers(params); const transformedUsers = response.data.map(transformUserData); @@ -138,50 +323,33 @@ export default function TenantUserManagementPage() { payload: error instanceof Error ? error.message : '加载用户数据失败' }); } - }; - - // 加载企业数据(这里暂时使用mock数据,后续可以添加企业API) - // const loadEnterprises = () => { - // // 这里可以添加企业API调用,现在使用mock数据 - // const mockEnterprises: Enterprise[] = [ - // { id: 'ent-1', name: '丰收现代农业集团' }, - // { id: 'ent-2', name: '绿色种植科技有限公司' }, - // { id: 'ent-3', name: '智慧农业示范区' }, - // ]; - // dispatch({ type: 'SET_ENTERPRISES', payload: mockEnterprises }); - // }; + }, [state.pagination.page, state.pagination.size, state.sortBy, state.sortOrder, searchFilters]); // 搜索处理 - const handleSearch = (value: string) => { - dispatch({ type: 'SET_FILTERS', payload: { searchKeyword: value } }); - }; - - // 状态筛选 - const handleStatusFilter = (value: string) => { - dispatch({ type: 'SET_FILTERS', payload: { statusFilter: value } }); - }; - - // 类型筛选 - const handleTypeFilter = (value: string) => { - dispatch({ type: 'SET_FILTERS', payload: { typeFilter: value } }); - }; + const handleSearch = useCallback((filters: Record) => { + setSearchFilters(filters); + dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } }); + }, []); // 排序处理 - const handleSort = (sortBy: string) => { - const newSortOrder = state.sortBy === sortBy && state.sortOrder === 'desc' ? 'asc' : 'desc'; - dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder: newSortOrder } }); - }; + const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => { + dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } }); + }, []); // 分页处理 - const handlePageChange = (page: number) => { - // 边界检查,确保页码在有效范围内 + const handlePageChange = useCallback((page: number) => { if (page < 1) { page = 1; } else if (page > state.pagination.totalPages && state.pagination.totalPages > 0) { page = state.pagination.totalPages; } dispatch({ type: 'SET_PAGINATION', payload: { page } }); - }; + }, [state.pagination.totalPages]); + + // 每页条数变化处理 + const handleSizeChange = useCallback((size: number) => { + dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } }); + }, []); // 查看详情 const handleViewDetail = (user: User) => { @@ -191,108 +359,86 @@ export default function TenantUserManagementPage() { // 编辑用户 const handleEdit = (user: User) => { - // 这里可以添加编辑逻辑,比如打开编辑对话框 toast.info('编辑功能开发中...'); }; - // 删除用户 - const handleDelete = (user: User) => { - if (!confirm(`确定要删除用户 ${user.fullName || user.username} 吗?`)) return; - // 这里可以添加删除逻辑,调用API删除用户 - toast.info('删除功能开发中...'); - }; - // 切换用户状态 const handleToggleStatus = (user: User) => { const newStatus = !user.isActive; const statusText = newStatus ? '激活' : '停用'; if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return; - // 这里可以添加状态切换逻辑,调用API更新用户状态 toast.info(`${statusText}功能开发中...`); }; // 重置密码 const handleResetPassword = (user: User) => { if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return; - // 这里可以添加重置密码逻辑,调用API重置密码 toast.info('重置密码功能开发中...'); }; - // 统计数据计算 - const stats = [ + const stats = useMemo(() => [ { label: '总用户数', value: state.pagination.total, - color: 'text-blue-600', - bg: 'bg-blue-100', + color: 'text-blue-600 dark:text-blue-400', + bg: 'bg-blue-50 dark:bg-blue-950', }, { label: '活跃用户', value: state.users.filter(u => u.isActive).length, - color: 'text-green-600', - bg: 'bg-green-100', + color: 'text-green-600 dark:text-green-400', + bg: 'bg-green-50 dark:bg-green-950', }, { label: '管理员', value: state.users.filter(u => u.isSuperuser).length, - color: 'text-purple-600', - bg: 'bg-purple-100', + color: 'text-purple-600 dark:text-purple-400', + bg: 'bg-purple-50 dark:bg-purple-950', }, { label: '已验证', value: state.users.filter(u => u.isVerified).length, - color: 'text-orange-600', - bg: 'bg-orange-100', + color: 'text-orange-600 dark:text-orange-400', + bg: 'bg-orange-50 dark:bg-orange-950', }, - ]; + ], [state.users, state.pagination.total]); + // 加载数据 useEffect(() => { - loadUsers(); - }, [state.filters.searchKeyword, state.filters.statusFilter, state.filters.typeFilter, state.sortBy, state.sortOrder]); - - useEffect(() => { - if (state.pagination.page > 1) { - loadUsers(); - } - }, [state.pagination.page]); + loadUsers(); + }, [loadUsers]); return (
- {/* 页面标题和统计 */} + {/* 页面标题 */} {/* 统计卡片 */} - {/* 搜索和筛选 */} - - - {/* 错误显示 */} - {state.error && ( -
-
- {state.error} -
-
- )} - - {/* 用户列表 */} - toast.info('新建用户功能开发中...')}> + 新建用户 + + } + searchFields={searchFields} + columns={columns} + data={state.users} loading={state.loading} + error={state.error} pagination={state.pagination} + sortBy={state.sortBy} + sortOrder={state.sortOrder} onPageChange={handlePageChange} - onViewDetail={handleViewDetail} - onEdit={handleEdit} - onDelete={handleDelete} - onToggleStatus={handleToggleStatus} - onResetPassword={handleResetPassword} + onSizeChange={handleSizeChange} + onSearch={handleSearch} + onSort={handleSort} + emptyText="暂无用户数据" + sizeOptions={[10, 20, 50, 100]} /> {/* 用户详情对话框 */} diff --git a/crop-x/src/components/common/searchFormPagination/components/SearchFormComponent.tsx b/crop-x/src/components/common/searchFormPagination/components/SearchFormComponent.tsx index fcd66db..c19c8bf 100644 --- a/crop-x/src/components/common/searchFormPagination/components/SearchFormComponent.tsx +++ b/crop-x/src/components/common/searchFormPagination/components/SearchFormComponent.tsx @@ -53,16 +53,22 @@ export function SearchFormComponent({ setLocalFilters(filters); }, [filters]); - // 处理输入变化 - 防抖搜索避免频繁刷新导致失焦 - const handleInputChange = (key: string, value: string) => { + // 处理输入变化 - 区分文本输入和下拉选择 + const handleInputChange = (key: string, value: string, fieldType: 'text' | 'select') => { const newFilters = { ...localFilters, [key]: value, }; setLocalFilters(newFilters); + + // 下拉框选择立即触发查询,文本输入使用防抖 + if (fieldType === 'select') { + onFiltersChangeRef.current(newFilters); + } + // 文本输入的防抖在useEffect中处理 }; - // 使用防抖来减少搜索频率,避免频繁刷新导致失焦 + // 使用防抖来减少搜索频率,仅针对文本输入 useEffect(() => { const timer = setTimeout(() => { // 使用ref引用最新的onFiltersChange函数,避免依赖变化导致重复触发 @@ -89,7 +95,7 @@ export function SearchFormComponent({
handleInputChange(field.key, e.target.value)} + onChange={(e) => handleInputChange(field.key, e.target.value, 'text')} disabled={false} // 始终允许输入,不因加载而禁用 className="pl-10 w-64" /> @@ -130,7 +136,7 @@ export function SearchFormComponent({ handleInputChange('search', e.target.value)} + onChange={(e) => handleInputChange('search', e.target.value, 'text')} disabled={false} // 始终允许输入,不因加载而禁用 className="pl-10" /> diff --git a/crop-x/src/components/common/searchFormPagination/components/example.tsx b/crop-x/src/components/common/searchFormPagination/components/example.tsx deleted file mode 100644 index 1e852b9..0000000 --- a/crop-x/src/components/common/searchFormPagination/components/example.tsx +++ /dev/null @@ -1,248 +0,0 @@ -/** - * filekorolheader: 搜索表单分页组件使用示例 - 展示如何使用该组件 - * 功能:使用示例、配置示例、最佳实践展示 - * 路径:/components/common/searchFormPagination/components/example - * 规范:遵循crop-x/docs/开发项目规范.md,提供完整的使用示例 - */ -'use client'; - -import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '../index'; -import { Badge } from '@/components/ui/badge'; -import { Button } from '@/components/ui/button'; -import { Building2, Eye, Power, PowerOff, Plus } from 'lucide-react'; -import { toast } from 'sonner'; - -// 模拟数据类型 -interface MockEnterprise { - id: string; - code: string; - name: string; - type: string; - registrant?: string; - contactPhone?: string; - createdAt: string; - auditStatus: 'draft' | 'pending' | 'approved' | 'rejected'; - status: 'active' | 'inactive'; -} - -// 示例使用 -export function EnterpriseManagementExample() { - // 搜索字段配置 - const searchFields: SearchFieldConfig[] = [ - { - key: 'search', - label: '企业搜索', - type: 'text', - placeholder: '搜索企业名称、编码...', - }, - { - key: 'audit_status', - label: '审核状态', - type: 'select', - placeholder: '选择审核状态', - options: [ - { value: '', label: '全部状态' }, - { value: '草稿', label: '草稿' }, - { value: '待审核', label: '待审核' }, - { value: '已通过', label: '审核通过' }, - { value: '已拒绝', label: '已拒绝' }, - ], - }, - ]; - - // 表格列配置 - const columns: TableColumnConfig[] = [ - { - key: 'code', - label: '企业编码', - sortable: true, - width: '120px', - }, - { - key: 'name', - label: '企业名称', - sortable: true, - render: (value: string, row: MockEnterprise) => ( -
- - {value} -
- ), - }, - { - key: 'type', - label: '企业类型', - render: (value: string) => ( - {value} - ), - }, - { - key: 'registrant', - label: '登记人', - render: (value?: string) => value || '-', - }, - { - key: 'contactPhone', - label: '联系电话', - render: (value?: string) => value || '-', - }, - { - key: 'createdAt', - label: '创建时间', - sortable: true, - width: '160px', - }, - { - key: 'auditStatus', - label: '审核状态', - render: (value: MockEnterprise['auditStatus']) => { - const getAuditStatusBadge = (status: MockEnterprise['auditStatus']) => { - switch (status) { - case 'draft': - return 草稿; - case 'pending': - return 待审核; - case 'approved': - return 审核通过; - case 'rejected': - return 已拒绝; - default: - return 草稿; - } - }; - return getAuditStatusBadge(value); - }, - }, - { - key: 'status', - label: '状态', - render: (value: MockEnterprise['status']) => { - const getStatusBadge = (status: MockEnterprise['status']) => { - if (status === 'active') { - return 启用; - } - return 禁用; - }; - return getStatusBadge(value); - }, - }, - { - key: 'actions', - label: '操作', - render: (_: any, row: MockEnterprise) => ( -
- - {row.status === 'active' ? ( - - ) : ( - - )} -
- ), - }, - ]; - - // 模拟数据 - const mockData: MockEnterprise[] = [ - { - id: '1', - code: 'ENT001', - name: '示例科技有限公司', - type: '科技有限公司', - registrant: '张三', - contactPhone: '13800138000', - createdAt: '2024-01-15 10:30:00', - auditStatus: 'approved', - status: 'active', - }, - { - id: '2', - code: 'ENT002', - name: '测试农业发展有限公司', - type: '农业发展有限公司', - registrant: '李四', - contactPhone: '13900139000', - createdAt: '2024-01-16 14:20:00', - auditStatus: 'pending', - status: 'active', - }, - ]; - - // 模拟分页配置 - const mockPagination = { - page: 1, - size: 10, - total: 2, - totalPages: 1, - hasNext: false, - hasPrev: false, - }; - - // 处理搜索 - const handleSearch = (filters: Record) => { - console.log('搜索条件:', filters); - toast.success('搜索条件已更新'); - }; - - // 处理排序 - const handleSort = (sortBy: string, sortOrder: 'asc' | 'desc') => { - console.log('排序:', { sortBy, sortOrder }); - toast.success(`排序: ${sortBy} ${sortOrder}`); - }; - - // 处理分页 - const handlePageChange = (page: number) => { - console.log('切换到页面:', page); - toast.success(`切换到第 ${page} 页`); - }; - - // 操作按钮 - const actionButtons = ( - - ); - - return ( - } - emptyText="暂无企业数据" - /> - ); -} - -export default EnterpriseManagementExample; \ No newline at end of file diff --git a/crop-x/src/components/common/searchFormPagination/page.tsx b/crop-x/src/components/common/searchFormPagination/page.tsx index 43b7c40..f015b21 100644 --- a/crop-x/src/components/common/searchFormPagination/page.tsx +++ b/crop-x/src/components/common/searchFormPagination/page.tsx @@ -33,7 +33,6 @@ export interface SearchFieldConfig { export interface TableColumnConfig { key: string; label: string; - sortable?: boolean; width?: string; render?: (value: any, row: any, index: number) => React.ReactNode; } @@ -68,11 +67,6 @@ export interface SearchFormPaginationProps { onPageChange?: (page: number) => void; onSizeChange?: (size: number) => void; - // 排序配置 - sortBy?: string; - sortOrder?: 'asc' | 'desc'; - onSort?: (sortBy: string, sortOrder: 'asc' | 'desc') => void; - // 空状态配置 emptyIcon?: React.ReactNode; emptyText?: string; @@ -109,9 +103,6 @@ export function SearchFormPagination({ pagination, onPageChange, onSizeChange, - sortBy, - sortOrder, - onSort, emptyIcon, emptyText = '暂无数据', showSizeSelector = true, @@ -130,12 +121,6 @@ export function SearchFormPagination({ }, {} as Record) ); - // 同步外部排序状态 - const [currentSort, setCurrentSort] = useState<{ sortBy?: string; sortOrder: 'asc' | 'desc' }>({ - sortBy, - sortOrder: sortOrder || 'asc' - }); - // 数据更新回调 - 通知父组件数据变化 useEffect(() => { onDataUpdate?.({ @@ -159,23 +144,6 @@ export function SearchFormPagination({ onSearch?.(newFilters); }, [onSearch]); - const handleSort = useCallback((columnKey: string) => { - const column = columns.find(col => col.key === columnKey); - if (!column?.sortable) return; - - // 计算新的排序状态 - let newSortOrder: 'asc' | 'desc'; - if (currentSort.sortBy === columnKey) { - newSortOrder = currentSort.sortOrder === 'desc' ? 'asc' : 'desc'; - } else { - newSortOrder = 'asc'; - } - - const newSort = { sortBy: columnKey, sortOrder: newSortOrder }; - setCurrentSort(newSort); - onSort?.(columnKey, newSortOrder); - }, [columns, currentSort, onSort]); - const handlePageChange = useCallback((page: number) => { onPageChange?.(page); }, [onPageChange]); @@ -210,7 +178,6 @@ export function SearchFormPagination({ {columns.map((column) => ( ({ textOverflow: 'ellipsis', whiteSpace: 'nowrap' }} - onClick={() => column.sortable && handleSort(column.key)} >
{column.label}
- {column.sortable && currentSort.sortBy === column.key && ( - {currentSort.sortOrder === 'asc' ? '↑' : '↓'} - )}
))}