生产管理系统 - 企业审核与审核历史联调

This commit is contained in:
2025-11-03 22:30:49 +08:00
parent c690d50baa
commit 394e6d8342
13 changed files with 2580 additions and 582 deletions

View File

@@ -0,0 +1,240 @@
/**
* filekorolheader: 审核历史API接口 - 企业审核记录查询接口服务
* 功能API请求封装、数据转换、错误处理、分页查询
* 路径:/central-config/tenant/audit-history/components/auditHistoryApi
* 规范遵循crop-x/docs/开发项目规范.md使用SDK API调用TypeScript类型安全
*/
// API响应数据类型定义
import { getAuthToken } from "@/utils/token.ts";
import {
getTenantAuditLogsApiV1TenantsAuditLogsGet,
} from "@/lib/api/sdk.gen";
export interface AuditLogData {
id: string;
action: string;
action_by: string;
action_time: string;
snapshot_company_name: string;
snapshot_company_type: string | null;
snapshot_province: string | null;
snapshot_city: string | null;
snapshot_district: string | null;
snapshot_detailed_address: string | null;
snapshot_registrant: string | null;
snapshot_contact_phone: string | null;
snapshot_bank_account: string | null;
snapshot_bank_name: string | null;
snapshot_bank_full_name: string | null;
snapshot_bank_address: string | null;
snapshot_social_credit_code: string | null;
snapshot_legal_person_name: string | null;
snapshot_audit_status: string;
snapshot_audit_comment: string | null;
snapshot_company_scale: string | null;
snapshot_registered_capital: string | null;
change_summary: string;
ip_address: string | null;
user_agent: string | null;
request_id: string | null;
created_at: string;
}
// API响应接口
export interface AuditLogsApiResponse {
data: AuditLogData[];
total: number;
page: number;
size: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
}
// 查询参数接口
export interface AuditLogsQueryParams {
tenant_id?: string;
page?: number;
size?: number;
order_by?: string;
sort_order?: 'asc' | 'desc';
}
// 审核记录页面数据类型(转换后的)
export interface AuditRecord {
id: string;
enterpriseId?: string;
enterpriseName: string;
action: 'SUBMIT' | 'AUDIT';
auditType: 'register' | 'update';
submitTime: string;
actionTime: string;
actionBy: string;
result: 'pending' | 'approved' | 'rejected' | 'draft';
auditStatus: string;
auditComment?: string;
changeSummary: string;
ipAddress?: string;
userAgent?: string;
requestId?: string;
createdAt: string;
// 快照数据
snapshot: {
companyName: string;
companyType: string | null;
province: string | null;
city: string | null;
district: string | null;
detailedAddress: string | null;
registrant: string | null;
contactPhone: string | null;
bankAccount: string | null;
bankName: string | null;
bankFullName: string | null;
bankAddress: string | null;
socialCreditCode: string | null;
legalPersonName: string | null;
auditStatus: string;
auditComment: string | null;
companyScale: string | null;
registeredCapital: string | null;
};
}
/**
* 获取审核历史记录数据
*/
export async function fetchAuditLogs(params: AuditLogsQueryParams = {}): Promise<AuditLogsApiResponse> {
try {
// 构建查询参数对象
const queryParams: any = {};
queryParams.tenant_id = "";
if (params.page) queryParams.page = params.page;
if (params.size) queryParams.size = params.size;
if (params.order_by) queryParams.order_by = params.order_by;
if (params.sort_order) queryParams.sort_order = params.sort_order;
// 默认参数
if (!params.page) queryParams.page = 1;
if (!params.size) queryParams.size = 10;
// 使用SDK API调用审核历史查询接口添加缓存破坏器和认证头部
const token = getAuthToken();
const response = await getTenantAuditLogsApiV1TenantsAuditLogsGet({
query: {
...queryParams,
// 添加时间戳防止缓存
_t: Date.now(),
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
}
const data = response.data as any;
// 转换响应数据格式以匹配现有的接口
return {
data: data?.data || [],
total: data?.total || 0,
page: data?.page || 1,
size: data?.size || 10,
total_pages: data?.total_pages || 0,
has_next: data?.has_next || false,
has_prev: data?.has_prev || false,
};
} catch (error) {
console.error('Failed to fetch audit logs:', error);
throw error;
}
}
/**
* 将API数据转换为页面所需的审核记录格式
*/
export function transformAuditLogData(log: AuditLogData): AuditRecord {
// 判断审核类型
let auditType: 'register' | 'update' = 'register';
if (log.change_summary.includes('更新') || log.change_summary.includes('修改')) {
auditType = 'update';
}
// 映射审核状态
let result: 'pending' | 'approved' | 'rejected' | 'draft' = 'pending';
const status = log.snapshot_audit_status.toLowerCase();
if (status.includes('草稿') || status.includes('draft')) {
result = 'draft';
} else if (status.includes('待审核') || status.includes('pending')) {
result = 'pending';
} else if (status.includes('已通过') || status.includes('approved')) {
result = 'approved';
} else if (status.includes('已拒绝') || status.includes('rejected')) {
result = 'rejected';
}
return {
id: log.id,
enterpriseId: log.action_by, // 使用操作人作为企业ID关联
enterpriseName: log.snapshot_company_name,
action: log.action as 'SUBMIT' | 'AUDIT',
auditType,
submitTime: formatDate(log.action_time),
actionTime: formatDate(log.action_time),
actionBy: log.action_by,
result,
auditStatus: log.snapshot_audit_status,
auditComment: log.snapshot_audit_comment,
changeSummary: log.change_summary,
ipAddress: log.ip_address,
userAgent: log.user_agent,
requestId: log.request_id,
createdAt: formatDate(log.created_at),
// 快照数据
snapshot: {
companyName: log.snapshot_company_name,
companyType: log.snapshot_company_type,
province: log.snapshot_province,
city: log.snapshot_city,
district: log.snapshot_district,
detailedAddress: log.snapshot_detailed_address,
registrant: log.snapshot_registrant,
contactPhone: log.snapshot_contact_phone,
bankAccount: log.snapshot_bank_account,
bankName: log.snapshot_bank_name,
bankFullName: log.snapshot_bank_full_name,
bankAddress: log.snapshot_bank_address,
socialCreditCode: log.snapshot_social_credit_code,
legalPersonName: log.snapshot_legal_person_name,
auditStatus: log.snapshot_audit_status,
auditComment: log.snapshot_audit_comment,
companyScale: log.snapshot_company_scale,
registeredCapital: log.snapshot_registered_capital,
},
};
}
/**
* 格式化日期
*/
function formatDate(dateString: string): string {
try {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).replace(/\//g, '-');
} catch (error) {
return dateString;
}
}

View File

@@ -0,0 +1,125 @@
/**
* filekorolheader: 审核历史状态管理 - 审核记录数据状态管理核心
* 功能API数据管理、分页状态、加载状态、错误处理、筛选状态
* 路径:/central-config/tenant/audit-history/components/auditHistoryReducer
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理模式
*/
import { AuditRecord } from '../types';
export interface FilterOptions {
searchKeyword: string;
resultFilter: string;
typeFilter: string;
dateRange: string;
}
export interface AuditHistoryState {
records: AuditRecord[];
loading: boolean;
error: string | null;
pagination: {
page: number;
size: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
filters: FilterOptions;
showDetailDialog: boolean;
selectedRecord: AuditRecord | null;
sortBy?: string;
sortOrder: 'asc' | 'desc';
}
export type AuditHistoryAction =
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'SET_RECORDS'; payload: { data: AuditRecord[]; pagination: AuditHistoryState['pagination'] } }
| { type: 'SET_FILTERS'; payload: Partial<FilterOptions> }
| { type: 'SET_PAGINATION'; payload: Partial<AuditHistoryState['pagination']> }
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
| { type: 'SET_SELECTED_RECORD'; payload: AuditRecord | null }
| { type: 'REFRESH_DATA' };
// 初始状态
export const initialState: AuditHistoryState = {
records: [],
loading: false,
error: null,
pagination: {
page: 1,
size: 10,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
},
filters: {
searchKeyword: '',
resultFilter: 'all',
typeFilter: 'all',
dateRange: 'all',
},
showDetailDialog: false,
selectedRecord: null,
sortBy: undefined,
sortOrder: 'desc',
};
// Reducer
export function auditHistoryReducer(state: AuditHistoryState, action: AuditHistoryAction): AuditHistoryState {
switch (action.type) {
case 'SET_LOADING':
return { ...state, loading: action.payload };
case 'SET_ERROR':
return { ...state, error: action.payload, loading: false };
case 'SET_RECORDS':
return {
...state,
records: action.payload.data,
pagination: action.payload.pagination,
loading: false,
error: null,
};
case 'SET_FILTERS':
return {
...state,
filters: { ...state.filters, ...action.payload },
pagination: { ...state.pagination, page: 1 }, // 重置到第一页
};
case 'SET_PAGINATION':
return {
...state,
pagination: { ...state.pagination, ...action.payload },
};
case 'SET_SORT':
return {
...state,
sortBy: action.payload.sortBy,
sortOrder: action.payload.sortOrder,
};
case 'TOGGLE_DETAIL_DIALOG':
return { ...state, showDetailDialog: action.payload };
case 'SET_SELECTED_RECORD':
return { ...state, selectedRecord: action.payload };
case 'REFRESH_DATA':
return {
...state,
error: null, // 清除错误状态
};
default:
return state;
}
}

View File

@@ -1,211 +1,634 @@
/**
* filekorolheader: 审核历史页面 - 企业审核记录查询和管理页面
* 功能:审核历史记录查询、搜索筛选、详情查看、数据分析
* 路径:/central-config/tenant/audit-history
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理API集成shadcn语义化样式
*/
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useReducer, useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Download } from 'lucide-react'; import { Badge } from '@/components/ui/badge';
import { AuditHistoryStatsCards } from './components/AuditHistoryStatsCards'; import { Input } from '@/components/ui/input';
import { AuditHistoryFilters } from './components/AuditHistoryFilters'; import { Label } from '@/components/ui/label';
import { AuditHistoryList } from './components/AuditHistoryList'; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
import { AuditHistoryDetailDialog } from './components/AuditHistoryDetailDialog'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { AuditHistoryInstructions } from './components/AuditHistoryInstructions'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { AuditRecord, Enterprise, FilterOptions } from './types'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import {
History,
Search,
Calendar,
FileText,
AlertCircle,
RefreshCw,
ChevronLeft,
ChevronRight,
Building,
User,
CreditCard,
Smartphone
} from 'lucide-react';
import { fetchAuditLogs, transformAuditLogData, AuditLogsQueryParams, AuditLogData } from './components/auditHistoryApi';
// 审核历史状态管理
interface AuditHistoryState {
records: AuditLogData[];
loading: boolean;
error: string | null;
pagination: {
page: number;
size: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
};
filters: {
search_keyword: string;
typeFilter: string;
resultFilter: string;
dateRange: string;
};
sortBy?: string;
sortOrder: 'asc' | 'desc';
selectedRecord: AuditLogData | null;
showDetailDialog: boolean;
}
type AuditHistoryAction =
| { type: 'SET_RECORDS'; payload: { data: AuditLogData[]; pagination: AuditHistoryState['pagination'] } }
| { type: 'SET_LOADING'; payload: boolean }
| { type: 'SET_ERROR'; payload: string | null }
| { type: 'SET_FILTERS'; payload: Partial<AuditHistoryState['filters']> }
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
| { type: 'SET_PAGINATION'; payload: Partial<AuditHistoryState['pagination']> }
| { type: 'SET_SELECTED_RECORD'; payload: AuditLogData | null }
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
| { type: 'REFRESH_DATA' };
const auditHistoryReducer = (state: AuditHistoryState, action: AuditHistoryAction): AuditHistoryState => {
switch (action.type) {
case 'SET_RECORDS':
return {
...state,
records: 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_RECORD':
return { ...state, selectedRecord: action.payload };
case 'TOGGLE_DETAIL_DIALOG':
return { ...state, showDetailDialog: !state.showDetailDialog };
case 'REFRESH_DATA':
return { ...state, error: null };
default:
return state;
}
};
const initialState: AuditHistoryState = {
records: [],
loading: false,
error: null,
pagination: {
page: 1,
size: 10,
total: 0,
totalPages: 0,
hasNext: false,
hasPrev: false,
},
filters: {
search_keyword: '',
typeFilter: 'all',
resultFilter: 'all',
dateRange: 'all',
},
sortBy: 'action_time',
sortOrder: 'desc',
selectedRecord: null,
showDetailDialog: false,
};
export default function AuditHistoryPage() { export default function AuditHistoryPage() {
const [records, setRecords] = useState<AuditRecord[]>([]); const [state, dispatch] = useReducer(auditHistoryReducer, initialState);
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
const [showDetailDialog, setShowDetailDialog] = useState(false);
const [selectedRecord, setSelectedRecord] = useState<AuditRecord | null>(null);
const [selectedEnterprise, setSelectedEnterprise] = useState<Enterprise | null>(null);
const [filters, setFilters] = useState<FilterOptions>({ // 加载审核历史数据
searchKeyword: '', const loadAuditHistory = async (resetPage = false) => {
resultFilter: 'all', try {
typeFilter: 'all', dispatch({ type: 'SET_LOADING', payload: true });
dateRange: 'all'
});
const params: AuditLogsQueryParams = {
search: state.filters.search_keyword || undefined,
page: resetPage ? 1 : state.pagination.page,
size: state.pagination.size
};
const response = await fetchAuditLogs(params);
const transformedRecords = response.data.map(transformAuditLogData);
dispatch({
type: 'SET_RECORDS',
payload: {
data: transformedRecords,
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);
dispatch({
type: 'SET_ERROR',
payload: error instanceof Error ? error.message : '加载审核历史失败'
});
}
};
// 搜索处理
const handleSearch = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { search_keyword: value } });
};
// 类型筛选
const handleTypeFilter = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { typeFilter: value } });
};
// 结果筛选
const handleResultFilter = (value: string) => {
dispatch({ type: 'SET_FILTERS', payload: { resultFilter: 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: AuditLogData) => {
dispatch({ type: 'SET_SELECTED_RECORD', payload: record });
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
};
// 刷新数据
const handleRefresh = () => {
dispatch({ type: 'REFRESH_DATA' });
loadAuditHistory(true);
toast.success('数据已刷新');
};
// 初始化和监听器
useEffect(() => { useEffect(() => {
loadEnterprises();
loadAuditHistory(); loadAuditHistory();
}, []); }, []);
const loadEnterprises = () => { useEffect(() => {
const data = localStorage.getItem('smart_agriculture_enterprises'); const timer = setTimeout(() => {
if (data) { loadAuditHistory();
setEnterprises(JSON.parse(data)); }, 300);
return () => clearTimeout(timer);
}, [state.filters.search_keyword, state.filters.typeFilter, state.filters.resultFilter, state.filters.dateRange, state.sortBy, state.sortOrder]);
useEffect(() => {
if (state.pagination.page > 1) {
loadAuditHistory();
}
}, [state.pagination.page]);
// 工具函数
const getActionBadge = (action: string) => {
switch (action) {
case 'register':
return <Badge className="bg-blue-100 text-blue-700"></Badge>;
case 'update':
return <Badge className="bg-orange-100 text-orange-700"></Badge>;
default:
return <Badge variant="outline">{action}</Badge>;
} }
}; };
const loadAuditHistory = () => { const getResultBadge = (result: string) => {
const data = localStorage.getItem('smart_agriculture_audit_records'); switch (result) {
if (data) { case 'approved':
setRecords(JSON.parse(data)); return <Badge className="bg-green-100 text-green-700"></Badge>;
} else { case 'rejected':
// 初始化审核历史数据 return <Badge className="bg-red-100 text-red-700"></Badge>;
const mockRecords: AuditRecord[] = [ case 'pending':
{ return <Badge className="bg-yellow-100 text-yellow-700"></Badge>;
id: 'audit-1', default:
enterpriseId: 'ent-2', return <Badge variant="outline">{result}</Badge>;
enterpriseName: '丰收现代农业集团',
auditType: 'register',
submitTime: '2024-10-05T10:00:00',
auditTime: '2024-10-08T14:30:00',
auditor: '系统管理员',
result: 'approved',
remarks: '企业资质完整,审核通过',
},
{
id: 'audit-2',
enterpriseId: 'ent-3',
enterpriseName: '金穗农机服务中心',
auditType: 'register',
submitTime: '2024-10-06T09:00:00',
auditTime: '2024-10-09T16:00:00',
auditor: '系统管理员',
result: 'rejected',
reason: '资质材料不完整,请补充营业执照副本',
remarks: '缺少必要的资质证明文件',
},
{
id: 'audit-3',
enterpriseId: 'ent-1',
enterpriseName: '绿野农业科技有限公司',
auditType: 'register',
submitTime: '2024-10-10T08:00:00',
result: 'pending',
},
{
id: 'audit-4',
enterpriseId: 'ent-2',
enterpriseName: '丰收现代农业集团',
auditType: 'update',
submitTime: '2024-10-12T15:30:00',
auditTime: '2024-10-13T10:00:00',
auditor: '系统管理员',
result: 'approved',
remarks: '企业地址变更审核通过',
},
{
id: 'audit-5',
enterpriseId: 'ent-4',
enterpriseName: '智慧农田科技公司',
auditType: 'register',
submitTime: '2024-09-28T11:00:00',
auditTime: '2024-09-30T09:30:00',
auditor: '系统管理员',
result: 'approved',
remarks: '优质企业,快速审核通过',
},
{
id: 'audit-6',
enterpriseId: 'ent-5',
enterpriseName: '农业机械租赁中心',
auditType: 'register',
submitTime: '2024-10-03T14:20:00',
auditTime: '2024-10-05T11:00:00',
auditor: '系统管理员',
result: 'rejected',
reason: '企业经营范围与平台业务不符',
remarks: '建议企业完善相关资质后重新申请',
},
];
localStorage.setItem('smart_agriculture_audit_records', JSON.stringify(mockRecords));
setRecords(mockRecords);
} }
}; };
const filteredRecords = records.filter(record => {
const matchKeyword = !filters.searchKeyword ||
record.enterpriseName.includes(filters.searchKeyword) ||
(record.auditor && record.auditor.includes(filters.searchKeyword));
const matchResult = filters.resultFilter === 'all' || record.result === filters.resultFilter;
const matchType = filters.typeFilter === 'all' || record.auditType === filters.typeFilter;
// 日期筛选
let matchDate = true;
if (filters.dateRange !== 'all' && record.auditTime) {
const auditDate = new Date(record.auditTime);
const now = new Date();
const diffDays = Math.floor((now.getTime() - auditDate.getTime()) / (1000 * 60 * 60 * 24));
switch (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;
});
const handleViewDetail = (record: AuditRecord) => {
setSelectedRecord(record);
// 查找对应的企业信息
const enterprise = enterprises.find(e => e.id === record.enterpriseId);
setSelectedEnterprise(enterprise || null);
setShowDetailDialog(true);
};
const handleExport = () => {
const dataStr = JSON.stringify(filteredRecords, 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
<div className="flex items-center justify-between"> {/* Page Header */}
<div> <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">
<h2 className="text-green-800"></h2> <div className="flex items-start justify-between">
<p className="text-muted-foreground"></p> <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 variant="outline" size="sm" onClick={handleRefresh} disabled={state.loading}>
<RefreshCw className={`w-4 h-4 mr-1 ${state.loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div> </div>
<Button onClick={handleExport}> </Card>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
{/* 统计卡片 */} {/* Filters */}
<AuditHistoryStatsCards records={records} /> <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.search_keyword}
onChange={(e) => handleSearch(e.target.value)}
className="pl-10"
/>
</div>
</div>
{/* 搜索和筛选 */} <div className="flex gap-4">
<AuditHistoryFilters <div>
filters={filters} <Label className="text-sm"></Label>
onFiltersChange={setFilters} <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>
<AuditHistoryList <Label className="text-sm"></Label>
records={filteredRecords} <Select value={state.filters.resultFilter} onValueChange={handleResultFilter}>
onViewDetail={handleViewDetail} <SelectTrigger className="w-32 mt-2">
/> <SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="approved"></SelectItem>
<SelectItem value="rejected"></SelectItem>
<SelectItem value="pending"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</Card>
{/* 详情对话框 */} {/* Time Range Filter */}
<AuditHistoryDetailDialog <Card className="p-4 bg-card">
record={selectedRecord} <Label className="text-sm text-muted-foreground mb-2 block"></Label>
enterprise={selectedEnterprise} <div className="flex gap-2">
open={showDetailDialog} {[
onOpenChange={setShowDetailDialog} { 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 */}
<AuditHistoryInstructions /> {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 && (
<Card>
<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>
</TableRow>
</TableHeader>
<TableBody>
{state.records.length === 0 ? (
<TableRow>
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
state.records.map((record) => (
<TableRow key={record.id}>
<TableCell>
<div className="flex items-center gap-2">
<Building className="w-4 h-4 text-blue-500" />
<span className="font-medium">{record.snapshot_company_name}</span>
</div>
</TableCell>
<TableCell>{getActionBadge(record.action)}</TableCell>
<TableCell>
<div className="flex items-center gap-2">
<User className="w-4 h-4 text-gray-500" />
<span>{record.action_by}</span>
</div>
</TableCell>
<TableCell className="text-sm">{record.action_time}</TableCell>
<TableCell>{getResultBadge(record.result)}</TableCell>
<TableCell>
<Button
size="sm"
variant="outline"
onClick={() => handleViewDetail(record)}
>
<FileText className="w-3 h-3 mr-1" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</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>
)}
{/* Detail Dialog */}
<Dialog open={state.showDetailDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}>
<DialogContent className="w-[90vw] max-w-6xl max-h-[90vh]">
<DialogHeader>
<DialogTitle></DialogTitle>
<DialogDescription>
</DialogDescription>
</DialogHeader>
{state.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="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_company_name}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{getActionBadge(state.selectedRecord.action)}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.action_by}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.action_time}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{getResultBadge(state.selectedRecord.result)}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
{state.selectedRecord.action_summary || '-'}
</div>
</div>
</div>
</TabsContent>
{/* 企业快照 */}
<TabsContent value="snapshot" className="space-y-4">
<div className="grid grid-cols-2 gap-6">
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_company_type || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
{state.selectedRecord.snapshot_province} {state.selectedRecord.snapshot_city}
</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_address || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_registrant || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot_contact_phone || '-'}</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="mt-1.5 p-3 bg-gray-50 rounded-md">
<code className="text-sm">{state.selectedRecord.id}</code>
</div>
</div>
<div>
<Label>ID</Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
<code className="text-sm">{state.selectedRecord.tenant_id}</code>
</div>
</div>
<div>
<Label>IP地址</Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.ip_address || '-'}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md text-sm">
{state.selectedRecord.user_agent || '-'}
</div>
</div>
</div>
</TabsContent>
</Tabs>
</ScrollArea>
)}
<DialogFooter>
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false })}>
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
</div> </div>
); );
} }

View File

@@ -0,0 +1,525 @@
/**
* 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>

View File

@@ -46,20 +46,48 @@ export interface Enterprise {
} }
export type EnterpriseStatus = 'active' | 'inactive' | 'suspended'; export type EnterpriseStatus = 'active' | 'inactive' | 'suspended';
export type AuditStatus = 'pending' | 'approved' | 'rejected'; export type AuditStatus = 'pending' | 'approved' | 'rejected' | 'draft';
// 审核记录 // 审核记录基于API响应结构
export interface AuditRecord { export interface AuditRecord {
id: string; id: string;
enterpriseId: string; enterpriseId?: string;
enterpriseName: string; enterpriseName: string;
action: 'SUBMIT' | 'AUDIT';
auditType: 'register' | 'update'; auditType: 'register' | 'update';
submitTime: string; submitTime: string;
auditTime?: string; actionTime: string;
auditor?: string; actionBy: string;
result: AuditStatus; result: AuditStatus;
reason?: string; auditStatus: string;
remarks?: string; auditComment?: string;
changeSummary: string;
ipAddress?: string;
userAgent?: string;
requestId?: string;
createdAt: string;
// 快照数据
snapshot: {
companyName: string;
companyType: string | null;
province: string | null;
city: string | null;
district: string | null;
detailedAddress: string | null;
registrant: string | null;
contactPhone: string | null;
bankAccount: string | null;
bankName: string | null;
bankFullName: string | null;
bankAddress: string | null;
socialCreditCode: string | null;
legalPersonName: string | null;
auditStatus: string;
auditComment: string | null;
companyScale: string | null;
registeredCapital: string | null;
};
} }
// 统计数据 // 统计数据

View File

@@ -0,0 +1,83 @@
/**
* filekorolheader: 企业审核分页组件 - 分页控制界面
* 功能:分页导航、页面跳转、分页信息展示
* 路径:/central-config/tenant/enterprise-audit/components/AuditPagination
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { ChevronLeft, ChevronRight } from 'lucide-react';
interface PaginationInfo {
page: number;
size: number;
total: number;
totalPages: number;
hasNext: boolean;
hasPrev: boolean;
}
interface AuditPaginationProps {
pagination: PaginationInfo;
onPageChange: (page: number) => void;
loading?: boolean;
}
export function AuditPagination({
pagination,
onPageChange,
loading = false
}: AuditPaginationProps) {
const { page, size, total, totalPages, hasNext, hasPrev } = pagination;
if (total === 0) return null;
return (
<div className="flex items-center justify-between">
<div className="text-sm text-muted-foreground">
{(page - 1) * size + 1} - {Math.min(page * size, total)} {total}
</div>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(page - 1)}
disabled={!hasPrev || loading}
>
<ChevronLeft className="w-4 h-4" />
</Button>
<div className="flex items-center gap-1">
<span className="text-sm text-muted-foreground"></span>
<Input
type="number"
min={1}
max={totalPages}
value={page}
onChange={(e) => {
const newPage = parseInt(e.target.value);
if (!isNaN(newPage)) {
onPageChange(newPage);
}
}}
className="w-16 h-8 text-center"
disabled={loading}
/>
<span className="text-sm text-muted-foreground">/ {totalPages} </span>
</div>
<Button
variant="outline"
size="sm"
onClick={() => onPageChange(page + 1)}
disabled={!hasNext || loading}
>
<ChevronRight className="w-4 h-4" />
</Button>
</div>
</div>
);
}

View File

@@ -0,0 +1,68 @@
/**
* filekorolheader: 企业审核搜索筛选组件 - 搜索和筛选功能界面
* 功能:关键词搜索、状态筛选、搜索功能实现
* 路径:/central-config/tenant/enterprise-audit/components/AuditSearchAndFilter
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Search, RotateCcw } from 'lucide-react';
interface AuditSearchAndFilterProps {
searchKeyword: string;
onSearchChange: (keyword: string) => void;
statusFilter: string;
onStatusFilterChange: (status: string) => void;
onRefresh: () => void;
loading?: boolean;
}
export function AuditSearchAndFilter({
searchKeyword,
onSearchChange,
statusFilter,
onStatusFilterChange,
onRefresh,
loading = false
}: AuditSearchAndFilterProps) {
return (
<Card className="p-4">
<div className="flex gap-4">
<div className="flex-1">
<div className="relative">
<Search className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-muted-foreground" />
<Input
placeholder="搜索企业名称、信用代码、登记人..."
value={searchKeyword}
onChange={(e) => onSearchChange(e.target.value)}
className="pl-10"
/>
</div>
</div>
<Select value={statusFilter} onValueChange={onStatusFilterChange}>
<SelectTrigger className="w-40">
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="all"></SelectItem>
<SelectItem value="草稿">稿</SelectItem>
<SelectItem value="待审核"></SelectItem>
<SelectItem value="已通过"></SelectItem>
<SelectItem value="已驳回"></SelectItem>
</SelectContent>
</Select>
<Button
variant="outline"
onClick={onRefresh}
disabled={loading}
>
<RotateCcw className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
);
}

View File

@@ -1,30 +1,38 @@
'use client'; /**
* filekorolheader: 企业审核统计卡片组件 - 统计数据展示界面
* 功能:待审核、已通过、已驳回、总企业数统计展示
* 路径:/central-config/tenant/enterprise-audit/components/AuditStatsCards
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import React from 'react'; import { Enterprise } from './enterpriseAuditApi';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { AuditStats, Enterprise } from '../types';
interface AuditStatsCardsProps { interface AuditStatsCardsProps {
enterprises: Enterprise[]; enterprises: Enterprise[];
loading?: boolean;
} }
export function AuditStatsCards({ enterprises }: AuditStatsCardsProps) { export function AuditStatsCards({
const stats: AuditStats[] = [ enterprises,
loading = false
}: AuditStatsCardsProps) {
const stats = [
{ {
label: '待审核', label: '待审核',
value: enterprises.filter(e => e.auditStatus === 'pending').length, value: enterprises.filter(e => e.auditStatus === '待审核').length,
color: 'text-yellow-600', color: 'text-yellow-600',
bg: 'bg-yellow-100', bg: 'bg-yellow-100',
}, },
{ {
label: '已通过', label: '已通过',
value: enterprises.filter(e => e.auditStatus === 'approved').length, value: enterprises.filter(e => e.auditStatus === '已通过').length,
color: 'text-green-600', color: 'text-green-600',
bg: 'bg-green-100', bg: 'bg-green-100',
}, },
{ {
label: '已驳回', label: '已驳回',
value: enterprises.filter(e => e.auditStatus === 'rejected').length, value: enterprises.filter(e => e.auditStatus === '已驳回').length,
color: 'text-red-600', color: 'text-red-600',
bg: 'bg-red-100', bg: 'bg-red-100',
}, },
@@ -36,12 +44,27 @@ export function AuditStatsCards({ enterprises }: AuditStatsCardsProps) {
}, },
]; ];
if (loading) {
return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((_, index) => (
<Card key={index} className="p-4">
<div className="animate-pulse">
<div className="h-4 bg-gray-200 rounded mb-2"></div>
<div className="h-8 bg-gray-200 rounded"></div>
</div>
</Card>
))}
</div>
);
}
return ( return (
<div className="grid grid-cols-2 md:grid-cols-4 gap-4"> <div className="grid grid-cols-2 md:grid-cols-4 gap-4">
{stats.map((stat, index) => ( {stats.map((stat, index) => (
<Card key={index} className="p-4"> <Card key={index} className="p-4">
<div className="text-sm text-muted-foreground">{stat.label}</div> <div className="text-sm text-muted-foreground">{stat.label}</div>
<div className={`mt-2 ${stat.color} text-2xl font-semibold`}>{stat.value}</div> <div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
</Card> </Card>
))} ))}
</div> </div>

View File

@@ -0,0 +1,121 @@
/**
* filekorolheader: 企业审核表格组件 - 企业数据列表展示和管理界面
* 功能:企业数据表格展示、搜索过滤、分页管理、操作按钮
* 路径:/central-config/tenant/enterprise-audit/components/EnterpriseAuditTable
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import { Enterprise } from './enterpriseAuditApi';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { Card } from '@/components/ui/card';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { Eye } from 'lucide-react';
interface EnterpriseAuditTableProps {
enterprises: Enterprise[];
loading: boolean;
onViewDetails: (enterprise: Enterprise) => void;
}
export function EnterpriseAuditTable({
enterprises,
loading,
onViewDetails
}: EnterpriseAuditTableProps) {
const getAuditStatusBadge = (status: Enterprise['auditStatus']) => {
switch (status) {
case '草稿':
return <Badge variant="secondary">稿</Badge>;
case '待审核':
return <Badge className="bg-yellow-100 text-yellow-700"></Badge>;
case '已通过':
return <Badge className="bg-green-100 text-green-700"></Badge>;
case '已驳回':
return <Badge className="bg-red-100 text-red-700"></Badge>;
default:
return <Badge variant="outline">{status}</Badge>;
}
};
const getStatusBadge = (status: Enterprise['status']) => {
return status === 'active' ? (
<Badge variant="default"></Badge>
) : (
<Badge variant="secondary"></Badge>
);
};
if (loading) {
return (
<Card>
<div className="flex items-center justify-center h-96">
<div className="text-muted-foreground">...</div>
</div>
</Card>
);
}
return (
<Card>
<Table>
<TableHeader>
<TableRow>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
<TableHead></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{enterprises.length === 0 ? (
<TableRow>
<TableCell colSpan={9} className="text-center text-muted-foreground py-8">
</TableCell>
</TableRow>
) : (
enterprises.map((enterprise) => (
<TableRow key={enterprise.id}>
<TableCell className="font-medium">{enterprise.name}</TableCell>
<TableCell className="text-muted-foreground">{enterprise.type}</TableCell>
<TableCell className="text-muted-foreground">
<code className="text-xs bg-gray-100 px-1 py-0.5 rounded">
{enterprise.socialCreditCode}
</code>
</TableCell>
<TableCell>{enterprise.legalPerson || '-'}</TableCell>
<TableCell className="text-muted-foreground">
{enterprise.province && enterprise.city ?
`${enterprise.province} ${enterprise.city}` :
'-'
}
</TableCell>
<TableCell>{getAuditStatusBadge(enterprise.auditStatus)}</TableCell>
<TableCell>{getStatusBadge(enterprise.status)}</TableCell>
<TableCell className="text-muted-foreground">
{enterprise.submitTime || '-'}
</TableCell>
<TableCell>
<Button
variant="ghost"
size="sm"
onClick={() => onViewDetails(enterprise)}
>
<Eye className="w-4 h-4 mr-1" />
</Button>
</TableCell>
</TableRow>
))
)}
</TableBody>
</Table>
</Card>
);
}

View File

@@ -1,46 +1,54 @@
'use client'; /**
* filekorolheader: 企业详情对话框组件 - 企业详情展示和审核操作界面
* 功能:企业详细信息展示、多标签页布局、审核操作界面
* 路径:/central-config/tenant/enterprise-audit/components/EnterpriseDetailDialog
* 规范遵循crop-x/docs/开发项目规范.md使用shadcn/ui组件TypeScript类型安全
*/
import React from 'react'; import { Enterprise } from './enterpriseAuditApi';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog'; import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button'; import { Button } from '@/components/ui/button';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
import { Label } from '@/components/ui/label'; import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea'; import { Textarea } from '@/components/ui/textarea';
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { ScrollArea } from '@/components/ui/scroll-area'; import { ScrollArea } from '@/components/ui/scroll-area';
import { Card } from '@/components/ui/card'; import { Card } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge'; import { Building2, FileText, CreditCard, User, CheckCircle, XCircle } from 'lucide-react';
import { Building, FileText, CreditCard, User, CheckCircle, XCircle, Image as ImageIcon } from 'lucide-react';
import { Enterprise, AuditStatus } from '../types';
interface EnterpriseDetailDialogProps { interface EnterpriseDetailDialogProps {
enterprise: Enterprise | null;
open: boolean; open: boolean;
onOpenChange: (open: boolean) => void; onOpenChange: (open: boolean) => void;
enterprise: Enterprise | null;
auditReason: string; auditReason: string;
onAuditReasonChange: (reason: string) => void; onAuditReasonChange: (reason: string) => void;
onApprove: () => void; onApprove: () => void;
onReject: () => void; onReject: () => void;
loading?: boolean;
} }
export function EnterpriseDetailDialog({ export function EnterpriseDetailDialog({
enterprise,
open, open,
onOpenChange, onOpenChange,
enterprise,
auditReason, auditReason,
onAuditReasonChange, onAuditReasonChange,
onApprove, onApprove,
onReject onReject,
loading = false
}: EnterpriseDetailDialogProps) { }: EnterpriseDetailDialogProps) {
const getAuditStatusBadge = (status: AuditStatus) => { const getAuditStatusBadge = (status: Enterprise['auditStatus']) => {
switch (status) { switch (status) {
case 'pending': case '草稿':
return <Badge variant="secondary">稿</Badge>;
case '待审核':
return <Badge className="bg-yellow-100 text-yellow-700"></Badge>; return <Badge className="bg-yellow-100 text-yellow-700"></Badge>;
case 'approved': case '已通过':
return <Badge className="bg-green-100 text-green-700"></Badge>; return <Badge className="bg-green-100 text-green-700"></Badge>;
case 'rejected': case '已驳回':
return <Badge className="bg-red-100 text-red-700"></Badge>; return <Badge className="bg-red-100 text-red-700"></Badge>;
default: default:
return <Badge>{status}</Badge>; return <Badge variant="outline">{status}</Badge>;
} }
}; };
@@ -48,14 +56,16 @@ export function EnterpriseDetailDialog({
return ( return (
<Dialog open={open} onOpenChange={onOpenChange}> <Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="max-w-5xl max-h-[90vh]"> <DialogContent className="w-[80vw] max-w-6xl max-h-[90vh]">
<DialogHeader> <DialogHeader>
<div className="flex items-center justify-between pr-8"> <div className="flex items-center justify-between pr-8">
<DialogTitle></DialogTitle> <DialogTitle></DialogTitle>
{getAuditStatusBadge(enterprise.auditStatus)} <div className="flex gap-2">
{getAuditStatusBadge(enterprise.auditStatus)}
</div>
</div> </div>
<DialogDescription className="sr-only"> <DialogDescription className="sr-only">
</DialogDescription> </DialogDescription>
</DialogHeader> </DialogHeader>
@@ -63,7 +73,7 @@ export function EnterpriseDetailDialog({
<Tabs defaultValue="basic" className="space-y-4"> <Tabs defaultValue="basic" className="space-y-4">
<TabsList className="grid grid-cols-4 w-full"> <TabsList className="grid grid-cols-4 w-full">
<TabsTrigger value="basic"> <TabsTrigger value="basic">
<Building className="w-4 h-4 mr-2" /> <Building2 className="w-4 h-4 mr-2" />
</TabsTrigger> </TabsTrigger>
<TabsTrigger value="other"> <TabsTrigger value="other">
@@ -80,36 +90,103 @@ export function EnterpriseDetailDialog({
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
{/* 企业基本信息 */} {/* 基本信息 */}
<TabsContent value="basic" className="space-y-4"> <TabsContent value="basic" className="space-y-6">
<div className="grid grid-cols-2 gap-6"> {/* 企业基本信息 */}
<div> <div className="space-y-4">
<Label></Label> <h4 className="font-semibold text-lg"></h4>
<div className="field-value">{enterprise.name}</div> <div className="grid grid-cols-2 gap-6">
</div> <div>
<div> <Label></Label>
<Label></Label> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.name}</div>
<div className="field-value">{enterprise.type}</div> </div>
</div> <div>
<div> <Label></Label>
<Label></Label> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.type}</div>
<div className="field-value"> </div>
{enterprise.province} {enterprise.city} {enterprise.district} <div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
{enterprise.province} {enterprise.city} {enterprise.district}
</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.address}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registrant}</div>
</div>
<div>
<Label></Label>
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.contactPhone}</div>
</div> </div>
</div> </div>
<div>
<Label></Label>
<div className="field-value">{enterprise.address}</div>
</div>
<div>
<Label></Label>
<div className="field-value">{enterprise.registrant}</div>
</div>
<div>
<Label></Label>
<div className="field-value">{enterprise.contactPhone}</div>
</div>
</div> </div>
{/* 审核信息 */}
<div className="space-y-4">
<h4 className="font-semibold text-lg"></h4>
<Card className="p-6 bg-gray-50 border">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">{enterprise.createdAt}</div>
</div>
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">{enterprise.updatedAt}</div>
</div>
{enterprise.submitTime && (
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">{enterprise.submitTime}</div>
</div>
)}
{enterprise.auditTime && (
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">{enterprise.auditTime}</div>
</div>
)}
{enterprise.auditor && (
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">{enterprise.auditor}</div>
</div>
)}
{enterprise.auditComment && (
<div className="col-span-2 pt-4 mt-2 border-t">
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base p-3 bg-white rounded-md">
{enterprise.auditComment}
</div>
</div>
)}
</div>
</Card>
</div>
{/* 审核操作区 - 仅待审核状态显示 */}
{enterprise.auditStatus === '待审核' && (
<div className="space-y-4">
<h4 className="font-semibold text-lg"></h4>
<div>
<Label></Label>
<div className="text-xs text-muted-foreground mt-1">
</div>
<Textarea
value={auditReason}
onChange={(e) => onAuditReasonChange(e.target.value)}
rows={4}
placeholder="请填写审核意见..."
className="mt-2"
/>
</div>
</div>
)}
</TabsContent> </TabsContent>
{/* 其他信息 */} {/* 其他信息 */}
@@ -117,44 +194,30 @@ export function EnterpriseDetailDialog({
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
<div> <div>
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.companySize || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.companySize || '-'}</div>
</div> </div>
<div> <div>
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.registeredCapital || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registeredCapital || '-'}</div>
</div> </div>
<div> <div>
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.establishmentDate || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.establishmentDate || '-'}</div>
</div> </div>
<div> <div>
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.invoiceType || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.invoiceType || '-'}</div>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label></Label> <Label></Label>
<div className="field-value"> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">
<code className="text-sm font-mono"> <code className="text-sm font-mono">{enterprise.socialCreditCode || '-'}</code>
{enterprise.socialCreditCode}
</code>
</div> </div>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.businessScope || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
</div> {enterprise.businessScope || '-'}
<div className="col-span-2">
<Label></Label>
<div className="mt-2">
{enterprise.businessLicense ? (
<img
src={enterprise.businessLicense}
alt="营业执照"
className="w-64 h-auto border rounded-lg"
/>
) : (
<span className="text-muted-foreground"></span>
)}
</div> </div>
</div> </div>
</div> </div>
@@ -165,153 +228,69 @@ export function EnterpriseDetailDialog({
<div className="grid grid-cols-2 gap-6"> <div className="grid grid-cols-2 gap-6">
<div> <div>
<Label></Label> <Label></Label>
<div className="field-value"> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">
{enterprise.bankAccount ? ( {enterprise.bankAccount ? (
<code className="text-sm font-mono"> <code className="text-sm font-mono">{enterprise.bankAccount}</code>
{enterprise.bankAccount}
</code>
) : '-'} ) : '-'}
</div> </div>
</div> </div>
<div> <div>
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.bankName || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankName || '-'}</div>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.bankFullName || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankFullName || '-'}</div>
</div> </div>
<div className="col-span-2"> <div className="col-span-2">
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.bankAddress || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankAddress || '-'}</div>
</div>
<div className="col-span-2">
<Label></Label>
<div className="mt-2">
{enterprise.bankLicense ? (
<img
src={enterprise.bankLicense}
alt="开户许可证"
className="w-64 h-auto border rounded-lg"
/>
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div> </div>
</div> </div>
</TabsContent> </TabsContent>
{/* 法人信息 */} {/* 法人信息 */}
<TabsContent value="legal" className="space-y-4"> <TabsContent value="legal" className="space-y-4">
<div className="grid grid-cols-1 gap-6"> <div className="grid grid-cols-2 gap-6">
<div> <div>
<Label></Label> <Label></Label>
<div className="field-value">{enterprise.legalPerson || '-'}</div> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.legalPerson || '-'}</div>
</div> </div>
<div> <div>
<Label></Label> <Label></Label>
<div className="mt-2"> <div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registrant || '-'}</div>
{enterprise.idCardFront ? (
<img
src={enterprise.idCardFront}
alt="身份证正面"
className="w-80 h-auto border rounded-lg"
/>
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div>
<div>
<Label></Label>
<div className="mt-2">
{enterprise.idCardBack ? (
<img
src={enterprise.idCardBack}
alt="身份证反面"
className="w-80 h-auto border rounded-lg"
/>
) : (
<span className="text-muted-foreground"></span>
)}
</div>
</div> </div>
</div> </div>
</TabsContent> </TabsContent>
</Tabs> </Tabs>
{/* 审核信息 */} </ScrollArea>
<div className="mt-6 pt-6 border-t">
<h4 className="mb-4 font-bold"></h4>
<Card className="p-6 bg-gray-50 border">
<div className="grid grid-cols-2 gap-x-8 gap-y-4">
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{new Date(enterprise.createdAt).toLocaleString('zh-CN')}
</div>
</div>
{enterprise.auditTime && (
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{new Date(enterprise.auditTime).toLocaleString('zh-CN')}
</div>
</div>
)}
{enterprise.auditor && (
<div>
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{enterprise.auditor}
</div>
</div>
)}
{enterprise.auditReason && (
<div className="col-span-2 pt-4 mt-2 border-t">
<Label className="text-xs"></Label>
<div className="mt-1.5 text-base">
{enterprise.auditReason}
</div>
</div>
)}
</div>
</Card>
{/* 审核操作区 - 仅待审核状态显示 */} {/* 底部操作按钮 */}
{enterprise.auditStatus === 'pending' && ( <div className="flex justify-end gap-3 mt-6 pt-4 border-t">
<div className="mt-6"> <Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
<Label></Label>
<Textarea
value={auditReason}
onChange={(e) => onAuditReasonChange(e.target.value)}
rows={3}
placeholder="请填写审核意见(驳回时必填)..."
className="mt-2"
/>
</div>
)}
</div>
</ScrollArea>
<DialogFooter className="border-t pt-4">
<Button variant="outline" onClick={() => onOpenChange(false)}>
</Button> </Button>
{enterprise.auditStatus === 'pending' && ( {enterprise.auditStatus === '待审核' && (
<> <>
<Button variant="destructive" onClick={onReject}> <Button
variant="destructive"
onClick={onReject}
disabled={loading}
>
<XCircle className="w-4 h-4 mr-2" /> <XCircle className="w-4 h-4 mr-2" />
</Button> </Button>
<Button onClick={onApprove}> <Button
onClick={onApprove}
disabled={loading}
>
<CheckCircle className="w-4 h-4 mr-2" /> <CheckCircle className="w-4 h-4 mr-2" />
</Button> </Button>
</> </>
)} )}
</DialogFooter> </div>
</DialogContent> </DialogContent>
</Dialog> </Dialog>
); );

View File

@@ -0,0 +1,279 @@
/**
* filekorolheader: 企业审核API接口 - 企业审核数据查询接口服务
* 功能API请求封装、数据转换、错误处理、分页查询
* 路径:/central-config/tenant/enterprise-audit/components/enterpriseAuditApi
* 规范遵循crop-x/docs/开发项目规范.md使用SDK API调用TypeScript类型安全
*/
import { getAuthToken } from "@/utils/token";
import { listTenantsApiV1TenantsGet, auditTenantApiV1TenantsTenantIdAuditPost } from "@/lib/api/sdk.gen";
// 企业数据类型(与企业管理页面相同)
export interface TenantData {
id: string;
tenant_code: string;
is_active: boolean;
company_name: string;
company_type: string | null;
province: string | null;
city: string | null;
district: string | null;
detailed_address: string | null;
registrant: string | null;
contact_phone: string | null;
bank_account: string | null;
bank_name: string | null;
bank_full_name: string | null;
bank_address: string | null;
social_credit_code: string | null;
legal_person_name: string | null;
company_scale: string | null;
registered_capital: string | null;
established_date: string | null;
invoice_type: string | null;
business_scope: string | null;
submit_time: string | null;
audit_time: string | null;
auditor: string | null;
audit_status: string;
audit_comment: string | null;
created_at: string;
updated_at: string;
}
// API响应接口
export interface TenantsApiResponse {
data: TenantData[];
total: number;
page: number;
size: number;
total_pages: number;
has_next: boolean;
has_prev: boolean;
}
// 查询参数接口
export interface TenantsQueryParams {
search?: string;
audit_status?: string;
page?: number;
size?: number;
order_by?: string;
sort_order?: 'asc' | 'desc';
}
// 企业页面数据类型(转换后的)
export interface Enterprise {
id: string;
name: string;
code: string;
type: string;
status: 'active' | 'inactive';
auditStatus: '草稿' | '待审核' | '已通过' | '已驳回';
createdAt: string;
updatedAt: string;
contact?: string;
phone?: string;
contactPhone?: string;
province?: string;
city?: string;
district?: string;
address?: string;
registrant?: string;
companySize?: string;
registeredCapital?: string;
establishmentDate?: string;
invoiceType?: string;
socialCreditCode?: string;
businessScope?: string;
legalPerson?: string;
bankAccount?: string;
bankName?: string;
bankFullName?: string;
bankAddress?: string;
submitTime?: string;
auditTime?: string;
auditor?: string;
auditComment?: string;
}
/**
* 获取待审核企业列表数据
*/
export async function fetchTenantsForAudit(params: TenantsQueryParams = {}): Promise<TenantsApiResponse> {
try {
// 构建查询参数对象
const queryParams: any = {};
if (params.search) queryParams.search = params.search;
if (params.audit_status) queryParams.audit_status = params.audit_status;
if (params.page) queryParams.page = params.page;
if (params.size) queryParams.size = params.size;
if (params.order_by) queryParams.order_by = params.order_by;
if (params.sort_order) queryParams.sort_order = params.sort_order;
// 默认参数
if (!params.page) queryParams.page = 1;
if (!params.size) queryParams.size = 10;
if (!params.sort_order) queryParams.sort_order = 'desc';
// 使用SDK API调用企业查询接口添加缓存破坏器和认证头部
const token = getAuthToken();
console.log('审核页面API调用参数:', queryParams);
const response = await listTenantsApiV1TenantsGet({
query: {
...queryParams,
// 添加时间戳防止缓存
_t: Date.now(),
},
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
}
const data = response.data as any;
console.log('审核页面API响应:', data);
// 转换响应数据格式以匹配现有的接口
// API返回的数据结构: { data: [...], total: 25, page: 1, size: 10, ... }
return {
data: data?.data || [], // 注意:实际数据在 data.data 中
total: data?.total || 0,
page: data?.page || 1,
size: data?.size || 10,
total_pages: data?.total_pages || 0,
has_next: data?.has_next || false,
has_prev: data?.has_prev || false,
};
} catch (error) {
console.error('Failed to fetch tenants for audit:', error);
throw error;
}
}
/**
* 审核企业
*/
export async function auditTenant(
tenantId: string,
auditData: {
audit_status: '草稿' | '待审核' | '已通过' | '已驳回';
audit_comment?: string;
}
): Promise<TenantData> {
try {
const token = getAuthToken();
console.log('审核企业API调用:', tenantId, auditData);
const response = await auditTenantApiV1TenantsTenantIdAuditPost({
path: {
tenant_id: tenantId,
},
body: auditData,
headers: token ? {
'Authorization': `Bearer ${token}`,
} : undefined,
});
if (response.error) {
throw new Error(`审核企业失败: ${response.error.message || 'Unknown error'}`);
}
const data = response.data as TenantData;
console.log('审核企业API响应:', data);
// 验证返回的数据中audit_status是否正确
if (data.audit_status !== auditData.audit_status) {
throw new Error('审核企业失败:返回数据状态不正确');
}
return data;
} catch (error) {
console.error('Failed to audit tenant:', error);
throw error;
}
}
/**
* 将API数据转换为页面所需的企业数据格式
*/
export function transformTenantData(tenant: TenantData): Enterprise {
return {
id: tenant.id,
name: tenant.company_name,
code: tenant.tenant_code,
type: tenant.company_type || '未分类',
status: tenant.is_active ? 'active' : 'inactive',
auditStatus: mapAuditStatus(tenant.audit_status),
createdAt: formatDate(tenant.created_at),
updatedAt: formatDate(tenant.updated_at),
contact: tenant.registrant,
phone: tenant.contact_phone,
contactPhone: tenant.contact_phone,
province: tenant.province,
city: tenant.city,
district: tenant.district,
address: tenant.detailed_address,
registrant: tenant.registrant,
companySize: tenant.company_scale,
registeredCapital: tenant.registered_capital,
establishmentDate: tenant.established_date ?
new Date(tenant.established_date).toLocaleDateString('zh-CN') : undefined,
invoiceType: tenant.invoice_type,
socialCreditCode: tenant.social_credit_code,
businessScope: tenant.business_scope,
legalPerson: tenant.legal_person_name,
bankAccount: tenant.bank_account,
bankName: tenant.bank_name,
bankFullName: tenant.bank_full_name,
bankAddress: tenant.bank_address,
submitTime: tenant.submit_time ? formatDate(tenant.submit_time) : undefined,
auditTime: tenant.audit_time ? formatDate(tenant.audit_time) : undefined,
auditor: tenant.auditor,
auditComment: tenant.audit_comment,
};
}
/**
* 映射审核状态
*/
function mapAuditStatus(status: string): Enterprise['auditStatus'] {
switch (status) {
case '未提交':
case '草稿':
return '草稿';
case '待审核':
return '待审核';
case '已通过':
case '审核通过':
return '已通过';
case '已拒绝':
case '已驳回':
return '已驳回';
default:
return '草稿';
}
}
/**
* 格式化日期
*/
function formatDate(dateString: string): string {
try {
const date = new Date(dateString);
return date.toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
}).replace(/\//g, '-');
} catch (error) {
return dateString;
}
}

View File

@@ -1,256 +1,354 @@
/**
* filekorolheader: 企业审核页面 - 企业注册审核管理页面
* 功能:企业审核列表、搜索筛选、审核操作、详情查看
* 路径:/central-config/tenant/enterprise-audit
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理API集成模块化组件
*/
'use client'; 'use client';
import { useState, useEffect } from 'react'; import { useReducer, useEffect, useMemo } from 'react';
import { toast } from 'sonner'; 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 { AuditStatsCards } from './components/AuditStatsCards';
import { SearchFilters } from './components/SearchFilters'; import { AuditSearchAndFilter } from './components/AuditSearchAndFilter';
import { EnterpriseList } from './components/EnterpriseList'; import { EnterpriseAuditTable } from './components/EnterpriseAuditTable';
import { EnterpriseDetailDialog } from './components/EnterpriseDetailDialog'; import { EnterpriseDetailDialog } from './components/EnterpriseDetailDialog';
import { Enterprise, AuditStatus } from './types'; 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() { export default function EnterpriseAuditPage() {
const [enterprises, setEnterprises] = useState<Enterprise[]>([]); const [state, dispatch] = useReducer(auditReducer, initialState);
const [searchKeyword, setSearchKeyword] = useState('');
const [statusFilter, setStatusFilter] = useState<string>('all');
const [showDetailDialog, setShowDetailDialog] = useState(false);
const [selectedEnterprise, setSelectedEnterprise] = useState<Enterprise | null>(null);
const [auditReason, setAuditReason] = useState('');
useEffect(() => { // 加载企业数据
loadEnterprises(); const loadEnterprises = async (resetPage = false) => {
}, []); try {
dispatch({ type: 'SET_LOADING', payload: true });
const loadEnterprises = () => { const params: TenantsQueryParams = {
const data = localStorage.getItem('smart_agriculture_enterprises'); search: state.filters.search || undefined,
if (data) { audit_status: state.filters.audit_status === 'all' ? undefined : state.filters.audit_status,
setEnterprises(JSON.parse(data)); page: resetPage ? 1 : state.pagination.page,
} else { size: state.pagination.size,
// 初始化示例数据 order_by: state.sortBy,
const mockEnterprises: Enterprise[] = [ sort_order: state.sortOrder,
{ };
id: 'ent-1',
name: '绿野农业科技有限公司', const response = await fetchTenantsForAudit(params);
type: '有限责任公司', const transformedData = response.data.map(transformTenantData);
province: '北京市',
city: '海淀区', dispatch({
district: '中关村街道', type: 'SET_ENTERPRISES',
companySize: '50-200人', payload: {
registeredCapital: '1000万元', data: transformedData,
establishmentDate: '2020-03-15', pagination: {
invoiceType: '增值税专用发票', page: response.page,
socialCreditCode: '91110000123456789X', size: response.size,
businessScope: '农业技术开发、技术咨询、技术服务;销售机械设备、电子产品。', total: response.total,
bankAccount: '1234567890123456789', totalPages: response.total_pages,
bankName: '中国工商银行', hasNext: response.has_next,
bankFullName: '中国工商银行股份有限公司北京中关村支行', hasPrev: response.has_prev,
bankAddress: '北京市海淀区中关村大街1号', }
legalPerson: '张伟', }
registrant: '张经理', });
contactPhone: '13800138001', } catch (error) {
address: '北京市海淀区中关村大街1号科技大厦', console.error('Failed to load enterprises for audit:', error);
status: 'active', const errorMessage = error instanceof Error ? error.message : '加载企业审核数据失败';
auditStatus: 'pending', dispatch({ type: 'SET_ERROR', payload: errorMessage });
createdAt: '2024-10-10T08:00:00', toast.error(errorMessage);
updatedAt: '2024-10-10T08:00:00',
},
{
id: 'ent-2',
name: '丰收现代农业集团',
type: '股份有限公司',
province: '江苏省',
city: '南京市',
district: '江宁区',
companySize: '200-500人',
registeredCapital: '5000万元',
establishmentDate: '2018-06-20',
invoiceType: '增值税专用发票',
socialCreditCode: '91320000987654321Y',
businessScope: '现代农业种植、农产品加工与销售、农业技术推广服务。',
bankAccount: '9876543210987654321',
bankName: '中国农业银行',
bankFullName: '中国农业银行股份有限公司南京江宁支行',
bankAddress: '江苏省南京市江宁区农业大道88号',
legalPerson: '李明',
registrant: '李总',
contactPhone: '13900139002',
address: '江苏省南京市江宁区农业大道88号',
status: 'active',
auditStatus: 'approved',
auditTime: '2024-10-08T14:30:00',
auditor: '系统管理员',
createdAt: '2024-10-05T10:00:00',
updatedAt: '2024-10-08T14:30:00',
},
{
id: 'ent-3',
name: '金穗农机服务中心',
type: '个人独资企业',
province: '山东省',
city: '济南市',
district: '历城区',
companySize: '1-50人',
registeredCapital: '200万元',
establishmentDate: '2021-09-10',
invoiceType: '增值税普通发票',
socialCreditCode: '91370000456789012Z',
businessScope: '农业机械租赁、维修服务、农机作业服务。',
bankAccount: '5555666677778888',
bankName: '中国建设银行',
bankFullName: '中国建设银行股份有限公司济南历城支行',
bankAddress: '山东省济南市历城区农机路66号',
legalPerson: '王刚',
registrant: '王主任',
contactPhone: '13700137003',
address: '山东省济南市历城区农机路66号',
status: 'inactive',
auditStatus: 'rejected',
auditReason: '资质材料不完整,请补充营业执照副本和法人身份证复印件',
auditTime: '2024-10-09T16:00:00',
auditor: '系统管理员',
createdAt: '2024-10-06T09:00:00',
updatedAt: '2024-10-09T16:00:00',
},
];
localStorage.setItem('smart_agriculture_enterprises', JSON.stringify(mockEnterprises));
setEnterprises(mockEnterprises);
} }
}; };
const filteredEnterprises = enterprises.filter(ent => { // 初始加载
const matchKeyword = !searchKeyword || useEffect(() => {
ent.name.includes(searchKeyword) || loadEnterprises(true);
ent.socialCreditCode.includes(searchKeyword) || }, [state.filters.search, state.filters.audit_status, state.sortBy, state.sortOrder]);
ent.registrant.includes(searchKeyword);
const matchStatus = statusFilter === 'all' || ent.auditStatus === statusFilter; // 分页加载
useEffect(() => {
if (state.pagination.page > 1) {
loadEnterprises(false);
}
}, [state.pagination.page]);
return matchKeyword && matchStatus; // 计算统计数据
}); 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) => { const handleViewDetail = (enterprise: Enterprise) => {
setSelectedEnterprise(enterprise); dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
setAuditReason(''); dispatch({ type: 'SET_AUDIT_REASON', payload: '' });
setShowDetailDialog(true); dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
}; };
const handleApprove = () => { const handleAuditReasonChange = (value: string) => {
if (!selectedEnterprise) return; dispatch({ type: 'SET_AUDIT_REASON', payload: value });
const now = new Date().toISOString();
const updated = enterprises.map(ent =>
ent.id === selectedEnterprise.id
? {
...ent,
auditStatus: 'approved' as AuditStatus,
status: 'active' as const,
auditTime: now,
auditor: '系统管理员',
auditReason: auditReason || undefined,
updatedAt: now,
}
: ent
);
// 创建审核历史记录
const auditRecords = JSON.parse(localStorage.getItem('smart_agriculture_audit_records') || '[]');
const newRecord = {
id: `audit-${Date.now()}`,
enterpriseId: selectedEnterprise.id,
enterpriseName: selectedEnterprise.name,
auditType: 'register',
submitTime: selectedEnterprise.createdAt,
auditTime: now,
auditor: '系统管理员',
result: 'approved',
remarks: auditReason || '审核通过',
};
auditRecords.push(newRecord);
localStorage.setItem('smart_agriculture_audit_records', JSON.stringify(auditRecords));
setEnterprises(updated);
localStorage.setItem('smart_agriculture_enterprises', JSON.stringify(updated));
setShowDetailDialog(false);
toast.success('审核通过');
}; };
const handleReject = () => { const handleApprove = async () => {
if (!selectedEnterprise) return; if (!state.selectedEnterprise) return;
if (!auditReason.trim()) {
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('请填写驳回原因'); toast.error('请填写驳回原因');
return; return;
} }
const now = new Date().toISOString(); try {
const updated = enterprises.map(ent => dispatch({ type: 'SET_ACTION_LOADING', payload: true });
ent.id === selectedEnterprise.id
? {
...ent,
auditStatus: 'rejected' as AuditStatus,
status: 'inactive' as const,
auditTime: now,
auditor: '系统管理员',
auditReason: auditReason,
updatedAt: now,
}
: ent
);
// 创建审核历史记录 const updatedTenant = await auditTenant(state.selectedEnterprise.id, {
const auditRecords = JSON.parse(localStorage.getItem('smart_agriculture_audit_records') || '[]'); audit_status: '已驳回',
const newRecord = { audit_comment: state.auditReason,
id: `audit-${Date.now()}`, });
enterpriseId: selectedEnterprise.id,
enterpriseName: selectedEnterprise.name,
auditType: 'register',
submitTime: selectedEnterprise.createdAt,
auditTime: now,
auditor: '系统管理员',
result: 'rejected',
reason: auditReason,
remarks: '审核驳回',
};
auditRecords.push(newRecord);
localStorage.setItem('smart_agriculture_audit_records', JSON.stringify(auditRecords));
setEnterprises(updated); // 更新本地状态
localStorage.setItem('smart_agriculture_enterprises', JSON.stringify(updated)); const updatedEnterprise = transformTenantData(updatedTenant);
setShowDetailDialog(false); dispatch({
toast.success('已驳回'); 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 ( return (
<div className="space-y-6"> <div className="space-y-6">
{/* 页面标题和描述 */}
<div> <div>
<h2 className="text-green-800"></h2> <h2 className="text-green-800"></h2>
<p className="text-muted-foreground"></p> <p className="text-muted-foreground"></p>
</div> </div>
{/* 统计卡片 */} {/* 统计卡片 */}
<AuditStatsCards enterprises={enterprises} /> <AuditStatsCards
enterprises={state.enterprises}
loading={state.loading}
/>
{/* 搜索和筛选 */} {/* 搜索和筛选 */}
<SearchFilters <AuditSearchAndFilter
searchKeyword={searchKeyword} searchKeyword={state.filters.search}
setSearchKeyword={setSearchKeyword} onSearchChange={(value) => dispatch({ type: 'SET_FILTERS', payload: { search: value } })}
statusFilter={statusFilter} statusFilter={state.filters.audit_status}
setStatusFilter={setStatusFilter} onStatusFilterChange={(value) => dispatch({ type: 'SET_FILTERS', payload: { audit_status: value } })}
onRefresh={handleRefresh}
loading={state.loading}
/> />
{/* 企业列表 */} {/* 企业列表 */}
<EnterpriseList <EnterpriseAuditTable
enterprises={filteredEnterprises} enterprises={state.enterprises}
onViewDetail={handleViewDetail} loading={state.loading}
onViewDetails={handleViewDetail}
/> />
{/* 详情审核对话框 */} {/* 分页 */}
{state.pagination.total > 0 && (
<AuditPagination
pagination={state.pagination}
onPageChange={handlePageChange}
loading={state.loading}
/>
)}
{/* 企业详情对话框 */}
<EnterpriseDetailDialog <EnterpriseDetailDialog
enterprise={selectedEnterprise} open={state.showDetailDialog}
open={showDetailDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
onOpenChange={setShowDetailDialog} enterprise={state.selectedEnterprise}
auditReason={auditReason} auditReason={state.auditReason}
onAuditReasonChange={setAuditReason} onAuditReasonChange={(reason) => dispatch({ type: 'SET_AUDIT_REASON', payload: reason })}
onApprove={handleApprove} onApprove={handleApprove}
onReject={handleReject} onReject={handleReject}
loading={state.actionLoading}
/> />
</div> </div>
); );

View File

@@ -126,6 +126,12 @@ export default function EnterpriseManagement() {
}; };
const handlePageChange = (page: number) => { 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 } }); dispatch({ type: 'SET_PAGINATION', payload: { page } });
}; };