Files
smart-cropx-ui/src/app/(app)/central-config/tenant/audit-history/page.tsx
peng dfc29ce01f fix: 修复系统模块TypeScript类型错误和组件功能问题
- 修复消息组件JSX.Element类型错误,改为React.ReactNode
- 完善审核历史页面类型定义和API接口调用
- 优化验证码组件,移除备用验证码逻辑避免无限循环
- 简化系统设置页面,仅保留基本设置和外观设置
- 修复用户管理页面编辑模态框数据加载和CRUD操作
- 移除废弃的作物推荐组件文件

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-11-12 17:28:11 +08:00

716 lines
26 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/audit-history
* 规范遵循crop-x-new/docs/开发项目规范.md使用SearchFormPagination组件API集成shadcn语义化样式
*/
'use client';
import React, { useState, useCallback, useEffect ,useRef} from 'react';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Label } from '@/components/ui/label';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import {
FileText,
AlertCircle,
RefreshCw,
Building,
User,
Smartphone,
Search
} from 'lucide-react';
import { toast } from 'sonner';
import SearchFormPagination, {
type SearchFieldConfig,
type TableColumnConfig
} from '@/components/common/searchFormPagination';
import { fetchAuditLogs, transformAuditLogData, AuditLogsQueryParams, AuditRecord, AuditLogData } from './components/auditHistoryApi';
// URL参数类型定义
interface UrlParams {
search?: string;
action?: string;
audit_status?: string;
date_range?: string;
page?: number;
size?: number;
}
// 分页状态类型定义
interface PaginationState {
page: number;
size: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
// Utility functions
const getActionBadge = (action: string) => {
switch (action) {
case 'SUBMIT':
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border border-blue-200 dark:border-blue-800">
</div>
);
case 'AUDIT':
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400 border border-orange-200 dark:border-orange-800">
</div>
);
default:
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800">
{action}
</div>
);
}
};
const getResultBadge = (result: string) => {
switch (result) {
case 'approved':
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border border-green-200 dark:border-green-800">
</div>
);
case 'rejected':
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800">
</div>
);
case 'pending':
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800">
</div>
);
case 'draft':
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-800">
稿
</div>
);
default:
return (
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800">
{result}
</div>
);
}
};
export default function AuditHistoryPage() {
// 对话框状态管理
const [dialogs, setDialogs] = useState({
showViewDialog: false,
selectedRecord: null as AuditRecord | null
});
const dispatch = (action: any) => {
switch (action.type) {
case 'SET_SELECTED_RECORD':
setDialogs(prev => ({ ...prev, selectedRecord: action.payload }));
break;
case 'TOGGLE_VIEW_DIALOG':
setDialogs(prev => ({ ...prev, showViewDialog: action.payload }));
break;
case 'RESET_FORM_DATA':
setDialogs(prev => ({ ...prev, selectedRecord: null }));
break;
}
};
// 搜索字段配置
const searchFields: SearchFieldConfig[] = [
{
key: 'search',
label: '搜索',
type: 'text',
placeholder: '搜索企业名称、变更摘要...',
},
{
key: 'action',
label: '审核类型',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部类型' },
{ value: 'SUBMIT', label: '提交审核' },
{ value: 'AUDIT', label: '审核操作' },
],
},
{
key: 'audit_status',
label: '审核结果',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部结果' },
{ value: 'approved', label: '已通过' },
{ value: 'rejected', label: '已拒绝' },
{ value: 'pending', label: '待审核' },
{ value: 'draft', label: '草稿' },
],
},
{
key: 'date_range',
label: '时间范围',
type: 'select',
defaultValue: 'all',
options: [
{ value: 'all', label: '全部' },
{ value: 'today', label: '今天' },
{ value: 'week', label: '近7天' },
{ value: 'month', label: '近30天' },
{ value: 'quarter', label: '近90天' },
],
},
];
// 表格列配置
const columns: TableColumnConfig[] = [
{
key: 'enterpriseName',
label: '企业名称',
render: (value: string) => (
<div className="flex items-center gap-2">
<Building className="w-4 h-4 text-blue-500" />
<span className="font-medium">{value}</span>
</div>
),
},
{
key: 'action',
label: '审核类型',
render: (value: string) => getActionBadge(value),
},
{
key: 'submitTime',
label: '提交时间',
render: (value: string, record: AuditLogData) => (
<div className="text-sm text-muted-foreground">
{record.action === 'SUBMIT' ? value : '-'}
</div>
),
},
{
key: 'actionTime',
label: '审核时间',
render: (value: string, record: AuditLogData) => (
<div className="text-sm text-muted-foreground">
{record.action === 'AUDIT' ? value : '-'}
</div>
),
},
{
key: 'actionBy',
label: '审核人',
render: (value: string) => value ? (
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-500" />
<span>{value}</span>
</div>
) : '-',
},
{
key: 'result',
label: '审核结果',
render: (value: string) => getResultBadge(value),
},
{
key: 'actions',
label: '操作',
render: (_: any, row: AuditLogData) => (
<Button
size="sm"
variant="outline"
onClick={() => handleView(row)}
>
<FileText className="w-3 h-3 mr-1" />
</Button>
),
},
];
// 简化的状态管理 - 只需要存储数据和加载状态
const [records, setRecords] = useState<AuditRecord[]>([]);
const [pagination, setPagination] = useState<PaginationState>({
page: 1,
size: 10,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
});
const [loading, setLoading] = useState(false);
const [error, setError] = useState<string | null>(null);
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({
search: '',
action: 'all',
audit_status: 'all',
date_range: 'all'
});
// 数据加载函数 - 优先从浏览器URL参数读取
const loadAuditHistory = useCallback(async (options: {
resetPage?: boolean;
filters?: Record<string, string>;
sortBy?: string;
sortOrder?: 'asc' | 'desc';
page?: number;
size?: number;
} = {}) => {
try {
// 优先从URL读取参数
let urlParams: UrlParams = {};
if (typeof window !== 'undefined') {
const params = new URLSearchParams(window.location.search);
urlParams = {
search: params.get('search') || undefined,
action: params.get('action') || undefined,
audit_status: params.get('audit_status') || undefined,
date_range: params.get('date_range') || undefined,
page: params.get('page') ? parseInt(params.get('page')!, 10) : undefined,
size: params.get('size') ? parseInt(params.get('size')!, 10) : undefined
};
console.log('从URL读取的参数:', urlParams);
}
console.log('========================================');
setLoading(true);
setError(null);
// 解构选项参数,提供默认值
const {
resetPage = false,
filters,
sortBy,
sortOrder,
page,
size
} = options;
// 优先级URL参数 > 传入参数 > 父组件状态
const finalPage = resetPage ? 1 : (urlParams.page || page || pagination.page);
const finalSize = urlParams.size || size || pagination.size;
const params: AuditLogsQueryParams = {
page: finalPage,
size: finalSize,
};
// 使用正确的优先级URL参数 > 传入参数 > 父组件状态
const currentFilters = {
search: urlParams.search || (filters?.search) || searchFilters.search,
action: urlParams.action || (filters?.action) || searchFilters.action,
audit_status: urlParams.audit_status || (filters?.audit_status) || searchFilters.audit_status,
date_range: urlParams.date_range || (filters?.date_range) || searchFilters.date_range
};
const currentSortBy = sortBy || 'created_at';
const currentSortOrder = sortOrder || 'desc';
// 添加搜索条件
if (currentFilters.search) {
params.search_keyword = currentFilters.search;
}
// 添加排序条件
if (currentSortBy) {
params.order_by = currentSortBy;
params.sort_order = currentSortOrder;
}
if (currentFilters.action && currentFilters.action !== 'all') {
params.action = currentFilters.action;
}
if (currentFilters.audit_status && currentFilters.audit_status !== 'all') {
params.audit_status = currentFilters.audit_status;
}
if (currentFilters.date_range && currentFilters.date_range !== 'all') {
params.date_range = currentFilters.date_range;
}
if (currentSortBy) {
params.order_by = currentSortBy;
params.sort_order = currentSortOrder;
}
console.log('=== 审核历史页面 - 最终API参数 ===');
console.log('API调用参数 params:', params);
console.log('参数优先级正确: URL参数 > 函数传递参数 > 父组件状态');
console.log('当前currentFilters:', currentFilters);
console.log('==================================');
const response = await fetchAuditLogs(params);
const transformedData = response.data.map(transformAuditLogData);
setRecords(transformedData);
setPagination({
page: response.page,
size: response.size,
total: response.total,
totalPages: response.total_pages,
hasNext: response.has_next,
hasPrev: response.has_prev,
});
} catch (err) {
const errorMessage = err instanceof Error ? err.message : '加载审核历史失败';
setError(errorMessage);
toast.error(errorMessage);
} finally {
setLoading(false);
}
}, []); // 移除依赖项,通过参数传递
const didFetchRef = useRef(false)
useEffect(() => {
if (didFetchRef.current) return
didFetchRef.current = true
loadAuditHistory()
}, [])
// 搜索处理 - 保持传统的简洁方式
const handleSearch = useCallback((filters: Record<string, string>) => {
console.log('审核历史页面 - 收到搜索条件:', filters);
// 更新过滤器状态
setSearchFilters(filters);
// 搜索时重置到第1页
setPagination(prev => ({ ...prev, page: 1 }));
// 执行查询
loadAuditHistory({
resetPage: true,
page: 1,
filters: filters,
size: pagination.size
});
console.log('触发审核历史查询 - 参数:', {
resetPage: true,
page: 1,
filters: filters,
size: pagination.size
});
}, [pagination.size, loadAuditHistory]);
// 排序处理
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
// 排序时重置到第一页
setPagination(prev => ({ ...prev, page: 1 }));
loadAuditHistory({
resetPage: true,
page: 1,
filters: searchFilters,
sortBy,
sortOrder,
size: pagination.size
});
}, [searchFilters, pagination.size, loadAuditHistory]);
// 分页处理
const handlePageChange = useCallback((page: number) => {
if (page < 1) {
page = 1;
} else if (page > pagination.totalPages && pagination.totalPages > 0) {
page = pagination.totalPages;
}
setPagination(prev => ({ ...prev, page }));
loadAuditHistory({
page,
filters: searchFilters,
size: pagination.size
});
}, [searchFilters, pagination.size, pagination.totalPages, loadAuditHistory]);
// 每页条数变化处理
const handleSizeChange = useCallback((size: number) => {
setPagination(prev => ({ ...prev, size, page: 1 }));
loadAuditHistory({
resetPage: true,
page: 1,
size,
filters: searchFilters
});
}, [searchFilters, loadAuditHistory]);
// URL状态变化处理 - 处理浏览器前进后退时的参数恢复
const handleUrlStateChange = useCallback((urlState: {
filters: Record<string, string>;
pagination: { page: number; size: number };
}) => {
console.log('审核历史页面 - URL状态变化:', urlState);
// 更新内部状态
setSearchFilters(urlState.filters);
setPagination(prev => ({
...prev,
page: urlState.pagination.page,
size: urlState.pagination.size
}));
// 触发数据加载
loadAuditHistory({
page: urlState.pagination.page,
size: urlState.pagination.size,
filters: urlState.filters
});
}, [loadAuditHistory]);
// 业务事件处理器
const handleView = (record: AuditLogData) => {
dispatch({ type: 'SET_SELECTED_RECORD', payload: record });
dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: true });
};
return (
<div className="space-y-6">
{/* Page Header - 自定义页面头部 */}
<Card className="p-6 bg-gradient-to-r from-green-50 dark:from-green-950 to-emerald-50 dark:to-emerald-950 border-green-200 dark:border-green-800">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<FileText className="w-6 h-6 text-green-600 dark:text-green-400 flex-shrink-0 mt-1" />
<div className="flex-1">
<h2 className="mb-2"></h2>
<p className="text-sm text-muted-foreground mb-3">
</p>
<div className="flex flex-wrap gap-2">
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800">
<Search className="w-3 h-3 mr-1" />
</div>
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800">
<AlertCircle className="w-3 h-3 mr-1" />
</div>
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800">
<FileText className="w-3 h-3 mr-1" />
</div>
</div>
</div>
</div>
</div>
</Card>
{/* 使用SearchFormPagination组件 */}
{React.createElement(SearchFormPagination as any, {
formTitle: "审核历史记录",
searchFields,
columns,
data: records,
loading,
error,
pagination: pagination as any,
onPageChange: handlePageChange,
onSizeChange: handleSizeChange,
onSearch: handleSearch,
onSort: handleSort,
emptyIcon: <FileText className="w-12 h-12 mx-auto mb-4 opacity-20" />,
emptyText: "暂无审核记录",
sizeOptions: [10, 20, 50, 100]
})}
{/* View Audit Record Details Dialog */}
<Dialog open={dialogs.showViewDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
<DialogContent className="w-[90vw] max-w-6xl max-h-[90vh]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{dialogs.selectedRecord && (
<ScrollArea className="max-h-[calc(90vh-200px)]">
<Tabs defaultValue="basic" className="space-y-4">
<TabsList className="grid grid-cols-3 w-full">
<TabsTrigger value="basic">
<FileText className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="snapshot">
<Building className="w-4 h-4 mr-2" />
</TabsTrigger>
<TabsTrigger value="system">
<Smartphone className="w-4 h-4 mr-2" />
</TabsTrigger>
</TabsList>
{/* 基本信息 */}
<TabsContent value="basic" className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.enterpriseName}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{getActionBadge(dialogs.selectedRecord.action)}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">
{dialogs.selectedRecord.action === 'SUBMIT' ? dialogs.selectedRecord.submitTime : '-'}
</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">
{dialogs.selectedRecord.action === 'AUDIT' ? dialogs.selectedRecord.actionTime : '-'}
</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.actionBy || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{getResultBadge(dialogs.selectedRecord.result)}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md min-h-[80px] whitespace-pre-wrap">
{dialogs.selectedRecord.changeSummary || '-'}
</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md min-h-[80px] whitespace-pre-wrap">
{dialogs.selectedRecord.auditComment || '-'}
</div>
</div>
</div>
</TabsContent>
{/* 企业快照 */}
<TabsContent value="snapshot" className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.companyType || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">
{dialogs.selectedRecord.snapshot?.province} {dialogs.selectedRecord.snapshot?.city}
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.detailedAddress || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.registrant || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.contactPhone || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.companyScale || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.registeredCapital || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">
<code className="text-sm">{dialogs.selectedRecord.snapshot?.socialCreditCode || '-'}</code>
</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.legalPersonName || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">
<code className="text-sm">{dialogs.selectedRecord.snapshot?.bankAccount || '-'}</code>
</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.bankName || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.bankFullName || '-'}</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.snapshot?.bankAddress || '-'}</div>
</div>
</div>
</TabsContent>
{/* 系统信息 */}
<TabsContent value="system" className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<Label>ID</Label>
<div className="field-value p-3 bg-muted rounded-md">
<code className="text-sm">{dialogs.selectedRecord.id}</code>
</div>
</div>
<div>
<Label>ID</Label>
<div className="field-value p-3 bg-muted rounded-md">
<code className="text-sm">{dialogs.selectedRecord.enterpriseId || '-'}</code>
</div>
</div>
<div>
<Label>IP地址</Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.ipAddress || '-'}</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md text-sm">
{dialogs.selectedRecord.userAgent || '-'}
</div>
</div>
<div>
<Label>ID</Label>
<div className="field-value p-3 bg-muted rounded-md">
<code className="text-sm">{dialogs.selectedRecord.requestId || '-'}</code>
</div>
</div>
<div>
<Label></Label>
<div className="field-value p-3 bg-muted rounded-md">{dialogs.selectedRecord.createdAt}</div>
</div>
</div>
</TabsContent>
</Tabs>
</ScrollArea>
)}
<DialogFooter>
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: false })}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div>
);
}