@@ -1,525 +0,0 @@
/**
* filekorolheader: 审核历史 - 企业审核记录查询与详情查看页面
* 功能:审核历史查询、筛选过滤、详情查看、数据导出、分页控制
* 路径:/central-config/tenant/audit-history
* 规范: 遵循crop-x/docs/开发项目规范.md, 使用useReducer状态管理, API集成, shadcn语义化样式
*/
'use client';
import { useReducer, useEffect, useMemo } from 'react';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
History,
Download,
Search,
Eye,
AlertCircle,
ChevronLeft,
ChevronRight,
Calendar,
Clock,
User,
FileText,
Building2,
MapPin,
CreditCard,
Phone
} from 'lucide-react';
import { toast } from 'sonner';
import { auditHistoryReducer, initialState, AuditHistoryState, AuditHistoryAction } from './components/auditHistoryReducer';
import { fetchAuditLogs, transformAuditLogData, AuditLogsQueryParams } from './components/auditHistoryApi';
import { AuditRecord, FilterOptions, AuditStatus } from './types';
// Utility functions
const getStatusBadge = (status: 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>;
}
};
const getActionBadge = (action: string) => {
if (action === 'SUBMIT') {
return <Badge className="bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400 border-blue-200 dark:border-blue-800 font-light">提交</Badge>;
} else if (action === 'AUDIT') {
return <Badge className="bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400 border-purple-200 dark:border-purple-800 font-light">审核</Badge>;
}
return <Badge variant="outline" className="font-light">{action}</Badge>;
};
export default function AuditHistoryPage() {
const [state, dispatch] = useReducer(auditHistoryReducer, initialState);
// 加载审核历史数据
const loadAuditHistory = async (resetPage = false) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
const params: AuditLogsQueryParams = {
page: resetPage ? 1 : state.pagination.page,
size: state.pagination.size,
order_by: state.sortBy,
sort_order: state.sortOrder,
};
const response = await fetchAuditLogs(params);
const transformedData = response.data.map(transformAuditLogData);
console.log('Audit Logs API Response:', response);
console.log('Transformed Audit Data:', transformedData);
dispatch({
type: 'SET_RECORDS',
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 audit history:', error);
const errorMessage = error instanceof Error ? error.message : '加载审核历史失败';
dispatch({ type: 'SET_ERROR', payload: errorMessage });
toast.error(errorMessage);
}
};
// 初始加载
useEffect(() => {
loadAuditHistory(true);
}, [state.sortBy, state.sortOrder]);
// 分页加载
useEffect(() => {
if (state.pagination.page > 1) {
loadAuditHistory(false);
}
}, [state.pagination.page]);
// 计算统计数据 - 按照参考组件的顺序
const stats = useMemo(() => [
{
label: '审核总数',
value: state.records.length,
color: 'text-blue-600 dark:text-blue-400',
bg: 'bg-blue-50 dark:bg-blue-950',
borderColor: 'border-blue-200 dark:border-blue-800',
},
{
label: '已通过',
value: state.records.filter(r => r.result === 'approved').length,
color: 'text-green-600 dark:text-green-400',
bg: 'bg-green-50 dark:bg-green-950',
borderColor: 'border-green-200 dark:border-green-800',
},
{
label: '已驳回',
value: state.records.filter(r => r.result === 'rejected').length,
color: 'text-red-600 dark:text-red-400',
bg: 'bg-red-50 dark:bg-red-950',
borderColor: 'border-red-200 dark:border-red-800',
},
{
label: '待审核',
value: state.records.filter(r => r.result === 'pending').length,
color: 'text-yellow-600 dark:text-yellow-400',
bg: 'bg-yellow-50 dark:bg-yellow-950',
borderColor: 'border-yellow-200 dark:border-yellow-800',
},
], [state.records]);
// 筛选记录
const filteredRecords = useMemo(() => {
return state.records.filter(record => {
const matchKeyword = !state.filters.searchKeyword ||
record.enterpriseName.toLowerCase().includes(state.filters.searchKeyword.toLowerCase()) ||
record.changeSummary.toLowerCase().includes(state.filters.searchKeyword.toLowerCase());
const matchResult = state.filters.resultFilter === 'all' || record.result === state.filters.resultFilter;
const matchType = state.filters.typeFilter === 'all' || record.auditType === state.filters.typeFilter;
// 日期筛选
let matchDate = true;
if (state.filters.dateRange !== 'all' && record.actionTime) {
const auditDate = new Date(record.actionTime);
const now = new Date();
const diffDays = Math.floor((now.getTime() - auditDate.getTime()) / (1000 * 60 * 60 * 24));
switch (state.filters.dateRange) {
case 'today':
matchDate = diffDays === 0;
break;
case 'week':
matchDate = diffDays <= 7;
break;
case 'month':
matchDate = diffDays <= 30;
break;
case 'quarter':
matchDate = diffDays <= 90;
break;
}
}
return matchKeyword && matchResult && matchType && matchDate;
});
}, [state.records, state.filters]);
// 事件处理器
const handleSearch = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { searchKeyword: value } });
};
const handleResultFilter = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { resultFilter: value } });
};
const handleTypeFilter = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { typeFilter: value } });
};
const handleDateFilter = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { dateRange: 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 handleViewDetail = (record: AuditRecord) => {
dispatch({ type: 'SET_SELECTED_RECORD', payload: record });
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
};
const handleExport = () => {
const exportData = filteredRecords.map(record => ({
企业名称: record.enterpriseName,
操作类型: record.action === 'SUBMIT' ? '提交' : '审核',
审核类型: record.auditType === 'register' ? '注册' : '更新',
操作时间: record.actionTime,
操作人: record.actionBy,
审核结果: record.result === 'approved' ? '审核通过' :
record.result === 'rejected' ? '审核拒绝' :
record.result === 'pending' ? '待审核' : '草稿',
变更摘要: record.changeSummary,
}));
const dataStr = JSON.stringify(exportData, null, 2);
const dataBlob = new Blob([dataStr], { type: 'application/json' });
const url = URL.createObjectURL(dataBlob);
const link = document.createElement('a');
link.href = url;
link.download = `audit_history_${new Date().getTime()}.json`;
link.click();
toast.success('审核历史数据导出成功');
};
return (
<div className="space-y-6">
{/* Page Header */}
<Card className="p-6 bg-gradient-to-r from-purple-50 dark:from-purple-950 to-pink-50 dark:to-pink-950 border-purple-200 dark:border-purple-800">
<div className="flex items-start justify-between">
<div className="flex items-start gap-3">
<History className="w-6 h-6 text-purple-600 dark:text-purple-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">
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
<Search className="w-3 h-3 mr-1" />
智能查询
</Badge>
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
<Calendar className="w-3 h-3 mr-1" />
时间筛选
</Badge>
<Badge variant="outline" className="bg-white dark:bg-gray-800 font-light">
<FileText className="w-3 h-3 mr-1" />
详情查看
</Badge>
</div>
</div>
</div>
<div className="flex items-center gap-2">
<Button onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
导出记录
</Button>
</div>
</div>
</Card>
{/* Statistics Cards */}
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
{stats.map((stat, index) => (
<Card key={index} className={`p-6 bg-card hover:bg-muted transition-colors ${stat.borderColor} border-2`}>
<div className="flex items-center justify-between mb-2">
<div className="text-sm text-muted-foreground">{stat.label}</div>
<History className="w-5 h-5 text-purple-500" />
</div>
<div className={`text-3xl font-bold mb-1 ${stat.color}`}>{stat.value}</div>
<div className="text-xs text-muted-foreground">
条记录
</div>
</Card>
))}
</div>
{/* Filters */}
<Card className="p-6 bg-card">
<div className="flex flex-col lg:flex-row gap-4 mb-4">
<div className="flex-1">
<Label className="text-sm">搜索关键词</Label>
<div className="relative mt-2">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索企业名称、变更摘要..."
value={state.filters.searchKeyword}
onChange={(e) => handleSearch(e.target.value)}
className="pl-10"
/>
</div>
</div>
<div className="flex gap-4">
<div>
<Label className="text-sm">全部类型</Label>
<Select value={state.filters.typeFilter} onValueChange={handleTypeFilter}>
<SelectTrigger className="w-32 mt-2">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部类型</SelectItem>
<SelectItem value="register">注册审核</SelectItem>
<SelectItem value="update">变更审核</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label className="text-sm">全部结果</Label>
<Select value={state.filters.resultFilter} onValueChange={handleResultFilter}>
<SelectTrigger className="w-32 mt-2">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all">全部结果</SelectItem>
<SelectItem value="pending">待审核</SelectItem>
<SelectItem value="approved">已通过</SelectItem>
<SelectItem value="rejected">已驳回</SelectItem>
<SelectItem value="draft">草稿</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</Card>
{/* Time Range Filter */}
<Card className="p-4 bg-card">
<Label className="text-sm text-muted-foreground mb-2 block">时间范围</Label>
<div className="flex gap-2">
{[
{ value: 'all', label: '全部' },
{ value: 'today', label: '今天' },
{ value: 'week', label: '近7天' },
{ value: 'month', label: '近30天' },
{ value: 'quarter', label: '近90天' },
].map((option) => (
<Button
key={option.value}
variant={state.filters.dateRange === option.value ? 'default' : 'outline'}
size="sm"
onClick={() => handleDateFilter(option.value)}
>
{option.label}
</Button>
))}
</div>
</Card>
{/* Error Display */}
{state.error && (
<div className="mb-4 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">
<AlertCircle className="w-4 h-4" />
<span>{state.error}</span>
</div>
</div>
)}
{/* Loading State */}
{state.loading && (
<div className="text-center py-12 text-muted-foreground">
<RefreshCw className="w-8 h-8 mx-auto mb-2 animate-spin" />
<p>加载中...</p>
</div>
)}
{/* Data Table */}
{!state.loading && !state.error && (
<>
<div className="border rounded-lg overflow-hidden">
<Table>
<TableHeader>
<TableRow>
<TableHead
className="cursor-pointer hover:bg-muted"
onClick={() => handleSort('enterprise_name')}
>
企业名称
{state.sortBy === 'enterprise_name' && (
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted"
onClick={() => handleSort('action')}
>
操作类型
{state.sortBy === 'action' && (
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</TableHead>
<TableHead>操作人</TableHead>
<TableHead
className="cursor-pointer hover:bg-muted"
onClick={() => handleSort('action_time')}
>
操作时间
{state.sortBy === 'action_time' && (
<span className="ml-1">{state.sortOrder === 'asc' ? '↑' : '↓'}</span>
)}
</TableHead>
<TableHead>审核结果</TableHead>
<TableHead>变更摘要</TableHead>
<TableHead>操作</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredRecords.map((record) => (
<TableRow key={record.id}>
<TableCell>
<div className="flex items-center gap-2">
<Building2 className="w-4 h-4 text-purple-500" />
<span className="font-medium">{record.enterpriseName}</span>
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-2">
{getActionBadge(record.action)}
{record.auditType === 'register' && (
<Badge variant="outline" className="font-light">注册</Badge>
)}
{record.auditType === 'update' && (
<Badge variant="outline" className="font-light">更新</Badge>
)}
</div>
</TableCell>
<TableCell>
<div className="flex items-center gap-1">
<User className="w-4 h-4 text-gray-500" />
<span className="text-sm">{record.actionBy}</span>
</div>
</TableCell>
<TableCell className="text-sm">{record.actionTime}</TableCell>
<TableCell>{getStatusBadge(record.result)}</TableCell>
<TableCell className="text-sm max-w-xs truncate" title={record.changeSummary}>
{record.changeSummary}
</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => handleViewDetail(record)}
>
<Eye className="w-3 h-3 mr-1" />
查看
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</div>
{filteredRecords.length === 0 && (
<div className="text-center py-12 text-muted-foreground">
<History className="w-12 h-12 mx-auto mb-4 opacity-20" />
<p>暂无审核历史记录</p>
</div>
)}
{/* Pagination */}
{state.pagination.totalPages > 1 && (
<div className="flex items-center justify-between mt-4">
<div className="text-sm text-muted-foreground">
显示第 {state.pagination.page} 页,共 {state.pagination.totalPages} 页
<span className="ml-2">总计 {state.pagination.total} 条记录</span>
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(state.pagination.page - 1)}
disabled={!state.pagination.hasPrev || state.loading}
>
<ChevronLeft className="w-4 h-4" />
上一页
</Button>
<span className="text-sm font-medium px-2">
{state.pagination.page} / {state.pagination.totalPages}
</span>
<Button
variant="outline"
size="sm"
onClick={() => handlePageChange(state.pagination.page + 1)}
disabled={!state.pagination.hasNext || state.loading}
>
下一页
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
)}
</>
)}
</Card>