子仓库提交
This commit is contained in:
484
src/app/(app)/central-config/tenant/enterprise-audit/page.tsx
Normal file
484
src/app/(app)/central-config/tenant/enterprise-audit/page.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
/**
|
||||
* filekorolheader: 企业审核页面 - 企业注册审核管理页面
|
||||
* 功能:企业审核列表、搜索筛选、审核操作、详情查看
|
||||
* 路径:/central-config/tenant/enterprise-audit
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,模块化组件,SearchFormPagination重构
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useReducer, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
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 { EnterpriseDetailDialog } from './components/EnterpriseDetailDialog';
|
||||
|
||||
// 审核状态管理
|
||||
interface AuditState {
|
||||
enterprises: Enterprise[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
pagination: {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
};
|
||||
filters: {
|
||||
search: string;
|
||||
audit_status: string;
|
||||
};
|
||||
sortBy?: string;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
selectedEnterprise: Enterprise | null;
|
||||
showDetailDialog: boolean;
|
||||
auditReason: string;
|
||||
actionLoading: boolean;
|
||||
}
|
||||
|
||||
type AuditAction =
|
||||
| { type: 'SET_ENTERPRISES'; payload: { data: Enterprise[]; pagination: AuditState['pagination'] } }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_FILTERS'; payload: Partial<AuditState['filters']> }
|
||||
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||||
| { type: 'SET_PAGINATION'; payload: Partial<AuditState['pagination']> }
|
||||
| { type: 'SET_SELECTED_ENTERPRISE'; payload: Enterprise | null }
|
||||
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||||
| { type: 'SET_AUDIT_REASON'; payload: string }
|
||||
| { type: 'SET_ACTION_LOADING'; payload: boolean }
|
||||
| { type: 'REFRESH_DATA' };
|
||||
|
||||
const auditReducer = (state: AuditState, action: AuditAction): AuditState => {
|
||||
switch (action.type) {
|
||||
case 'SET_ENTERPRISES':
|
||||
return {
|
||||
...state,
|
||||
enterprises: action.payload.data,
|
||||
pagination: action.payload.pagination,
|
||||
loading: false,
|
||||
error: null,
|
||||
};
|
||||
case 'SET_LOADING':
|
||||
return { ...state, loading: action.payload };
|
||||
case 'SET_ERROR':
|
||||
return { ...state, error: action.payload, loading: false };
|
||||
case 'SET_FILTERS':
|
||||
return { ...state, filters: { ...state.filters, ...action.payload } };
|
||||
case 'SET_SORT':
|
||||
return { ...state, sortBy: action.payload.sortBy, sortOrder: action.payload.sortOrder };
|
||||
case 'SET_PAGINATION':
|
||||
return { ...state, pagination: { ...state.pagination, ...action.payload } };
|
||||
case 'SET_SELECTED_ENTERPRISE':
|
||||
return { ...state, selectedEnterprise: action.payload };
|
||||
case 'TOGGLE_DETAIL_DIALOG':
|
||||
return { ...state, showDetailDialog: !state.showDetailDialog };
|
||||
case 'SET_AUDIT_REASON':
|
||||
return { ...state, auditReason: action.payload };
|
||||
case 'SET_ACTION_LOADING':
|
||||
return { ...state, actionLoading: action.payload };
|
||||
case 'REFRESH_DATA':
|
||||
return { ...state, error: null };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: AuditState = {
|
||||
enterprises: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
},
|
||||
filters: {
|
||||
search: '',
|
||||
audit_status: 'all',
|
||||
},
|
||||
sortBy: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
selectedEnterprise: null,
|
||||
showDetailDialog: false,
|
||||
auditReason: '',
|
||||
actionLoading: false,
|
||||
};
|
||||
|
||||
|
||||
export default function EnterpriseAuditPage() {
|
||||
const [state, dispatch] = useReducer(auditReducer, initialState);
|
||||
const isFirstLoad = useRef(true);
|
||||
|
||||
// 搜索字段配置
|
||||
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"
|
||||
title="查看"
|
||||
>
|
||||
<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 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,
|
||||
};
|
||||
|
||||
// 处理audit_status,如果为'all'则不传该参数
|
||||
if (finalParams.audit_status === 'all') {
|
||||
finalParams.audit_status = undefined;
|
||||
}
|
||||
|
||||
const response = await fetchTenantsForAudit(finalParams);
|
||||
const transformedData = response.data.map(transformTenantData);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_ENTERPRISES',
|
||||
payload: {
|
||||
data: transformedData,
|
||||
pagination: {
|
||||
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 for audit:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '加载企业审核数据失败';
|
||||
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
}, []); // 移除所有依赖,使用参数传递状态变化
|
||||
|
||||
// 首次加载数据 - 使用事件驱动,避免useEffect
|
||||
const initializeData = useCallback(() => {
|
||||
if (isFirstLoad.current) {
|
||||
isFirstLoad.current = false;
|
||||
loadEnterprises({ resetPage: true });
|
||||
}
|
||||
}, [loadEnterprises]);
|
||||
|
||||
// 页面加载时初始化数据
|
||||
useEffect(() => {
|
||||
initializeData();
|
||||
}, []); // 只在组件挂载时执行一次
|
||||
|
||||
// 计算统计数据
|
||||
const stats = useMemo(() => ({
|
||||
total: state.pagination.total,
|
||||
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 = 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 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 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 } });
|
||||
loadEnterprises({
|
||||
filters: state.filters,
|
||||
pagination: { page, size: state.pagination.size }
|
||||
});
|
||||
}, [loadEnterprises, state.filters, state.pagination.size, state.pagination.totalPages]);
|
||||
|
||||
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({ resetPage: true });
|
||||
toast.success('数据已刷新');
|
||||
}, [loadEnterprises]);
|
||||
|
||||
const handleViewDetail = (enterprise: Enterprise) => {
|
||||
dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
|
||||
dispatch({ type: 'SET_AUDIT_REASON', payload: '' });
|
||||
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const handleAuditReasonChange = (value: string) => {
|
||||
dispatch({ type: 'SET_AUDIT_REASON', payload: value });
|
||||
};
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (!state.selectedEnterprise) return;
|
||||
|
||||
try {
|
||||
dispatch({ type: 'SET_ACTION_LOADING', payload: true });
|
||||
|
||||
const updatedTenant = await auditTenant(state.selectedEnterprise.id, {
|
||||
audit_status: '已通过',
|
||||
audit_comment: state.auditReason || '审核通过',
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
const updatedEnterprise = transformTenantData(updatedTenant);
|
||||
dispatch({
|
||||
type: 'SET_ENTERPRISES',
|
||||
payload: {
|
||||
data: state.enterprises.map(ent =>
|
||||
ent.id === state.selectedEnterprise?.id ? updatedEnterprise : ent
|
||||
),
|
||||
pagination: state.pagination
|
||||
}
|
||||
});
|
||||
|
||||
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false });
|
||||
toast.success('审核通过');
|
||||
|
||||
// 立即刷新列表,无需延迟
|
||||
loadEnterprises({ resetPage: true });
|
||||
} catch (error) {
|
||||
console.error('Approve failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '审核通过失败';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
dispatch({ type: 'SET_ACTION_LOADING', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
const handleReject = async () => {
|
||||
if (!state.selectedEnterprise) return;
|
||||
if (!state.auditReason.trim()) {
|
||||
toast.error('请填写驳回原因');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
dispatch({ type: 'SET_ACTION_LOADING', payload: true });
|
||||
|
||||
const updatedTenant = await auditTenant(state.selectedEnterprise.id, {
|
||||
audit_status: '已驳回',
|
||||
audit_comment: state.auditReason,
|
||||
});
|
||||
|
||||
// 更新本地状态
|
||||
const updatedEnterprise = transformTenantData(updatedTenant);
|
||||
dispatch({
|
||||
type: 'SET_ENTERPRISES',
|
||||
payload: {
|
||||
data: state.enterprises.map(ent =>
|
||||
ent.id === state.selectedEnterprise?.id ? updatedEnterprise : ent
|
||||
),
|
||||
pagination: state.pagination
|
||||
}
|
||||
});
|
||||
|
||||
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false });
|
||||
toast.success('已驳回');
|
||||
|
||||
// 立即刷新列表,无需延迟
|
||||
loadEnterprises({ resetPage: true });
|
||||
} catch (error) {
|
||||
console.error('Reject failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '审核驳回失败';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
dispatch({ type: 'SET_ACTION_LOADING', payload: false });
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题和描述 */}
|
||||
<div>
|
||||
<h2 className="text-green-800">企业审核</h2>
|
||||
<p className="text-muted-foreground">管理企业注册与变更审核流程</p>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 - 保留原有功能 */}
|
||||
<AuditStatsCards
|
||||
enterprises={state.enterprises}
|
||||
loading={state.loading}
|
||||
/>
|
||||
|
||||
{/* 搜索、表格和分页 - 使用重构后的组件 */}
|
||||
<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]}
|
||||
/>
|
||||
|
||||
{/* 企业详情对话框 - 保留原有功能 */}
|
||||
<EnterpriseDetailDialog
|
||||
open={state.showDetailDialog}
|
||||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||||
enterprise={state.selectedEnterprise}
|
||||
auditReason={state.auditReason}
|
||||
onAuditReasonChange={handleAuditReasonChange}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
loading={state.actionLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user