生产管理系统 - 企业管理和用户管理 列表重构

This commit is contained in:
2025-11-06 17:47:14 +08:00
parent 191d218ec4
commit 008fc12db9
8 changed files with 1154 additions and 499 deletions

View File

@@ -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) => (
<div className="font-medium text-foreground">{value}</div>
),
},
{
key: 'code',
label: '企业编码',
sortable: false, // 禁用排序
render: (value: string) => (
<div className="font-mono text-sm text-muted-foreground">{value}</div>
),
},
{
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 (
<Badge className={`font-light ${config.className}`}>
{config.label}
</Badge>
);
},
},
{
key: 'contactPerson',
label: '联系人',
sortable: false, // 禁用排序
},
{
key: 'contactPhone',
label: '联系电话',
sortable: false, // 禁用排序
render: (value: string) => (
<div className="font-mono text-sm">{value || '-'}</div>
),
},
{
key: 'createdAt',
label: '创建时间',
sortable: false, // 禁用排序
render: (value: string) => (
<div className="text-sm text-muted-foreground">
{value ? new Date(value).toLocaleDateString('zh-CN') : '-'}
</div>
),
},
{
key: 'actions',
label: '操作',
sortable: false, // 操作列不能排序
render: (_: any, row: Enterprise) => (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => handleViewDetail(row)}
className="h-8 px-2"
>
<Eye className="w-3 h-3 mr-1" />
</Button>
</div>
),
},
];
// 加载企业数据 - 移除依赖项,通过参数传递状态
const loadEnterprises = useCallback(async (params?: {
filters?: Record<string, string>;
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<string, string>) => {
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() {
<p className="text-muted-foreground"></p>
</div>
{/* 统计卡片 */}
{/* 统计卡片 - 保留原有功能 */}
<AuditStatsCards
enterprises={state.enterprises}
loading={state.loading}
/>
{/* 搜索和筛选 */}
<AuditSearchAndFilter
searchKeyword={state.filters.search}
onSearchChange={(value) => dispatch({ type: 'SET_FILTERS', payload: { search: value } })}
statusFilter={state.filters.audit_status}
onStatusFilterChange={(value) => dispatch({ type: 'SET_FILTERS', payload: { audit_status: value } })}
onRefresh={handleRefresh}
{/* 搜索、表格和分页 - 使用重构后的组件 */}
<SearchFormPagination
formTitle="企业列表"
formRightContent={
<Button variant="outline" onClick={handleRefresh} disabled={state.loading}>
<RefreshCw className={`w-4 h-4 mr-2 ${state.loading ? 'animate-spin' : ''}`} />
</Button>
}
searchFields={searchFields}
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={<Building2 className="w-12 h-12" />}
emptyText="暂无企业审核数据"
showSizeSelector={true}
showPageInfo={true}
sizeOptions={[10, 20, 50, 100]}
/>
{/* 企业列表 */}
<EnterpriseAuditTable
enterprises={state.enterprises}
loading={state.loading}
onViewDetails={handleViewDetail}
/>
{/* 分页 */}
{state.pagination.total > 0 && (
<AuditPagination
pagination={state.pagination}
onPageChange={handlePageChange}
loading={state.loading}
/>
)}
{/* 企业详情对话框 */}
{/* 企业详情对话框 - 保留原有功能 */}
<EnterpriseDetailDialog
open={state.showDetailDialog}
onOpenChange={(open) => 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}

View File

@@ -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();
};
// 操作按钮配置

View File

@@ -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<UserFilters> }
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
| { type: 'SET_PAGINATION'; payload: Partial<PaginationState> }
| { 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<Record<string, string>>({});
// 搜索字段配置
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) => (
<div className="font-medium">{value}</div>
),
},
{
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) => (
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
value
? 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400'
: 'bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400'
}`}>
<div className={`w-2 h-2 rounded-full ${value ? 'bg-green-500' : 'bg-red-500'}`} />
{value ? '活跃' : '未激活'}
</div>
),
},
{
key: 'isSuperuser',
label: '角色',
sortable: true,
render: (value: boolean, user: User) => {
if (value) {
return (
<div className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400">
<div className="w-2 h-2 rounded-full bg-purple-500" />
</div>
);
}
return (
<div className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400">
<div className="w-2 h-2 rounded-full bg-blue-500" />
</div>
);
},
},
{
key: 'isVerified',
label: '验证',
sortable: true,
render: (value: boolean) => (
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
value
? 'bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400'
: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400'
}`}>
{value ? '已验证' : '未验证'}
</div>
),
},
{
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) => (
<div className="flex items-center gap-2">
<Button
variant="ghost"
size="sm"
onClick={() => handleViewDetail(user)}
>
<Eye className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleEdit(user)}
>
<Edit className="w-4 h-4" />
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleToggleStatus(user)}
>
{user.isActive ? (
<UserX className="w-4 h-4 text-orange-600" />
) : (
<UserCheck className="w-4 h-4 text-green-600" />
)}
</Button>
<Button
variant="ghost"
size="sm"
onClick={() => handleResetPassword(user)}
>
<Lock className="w-4 h-4" />
</Button>
</div>
),
},
], []);
// 加载用户数据
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<string, string>) => {
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 (
<div className="space-y-6">
{/* 页面标题和统计 */}
{/* 页面标题 */}
<UserManagementHeader stats={stats} />
{/* 统计卡片 */}
<UserManagementStatsCards stats={stats} />
{/* 搜索和筛选 */}
<UserManagementFilters
filters={state.filters}
onSearchChange={handleSearch}
onStatusFilterChange={handleStatusFilter}
onTypeFilterChange={handleTypeFilter}
/>
{/* 错误显示 */}
{state.error && (
<div className="p-4 bg-red-50 dark:bg-red-950 border border-red-200 dark:border-red-800 rounded-lg">
<div className="flex items-center gap-2 text-red-600 dark:text-red-400">
<span>{state.error}</span>
</div>
</div>
)}
{/* 用户列表 */}
<UserList
users={state.users}
{/* 搜索表单、数据表格和分页 */}
<SearchFormPagination
formTitle="用户列表"
formRightContent={
<Button onClick={() => toast.info('新建用户功能开发中...')}>
</Button>
}
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]}
/>
{/* 用户详情对话框 */}

View File

@@ -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({
<div key={field.key}>
<Select
value={value}
onValueChange={(newValue) => handleInputChange(field.key, newValue)}
onValueChange={(newValue) => handleInputChange(field.key, newValue, 'select')}
disabled={false} // 始终允许选择,不因加载而禁用
>
<SelectTrigger className="w-40">
@@ -114,7 +120,7 @@ export function SearchFormComponent({
<Input
placeholder={field.placeholder || placeholder}
value={value}
onChange={(e) => 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({
<Input
placeholder={placeholder}
value={localFilters.search || ''}
onChange={(e) => handleInputChange('search', e.target.value)}
onChange={(e) => handleInputChange('search', e.target.value, 'text')}
disabled={false} // 始终允许输入,不因加载而禁用
className="pl-10"
/>

View File

@@ -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) => (
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-blue-500" />
<span className="font-medium">{value}</span>
</div>
),
},
{
key: 'type',
label: '企业类型',
render: (value: string) => (
<Badge variant="outline" className="font-light">{value}</Badge>
),
},
{
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 <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light">稿</Badge>;
case 'pending':
return <Badge className="bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border-yellow-200 dark:border-yellow-800 font-light"></Badge>;
case 'approved':
return <Badge className="bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 font-light"></Badge>;
case 'rejected':
return <Badge className="bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border-red-200 dark:border-red-800 font-light"></Badge>;
default:
return <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light">稿</Badge>;
}
};
return getAuditStatusBadge(value);
},
},
{
key: 'status',
label: '状态',
render: (value: MockEnterprise['status']) => {
const getStatusBadge = (status: MockEnterprise['status']) => {
if (status === 'active') {
return <Badge className="bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border-green-200 dark:border-green-800 font-light"></Badge>;
}
return <Badge className="bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border-gray-200 dark:border-gray-800 font-light"></Badge>;
};
return getStatusBadge(value);
},
},
{
key: 'actions',
label: '操作',
render: (_: any, row: MockEnterprise) => (
<div className="flex gap-2">
<Button
size="sm"
variant="outline"
onClick={() => toast.success(`查看企业: ${row.name}`)}
>
<Eye className="w-3 h-3 mr-1" />
</Button>
{row.status === 'active' ? (
<Button
size="sm"
variant="outline"
className="text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600"
onClick={() => toast.success(`禁用企业: ${row.name}`)}
>
<PowerOff className="w-3 h-3 mr-1" />
</Button>
) : (
<Button
size="sm"
variant="outline"
className="text-green-600 dark:text-green-400 border-green-300 dark:border-green-600"
onClick={() => toast.success(`启用企业: ${row.name}`)}
>
<Power className="w-3 h-3 mr-1" />
</Button>
)}
</div>
),
},
];
// 模拟数据
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<string, string>) => {
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 = (
<Button onClick={() => toast.success('新建企业')}>
<Plus className="w-4 h-4 mr-2" />
</Button>
);
return (
<SearchFormPagination
title="企业管理"
description="管理平台所有企业信息,支持查询、查看详情、启用/禁用企业"
searchFields={searchFields}
onSearch={handleSearch}
columns={columns}
data={mockData}
pagination={mockPagination}
onPageChange={handlePageChange}
onSort={handleSort}
actionButtons={actionButtons}
emptyIcon={<Building2 className="w-12 h-12 mx-auto mb-4 opacity-20" />}
emptyText="暂无企业数据"
/>
);
}
export default EnterpriseManagementExample;

View File

@@ -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<T = any> {
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<T = any>({
pagination,
onPageChange,
onSizeChange,
sortBy,
sortOrder,
onSort,
emptyIcon,
emptyText = '暂无数据',
showSizeSelector = true,
@@ -130,12 +121,6 @@ export function SearchFormPagination<T = any>({
}, {} as Record<string, string>)
);
// 同步外部排序状态
const [currentSort, setCurrentSort] = useState<{ sortBy?: string; sortOrder: 'asc' | 'desc' }>({
sortBy,
sortOrder: sortOrder || 'asc'
});
// 数据更新回调 - 通知父组件数据变化
useEffect(() => {
onDataUpdate?.({
@@ -159,23 +144,6 @@ export function SearchFormPagination<T = any>({
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<T = any>({
{columns.map((column) => (
<TableHead
key={column.key}
className={column.sortable ? 'cursor-pointer hover:bg-muted' : ''}
style={{
width: getColumnWidth(column),
minWidth: column.render ? '120px' : '100px',
@@ -219,14 +186,10 @@ export function SearchFormPagination<T = any>({
textOverflow: 'ellipsis',
whiteSpace: 'nowrap'
}}
onClick={() => column.sortable && handleSort(column.key)}
>
<div className="truncate" title={column.label}>
{column.label}
</div>
{column.sortable && currentSort.sortBy === column.key && (
<span className="ml-1 flex-shrink-0">{currentSort.sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</TableHead>
))}
</TableRow>