640 lines
24 KiB
TypeScript
640 lines
24 KiB
TypeScript
/**
|
||
* filekorolheader: 审核历史页面 - 企业审核记录查询和管理页面
|
||
* 功能:审核历史记录查询、搜索筛选、详情查看、数据分析
|
||
* 路径:/central-config/tenant/audit-history
|
||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式
|
||
*/
|
||
'use client';
|
||
|
||
import { useReducer, useEffect, useMemo, useState, useRef, useCallback } from 'react';
|
||
import { toast } from 'sonner';
|
||
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, 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 [state, dispatch] = useReducer(auditHistoryReducer, initialState);
|
||
const isFirstLoad = useRef(true);
|
||
|
||
// 加载审核历史数据
|
||
const loadAuditHistory = useCallback(async () => {
|
||
try {
|
||
dispatch({ type: 'SET_LOADING', payload: true });
|
||
|
||
const params: AuditLogsQueryParams = {
|
||
search: state.filters.search_keyword || undefined,
|
||
page: 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' });
|
||
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
|
||
toast.success('数据已刷新');
|
||
};
|
||
|
||
// 合并所有状态变化,统一处理数据加载
|
||
useEffect(() => {
|
||
if (isFirstLoad.current) {
|
||
// 首次加载
|
||
isFirstLoad.current = false;
|
||
loadAuditHistory();
|
||
} else {
|
||
// 后续状态变化,使用防抖
|
||
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,
|
||
state.pagination.page,
|
||
state.pagination.size
|
||
]);
|
||
|
||
// 工具函数
|
||
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 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>;
|
||
}
|
||
};
|
||
|
||
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 variant="outline" size="sm" onClick={handleRefresh} disabled={state.loading}>
|
||
<RefreshCw className={`w-4 h-4 mr-1 ${state.loading ? 'animate-spin' : ''}`} />
|
||
刷新
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</Card>
|
||
|
||
{/* 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>
|
||
|
||
<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="approved">已通过</SelectItem>
|
||
<SelectItem value="rejected">已驳回</SelectItem>
|
||
<SelectItem value="pending">待审核</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 && (
|
||
<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>
|
||
);
|
||
} |