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

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

@@ -1,211 +1,634 @@
/**
* filekorolheader: 审核历史页面 - 企业审核记录查询和管理页面
* 功能:审核历史记录查询、搜索筛选、详情查看、数据分析
* 路径:/central-config/tenant/audit-history
* 规范遵循crop-x/docs/开发项目规范.md使用useReducer状态管理API集成shadcn语义化样式
*/
'use client';
import { useState, useEffect } from 'react';
import { useReducer, useEffect, useMemo, useState } from 'react';
import { toast } from 'sonner';
import { Card } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Download } from 'lucide-react';
import { AuditHistoryStatsCards } from './components/AuditHistoryStatsCards';
import { AuditHistoryFilters } from './components/AuditHistoryFilters';
import { AuditHistoryList } from './components/AuditHistoryList';
import { AuditHistoryDetailDialog } from './components/AuditHistoryDetailDialog';
import { AuditHistoryInstructions } from './components/AuditHistoryInstructions';
import { AuditRecord, Enterprise, FilterOptions } from './types';
import { Badge } from '@/components/ui/badge';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, 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,
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() {
const [records, setRecords] = useState<AuditRecord[]>([]);
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 [state, dispatch] = useReducer(auditHistoryReducer, initialState);
const [filters, setFilters] = useState<FilterOptions>({
searchKeyword: '',
resultFilter: 'all',
typeFilter: 'all',
dateRange: 'all'
});
// 加载审核历史数据
const loadAuditHistory = async (resetPage = false) => {
try {
dispatch({ type: 'SET_LOADING', payload: true });
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(() => {
loadEnterprises();
loadAuditHistory();
}, []);
const loadEnterprises = () => {
const data = localStorage.getItem('smart_agriculture_enterprises');
if (data) {
setEnterprises(JSON.parse(data));
useEffect(() => {
const timer = setTimeout(() => {
loadAuditHistory();
}, 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 data = localStorage.getItem('smart_agriculture_audit_records');
if (data) {
setRecords(JSON.parse(data));
} else {
// 初始化审核历史数据
const mockRecords: AuditRecord[] = [
{
id: 'audit-1',
enterpriseId: 'ent-2',
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 getResultBadge = (result: string) => {
switch (result) {
case 'approved':
return <Badge className="bg-green-100 text-green-700"></Badge>;
case 'rejected':
return <Badge className="bg-red-100 text-red-700"></Badge>;
case 'pending':
return <Badge className="bg-yellow-100 text-yellow-700"></Badge>;
default:
return <Badge variant="outline">{result}</Badge>;
}
};
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 (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-green-800"></h2>
<p className="text-muted-foreground"></p>
{/* 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 variant="outline" size="sm" onClick={handleRefresh} disabled={state.loading}>
<RefreshCw className={`w-4 h-4 mr-1 ${state.loading ? 'animate-spin' : ''}`} />
</Button>
</div>
</div>
<Button onClick={handleExport}>
<Download className="w-4 h-4 mr-2" />
</Button>
</div>
</Card>
{/* 统计卡片 */}
<AuditHistoryStatsCards records={records} />
{/* 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.search_keyword}
onChange={(e) => handleSearch(e.target.value)}
className="pl-10"
/>
</div>
</div>
{/* 搜索和筛选 */}
<AuditHistoryFilters
filters={filters}
onFiltersChange={setFilters}
/>
<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>
{/* 审核历史列表 */}
<AuditHistoryList
records={filteredRecords}
onViewDetail={handleViewDetail}
/>
<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="approved"></SelectItem>
<SelectItem value="rejected"></SelectItem>
<SelectItem value="pending"></SelectItem>
</SelectContent>
</Select>
</div>
</div>
</div>
</Card>
{/* 详情对话框 */}
<AuditHistoryDetailDialog
record={selectedRecord}
enterprise={selectedEnterprise}
open={showDetailDialog}
onOpenChange={setShowDetailDialog}
/>
{/* 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>
{/* 使用说明 */}
<AuditHistoryInstructions />
{/* 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 && (
<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>
);
}