Files
smart-crop-ui/crop-x/src/app/(app)/central-config/tenant/enterprise-audit/page.tsx

356 lines
11 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* filekorolheader: 企业审核页面 - 企业注册审核管理页面
* 功能:企业审核列表、搜索筛选、审核操作、详情查看
* 路径:/central-config/tenant/enterprise-audit
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理API集成模块化组件
*/
'use client';
import { useReducer, useEffect, useMemo } from 'react';
import { toast } from 'sonner';
import { Building2, RefreshCw } from 'lucide-react';
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 {
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 loadEnterprises = async (resetPage = false) => {
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 response = await fetchTenantsForAudit(params);
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(() => {
loadEnterprises(true);
}, [state.filters.search, state.filters.audit_status, state.sortBy, state.sortOrder]);
// 分页加载
useEffect(() => {
if (state.pagination.page > 1) {
loadEnterprises(false);
}
}, [state.pagination.page]);
// 计算统计数据
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,
}), [state.enterprises, state.pagination.total]);
// 事件处理器
const handleSearch = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { search: value } });
};
const handleAuditStatusFilter = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { audit_status: value === 'all' ? 'all' : value } });
};
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) => {
// 边界检查,确保页码在有效范围内
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 } });
};
const handleRefresh = () => {
dispatch({ type: 'REFRESH_DATA' });
loadEnterprises(true);
toast.success('数据已刷新');
};
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('审核通过');
// 1秒后刷新列表
setTimeout(() => {
loadEnterprises(true);
}, 1000);
} 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('已驳回');
// 1秒后刷新列表
setTimeout(() => {
loadEnterprises(true);
}, 1000);
} 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}
/>
{/* 搜索和筛选 */}
<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}
loading={state.loading}
/>
{/* 企业列表 */}
<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 })}
onApprove={handleApprove}
onReject={handleReject}
loading={state.actionLoading}
/>
</div>
);
}