子仓库提交
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { FileText, Building, CreditCard, User } from 'lucide-react';
|
||||
import { AuditRecord, Enterprise, AuditStatus } from '../types';
|
||||
|
||||
interface AuditHistoryDetailDialogProps {
|
||||
record: AuditRecord | null;
|
||||
enterprise: Enterprise | null;
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
}
|
||||
|
||||
export function AuditHistoryDetailDialog({
|
||||
record,
|
||||
enterprise,
|
||||
open,
|
||||
onOpenChange
|
||||
}: AuditHistoryDetailDialogProps) {
|
||||
const getResultBadge = (result: AuditStatus) => {
|
||||
switch (result) {
|
||||
case 'pending':
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||
case 'approved':
|
||||
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
||||
case 'rejected':
|
||||
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||
default:
|
||||
return <Badge>{result}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case 'register':
|
||||
return <Badge className="bg-blue-100 text-blue-700">注册审核</Badge>;
|
||||
case 'update':
|
||||
return <Badge className="bg-purple-100 text-purple-700">变更审核</Badge>;
|
||||
default:
|
||||
return <Badge>{type}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
if (!record || !enterprise) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<FileText className="w-5 h-5 text-green-600" />
|
||||
<span>审核记录详情</span>
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
{getResultBadge(record.result)}
|
||||
</div>
|
||||
</div>
|
||||
</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
查看企业审核的历史记录详情
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[calc(90vh-120px)]">
|
||||
<div className="space-y-6 pr-6">
|
||||
{/* 企业信息标签页 */}
|
||||
<Tabs defaultValue="basic" className="w-full">
|
||||
<TabsList className="grid w-full grid-cols-4">
|
||||
<TabsTrigger value="basic">
|
||||
<Building className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="other">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
其他信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="bank">
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
开户信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="legal">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
法人信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>企业名称</Label>
|
||||
<div className="field-value">{enterprise.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>企业类型</Label>
|
||||
<div className="field-value">{enterprise.type}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>所在地区</Label>
|
||||
<div className="field-value">
|
||||
{enterprise.province} {enterprise.city} {enterprise.district}
|
||||
</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>
|
||||
</TabsContent>
|
||||
|
||||
{/* 其他信息 */}
|
||||
<TabsContent value="other" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>公司规模</Label>
|
||||
<div className="field-value">{enterprise.companySize || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>注册资本</Label>
|
||||
<div className="field-value">{enterprise.registeredCapital || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>成立时间</Label>
|
||||
<div className="field-value">{enterprise.establishmentDate || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>发票类型</Label>
|
||||
<div className="field-value">{enterprise.invoiceType || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>社会信用代码</Label>
|
||||
<div className="field-value">
|
||||
<code className="text-sm font-mono">
|
||||
{enterprise.socialCreditCode}
|
||||
</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>经营范围</Label>
|
||||
<div className="field-value">{enterprise.businessScope || '-'}</div>
|
||||
</div>
|
||||
<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>
|
||||
</TabsContent>
|
||||
|
||||
{/* 开户信息 */}
|
||||
<TabsContent value="bank" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>银行账号</Label>
|
||||
<div className="field-value">
|
||||
{enterprise.bankAccount ? (
|
||||
<code className="text-sm font-mono">
|
||||
{enterprise.bankAccount}
|
||||
</code>
|
||||
) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>开户行</Label>
|
||||
<div className="field-value">{enterprise.bankName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行全称</Label>
|
||||
<div className="field-value">{enterprise.bankFullName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行地址</Label>
|
||||
<div className="field-value">{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>
|
||||
</TabsContent>
|
||||
|
||||
{/* 法人信息 */}
|
||||
<TabsContent value="legal" className="space-y-4">
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<div>
|
||||
<Label>法人名称</Label>
|
||||
<div className="field-value">{enterprise.legalPerson || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>身份证正面</Label>
|
||||
<div className="mt-2">
|
||||
{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>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 审核信息 */}
|
||||
<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">
|
||||
{getTypeBadge(record.auditType)}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label className="text-xs">提交时间</Label>
|
||||
<div className="mt-1.5 text-base">
|
||||
{new Date(record.submitTime).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
{record.auditTime && (
|
||||
<div>
|
||||
<Label className="text-xs">审核时间</Label>
|
||||
<div className="mt-1.5 text-base">
|
||||
{new Date(record.auditTime).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{record.auditor && (
|
||||
<div>
|
||||
<Label className="text-xs">审核人</Label>
|
||||
<div className="mt-1.5 text-base">
|
||||
{record.auditor}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{record.reason && (
|
||||
<div className="col-span-2 pt-4 mt-2 border-t">
|
||||
<Label className="text-xs">驳回原因</Label>
|
||||
<div className="mt-1.5 text-base text-red-800">
|
||||
{record.reason}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{record.remarks && (
|
||||
<div className="col-span-2 pt-4 mt-2 border-t">
|
||||
<Label className="text-xs">审核备注</Label>
|
||||
<div className="mt-1.5 text-base">
|
||||
{record.remarks}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{/* 审核流程时间线 */}
|
||||
<div className="pt-6 border-t">
|
||||
<h4 className="mb-4 font-bold">审核流程</h4>
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-start gap-3">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-600 mt-2" />
|
||||
<div>
|
||||
<div className="text-sm">提交审核</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(record.submitTime).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{record.auditTime && (
|
||||
<div className="flex items-start gap-3">
|
||||
<div className={`w-2 h-2 rounded-full mt-2 ${
|
||||
record.result === 'approved' ? 'bg-green-600' : 'bg-red-600'
|
||||
}`} />
|
||||
<div>
|
||||
<div className="text-sm">
|
||||
{record.result === 'approved' ? '审核通过' : '审核驳回'}
|
||||
</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
{new Date(record.auditTime).toLocaleString('zh-CN')}
|
||||
{record.auditor && ` · ${record.auditor}`}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Search } from 'lucide-react';
|
||||
import { FilterOptions } from '../types';
|
||||
|
||||
interface AuditHistoryFiltersProps {
|
||||
filters: FilterOptions;
|
||||
onFiltersChange: (filters: FilterOptions) => void;
|
||||
}
|
||||
|
||||
export function AuditHistoryFilters({ filters, onFiltersChange }: AuditHistoryFiltersProps) {
|
||||
const updateFilter = (key: keyof FilterOptions, value: string) => {
|
||||
onFiltersChange({
|
||||
...filters,
|
||||
[key]: value
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className="p-4">
|
||||
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
|
||||
<div className="md:col-span-2">
|
||||
<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={filters.searchKeyword}
|
||||
onChange={(e) => updateFilter('searchKeyword', e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={filters.typeFilter} onValueChange={(value) => updateFilter('typeFilter', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="审核类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="register">注册审核</SelectItem>
|
||||
<SelectItem value="update">变更审核</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={filters.resultFilter} onValueChange={(value) => updateFilter('resultFilter', value)}>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="审核结果" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部结果</SelectItem>
|
||||
<SelectItem value="pending">待审核</SelectItem>
|
||||
<SelectItem value="approved">已通过</SelectItem>
|
||||
<SelectItem value="rejected">已驳回</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div className="mt-4">
|
||||
<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={filters.dateRange === option.value ? 'default' : 'outline'}
|
||||
size="sm"
|
||||
onClick={() => updateFilter('dateRange', option.value)}
|
||||
>
|
||||
{option.label}
|
||||
</Button>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Calendar } from 'lucide-react';
|
||||
|
||||
export function AuditHistoryInstructions() {
|
||||
return (
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<h4 className="text-blue-900 mb-2">
|
||||
<Calendar className="w-4 h-4 inline mr-2" />
|
||||
审核历史说明
|
||||
</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li>• 完整记录所有企业的注册和变更审核历史</li>
|
||||
<li>• 支持按企业名称、审核时间、审核结果等多维度筛选</li>
|
||||
<li>• 包含审核人员、审核时间、审核结果及备注等完整信息</li>
|
||||
<li>• 提供审核流程时间线,清晰展示审核进度</li>
|
||||
<li>• 支持导出审核记录,满足审计和稽查需求</li>
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,93 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Eye } from 'lucide-react';
|
||||
import { AuditRecord, AuditStatus } from '../types';
|
||||
|
||||
interface AuditHistoryListProps {
|
||||
records: AuditRecord[];
|
||||
onViewDetail: (record: AuditRecord) => void;
|
||||
}
|
||||
|
||||
export function AuditHistoryList({ records, onViewDetail }: AuditHistoryListProps) {
|
||||
const getResultBadge = (result: AuditStatus) => {
|
||||
switch (result) {
|
||||
case 'pending':
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||
case 'approved':
|
||||
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
||||
case 'rejected':
|
||||
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||
default:
|
||||
return <Badge>{result}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getTypeBadge = (type: string) => {
|
||||
switch (type) {
|
||||
case 'register':
|
||||
return <Badge className="bg-blue-100 text-blue-700">注册审核</Badge>;
|
||||
case 'update':
|
||||
return <Badge className="bg-purple-100 text-purple-700">变更审核</Badge>;
|
||||
default:
|
||||
return <Badge>{type}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>企业名称</TableHead>
|
||||
<TableHead>审核类型</TableHead>
|
||||
<TableHead>提交时间</TableHead>
|
||||
<TableHead>审核时间</TableHead>
|
||||
<TableHead>审核人</TableHead>
|
||||
<TableHead>审核结果</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{records.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={7} className="text-center text-muted-foreground py-8">
|
||||
暂无审核记录
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
records.map((record) => (
|
||||
<TableRow key={record.id}>
|
||||
<TableCell className="font-medium">{record.enterpriseName}</TableCell>
|
||||
<TableCell>{getTypeBadge(record.auditType)}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(record.submitTime).toLocaleString('zh-CN')}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{record.auditTime
|
||||
? new Date(record.auditTime).toLocaleString('zh-CN')
|
||||
: '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-muted-foreground">{record.auditor || '-'}</TableCell>
|
||||
<TableCell>{getResultBadge(record.result)}</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetail(record)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { AuditHistoryStats, AuditRecord } from '../types';
|
||||
|
||||
interface AuditHistoryStatsCardsProps {
|
||||
records: AuditRecord[];
|
||||
}
|
||||
|
||||
export function AuditHistoryStatsCards({ records }: AuditHistoryStatsCardsProps) {
|
||||
const stats: AuditHistoryStats[] = [
|
||||
{
|
||||
label: '总审核数',
|
||||
value: records.length,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
{
|
||||
label: '已通过',
|
||||
value: records.filter(r => r.result === 'approved').length,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-100',
|
||||
},
|
||||
{
|
||||
label: '已驳回',
|
||||
value: records.filter(r => r.result === 'rejected').length,
|
||||
color: 'text-red-600',
|
||||
bg: 'bg-red-100',
|
||||
},
|
||||
{
|
||||
label: '待审核',
|
||||
value: records.filter(r => r.result === 'pending').length,
|
||||
color: 'text-yellow-600',
|
||||
bg: 'bg-yellow-100',
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 ${stat.color} text-2xl font-semibold`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
/**
|
||||
* 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 {
|
||||
// 调用计数器
|
||||
console.log(`[API] fetchAuditLogs 调用次数: ${++fetchAuditLogs.callCount || (fetchAuditLogs.callCount = 1)}`, params);
|
||||
|
||||
// 构建查询参数对象
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
666
src/app/(app)/central-config/tenant/audit-history/page.tsx
Normal file
666
src/app/(app)/central-config/tenant/audit-history/page.tsx
Normal file
@@ -0,0 +1,666 @@
|
||||
/**
|
||||
* 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 {
|
||||
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 = {
|
||||
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 });
|
||||
};
|
||||
|
||||
|
||||
// 合并所有状态变化,统一处理数据加载
|
||||
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 'SUBMIT':
|
||||
return <Badge className="bg-blue-100 text-blue-700">提交审核</Badge>;
|
||||
case 'AUDIT':
|
||||
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>;
|
||||
case 'draft':
|
||||
return <Badge className="bg-gray-100 text-gray-700">草稿</Badge>;
|
||||
default:
|
||||
return <Badge variant="outline">{result}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800 dark:text-green-400">审核历史</h2>
|
||||
<p className="text-muted-foreground">追溯查询全部企业的历史审核记录</p>
|
||||
</div>
|
||||
</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.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="SUBMIT">提交审核</SelectItem>
|
||||
<SelectItem value="AUDIT">审核操作</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>
|
||||
<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 && (
|
||||
<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>审核时间</TableHead>
|
||||
<TableHead>审核人</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.enterpriseName}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getActionBadge(record.action)}</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{record.action === 'SUBMIT' ? record.submitTime : '-'}
|
||||
</TableCell>
|
||||
<TableCell className="text-sm text-muted-foreground">
|
||||
{record.action === 'AUDIT' ? record.actionTime : '-'}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex items-center gap-2">
|
||||
<User className="w-4 h-4 text-gray-500" />
|
||||
<span>{record.actionBy || '-'}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>{getResultBadge(record.auditStatus)}</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.enterpriseName}</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 === 'SUBMIT' ? state.selectedRecord.submitTime : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>审核时间</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
{state.selectedRecord.action === 'AUDIT' ? state.selectedRecord.actionTime : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>审核人</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.actionBy || '-'}</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.changeSummary || '-'}
|
||||
</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.auditComment || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 企业快照 */}
|
||||
<TabsContent value="snapshot" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>企业类型</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.companyType || '-'}</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.detailedAddress || '-'}</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.contactPhone || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>企业规模</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.companyScale || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>注册资本</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.registeredCapital || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>社会信用代码</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
<code className="text-sm">{state.selectedRecord.snapshot.socialCreditCode || '-'}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>法人名称</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.legalPersonName || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>银行账号</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
<code className="text-sm">{state.selectedRecord.snapshot.bankAccount || '-'}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>开户行</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.bankName || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>开户行全称</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.bankFullName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行地址</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.snapshot.bankAddress || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<TabsContent value="system" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>记录ID</Label>
|
||||
<div className="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.enterpriseId || '-'}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>IP地址</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.ipAddress || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>用户代理</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md text-sm">
|
||||
{state.selectedRecord.userAgent || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>请求ID</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
<code className="text-sm">{state.selectedRecord.requestId || '-'}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>创建时间</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{state.selectedRecord.createdAt}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
)}
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: false })}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
525
src/app/(app)/central-config/tenant/audit-history/page.tsx.tmp
Normal file
525
src/app/(app)/central-config/tenant/audit-history/page.tsx.tmp
Normal 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>
|
||||
107
src/app/(app)/central-config/tenant/audit-history/types.ts
Normal file
107
src/app/(app)/central-config/tenant/audit-history/types.ts
Normal file
@@ -0,0 +1,107 @@
|
||||
// 审核历史相关类型定义
|
||||
|
||||
export interface Enterprise {
|
||||
id: string;
|
||||
// 企业基本信息
|
||||
name: string;
|
||||
type: string; // 企业类型
|
||||
province: string;
|
||||
city: string;
|
||||
district?: string;
|
||||
|
||||
// 其他信息
|
||||
companySize?: string; // 公司规模
|
||||
registeredCapital?: string; // 注册资本
|
||||
establishmentDate?: string; // 成立时间
|
||||
invoiceType?: string; // 发票类型
|
||||
socialCreditCode: string; // 社会信用代码
|
||||
businessScope?: string; // 经营范围
|
||||
businessLicense?: string; // 营业执照(图片URL)
|
||||
|
||||
// 开户信息
|
||||
bankAccount?: string; // 银行账号
|
||||
bankName?: string; // 开户行
|
||||
bankFullName?: string; // 开户行全称
|
||||
bankAddress?: string; // 开户行地址
|
||||
bankLicense?: string; // 开户许可证(图片URL)
|
||||
|
||||
// 法人信息
|
||||
legalPerson?: string; // 法人名称
|
||||
idCardFront?: string; // 身份证正面(图片URL)
|
||||
idCardBack?: string; // 身份证反面(图片URL)
|
||||
|
||||
// 联系信息
|
||||
registrant: string;
|
||||
contactPhone: string;
|
||||
address: string;
|
||||
|
||||
// 系统信息
|
||||
status: EnterpriseStatus;
|
||||
auditStatus: AuditStatus;
|
||||
auditReason?: string;
|
||||
auditTime?: string;
|
||||
auditor?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type EnterpriseStatus = 'active' | 'inactive' | 'suspended';
|
||||
export type AuditStatus = 'pending' | 'approved' | 'rejected' | 'draft';
|
||||
|
||||
// 审核记录(基于API响应结构)
|
||||
export interface AuditRecord {
|
||||
id: string;
|
||||
enterpriseId?: string;
|
||||
enterpriseName: string;
|
||||
action: 'SUBMIT' | 'AUDIT';
|
||||
auditType: 'register' | 'update';
|
||||
submitTime: string;
|
||||
actionTime: string;
|
||||
actionBy: string;
|
||||
result: AuditStatus;
|
||||
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 interface AuditHistoryStats {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
export interface FilterOptions {
|
||||
searchKeyword: string;
|
||||
resultFilter: string;
|
||||
typeFilter: string;
|
||||
dateRange: string;
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
/**
|
||||
* filekorolheader: 企业审核统计卡片组件 - 统计数据展示界面
|
||||
* 功能:待审核、已通过、已驳回、总企业数统计展示
|
||||
* 路径:/central-config/tenant/enterprise-audit/components/AuditStatsCards
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { Enterprise } from './enterpriseAuditApi';
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
interface AuditStatsCardsProps {
|
||||
enterprises: Enterprise[];
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function AuditStatsCards({
|
||||
enterprises,
|
||||
loading = false
|
||||
}: AuditStatsCardsProps) {
|
||||
const stats = [
|
||||
{
|
||||
label: '待审核',
|
||||
value: enterprises.filter(e => e.auditStatus === '待审核').length,
|
||||
color: 'text-yellow-600',
|
||||
bg: 'bg-yellow-100',
|
||||
},
|
||||
{
|
||||
label: '已通过',
|
||||
value: enterprises.filter(e => e.auditStatus === '已通过').length,
|
||||
color: 'text-green-600',
|
||||
bg: 'bg-green-100',
|
||||
},
|
||||
{
|
||||
label: '已驳回',
|
||||
value: enterprises.filter(e => e.auditStatus === '已驳回').length,
|
||||
color: 'text-red-600',
|
||||
bg: 'bg-red-100',
|
||||
},
|
||||
{
|
||||
label: '总企业数',
|
||||
value: enterprises.length,
|
||||
color: 'text-blue-600',
|
||||
bg: 'bg-blue-100',
|
||||
},
|
||||
];
|
||||
|
||||
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 (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,297 @@
|
||||
/**
|
||||
* filekorolheader: 企业详情对话框组件 - 企业详情展示和审核操作界面
|
||||
* 功能:企业详细信息展示、多标签页布局、审核操作界面
|
||||
* 路径:/central-config/tenant/enterprise-audit/components/EnterpriseDetailDialog
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { Enterprise } from './enterpriseAuditApi';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Building2, FileText, CreditCard, User, CheckCircle, XCircle } from 'lucide-react';
|
||||
|
||||
interface EnterpriseDetailDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
enterprise: Enterprise | null;
|
||||
auditReason: string;
|
||||
onAuditReasonChange: (reason: string) => void;
|
||||
onApprove: () => void;
|
||||
onReject: () => void;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function EnterpriseDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
enterprise,
|
||||
auditReason,
|
||||
onAuditReasonChange,
|
||||
onApprove,
|
||||
onReject,
|
||||
loading = false
|
||||
}: EnterpriseDetailDialogProps) {
|
||||
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>;
|
||||
}
|
||||
};
|
||||
|
||||
if (!enterprise) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[80vw] max-w-6xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle>企业详情审核</DialogTitle>
|
||||
<div className="flex gap-2">
|
||||
{getAuditStatusBadge(enterprise.auditStatus)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogDescription className="sr-only">
|
||||
查看企业的详细信息和进行审核操作
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<ScrollArea className="max-h-[calc(90vh-200px)]">
|
||||
<Tabs defaultValue="basic" className="space-y-4">
|
||||
<TabsList className="grid grid-cols-4 w-full">
|
||||
<TabsTrigger value="basic">
|
||||
<Building2 className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="other">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
其他信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="bank">
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
开户信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="legal">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
法人信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<TabsContent value="basic" className="space-y-6">
|
||||
{/* 企业基本信息 */}
|
||||
<div className="space-y-4">
|
||||
<h4 className="font-semibold text-lg">企业基本信息</h4>
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>企业名称</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>企业类型</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.type}</div>
|
||||
</div>
|
||||
<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 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 value="other" 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">{enterprise.companySize || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>注册资本</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registeredCapital || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>成立时间</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.establishmentDate || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>发票类型</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.invoiceType || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>社会信用代码</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
<code className="text-sm font-mono">{enterprise.socialCreditCode || '-'}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>经营范围</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
|
||||
{enterprise.businessScope || '-'}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 开户信息 */}
|
||||
<TabsContent value="bank" 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">
|
||||
{enterprise.bankAccount ? (
|
||||
<code className="text-sm font-mono">{enterprise.bankAccount}</code>
|
||||
) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>开户行</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行全称</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankFullName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行地址</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.bankAddress || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 法人信息 */}
|
||||
<TabsContent value="legal" 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">{enterprise.legalPerson || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>登记人</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{enterprise.registrant || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
</ScrollArea>
|
||||
|
||||
{/* 底部操作按钮 */}
|
||||
<div className="flex justify-end gap-3 mt-6 pt-4 border-t">
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)} disabled={loading}>
|
||||
关闭
|
||||
</Button>
|
||||
{enterprise.auditStatus === '待审核' && (
|
||||
<>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={onReject}
|
||||
disabled={loading}
|
||||
>
|
||||
<XCircle className="w-4 h-4 mr-2" />
|
||||
驳回
|
||||
</Button>
|
||||
<Button
|
||||
onClick={onApprove}
|
||||
disabled={loading}
|
||||
>
|
||||
<CheckCircle className="w-4 h-4 mr-2" />
|
||||
通过
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Eye } from 'lucide-react';
|
||||
import { Enterprise, AuditStatus } from '../types';
|
||||
|
||||
interface EnterpriseListProps {
|
||||
enterprises: Enterprise[];
|
||||
onViewDetail: (enterprise: Enterprise) => void;
|
||||
}
|
||||
|
||||
export function EnterpriseList({ enterprises, onViewDetail }: EnterpriseListProps) {
|
||||
const getAuditStatusBadge = (status: AuditStatus) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||
case 'approved':
|
||||
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
||||
case 'rejected':
|
||||
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<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={8} 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">{enterprise.socialCreditCode}</code>
|
||||
</TableCell>
|
||||
<TableCell>{enterprise.legalPerson || '-'}</TableCell>
|
||||
<TableCell>{`${enterprise.province} ${enterprise.city}`}</TableCell>
|
||||
<TableCell>{getAuditStatusBadge(enterprise.auditStatus)}</TableCell>
|
||||
<TableCell className="text-muted-foreground">
|
||||
{new Date(enterprise.createdAt).toLocaleDateString('zh-CN')}
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetail(enterprise)}
|
||||
>
|
||||
<Eye className="w-4 h-4 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Search } from 'lucide-react';
|
||||
|
||||
interface SearchFiltersProps {
|
||||
searchKeyword: string;
|
||||
setSearchKeyword: (value: string) => void;
|
||||
statusFilter: string;
|
||||
setStatusFilter: (value: string) => void;
|
||||
}
|
||||
|
||||
export function SearchFilters({
|
||||
searchKeyword,
|
||||
setSearchKeyword,
|
||||
statusFilter,
|
||||
setStatusFilter
|
||||
}: SearchFiltersProps) {
|
||||
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) => setSearchKeyword(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={statusFilter} onValueChange={setStatusFilter}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="pending">待审核</SelectItem>
|
||||
<SelectItem value="approved">已通过</SelectItem>
|
||||
<SelectItem value="rejected">已驳回</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
484
src/app/(app)/central-config/tenant/enterprise-audit/page.tsx
Normal file
484
src/app/(app)/central-config/tenant/enterprise-audit/page.tsx
Normal file
@@ -0,0 +1,484 @@
|
||||
/**
|
||||
* filekorolheader: 企业审核页面 - 企业注册审核管理页面
|
||||
* 功能:企业审核列表、搜索筛选、审核操作、详情查看
|
||||
* 路径:/central-config/tenant/enterprise-audit
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,模块化组件,SearchFormPagination重构
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useReducer, useEffect, useMemo, useRef, useCallback } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Building2, RefreshCw, Eye, Check, X } from 'lucide-react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
|
||||
import { SearchFormPagination, type SearchFieldConfig, type TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||
import { fetchTenantsForAudit, auditTenant, transformTenantData, TenantsQueryParams, Enterprise } from './components/enterpriseAuditApi';
|
||||
import { AuditStatsCards } from './components/AuditStatsCards';
|
||||
import { EnterpriseDetailDialog } from './components/EnterpriseDetailDialog';
|
||||
|
||||
// 审核状态管理
|
||||
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() {
|
||||
const [state, dispatch] = useReducer(auditReducer, initialState);
|
||||
const isFirstLoad = useRef(true);
|
||||
|
||||
// 搜索字段配置
|
||||
const searchFields: SearchFieldConfig[] = [
|
||||
{
|
||||
key: 'search',
|
||||
label: '搜索',
|
||||
type: 'text',
|
||||
placeholder: '搜索企业名称、编码...',
|
||||
},
|
||||
{
|
||||
key: 'audit_status',
|
||||
label: '审核状态',
|
||||
type: 'select',
|
||||
defaultValue: 'all',
|
||||
options: [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: '待审核', label: '待审核' },
|
||||
{ value: '已通过', label: '已通过' },
|
||||
{ value: '已驳回', label: '已驳回' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnConfig[] = [
|
||||
{
|
||||
key: 'name',
|
||||
label: '企业名称',
|
||||
sortable: false, // 禁用排序
|
||||
render: (value: string) => (
|
||||
<div className="font-medium text-foreground">{value}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'code',
|
||||
label: '企业编码',
|
||||
sortable: false, // 禁用排序
|
||||
render: (value: string) => (
|
||||
<div className="font-mono text-sm text-muted-foreground">{value}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'auditStatus',
|
||||
label: '审核状态',
|
||||
sortable: false, // 禁用排序
|
||||
render: (value: string) => {
|
||||
const statusConfig = {
|
||||
'待审核': { label: '待审核', variant: 'default' as const, className: 'bg-yellow-100 text-yellow-800 dark:bg-yellow-900 dark:text-yellow-200' },
|
||||
'已通过': { label: '已通过', variant: 'default' as const, className: 'bg-green-100 text-green-800 dark:bg-green-900 dark:text-green-200' },
|
||||
'已驳回': { label: '已驳回', variant: 'default' as const, className: 'bg-red-100 text-red-800 dark:bg-red-900 dark:text-red-200' },
|
||||
};
|
||||
|
||||
const config = statusConfig[value as keyof typeof statusConfig] || statusConfig['待审核'];
|
||||
return (
|
||||
<Badge className={`font-light ${config.className}`}>
|
||||
{config.label}
|
||||
</Badge>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'contactPerson',
|
||||
label: '联系人',
|
||||
sortable: false, // 禁用排序
|
||||
},
|
||||
{
|
||||
key: 'contactPhone',
|
||||
label: '联系电话',
|
||||
sortable: false, // 禁用排序
|
||||
render: (value: string) => (
|
||||
<div className="font-mono text-sm">{value || '-'}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: '创建时间',
|
||||
sortable: false, // 禁用排序
|
||||
render: (value: string) => (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
{value ? new Date(value).toLocaleDateString('zh-CN') : '-'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
sortable: false, // 操作列不能排序
|
||||
render: (_: any, row: Enterprise) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleViewDetail(row)}
|
||||
className="h-8 px-2"
|
||||
title="查看"
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 加载企业数据 - 移除依赖项,通过参数传递状态
|
||||
const loadEnterprises = useCallback(async (params?: {
|
||||
filters?: Record<string, string>;
|
||||
pagination?: { page: number; size: number };
|
||||
sort?: { sortBy?: string; sortOrder: 'asc' | 'desc' };
|
||||
resetPage?: boolean;
|
||||
}) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
|
||||
const finalParams: TenantsQueryParams = {
|
||||
search: (params?.filters?.search ?? state.filters.search) || undefined,
|
||||
audit_status: params?.filters?.audit_status ?? state.filters.audit_status,
|
||||
page: params?.resetPage ? 1 : (params?.pagination?.page || state.pagination.page),
|
||||
size: params?.pagination?.size || state.pagination.size,
|
||||
order_by: params?.sort?.sortBy,
|
||||
sort_order: params?.sort?.sortOrder,
|
||||
};
|
||||
|
||||
// 处理audit_status,如果为'all'则不传该参数
|
||||
if (finalParams.audit_status === 'all') {
|
||||
finalParams.audit_status = undefined;
|
||||
}
|
||||
|
||||
const response = await fetchTenantsForAudit(finalParams);
|
||||
const transformedData = response.data.map(transformTenantData);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_ENTERPRISES',
|
||||
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 enterprises for audit:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '加载企业审核数据失败';
|
||||
dispatch({ type: 'SET_ERROR', payload: errorMessage });
|
||||
toast.error(errorMessage);
|
||||
}
|
||||
}, []); // 移除所有依赖,使用参数传递状态变化
|
||||
|
||||
// 首次加载数据 - 使用事件驱动,避免useEffect
|
||||
const initializeData = useCallback(() => {
|
||||
if (isFirstLoad.current) {
|
||||
isFirstLoad.current = false;
|
||||
loadEnterprises({ resetPage: true });
|
||||
}
|
||||
}, [loadEnterprises]);
|
||||
|
||||
// 页面加载时初始化数据
|
||||
useEffect(() => {
|
||||
initializeData();
|
||||
}, []); // 只在组件挂载时执行一次
|
||||
|
||||
// 计算统计数据
|
||||
const stats = useMemo(() => ({
|
||||
total: state.pagination.total,
|
||||
pending: state.enterprises.filter(e => e.auditStatus === '待审核').length,
|
||||
approved: state.enterprises.filter(e => e.auditStatus === '已通过').length,
|
||||
rejected: state.enterprises.filter(e => e.auditStatus === '已驳回').length,
|
||||
}), [state.enterprises, state.pagination.total]);
|
||||
|
||||
// 事件处理器
|
||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||
dispatch({ type: 'SET_FILTERS', payload: filters });
|
||||
loadEnterprises({
|
||||
filters,
|
||||
pagination: { page: 1, size: state.pagination.size }
|
||||
});
|
||||
}, [loadEnterprises, state.pagination.size]);
|
||||
|
||||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } });
|
||||
loadEnterprises({
|
||||
filters: state.filters,
|
||||
sort: { sortBy, sortOrder },
|
||||
resetPage: true
|
||||
});
|
||||
}, [loadEnterprises, state.filters]);
|
||||
|
||||
const handlePageChange = useCallback((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 } });
|
||||
loadEnterprises({
|
||||
filters: state.filters,
|
||||
pagination: { page, size: state.pagination.size }
|
||||
});
|
||||
}, [loadEnterprises, state.filters, state.pagination.size, state.pagination.totalPages]);
|
||||
|
||||
const handleSizeChange = useCallback((size: number) => {
|
||||
dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } });
|
||||
loadEnterprises({
|
||||
filters: state.filters,
|
||||
pagination: { page: 1, size }
|
||||
});
|
||||
}, [loadEnterprises, state.filters]);
|
||||
|
||||
const handleRefresh = useCallback(() => {
|
||||
dispatch({ type: 'REFRESH_DATA' });
|
||||
loadEnterprises({ resetPage: true });
|
||||
toast.success('数据已刷新');
|
||||
}, [loadEnterprises]);
|
||||
|
||||
const handleViewDetail = (enterprise: Enterprise) => {
|
||||
dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
|
||||
dispatch({ type: 'SET_AUDIT_REASON', payload: '' });
|
||||
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const handleAuditReasonChange = (value: string) => {
|
||||
dispatch({ type: 'SET_AUDIT_REASON', payload: value });
|
||||
};
|
||||
|
||||
const handleApprove = async () => {
|
||||
if (!state.selectedEnterprise) return;
|
||||
|
||||
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('审核通过');
|
||||
|
||||
// 立即刷新列表,无需延迟
|
||||
loadEnterprises({ resetPage: true });
|
||||
} 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('请填写驳回原因');
|
||||
return;
|
||||
}
|
||||
|
||||
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('已驳回');
|
||||
|
||||
// 立即刷新列表,无需延迟
|
||||
loadEnterprises({ resetPage: true });
|
||||
} 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 (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题和描述 */}
|
||||
<div>
|
||||
<h2 className="text-green-800">企业审核</h2>
|
||||
<p className="text-muted-foreground">管理企业注册与变更审核流程</p>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 - 保留原有功能 */}
|
||||
<AuditStatsCards
|
||||
enterprises={state.enterprises}
|
||||
loading={state.loading}
|
||||
/>
|
||||
|
||||
{/* 搜索、表格和分页 - 使用重构后的组件 */}
|
||||
<SearchFormPagination
|
||||
formTitle="企业列表"
|
||||
formRightContent={
|
||||
<Button variant="outline" onClick={handleRefresh} disabled={state.loading}>
|
||||
<RefreshCw className={`w-4 h-4 mr-2 ${state.loading ? 'animate-spin' : ''}`} />
|
||||
刷新
|
||||
</Button>
|
||||
}
|
||||
searchFields={searchFields}
|
||||
columns={columns}
|
||||
data={state.enterprises}
|
||||
loading={state.loading}
|
||||
error={state.error}
|
||||
pagination={state.pagination}
|
||||
sortBy={state.sortBy}
|
||||
sortOrder={state.sortOrder}
|
||||
onPageChange={handlePageChange}
|
||||
onSizeChange={handleSizeChange}
|
||||
onSearch={handleSearch}
|
||||
onSort={handleSort}
|
||||
emptyIcon={<Building2 className="w-12 h-12" />}
|
||||
emptyText="暂无企业审核数据"
|
||||
showSizeSelector={true}
|
||||
showPageInfo={true}
|
||||
sizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
|
||||
{/* 企业详情对话框 - 保留原有功能 */}
|
||||
<EnterpriseDetailDialog
|
||||
open={state.showDetailDialog}
|
||||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||||
enterprise={state.selectedEnterprise}
|
||||
auditReason={state.auditReason}
|
||||
onAuditReasonChange={handleAuditReasonChange}
|
||||
onApprove={handleApprove}
|
||||
onReject={handleReject}
|
||||
loading={state.actionLoading}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// 企业审核相关类型定义
|
||||
|
||||
export interface Enterprise {
|
||||
id: string;
|
||||
// 企业基本信息
|
||||
name: string;
|
||||
type: string; // 企业类型
|
||||
province: string;
|
||||
city: string;
|
||||
district?: string;
|
||||
|
||||
// 其他信息
|
||||
companySize?: string; // 公司规模
|
||||
registeredCapital?: string; // 注册资本
|
||||
establishmentDate?: string; // 成立时间
|
||||
invoiceType?: string; // 发票类型
|
||||
socialCreditCode: string; // 社会信用代码
|
||||
businessScope?: string; // 经营范围
|
||||
businessLicense?: string; // 营业执照(图片URL)
|
||||
|
||||
// 开户信息
|
||||
bankAccount?: string; // 银行账号
|
||||
bankName?: string; // 开户行
|
||||
bankFullName?: string; // 开户行全称
|
||||
bankAddress?: string; // 开户行地址
|
||||
bankLicense?: string; // 开户许可证(图片URL)
|
||||
|
||||
// 法人信息
|
||||
legalPerson?: string; // 法人名称
|
||||
idCardFront?: string; // 身份证正面(图片URL)
|
||||
idCardBack?: string; // 身份证反面(图片URL)
|
||||
|
||||
// 联系信息
|
||||
registrant: string;
|
||||
contactPhone: string;
|
||||
address: string;
|
||||
|
||||
// 系统信息
|
||||
status: EnterpriseStatus;
|
||||
auditStatus: AuditStatus;
|
||||
auditReason?: string;
|
||||
auditTime?: string;
|
||||
auditor?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type EnterpriseStatus = 'active' | 'inactive' | 'suspended';
|
||||
export type AuditStatus = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
// 审核记录
|
||||
export interface AuditRecord {
|
||||
id: string;
|
||||
enterpriseId: string;
|
||||
enterpriseName: string;
|
||||
auditType: 'register' | 'update';
|
||||
submitTime: string;
|
||||
auditTime?: string;
|
||||
auditor?: string;
|
||||
result: AuditStatus;
|
||||
reason?: string;
|
||||
remarks?: string;
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
export interface AuditStats {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Enterprise, AuditStatus } from '../types';
|
||||
|
||||
interface AuditStatusAlertProps {
|
||||
enterprise: Enterprise;
|
||||
}
|
||||
|
||||
export function AuditStatusAlert({ enterprise }: AuditStatusAlertProps) {
|
||||
const getAuditStatusBadge = (status: AuditStatus) => {
|
||||
switch (status) {
|
||||
case 'pending':
|
||||
return <Badge className="bg-yellow-100 text-yellow-700">待审核</Badge>;
|
||||
case 'approved':
|
||||
return <Badge className="bg-green-100 text-green-700">已通过</Badge>;
|
||||
case 'rejected':
|
||||
return <Badge className="bg-red-100 text-red-700">已驳回</Badge>;
|
||||
default:
|
||||
return <Badge>{status}</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Card className={`p-4 ${
|
||||
enterprise.auditStatus === 'pending' ? 'bg-yellow-50 border-yellow-200' :
|
||||
enterprise.auditStatus === 'rejected' ? 'bg-red-50 border-red-200' :
|
||||
'bg-green-50 border-green-200'
|
||||
}`}>
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<div className="flex items-center gap-2">
|
||||
<span>当前审核状态:</span>
|
||||
{getAuditStatusBadge(enterprise.auditStatus)}
|
||||
</div>
|
||||
{enterprise.auditStatus === 'rejected' && enterprise.auditReason && (
|
||||
<p className="text-sm text-red-700 mt-2">
|
||||
驳回原因:{enterprise.auditReason}
|
||||
</p>
|
||||
)}
|
||||
{enterprise.auditStatus === 'pending' && (
|
||||
<p className="text-sm text-yellow-700 mt-2">
|
||||
信息修改后需要管理员重新审核
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
{enterprise.auditTime && (
|
||||
<div className="text-sm text-muted-foreground">
|
||||
审核时间:{new Date(enterprise.auditTime).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,132 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { Enterprise } from '../types';
|
||||
|
||||
interface BankInfoFormProps {
|
||||
enterprise: Enterprise;
|
||||
isEditing: boolean;
|
||||
formData: Partial<Enterprise>;
|
||||
onFieldChange: (field: keyof Enterprise, value: any) => void;
|
||||
onImageUpload: (field: keyof Enterprise) => void;
|
||||
}
|
||||
|
||||
export function BankInfoForm({
|
||||
enterprise,
|
||||
isEditing,
|
||||
formData,
|
||||
onFieldChange,
|
||||
onImageUpload
|
||||
}: BankInfoFormProps) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-6 pb-4 border-b">开户信息</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>银行账号</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.bankAccount || ''}
|
||||
onChange={(e) => onFieldChange('bankAccount', e.target.value)}
|
||||
placeholder="请输入银行账号"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">
|
||||
{enterprise.bankAccount ? (
|
||||
<code className="text-sm font-mono">
|
||||
{enterprise.bankAccount}
|
||||
</code>
|
||||
) : '-'}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>开户行</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.bankName || ''}
|
||||
onChange={(e) => onFieldChange('bankName', e.target.value)}
|
||||
placeholder="如:中国工商银行"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.bankName || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>开户行全称</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.bankFullName || ''}
|
||||
onChange={(e) => onFieldChange('bankFullName', e.target.value)}
|
||||
placeholder="如:中国工商银行股份有限公司北京中关村支行"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.bankFullName || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>开户行地址</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.bankAddress || ''}
|
||||
onChange={(e) => onFieldChange('bankAddress', e.target.value)}
|
||||
placeholder="请输入开户行地址"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.bankAddress || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>开户许可证</Label>
|
||||
<div className="mt-2">
|
||||
{enterprise.bankLicense ? (
|
||||
<div className="relative inline-block">
|
||||
<img
|
||||
src={enterprise.bankLicense}
|
||||
alt="开户许可证"
|
||||
className="w-48 h-auto border rounded-lg"
|
||||
/>
|
||||
{isEditing && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={() => onImageUpload('bankLicense')}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
重新上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
isEditing ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onImageUpload('bankLicense')}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
上传开户许可证
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Enterprise } from '../types';
|
||||
|
||||
interface BasicInfoFormProps {
|
||||
enterprise: Enterprise;
|
||||
isEditing: boolean;
|
||||
formData: Partial<Enterprise>;
|
||||
onFieldChange: (field: keyof Enterprise, value: any) => void;
|
||||
}
|
||||
|
||||
export function BasicInfoForm({
|
||||
enterprise,
|
||||
isEditing,
|
||||
formData,
|
||||
onFieldChange
|
||||
}: BasicInfoFormProps) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-6 pb-4 border-b">企业基本信息</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>企业名称 *</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => onFieldChange('name', e.target.value)}
|
||||
placeholder="请输入企业名称"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.name}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>企业类型 *</Label>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={formData.type || ''}
|
||||
onValueChange={(value) => onFieldChange('type', value)}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="选择企业类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="个体工商户">个体工商户</SelectItem>
|
||||
<SelectItem value="有限责任公司">有限责任公司</SelectItem>
|
||||
<SelectItem value="股份有限公司">股份有限公司</SelectItem>
|
||||
<SelectItem value="合伙企业">合伙企业</SelectItem>
|
||||
<SelectItem value="其他">其他</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.type}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>所在地区 *</Label>
|
||||
{isEditing ? (
|
||||
<div className="grid grid-cols-2 gap-2 mt-2">
|
||||
<Input
|
||||
value={formData.province || ''}
|
||||
onChange={(e) => onFieldChange('province', e.target.value)}
|
||||
placeholder="省份"
|
||||
/>
|
||||
<Input
|
||||
value={formData.city || ''}
|
||||
onChange={(e) => onFieldChange('city', e.target.value)}
|
||||
placeholder="城市"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.province} {enterprise.city}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>详细地址 *</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.address || ''}
|
||||
onChange={(e) => onFieldChange('address', e.target.value)}
|
||||
placeholder="请输入详细地址"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.address}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>登记人</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.registrant || ''}
|
||||
onChange={(e) => onFieldChange('registrant', e.target.value)}
|
||||
placeholder="请输入登记人姓名"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.registrant}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>联系电话 *</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.contactPhone || ''}
|
||||
onChange={(e) => onFieldChange('contactPhone', e.target.value)}
|
||||
placeholder="请输入联系电话"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.contactPhone}</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Edit, Save, X } from 'lucide-react';
|
||||
|
||||
interface EnterpriseInfoHeaderProps {
|
||||
isEditing: boolean;
|
||||
loading?: boolean;
|
||||
onEdit: () => void;
|
||||
onCancel: () => void;
|
||||
onSave: () => void;
|
||||
}
|
||||
|
||||
export function EnterpriseInfoHeader({
|
||||
isEditing,
|
||||
loading = false,
|
||||
onEdit,
|
||||
onCancel,
|
||||
onSave
|
||||
}: EnterpriseInfoHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800">企业信息</h2>
|
||||
<p className="text-muted-foreground">查看和管理本企业的完整注册信息</p>
|
||||
</div>
|
||||
{!isEditing ? (
|
||||
<Button onClick={onEdit}>
|
||||
<Edit className="w-4 h-4 mr-2" />
|
||||
编辑信息
|
||||
</Button>
|
||||
) : (
|
||||
<div className="flex gap-2">
|
||||
<Button variant="outline" onClick={onCancel} disabled={loading}>
|
||||
<X className="w-4 h-4 mr-2" />
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onSave} disabled={loading}>
|
||||
<Save className="w-4 h-4 mr-2" />
|
||||
{loading ? '保存中...' : '提交审核'}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,122 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Upload, Image as ImageIcon } from 'lucide-react';
|
||||
import { Enterprise } from '../types';
|
||||
|
||||
interface LegalInfoFormProps {
|
||||
enterprise: Enterprise;
|
||||
isEditing: boolean;
|
||||
formData: Partial<Enterprise>;
|
||||
onFieldChange: (field: keyof Enterprise, value: any) => void;
|
||||
onImageUpload: (field: keyof Enterprise) => void;
|
||||
}
|
||||
|
||||
export function LegalInfoForm({
|
||||
enterprise,
|
||||
isEditing,
|
||||
formData,
|
||||
onFieldChange,
|
||||
onImageUpload
|
||||
}: LegalInfoFormProps) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-6 pb-4 border-b">法人信息</h3>
|
||||
<div className="grid grid-cols-1 gap-6">
|
||||
<div>
|
||||
<Label>法人名称</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.legalPerson || ''}
|
||||
onChange={(e) => onFieldChange('legalPerson', e.target.value)}
|
||||
placeholder="请输入法人姓名"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.legalPerson || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>身份证正面</Label>
|
||||
<div className="mt-2">
|
||||
{enterprise.idCardFront ? (
|
||||
<div className="relative inline-block">
|
||||
<img
|
||||
src={enterprise.idCardFront}
|
||||
alt="身份证正面"
|
||||
className="w-64 h-auto border rounded-lg"
|
||||
/>
|
||||
{isEditing && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={() => onImageUpload('idCardFront')}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
重新上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
isEditing ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onImageUpload('idCardFront')}
|
||||
>
|
||||
<ImageIcon className="w-4 h-4 mr-2" />
|
||||
上传身份证正面
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>身份证反面</Label>
|
||||
<div className="mt-2">
|
||||
{enterprise.idCardBack ? (
|
||||
<div className="relative inline-block">
|
||||
<img
|
||||
src={enterprise.idCardBack}
|
||||
alt="身份证反面"
|
||||
className="w-64 h-auto border rounded-lg"
|
||||
/>
|
||||
{isEditing && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={() => onImageUpload('idCardBack')}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
重新上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
isEditing ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onImageUpload('idCardBack')}
|
||||
>
|
||||
<ImageIcon className="w-4 h-4 mr-2" />
|
||||
上传身份证反面
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { FileText } from 'lucide-react';
|
||||
|
||||
export function OperationTips() {
|
||||
return (
|
||||
<Card className="p-4 bg-blue-50 border-blue-200">
|
||||
<h4 className="text-blue-900 mb-2">
|
||||
<FileText className="w-4 h-4 inline mr-2" />
|
||||
温馨提示
|
||||
</h4>
|
||||
<ul className="space-y-1 text-sm text-blue-800">
|
||||
<li>• 企业信息修改后需要提交至超级管理员重新审核</li>
|
||||
<li>• 请确保填写的信息真实准确,与营业执照等证件保持一致</li>
|
||||
<li>• 社会信用代码为18位,请仔细核对</li>
|
||||
<li>• 图片上传功能需要对接后端服务,目前仅为演示</li>
|
||||
<li>• 如有疑问,请联系系统管理员</li>
|
||||
</ul>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Textarea } from '@/components/ui/textarea';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Upload } from 'lucide-react';
|
||||
import { Enterprise } from '../types';
|
||||
|
||||
interface OtherInfoFormProps {
|
||||
enterprise: Enterprise;
|
||||
isEditing: boolean;
|
||||
formData: Partial<Enterprise>;
|
||||
onFieldChange: (field: keyof Enterprise, value: any) => void;
|
||||
onImageUpload: (field: keyof Enterprise) => void;
|
||||
}
|
||||
|
||||
export function OtherInfoForm({
|
||||
enterprise,
|
||||
isEditing,
|
||||
formData,
|
||||
onFieldChange,
|
||||
onImageUpload
|
||||
}: OtherInfoFormProps) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-6 pb-4 border-b">其他信息</h3>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>公司规模</Label>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={formData.companySize || ''}
|
||||
onValueChange={(value) => onFieldChange('companySize', value)}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="选择公司规模" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="1-50人">1-50人</SelectItem>
|
||||
<SelectItem value="50-200人">50-200人</SelectItem>
|
||||
<SelectItem value="200-500人">200-500人</SelectItem>
|
||||
<SelectItem value="500-1000人">500-1000人</SelectItem>
|
||||
<SelectItem value="1000人以上">1000人以上</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.companySize || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>注册资本</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.registeredCapital || ''}
|
||||
onChange={(e) => onFieldChange('registeredCapital', e.target.value)}
|
||||
placeholder="如:1000万元"
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.registeredCapital || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>成立时间</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
type="date"
|
||||
value={formData.establishmentDate || ''}
|
||||
onChange={(e) => onFieldChange('establishmentDate', e.target.value)}
|
||||
className="mt-2"
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.establishmentDate || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<Label>发票类型</Label>
|
||||
{isEditing ? (
|
||||
<Select
|
||||
value={formData.invoiceType || ''}
|
||||
onValueChange={(value) => onFieldChange('invoiceType', value)}
|
||||
>
|
||||
<SelectTrigger className="mt-2">
|
||||
<SelectValue placeholder="选择发票类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="增值税专用发票">增值税专用发票</SelectItem>
|
||||
<SelectItem value="增值税普通发票">增值税普通发票</SelectItem>
|
||||
<SelectItem value="电子发票">电子发票</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.invoiceType || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>社会信用代码 *</Label>
|
||||
{isEditing ? (
|
||||
<Input
|
||||
value={formData.socialCreditCode || ''}
|
||||
onChange={(e) => onFieldChange('socialCreditCode', e.target.value)}
|
||||
placeholder="请输入18位社会信用代码"
|
||||
className="mt-2"
|
||||
maxLength={18}
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">
|
||||
<code className="text-sm font-mono">
|
||||
{enterprise.socialCreditCode}
|
||||
</code>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>经营范围</Label>
|
||||
{isEditing ? (
|
||||
<Textarea
|
||||
value={formData.businessScope || ''}
|
||||
onChange={(e) => onFieldChange('businessScope', e.target.value)}
|
||||
placeholder="请输入经营范围"
|
||||
className="mt-2"
|
||||
rows={4}
|
||||
/>
|
||||
) : (
|
||||
<div className="field-value">{enterprise.businessScope || '-'}</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
<div className="md:col-span-2">
|
||||
<Label>营业执照</Label>
|
||||
<div className="mt-2">
|
||||
{enterprise.businessLicense ? (
|
||||
<div className="relative inline-block">
|
||||
<img
|
||||
src={enterprise.businessLicense}
|
||||
alt="营业执照"
|
||||
className="w-48 h-auto border rounded-lg"
|
||||
/>
|
||||
{isEditing && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="mt-2"
|
||||
onClick={() => onImageUpload('businessLicense')}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
重新上传
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
isEditing ? (
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={() => onImageUpload('businessLicense')}
|
||||
>
|
||||
<Upload className="w-4 h-4 mr-2" />
|
||||
上传营业执照
|
||||
</Button>
|
||||
) : (
|
||||
<span className="text-muted-foreground">未上传</span>
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Enterprise } from '../types';
|
||||
|
||||
interface SystemInfoProps {
|
||||
enterprise: Enterprise;
|
||||
}
|
||||
|
||||
export function SystemInfo({ enterprise }: SystemInfoProps) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<h3 className="mb-6 pb-4 border-b">系统信息</h3>
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-6">
|
||||
<div>
|
||||
<Label>企业ID</Label>
|
||||
<div className="field-value">
|
||||
<code className="text-xs font-mono">{enterprise.id}</code>
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>注册时间</Label>
|
||||
<div className="field-value">
|
||||
{new Date(enterprise.createdAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>最后更新</Label>
|
||||
<div className="field-value">
|
||||
{new Date(enterprise.updatedAt).toLocaleString('zh-CN')}
|
||||
</div>
|
||||
</div>
|
||||
{enterprise.auditor && (
|
||||
<div>
|
||||
<Label>审核人</Label>
|
||||
<div className="field-value">{enterprise.auditor}</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,220 @@
|
||||
/**
|
||||
* filekorolheader: 企业信息API接口 - 企业详细信息获取和更新接口服务
|
||||
* 功能:API请求封装、数据转换、错误处理、企业信息管理
|
||||
* 路径:/central-config/tenant/enterprise-info/components/enterpriseInfoApi
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { getAuthToken } from "@/utils/token";
|
||||
import { getCurrentTenantApiV1TenantsMeGet, submitTenantAuditApiV1TenantsSubmitPost } from "@/lib/api/sdk.gen";
|
||||
import { Enterprise } from '../types';
|
||||
|
||||
// API返回的租户数据类型(根据实际API返回定义)
|
||||
export interface TenantApiData {
|
||||
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;
|
||||
}
|
||||
|
||||
// 提交审核请求参数接口
|
||||
export interface SubmitAuditRequest {
|
||||
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;
|
||||
bank_permit_image: string | null;
|
||||
social_credit_code: string | null;
|
||||
business_license_image: string | null;
|
||||
legal_person_name: string | null;
|
||||
id_card_front_image: string | null;
|
||||
id_card_back_image: string | null;
|
||||
company_scale: string | null;
|
||||
registered_capital: number | null;
|
||||
established_date: string | null;
|
||||
invoice_type: string | null;
|
||||
business_scope: string | null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取企业详细信息
|
||||
*/
|
||||
export async function fetchEnterpriseInfo(tenantId: string): Promise<Enterprise | null> {
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
console.log('🏢 获取企业信息API调用,租户ID:', tenantId);
|
||||
|
||||
const response = await getCurrentTenantApiV1TenantsMeGet({
|
||||
path: {
|
||||
tenant_id: tenantId,
|
||||
},
|
||||
headers: token ? {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
throw new Error(`API error: ${response.error.message || 'Unknown error'}`);
|
||||
}
|
||||
|
||||
const data = response.data as TenantApiData;
|
||||
console.log('🏢 获取企业信息API响应:', data);
|
||||
|
||||
return transformTenantData(data);
|
||||
} catch (error) {
|
||||
console.error('🏢 获取企业信息失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新企业信息
|
||||
* 注意:此功能待开发,当前不做任何操作
|
||||
*/
|
||||
export async function updateEnterpriseInfo(tenantId: string, formData: Partial<Enterprise>): Promise<Enterprise | null> {
|
||||
console.log('🏢 更新企业信息功能待开发,租户ID:', tenantId, '数据:', formData);
|
||||
|
||||
// 暂时返回null,等待更新API接口开发完成
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 提交企业审核
|
||||
* @param tenantId 租户ID
|
||||
* @param data 提交审核的数据
|
||||
* @returns 提交结果
|
||||
*/
|
||||
export async function submitEnterpriseAudit(tenantId: string, data: SubmitAuditRequest): Promise<void> {
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
console.log('🏢 提交企业审核API调用,租户ID:', tenantId, '数据:', data);
|
||||
|
||||
const response = await submitTenantAuditApiV1TenantsSubmitPost({
|
||||
path: {
|
||||
tenant_id: tenantId,
|
||||
},
|
||||
body: data,
|
||||
headers: token ? {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error('🏢 提交企业审核API错误:', response.error);
|
||||
throw new Error(`提交审核失败: ${response.error.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
console.log('🏢 提交企业审核API成功');
|
||||
} catch (error) {
|
||||
console.error('🏢 提交企业审核失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将API数据转换为页面所需的企业数据格式
|
||||
*/
|
||||
export function transformTenantData(tenant: TenantApiData): Enterprise {
|
||||
return {
|
||||
id: tenant.id,
|
||||
name: tenant.company_name || '',
|
||||
type: tenant.company_type || '未分类',
|
||||
province: tenant.province || '',
|
||||
city: tenant.city || '',
|
||||
district: tenant.district || undefined,
|
||||
companySize: tenant.company_scale || undefined,
|
||||
registeredCapital: tenant.registered_capital || undefined,
|
||||
establishmentDate: tenant.established_date ?
|
||||
new Date(tenant.established_date).toLocaleDateString('zh-CN') : undefined,
|
||||
invoiceType: tenant.invoice_type || undefined,
|
||||
socialCreditCode: tenant.social_credit_code || '',
|
||||
businessScope: tenant.business_scope || undefined,
|
||||
bankAccount: tenant.bank_account || undefined,
|
||||
bankName: tenant.bank_name || undefined,
|
||||
bankFullName: tenant.bank_full_name || undefined,
|
||||
bankAddress: tenant.bank_address || undefined,
|
||||
legalPerson: tenant.legal_person_name || undefined,
|
||||
registrant: tenant.registrant || '',
|
||||
contactPhone: tenant.contact_phone || '',
|
||||
address: tenant.detailed_address || '',
|
||||
status: tenant.is_active ? 'active' : 'inactive',
|
||||
auditStatus: mapAuditStatus(tenant.audit_status),
|
||||
auditReason: tenant.audit_comment || undefined,
|
||||
auditTime: tenant.audit_time ? formatDate(tenant.audit_time) : undefined,
|
||||
auditor: tenant.auditor || undefined,
|
||||
createdAt: formatDate(tenant.created_at),
|
||||
updatedAt: formatDate(tenant.updated_at),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 映射审核状态
|
||||
*/
|
||||
function mapAuditStatus(status: string): Enterprise['auditStatus'] {
|
||||
switch (status) {
|
||||
case '未提交':
|
||||
case '草稿':
|
||||
return 'pending';
|
||||
case '待审核':
|
||||
return 'pending';
|
||||
case '已通过':
|
||||
case '审核通过':
|
||||
return 'approved';
|
||||
case '已拒绝':
|
||||
case '已驳回':
|
||||
return 'rejected';
|
||||
default:
|
||||
return 'pending';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
269
src/app/(app)/central-config/tenant/enterprise-info/page.tsx
Normal file
269
src/app/(app)/central-config/tenant/enterprise-info/page.tsx
Normal file
@@ -0,0 +1,269 @@
|
||||
'use client';
|
||||
|
||||
import { useState, useEffect } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { Building, FileText, CreditCard, User } from 'lucide-react';
|
||||
|
||||
import { EnterpriseInfoHeader } from './components/EnterpriseInfoHeader';
|
||||
import { AuditStatusAlert } from './components/AuditStatusAlert';
|
||||
import { BasicInfoForm } from './components/BasicInfoForm';
|
||||
import { OtherInfoForm } from './components/OtherInfoForm';
|
||||
import { BankInfoForm } from './components/BankInfoForm';
|
||||
import { LegalInfoForm } from './components/LegalInfoForm';
|
||||
import { SystemInfo } from './components/SystemInfo';
|
||||
import { OperationTips } from './components/OperationTips';
|
||||
import { Enterprise } from './types';
|
||||
import { getAuthUser } from '@/stores/modules/auth';
|
||||
import { fetchEnterpriseInfo, updateEnterpriseInfo, submitEnterpriseAudit, SubmitAuditRequest } from './components/enterpriseInfoApi';
|
||||
|
||||
export default function EnterpriseInfoPage() {
|
||||
const [enterprise, setEnterprise] = useState<Enterprise | null>(null);
|
||||
const [isEditing, setIsEditing] = useState(false);
|
||||
const [formData, setFormData] = useState<Partial<Enterprise>>({});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [currentUser, setCurrentUser] = useState<any>(null);
|
||||
|
||||
useEffect(() => {
|
||||
// 获取用户信息
|
||||
const userInfo = getAuthUser();
|
||||
console.log('🏢 从 Zustand 获取的用户信息:', userInfo);
|
||||
console.log('🏢 用户租户 ID (tenant_id):', userInfo?.tenant_id);
|
||||
|
||||
if (!userInfo?.tenant_id) {
|
||||
toast.error('无法获取用户租户信息,请重新登录');
|
||||
return;
|
||||
}
|
||||
|
||||
setCurrentUser(userInfo);
|
||||
|
||||
// 加载企业信息
|
||||
loadEnterpriseInfo(userInfo.tenant_id);
|
||||
}, []);
|
||||
|
||||
const loadEnterpriseInfo = async (tenantId: string) => {
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🏢 开始加载企业信息,租户ID:', tenantId);
|
||||
|
||||
// 调用API获取企业信息
|
||||
const enterpriseData = await fetchEnterpriseInfo(tenantId);
|
||||
|
||||
if (enterpriseData) {
|
||||
setEnterprise(enterpriseData);
|
||||
setFormData(enterpriseData);
|
||||
console.log('🏢 企业信息加载成功:', enterpriseData);
|
||||
} else {
|
||||
console.log('🏢 企业信息为空,显示空状态');
|
||||
setEnterprise(null);
|
||||
setFormData({});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
console.error('🏢 加载企业信息失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '加载企业信息失败';
|
||||
toast.error(errorMessage);
|
||||
setEnterprise(null);
|
||||
setFormData({});
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleEdit = () => {
|
||||
setIsEditing(true);
|
||||
};
|
||||
|
||||
const handleCancel = () => {
|
||||
setIsEditing(false);
|
||||
if (enterprise) {
|
||||
setFormData(enterprise);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
if (!currentUser?.tenant_id) {
|
||||
toast.error('无法获取用户租户信息,请重新登录');
|
||||
return;
|
||||
}
|
||||
|
||||
// 验证必填字段
|
||||
if (!formData.name?.trim()) {
|
||||
toast.error('请填写企业名称');
|
||||
return;
|
||||
}
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🏢 开始提交企业审核');
|
||||
|
||||
// 构建提交审核的数据
|
||||
const submitData: SubmitAuditRequest = {
|
||||
company_name: formData.name || '',
|
||||
company_type: formData.type || null,
|
||||
province: formData.province || null,
|
||||
city: formData.city || null,
|
||||
district: formData.district || null,
|
||||
detailed_address: formData.address || null,
|
||||
registrant: formData.registrant || null,
|
||||
contact_phone: formData.contactPhone || null,
|
||||
bank_account: formData.bankAccount || null,
|
||||
bank_name: formData.bankName || null,
|
||||
bank_full_name: formData.bankFullName || null,
|
||||
bank_address: formData.bankAddress || null,
|
||||
bank_permit_image: null, // 暂时设为null,等待图片上传功能
|
||||
social_credit_code: formData.socialCreditCode || null,
|
||||
business_license_image: null, // 暂时设为null,等待图片上传功能
|
||||
legal_person_name: formData.legalPerson || null,
|
||||
id_card_front_image: null, // 暂时设为null,等待图片上传功能
|
||||
id_card_back_image: null, // 暂时设为null,等待图片上传功能
|
||||
company_scale: formData.companySize || null,
|
||||
registered_capital: formData.registeredCapital ? parseFloat(formData.registeredCapital.toString()) : null,
|
||||
established_date: formData.establishmentDate || null,
|
||||
invoice_type: formData.invoiceType || null,
|
||||
business_scope: formData.businessScope || null,
|
||||
};
|
||||
|
||||
// 调用API提交审核
|
||||
await submitEnterpriseAudit(currentUser.tenant_id, submitData);
|
||||
|
||||
// 提交成功,退出编辑模式
|
||||
setIsEditing(false);
|
||||
toast.success('企业信息已提交审核,请等待管理员审核');
|
||||
|
||||
// 重新加载企业信息以获取最新状态
|
||||
await loadEnterpriseInfo(currentUser.tenant_id);
|
||||
|
||||
} catch (error) {
|
||||
console.error('🏢 提交企业审核失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '提交审核失败';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleChange = (field: keyof Enterprise, value: any) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
const handleImageUpload = (field: keyof Enterprise) => {
|
||||
// 图片上传功能需要对接后端服务
|
||||
toast.info('图片上传功能正在开发中');
|
||||
};
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-green-800">企业信息</h2>
|
||||
<p className="text-muted-foreground">查看和管理本企业的完整注册信息</p>
|
||||
</div>
|
||||
<Card className="p-8 text-center text-muted-foreground">
|
||||
加载中...
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
if (!enterprise) {
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h2 className="text-green-800">企业信息</h2>
|
||||
<p className="text-muted-foreground">查看和管理本企业的完整注册信息</p>
|
||||
</div>
|
||||
<Card className="p-8 text-center text-muted-foreground">
|
||||
{loading ? '加载中...' : '暂无企业信息或加载失败,请刷新页面重试'}
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
<EnterpriseInfoHeader
|
||||
isEditing={isEditing}
|
||||
loading={loading}
|
||||
onEdit={handleEdit}
|
||||
onCancel={handleCancel}
|
||||
onSave={handleSave}
|
||||
/>
|
||||
|
||||
{/* 审核状态提示 */}
|
||||
<AuditStatusAlert enterprise={enterprise} />
|
||||
|
||||
{/* 分标签页展示信息 */}
|
||||
<Tabs defaultValue="basic" className="space-y-4">
|
||||
<TabsList className="grid grid-cols-4 w-full max-w-2xl">
|
||||
<TabsTrigger value="basic">
|
||||
<Building className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="other">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
其他信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="bank">
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
开户信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="legal">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
法人信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 企业基本信息 */}
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<BasicInfoForm
|
||||
enterprise={enterprise}
|
||||
isEditing={isEditing}
|
||||
formData={formData}
|
||||
onFieldChange={handleChange}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* 其他信息 */}
|
||||
<TabsContent value="other" className="space-y-4">
|
||||
<OtherInfoForm
|
||||
enterprise={enterprise}
|
||||
isEditing={isEditing}
|
||||
formData={formData}
|
||||
onFieldChange={handleChange}
|
||||
onImageUpload={handleImageUpload}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* 开户信息 */}
|
||||
<TabsContent value="bank" className="space-y-4">
|
||||
<BankInfoForm
|
||||
enterprise={enterprise}
|
||||
isEditing={isEditing}
|
||||
formData={formData}
|
||||
onFieldChange={handleChange}
|
||||
onImageUpload={handleImageUpload}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
{/* 法人信息 */}
|
||||
<TabsContent value="legal" className="space-y-4">
|
||||
<LegalInfoForm
|
||||
enterprise={enterprise}
|
||||
isEditing={isEditing}
|
||||
formData={formData}
|
||||
onFieldChange={handleChange}
|
||||
onImageUpload={handleImageUpload}
|
||||
/>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<SystemInfo enterprise={enterprise} />
|
||||
|
||||
{/* 操作提示 */}
|
||||
<OperationTips />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
55
src/app/(app)/central-config/tenant/enterprise-info/types.ts
Normal file
55
src/app/(app)/central-config/tenant/enterprise-info/types.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
// 企业信息相关类型定义
|
||||
|
||||
export interface Enterprise {
|
||||
id: string;
|
||||
// 企业基本信息
|
||||
name: string;
|
||||
type: string; // 企业类型
|
||||
province: string;
|
||||
city: string;
|
||||
district?: string;
|
||||
|
||||
// 其他信息
|
||||
companySize?: string; // 公司规模
|
||||
registeredCapital?: string; // 注册资本
|
||||
establishmentDate?: string; // 成立时间
|
||||
invoiceType?: string; // 发票类型
|
||||
socialCreditCode: string; // 社会信用代码
|
||||
businessScope?: string; // 经营范围
|
||||
businessLicense?: string; // 营业执照(图片URL)
|
||||
|
||||
// 开户信息
|
||||
bankAccount?: string; // 银行账号
|
||||
bankName?: string; // 开户行
|
||||
bankFullName?: string; // 开户行全称
|
||||
bankAddress?: string; // 开户行地址
|
||||
bankLicense?: string; // 开户许可证(图片URL)
|
||||
|
||||
// 法人信息
|
||||
legalPerson?: string; // 法人名称
|
||||
idCardFront?: string; // 身份证正面(图片URL)
|
||||
idCardBack?: string; // 身份证反面(图片URL)
|
||||
|
||||
// 联系信息
|
||||
registrant: string;
|
||||
contactPhone: string;
|
||||
address: string;
|
||||
|
||||
// 系统信息
|
||||
status: EnterpriseStatus;
|
||||
auditStatus: AuditStatus;
|
||||
auditReason?: string;
|
||||
auditTime?: string;
|
||||
auditor?: string;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
}
|
||||
|
||||
export type EnterpriseStatus = 'active' | 'inactive' | 'suspended';
|
||||
export type AuditStatus = 'pending' | 'approved' | 'rejected';
|
||||
|
||||
// 编辑状态
|
||||
export type EditingState = {
|
||||
isEditing: boolean;
|
||||
formData: Partial<Enterprise>;
|
||||
};
|
||||
@@ -0,0 +1,212 @@
|
||||
/**
|
||||
* filekorolheader: 新建企业弹窗组件 - 企业创建表单弹窗
|
||||
* 功能:企业信息表单、数据验证、API调用、状态管理
|
||||
* 路径:/central-config/tenant/enterprise-management/components/CreateEnterpriseDialog
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn语义化样式,TypeScript类型安全
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { useState } from 'react';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Building2, Hash } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
import { createEnterprise, CreateEnterpriseRequest } from './enterpriseApi';
|
||||
|
||||
interface CreateEnterpriseDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
onSuccess: () => void;
|
||||
}
|
||||
|
||||
export function CreateEnterpriseDialog({ open, onOpenChange, onSuccess }: CreateEnterpriseDialogProps) {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [formData, setFormData] = useState<CreateEnterpriseRequest>({
|
||||
company_name: '',
|
||||
tenant_code: '',
|
||||
company_type: '',
|
||||
});
|
||||
|
||||
// 企业类型选项
|
||||
const companyTypes = [
|
||||
'个体工商户',
|
||||
'有限责任公司',
|
||||
'股份有限公司',
|
||||
'合伙企业',
|
||||
'其他',
|
||||
];
|
||||
|
||||
// 重置表单
|
||||
const resetForm = () => {
|
||||
setFormData({
|
||||
company_name: '',
|
||||
tenant_code: '',
|
||||
company_type: '',
|
||||
});
|
||||
};
|
||||
|
||||
// 关闭弹窗
|
||||
const handleClose = () => {
|
||||
if (!loading) {
|
||||
onOpenChange(false);
|
||||
resetForm();
|
||||
}
|
||||
};
|
||||
|
||||
// 处理输入变化
|
||||
const handleInputChange = (field: keyof CreateEnterpriseRequest, value: string) => {
|
||||
setFormData(prev => ({ ...prev, [field]: value }));
|
||||
};
|
||||
|
||||
// 表单验证
|
||||
const validateForm = (): boolean => {
|
||||
if (!formData.company_name.trim()) {
|
||||
toast.error('请输入企业名称');
|
||||
return false;
|
||||
}
|
||||
if (!formData.tenant_code.trim()) {
|
||||
toast.error('请输入企业编码');
|
||||
return false;
|
||||
}
|
||||
if (!formData.company_type.trim()) {
|
||||
toast.error('请选择企业类型');
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
// 提交表单
|
||||
const handleSubmit = async () => {
|
||||
if (!validateForm()) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
console.log('🏢 开始创建企业:', formData);
|
||||
|
||||
// 调用API创建企业
|
||||
const result = await createEnterprise(formData);
|
||||
|
||||
console.log('🏢 企业创建成功:', result);
|
||||
toast.success('企业创建成功!');
|
||||
|
||||
// 关闭弹窗并重置表单
|
||||
handleClose();
|
||||
|
||||
// 调用成功回调,刷新主页面数据
|
||||
onSuccess();
|
||||
|
||||
} catch (error) {
|
||||
console.error('🏢 创建企业失败:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '创建企业失败,请稍后重试';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={handleClose}>
|
||||
<DialogContent className="sm:max-w-[500px]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Building2 className="w-5 h-5 text-blue-600 dark:text-blue-400" />
|
||||
新建企业
|
||||
</DialogTitle>
|
||||
<DialogDescription>
|
||||
创建新企业账号,填写基本信息后系统将自动生成企业实例
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-6 py-4">
|
||||
{/* 企业名称 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company_name">企业名称 *</Label>
|
||||
<div className="relative">
|
||||
<Building2 className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
id="company_name"
|
||||
placeholder="请输入企业全称"
|
||||
value={formData.company_name}
|
||||
onChange={(e) => handleInputChange('company_name', e.target.value)}
|
||||
className="pl-10"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 企业编码 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="tenant_code">企业编码 *</Label>
|
||||
<div className="relative">
|
||||
<Hash className="absolute left-3 top-1/2 transform -translate-y-1/2 w-4 h-4 text-gray-400" />
|
||||
<Input
|
||||
id="tenant_code"
|
||||
placeholder="请输入企业唯一编码,如:SHNY001"
|
||||
value={formData.tenant_code}
|
||||
onChange={(e) => handleInputChange('tenant_code', e.target.value.toUpperCase())}
|
||||
className="pl-10"
|
||||
disabled={loading}
|
||||
/>
|
||||
</div>
|
||||
<p className="text-xs text-muted-foreground">
|
||||
编码创建后不可修改,请谨慎填写
|
||||
</p>
|
||||
</div>
|
||||
|
||||
{/* 企业类型 */}
|
||||
<div className="space-y-2">
|
||||
<Label htmlFor="company_type">企业类型 *</Label>
|
||||
<Select
|
||||
value={formData.company_type}
|
||||
onValueChange={(value) => handleInputChange('company_type', value)}
|
||||
disabled={loading}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择企业类型" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{companyTypes.map((type) => (
|
||||
<SelectItem key={type} value={type}>
|
||||
{type}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
|
||||
{/* 温馨提示 */}
|
||||
<div className="p-4 bg-blue-50 dark:bg-blue-950 border border-blue-200 dark:border-blue-800 rounded-lg">
|
||||
<p className="text-sm text-blue-800 dark:text-blue-200 font-medium mb-2">温馨提示:</p>
|
||||
<ul className="text-sm text-blue-700 dark:text-blue-300 space-y-1">
|
||||
<li>• 企业创建后默认为待审核状态</li>
|
||||
<li>• 企业编码创建后不可修改,请确保准确无误</li>
|
||||
<li>• 创建成功后需要等待管理员审核</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="outline"
|
||||
onClick={handleClose}
|
||||
disabled={loading}
|
||||
className="font-light"
|
||||
>
|
||||
取消
|
||||
</Button>
|
||||
<Button
|
||||
onClick={handleSubmit}
|
||||
disabled={loading}
|
||||
className="font-light"
|
||||
>
|
||||
{loading ? '创建中...' : '创建企业'}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,352 @@
|
||||
/**
|
||||
* filekorolheader: 企业管理API接口 - 企业数据查询接口服务
|
||||
* 功能:API请求封装、数据转换、错误处理、分页查询
|
||||
* 路径:/central-config/tenant/enterprise-management/components/enterpriseApi
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||
*/
|
||||
|
||||
// API响应数据类型定义
|
||||
import { getAuthToken } from "@/utils/token.ts";
|
||||
import {
|
||||
listTenantsApiV1TenantsGet,
|
||||
enableTenantApiV1TenantsTenantIdEnablePatch,
|
||||
disableTenantApiV1TenantsTenantIdDisablePatch,
|
||||
createTenantApiV1TenantsPost
|
||||
} 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 CreateEnterpriseRequest {
|
||||
company_name: string;
|
||||
tenant_code: string;
|
||||
company_type: string;
|
||||
}
|
||||
|
||||
// 企业页面数据类型(转换后的)
|
||||
export interface Enterprise {
|
||||
id: string;
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
status: 'active' | 'inactive';
|
||||
auditStatus: 'not_submitted' | 'pending' | 'approved' | 'rejected' | 'draft';
|
||||
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 fetchTenants(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();
|
||||
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;
|
||||
|
||||
// 转换响应数据格式以匹配现有的接口
|
||||
// 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:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 启用企业
|
||||
*/
|
||||
export async function enableTenant(tenantId: string): Promise<TenantData> {
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
console.log('启用企业API调用:', tenantId);
|
||||
|
||||
const response = await enableTenantApiV1TenantsTenantIdEnablePatch({
|
||||
path: {
|
||||
tenant_id: tenantId,
|
||||
},
|
||||
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);
|
||||
|
||||
// 验证返回的数据中is_active是否为true
|
||||
if (data.is_active !== true) {
|
||||
throw new Error('启用企业失败:返回数据状态不正确');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to enable tenant:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 禁用企业
|
||||
*/
|
||||
export async function disableTenant(tenantId: string): Promise<TenantData> {
|
||||
try {
|
||||
const token = getAuthToken();
|
||||
console.log('禁用企业API调用:', tenantId);
|
||||
|
||||
const response = await disableTenantApiV1TenantsTenantIdDisablePatch({
|
||||
path: {
|
||||
tenant_id: tenantId,
|
||||
},
|
||||
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);
|
||||
|
||||
// 验证返回的数据中is_active是否为false
|
||||
if (data.is_active !== false) {
|
||||
throw new Error('禁用企业失败:返回数据状态不正确');
|
||||
}
|
||||
|
||||
return data;
|
||||
} catch (error) {
|
||||
console.error('Failed to disable 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 'draft';
|
||||
case '待审核':
|
||||
return 'pending';
|
||||
case '已通过':
|
||||
case '审核通过':
|
||||
return 'approved';
|
||||
case '已拒绝':
|
||||
case '已驳回':
|
||||
return 'rejected';
|
||||
default:
|
||||
return 'draft';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建新企业
|
||||
* @param data 企业创建数据
|
||||
* @returns 创建结果
|
||||
*/
|
||||
export async function createEnterprise(data: CreateEnterpriseRequest): Promise<TenantData> {
|
||||
try {
|
||||
console.log('🏢 创建企业API调用:', data);
|
||||
|
||||
// 获取认证token
|
||||
const token = getAuthToken();
|
||||
|
||||
// 使用SDK API调用创建企业接口
|
||||
const response = await createTenantApiV1TenantsPost({
|
||||
body: data,
|
||||
headers: token ? {
|
||||
'Authorization': `Bearer ${token}`,
|
||||
} : undefined,
|
||||
});
|
||||
|
||||
if (response.error) {
|
||||
console.error('🏢 创建企业API错误:', response.error);
|
||||
throw new Error(`创建失败: ${response.error.message || '未知错误'}`);
|
||||
}
|
||||
|
||||
const result = response.data as TenantData;
|
||||
console.log('🏢 创建企业API成功:', result);
|
||||
|
||||
return result;
|
||||
|
||||
} catch (error) {
|
||||
console.error('🏢 创建企业失败:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,164 @@
|
||||
/**
|
||||
* filekorolheader: 企业管理状态管理 - 企业数据状态管理核心
|
||||
* 功能:API数据管理、分页状态、加载状态、错误处理
|
||||
* 路径:/central-config/tenant/enterprise-management/components/enterpriseReducer
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理模式
|
||||
*/
|
||||
|
||||
import { Enterprise } from './enterpriseApi';
|
||||
|
||||
export interface FormData {
|
||||
name: string;
|
||||
code: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
export interface EnterpriseState {
|
||||
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;
|
||||
};
|
||||
showAddDialog: boolean;
|
||||
showViewDialog: boolean;
|
||||
showStatusDialog: boolean;
|
||||
selectedEnterprise: Enterprise | null;
|
||||
statusAction: 'enable' | 'disable';
|
||||
formData: FormData;
|
||||
sortBy?: string;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
export type EnterpriseAction =
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_ENTERPRISES'; payload: { data: Enterprise[]; pagination: EnterpriseState['pagination'] } }
|
||||
| { type: 'SET_FILTERS'; payload: Partial<EnterpriseState['filters']> }
|
||||
| { type: 'SET_PAGINATION'; payload: Partial<EnterpriseState['pagination']> }
|
||||
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||||
| { type: 'TOGGLE_ADD_DIALOG'; payload: boolean }
|
||||
| { type: 'TOGGLE_VIEW_DIALOG'; payload: boolean }
|
||||
| { type: 'TOGGLE_STATUS_DIALOG'; payload: boolean }
|
||||
| { type: 'SET_SELECTED_ENTERPRISE'; payload: Enterprise | null }
|
||||
| { type: 'SET_STATUS_ACTION'; payload: 'enable' | 'disable' }
|
||||
| { type: 'UPDATE_FORM_DATA'; payload: Partial<FormData> }
|
||||
| { type: 'RESET_FORM_DATA' }
|
||||
| { type: 'REFRESH_DATA' };
|
||||
|
||||
// 初始状态
|
||||
export const initialState: EnterpriseState = {
|
||||
enterprises: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
},
|
||||
filters: {
|
||||
search: '',
|
||||
audit_status: '',
|
||||
},
|
||||
showAddDialog: false,
|
||||
showViewDialog: false,
|
||||
showStatusDialog: false,
|
||||
selectedEnterprise: null,
|
||||
statusAction: 'enable',
|
||||
formData: {
|
||||
name: '',
|
||||
code: '',
|
||||
type: ''
|
||||
},
|
||||
sortBy: undefined,
|
||||
sortOrder: 'desc',
|
||||
};
|
||||
|
||||
// Reducer
|
||||
export function enterpriseReducer(state: EnterpriseState, action: EnterpriseAction): EnterpriseState {
|
||||
switch (action.type) {
|
||||
case 'SET_LOADING':
|
||||
return { ...state, loading: action.payload };
|
||||
|
||||
case 'SET_ERROR':
|
||||
return { ...state, error: action.payload, loading: false };
|
||||
|
||||
case 'SET_ENTERPRISES':
|
||||
return {
|
||||
...state,
|
||||
enterprises: 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_ADD_DIALOG':
|
||||
return {
|
||||
...state,
|
||||
showAddDialog: action.payload,
|
||||
...(action.payload === false ? { formData: initialState.formData } : {})
|
||||
};
|
||||
|
||||
case 'TOGGLE_VIEW_DIALOG':
|
||||
return { ...state, showViewDialog: action.payload };
|
||||
|
||||
case 'TOGGLE_STATUS_DIALOG':
|
||||
return { ...state, showStatusDialog: action.payload };
|
||||
|
||||
case 'SET_SELECTED_ENTERPRISE':
|
||||
return { ...state, selectedEnterprise: action.payload };
|
||||
|
||||
case 'SET_STATUS_ACTION':
|
||||
return { ...state, statusAction: action.payload };
|
||||
|
||||
case 'UPDATE_FORM_DATA':
|
||||
return {
|
||||
...state,
|
||||
formData: { ...state.formData, ...action.payload }
|
||||
};
|
||||
|
||||
case 'RESET_FORM_DATA':
|
||||
return { ...state, formData: initialState.formData };
|
||||
|
||||
case 'REFRESH_DATA':
|
||||
return {
|
||||
...state,
|
||||
error: null, // 清除错误状态
|
||||
};
|
||||
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,714 @@
|
||||
/**
|
||||
* filekorolheader: 企业管理 - 企业信息管理与维护页面
|
||||
* 功能:企业列表查询、详情查看、状态管理、分页筛选
|
||||
* 路径:/central-config/tenant/enterprise-management
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用useReducer状态管理,API集成,shadcn语义化样式
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useEffect, useMemo, useState, useCallback } from 'react';
|
||||
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter } from '@/components/ui/dialog';
|
||||
import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle } from '@/components/ui/alert-dialog';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Building2, Eye, Power, PowerOff, Plus, FileText, CreditCard, User, Search } from 'lucide-react';
|
||||
import { toast } from 'sonner';
|
||||
|
||||
import SearchFormPagination, {
|
||||
type SearchFieldConfig,
|
||||
type TableColumnConfig
|
||||
} from '@/components/common/searchFormPagination';
|
||||
|
||||
import { fetchTenants, transformTenantData, enableTenant, disableTenant, createEnterprise, TenantsQueryParams, Enterprise } from './components/enterpriseApi';
|
||||
import { CreateEnterpriseDialog } from './components/CreateEnterpriseDialog';
|
||||
|
||||
// Utility functions
|
||||
const getStatusBadge = (status: 'active' | 'inactive') => {
|
||||
if (status === 'active') {
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border border-green-200 dark:border-green-800">
|
||||
启用
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-800">
|
||||
禁用
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const getAuditStatusBadge = (auditStatus: Enterprise['auditStatus']) => {
|
||||
switch (auditStatus) {
|
||||
case 'draft':
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-800">
|
||||
草稿
|
||||
</div>
|
||||
);
|
||||
case 'pending':
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-yellow-50 dark:bg-yellow-950 text-yellow-600 dark:text-yellow-400 border border-yellow-200 dark:border-yellow-800">
|
||||
待审核
|
||||
</div>
|
||||
);
|
||||
case 'approved':
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400 border border-green-200 dark:border-green-800">
|
||||
审核通过
|
||||
</div>
|
||||
);
|
||||
case 'rejected':
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400 border border-red-200 dark:border-red-800">
|
||||
已拒绝
|
||||
</div>
|
||||
);
|
||||
default:
|
||||
return (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400 border border-gray-200 dark:border-gray-800">
|
||||
草稿
|
||||
</div>
|
||||
);
|
||||
}
|
||||
};
|
||||
|
||||
export default function EnterpriseManagement() {
|
||||
// 对话框状态管理
|
||||
const [dialogs, setDialogs] = useState({
|
||||
showViewDialog: false,
|
||||
showAddDialog: false,
|
||||
showStatusDialog: false,
|
||||
selectedEnterprise: null as Enterprise | null,
|
||||
statusAction: 'enable' as 'enable' | 'disable'
|
||||
});
|
||||
|
||||
const dispatch = (action: any) => {
|
||||
switch (action.type) {
|
||||
case 'SET_SELECTED_ENTERPRISE':
|
||||
setDialogs(prev => ({ ...prev, selectedEnterprise: action.payload }));
|
||||
break;
|
||||
case 'TOGGLE_VIEW_DIALOG':
|
||||
setDialogs(prev => ({ ...prev, showViewDialog: action.payload }));
|
||||
break;
|
||||
case 'TOGGLE_ADD_DIALOG':
|
||||
setDialogs(prev => ({ ...prev, showAddDialog: action.payload }));
|
||||
break;
|
||||
case 'TOGGLE_STATUS_DIALOG':
|
||||
setDialogs(prev => ({ ...prev, showStatusDialog: action.payload }));
|
||||
break;
|
||||
case 'SET_STATUS_ACTION':
|
||||
setDialogs(prev => ({ ...prev, statusAction: action.payload }));
|
||||
break;
|
||||
case 'RESET_FORM_DATA':
|
||||
setDialogs(prev => ({ ...prev, selectedEnterprise: null }));
|
||||
break;
|
||||
}
|
||||
};
|
||||
|
||||
// 搜索字段配置
|
||||
const searchFields: SearchFieldConfig[] = [
|
||||
{
|
||||
key: 'search',
|
||||
label: '搜索',
|
||||
type: 'text',
|
||||
placeholder: '搜索企业名称、编码...',
|
||||
},
|
||||
{
|
||||
key: 'audit_status',
|
||||
label: '审核状态',
|
||||
type: 'select',
|
||||
placeholder: '审核状态',
|
||||
defaultValue: 'all',
|
||||
options: [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: '草稿', label: '草稿' },
|
||||
{ value: '待审核', label: '待审核' },
|
||||
{ value: '已通过', label: '审核通过' },
|
||||
{ value: '已拒绝', label: '已拒绝' },
|
||||
],
|
||||
},
|
||||
];
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnConfig[] = [
|
||||
{
|
||||
key: 'code',
|
||||
label: '企业编码',
|
||||
width: '120px',
|
||||
},
|
||||
{
|
||||
key: 'name',
|
||||
label: '企业名称',
|
||||
render: (value: string) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Building2 className="w-4 h-4 text-blue-500" />
|
||||
<span className="font-medium">{value}</span>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: '企业类型',
|
||||
render: (value: string) => (
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800">
|
||||
{value}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'registrant',
|
||||
label: '登记人',
|
||||
render: (value?: string) => value || '-',
|
||||
},
|
||||
{
|
||||
key: 'contactPhone',
|
||||
label: '联系电话',
|
||||
render: (value?: string) => value || '-',
|
||||
},
|
||||
{
|
||||
key: 'createdAt',
|
||||
label: '创建时间',
|
||||
width: '160px',
|
||||
},
|
||||
{
|
||||
key: 'auditStatus',
|
||||
label: '审核状态',
|
||||
render: (value: Enterprise['auditStatus']) => getAuditStatusBadge(value),
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '状态',
|
||||
render: (value: Enterprise['status']) => getStatusBadge(value),
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
render: (_: any, row: Enterprise) => (
|
||||
<div className="flex gap-2">
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
onClick={() => handleView(row)}
|
||||
>
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
查看
|
||||
</Button>
|
||||
{row.status === 'active' ? (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-gray-600 dark:text-gray-400 border-gray-300 dark:border-gray-600"
|
||||
onClick={() => handleStatusChange(row, 'disable')}
|
||||
>
|
||||
<PowerOff className="w-3 h-3 mr-1" />
|
||||
禁用
|
||||
</Button>
|
||||
) : (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="outline"
|
||||
className="text-green-600 dark:text-green-400 border-green-300 dark:border-green-600"
|
||||
onClick={() => handleStatusChange(row, 'enable')}
|
||||
>
|
||||
<Power className="w-3 h-3 mr-1" />
|
||||
启用
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
|
||||
// 简化的状态管理 - 只需要存储数据和加载状态
|
||||
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
|
||||
const [pagination, setPagination] = useState({
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
});
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({
|
||||
search: '',
|
||||
audit_status: 'all'
|
||||
});
|
||||
|
||||
// 数据加载函数 - 移除不必要的依赖避免重复调用
|
||||
const loadEnterprises = useCallback(async (params?: {
|
||||
filters?: Record<string, string>;
|
||||
pagination?: { page: number; size: number };
|
||||
sort?: { sortBy?: string; sortOrder?: 'asc' | 'desc' };
|
||||
}) => {
|
||||
try {
|
||||
console.log('调用了loadEnterprises')
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
|
||||
const finalParams: TenantsQueryParams = {
|
||||
search: (params?.filters?.search ?? searchFilters.search) || undefined,
|
||||
audit_status: params?.filters?.audit_status ?? searchFilters.audit_status,
|
||||
page: params?.pagination?.page || pagination.page,
|
||||
size: params?.pagination?.size || pagination.size,
|
||||
order_by: params?.sort?.sortBy,
|
||||
sort_order: params?.sort?.sortOrder,
|
||||
};
|
||||
|
||||
// 处理audit_status,如果为'all'则不传该参数
|
||||
if (finalParams.audit_status === 'all') {
|
||||
finalParams.audit_status = undefined;
|
||||
}
|
||||
const response = await fetchTenants(finalParams);
|
||||
const transformedData = response.data.map(transformTenantData);
|
||||
|
||||
setEnterprises(transformedData);
|
||||
setPagination({
|
||||
page: response.page,
|
||||
size: response.size,
|
||||
total: response.total,
|
||||
totalPages: response.total_pages,
|
||||
hasNext: response.has_next,
|
||||
hasPrev: response.has_prev,
|
||||
});
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : '加载企业数据失败';
|
||||
setError(errorMessage);
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}, []); // 移除所有依赖,使用参数传递状态变化
|
||||
|
||||
// 事件处理器
|
||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||
setSearchFilters(filters);
|
||||
// 搜索时重置到第一页
|
||||
loadEnterprises({
|
||||
filters,
|
||||
pagination: { page: 1, size: pagination.size }
|
||||
});
|
||||
}, [loadEnterprises, pagination.size]);
|
||||
|
||||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||
// 排序时重置到第一页
|
||||
loadEnterprises({
|
||||
pagination: { page: 1, size: pagination.size },
|
||||
sort: { sortBy, sortOrder }
|
||||
});
|
||||
}, [loadEnterprises, pagination.size]);
|
||||
|
||||
// 统一的数据重载函数 - 避免重复代码
|
||||
const reloadData = useCallback(() => {
|
||||
const reloadParams = {
|
||||
filters: searchFilters,
|
||||
pagination: {
|
||||
page: pagination.page,
|
||||
size: pagination.size
|
||||
}
|
||||
};
|
||||
loadEnterprises(reloadParams);
|
||||
}, [loadEnterprises, searchFilters, pagination]);
|
||||
|
||||
const handlePageChange = useCallback((page: number) => {
|
||||
setPagination(prev => ({ ...prev, page }));
|
||||
loadEnterprises({
|
||||
filters: searchFilters,
|
||||
pagination: { page, size: pagination.size }
|
||||
});
|
||||
}, [loadEnterprises, searchFilters, pagination.size]);
|
||||
|
||||
const handleSizeChange = useCallback((size: number) => {
|
||||
setPagination(prev => ({ ...prev, size, page: 1 }));
|
||||
loadEnterprises({
|
||||
filters: searchFilters,
|
||||
pagination: { page: 1, size }
|
||||
});
|
||||
}, [loadEnterprises, searchFilters]);
|
||||
|
||||
// 初始化数据加载
|
||||
// useEffect(() => {
|
||||
// loadEnterprises();
|
||||
// }, []);
|
||||
|
||||
// 计算统计数据
|
||||
const stats = useMemo(() => {
|
||||
if (enterprises.length === 0) {
|
||||
return { total: pagination.total, active: 0, inactive: 0 };
|
||||
}
|
||||
const active = enterprises.filter(e => e.status === 'active').length;
|
||||
const inactive = enterprises.filter(e => e.status === 'inactive').length;
|
||||
return { total: pagination.total, active, inactive };
|
||||
}, [enterprises, pagination.total]);
|
||||
|
||||
// 业务事件处理器
|
||||
const handleView = (enterprise: Enterprise) => {
|
||||
dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
|
||||
dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const handleStatusChange = (enterprise: Enterprise, action: 'enable' | 'disable') => {
|
||||
dispatch({ type: 'SET_SELECTED_ENTERPRISE', payload: enterprise });
|
||||
dispatch({ type: 'SET_STATUS_ACTION', payload: action });
|
||||
dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const confirmStatusChange = async () => {
|
||||
if (!dialogs.selectedEnterprise) return;
|
||||
|
||||
try {
|
||||
setLoading(true);
|
||||
|
||||
const tenantId = dialogs.selectedEnterprise.id;
|
||||
let updatedTenant;
|
||||
|
||||
if (dialogs.statusAction === 'enable') {
|
||||
updatedTenant = await enableTenant(tenantId);
|
||||
toast.success('企业已启用');
|
||||
} else {
|
||||
updatedTenant = await disableTenant(tenantId);
|
||||
toast.success('企业已禁用');
|
||||
}
|
||||
|
||||
// 状态更新成功后关闭对话框
|
||||
dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: false });
|
||||
|
||||
// 重新加载数据来反映状态变化
|
||||
reloadData();
|
||||
} catch (error) {
|
||||
console.error('Status change failed:', error);
|
||||
const errorMessage = error instanceof Error ? error.message : '状态更新失败';
|
||||
toast.error(errorMessage);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
dispatch({ type: 'RESET_FORM_DATA' });
|
||||
dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
const handleCreateSuccess = () => {
|
||||
// 创建成功后重新加载数据,保持当前搜索条件和分页状态
|
||||
reloadData();
|
||||
};
|
||||
|
||||
// 操作按钮配置
|
||||
const actionButtons = (
|
||||
<Button onClick={handleCreateNew} disabled={loading}>
|
||||
<Plus className="w-4 h-4 mr-2" />
|
||||
新建企业
|
||||
</Button>
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Page Header - 自定义页面头部 */}
|
||||
<Card className="p-6 bg-gradient-to-r from-blue-50 dark:from-blue-950 to-indigo-50 dark:to-indigo-950 border-blue-200 dark:border-blue-800">
|
||||
<div className="flex items-start justify-between">
|
||||
<div className="flex items-start gap-3">
|
||||
<Building2 className="w-6 h-6 text-blue-600 dark:text-blue-400 flex-shrink-0 mt-1" />
|
||||
<div className="flex-1">
|
||||
<h2 className="mb-2">企业管理</h2>
|
||||
<p className="text-sm text-muted-foreground mb-3">
|
||||
管理平台所有企业信息,支持查询、查看详情、启用/禁用企业
|
||||
</p>
|
||||
<div className="flex flex-wrap gap-2">
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800">
|
||||
<Search className="w-3 h-3 mr-1" />
|
||||
智能查询
|
||||
</div>
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800">
|
||||
<Power className="w-3 h-3 mr-1" />
|
||||
状态管理
|
||||
</div>
|
||||
<div className="inline-flex items-center rounded-full px-2.5 py-0.5 text-xs font-medium border border-gray-200 dark:border-gray-800 bg-white dark:bg-gray-800">
|
||||
<Eye className="w-3 h-3 mr-1" />
|
||||
详情查看
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* Statistics Cards - 保持原有统计功能 */}
|
||||
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
||||
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">企业总数</div>
|
||||
<Building2 className="w-5 h-5 text-blue-500" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold mb-1">{pagination.total}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
全部企业数量
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">启用企业</div>
|
||||
<Power className="w-5 h-5 text-green-500" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold mb-1 text-green-600 dark:text-green-400">{stats.active}</div>
|
||||
<div className="text-xs text-green-600 dark:text-green-400">
|
||||
正常运营中
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="p-6 bg-card hover:bg-muted transition-colors border rounded-lg">
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<div className="text-sm text-muted-foreground">禁用企业</div>
|
||||
<PowerOff className="w-5 h-5 text-gray-500" />
|
||||
</div>
|
||||
<div className="text-3xl font-bold mb-1 text-gray-600 dark:text-gray-400">{stats.inactive}</div>
|
||||
<div className="text-xs text-muted-foreground">
|
||||
已暂停使用
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 使用SearchFormPagination组件替换原有的企业列表 */}
|
||||
<SearchFormPagination
|
||||
formTitle="企业列表"
|
||||
formRightContent={actionButtons}
|
||||
searchFields={searchFields}
|
||||
columns={columns}
|
||||
data={enterprises}
|
||||
loading={loading}
|
||||
error={error}
|
||||
pagination={pagination}
|
||||
onPageChange={handlePageChange}
|
||||
onSizeChange={handleSizeChange}
|
||||
onSearch={handleSearch}
|
||||
onSort={handleSort}
|
||||
emptyIcon={<Building2 className="w-12 h-12 mx-auto mb-4 opacity-20" />}
|
||||
emptyText="暂无企业数据"
|
||||
/>
|
||||
|
||||
{/* View Enterprise Details Dialog */}
|
||||
<Dialog open={dialogs.showViewDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: open })}>
|
||||
<DialogContent className="w-[80vw] max-w-6xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle>企业详情</DialogTitle>
|
||||
{dialogs.selectedEnterprise && (
|
||||
<div className="flex gap-2">
|
||||
{getAuditStatusBadge(dialogs.selectedEnterprise.auditStatus)}
|
||||
{getStatusBadge(dialogs.selectedEnterprise.status)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<DialogDescription className="sr-only">
|
||||
查看企业的详细信息
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
{dialogs.selectedEnterprise && (
|
||||
<ScrollArea className="max-h-[calc(90vh-200px)]">
|
||||
<Tabs defaultValue="basic" className="space-y-4">
|
||||
<TabsList className="grid grid-cols-4 w-full">
|
||||
<TabsTrigger value="basic">
|
||||
<Building2 className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="other">
|
||||
<FileText className="w-4 h-4 mr-2" />
|
||||
其他信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="bank">
|
||||
<CreditCard className="w-4 h-4 mr-2" />
|
||||
开户信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="legal">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
法人信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* Basic Information */}
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>企业名称</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.name}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>企业编码</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.code}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>企业类型</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.type}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>所在地区</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">
|
||||
{dialogs.selectedEnterprise.province || '-'} {dialogs.selectedEnterprise.city || ''} {dialogs.selectedEnterprise.district || ''}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>详细地址</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.address || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>登记人</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.registrant || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>联系电话</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.contactPhone || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Other Information */}
|
||||
<TabsContent value="other" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>公司规模</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.companySize || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>注册资本</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.registeredCapital || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>成立时间</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.establishmentDate || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>发票类型</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.invoiceType || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>社会信用代码</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">
|
||||
{dialogs.selectedEnterprise.socialCreditCode ? (
|
||||
<code className="text-sm font-mono">
|
||||
{dialogs.selectedEnterprise.socialCreditCode}
|
||||
</code>
|
||||
) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>经营范围</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.businessScope || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>提交时间</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.submitTime || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>审核时间</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.auditTime || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Bank Information */}
|
||||
<TabsContent value="bank" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>银行账号</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">
|
||||
{dialogs.selectedEnterprise.bankAccount ? (
|
||||
<code className="text-sm font-mono">
|
||||
{dialogs.selectedEnterprise.bankAccount}
|
||||
</code>
|
||||
) : '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>开户行</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.bankName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行全称</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.bankFullName || '-'}</div>
|
||||
</div>
|
||||
<div className="col-span-2">
|
||||
<Label>开户行地址</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.bankAddress || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* Legal Person Information */}
|
||||
<TabsContent value="legal" className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>法人姓名</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.legalPerson || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>联系人</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.registrant || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>审核人</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.auditor || '-'}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>审核意见</Label>
|
||||
<div className="field-value p-2 bg-muted rounded">{dialogs.selectedEnterprise.auditComment || '-'}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
)}
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => dispatch({ type: 'TOGGLE_VIEW_DIALOG', payload: false })}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
{/* Status Change Confirmation Dialog */}
|
||||
<AlertDialog open={dialogs.showStatusDialog} onOpenChange={(open) => dispatch({ type: 'TOGGLE_STATUS_DIALOG', payload: open })}>
|
||||
<AlertDialogContent className="w-[80vw] max-w-md">
|
||||
<AlertDialogHeader>
|
||||
<AlertDialogTitle>
|
||||
确认{dialogs.statusAction === 'enable' ? '启用' : '禁用'}企业
|
||||
</AlertDialogTitle>
|
||||
<AlertDialogDescription>
|
||||
{dialogs.statusAction === 'enable' ? (
|
||||
<>
|
||||
启用企业 <strong>{dialogs.selectedEnterprise?.name}</strong> 后,该企业用户将恢复正常登录和使用权限。
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
禁用企业 <strong>{dialogs.selectedEnterprise?.name}</strong> 后,该企业所有用户将无法登录系统。此操作不会删除企业数据,可随时重新启用。
|
||||
</>
|
||||
)}
|
||||
</AlertDialogDescription>
|
||||
</AlertDialogHeader>
|
||||
<AlertDialogFooter>
|
||||
<AlertDialogCancel>取消</AlertDialogCancel>
|
||||
<AlertDialogAction
|
||||
onClick={confirmStatusChange}
|
||||
className={dialogs.statusAction === 'enable' ? 'bg-green-600 hover:bg-green-700' : 'bg-gray-600 hover:bg-gray-700'}
|
||||
>
|
||||
确认{dialogs.statusAction === 'enable' ? '启用' : '禁用'}
|
||||
</AlertDialogAction>
|
||||
</AlertDialogFooter>
|
||||
</AlertDialogContent>
|
||||
</AlertDialog>
|
||||
|
||||
{/* Create Enterprise Dialog */}
|
||||
<CreateEnterpriseDialog
|
||||
open={dialogs.showAddDialog}
|
||||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_ADD_DIALOG', payload: open })}
|
||||
onSuccess={handleCreateSuccess}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
30
src/app/(app)/central-config/tenant/page.tsx
Normal file
30
src/app/(app)/central-config/tenant/page.tsx
Normal file
@@ -0,0 +1,30 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import Link from 'next/link';
|
||||
|
||||
export default function TenantPage() {
|
||||
return (
|
||||
<div className="p-6">
|
||||
<h1 className="text-2xl font-bold mb-4">租户管理</h1>
|
||||
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-4">
|
||||
<Link href="/central-config/tenant/enterprise-audit" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">企业审核</h3>
|
||||
<p className="text-gray-600 text-sm">审核企业入驻申请</p>
|
||||
</Link>
|
||||
<Link href="/central-config/tenant/audit-history" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">审核历史</h3>
|
||||
<p className="text-gray-600 text-sm">查看历史审核记录</p>
|
||||
</Link>
|
||||
<Link href="/central-config/tenant/enterprise-info" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">企业信息</h3>
|
||||
<p className="text-gray-600 text-sm">管理企业基本信息</p>
|
||||
</Link>
|
||||
<Link href="/central-config/tenant/user-management" className="bg-white rounded-lg shadow p-4 hover:shadow-md transition-shadow">
|
||||
<h3 className="text-lg font-semibold mb-2">用户管理</h3>
|
||||
<p className="text-gray-600 text-sm">管理平台用户</p>
|
||||
</Link>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,288 @@
|
||||
/**
|
||||
* filekorolheader: 用户详情对话框组件 - 用户详细信息展示界面
|
||||
* 功能:用户详细信息展示、多标签页布局、状态和权限信息
|
||||
* 路径:/central-config/tenant/user-management/components/UserDetailDialog
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { User } from '../types';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { User, Mail, Phone, Calendar, Building, Shield, CheckCircle, XCircle, Clock } from 'lucide-react';
|
||||
|
||||
interface UserDetailDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
user: User | null;
|
||||
}
|
||||
|
||||
export function UserDetailDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
user
|
||||
}: UserDetailDialogProps) {
|
||||
const getStatusBadge = (isActive: boolean) => {
|
||||
return isActive ? (
|
||||
<Badge className="bg-green-100 text-green-700">正常</Badge>
|
||||
) : (
|
||||
<Badge className="bg-red-100 text-red-700">停用</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getRoleBadge = (isSuperuser: boolean) => {
|
||||
return isSuperuser ? (
|
||||
<Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>
|
||||
) : (
|
||||
<Badge className="bg-blue-100 text-blue-700">普通用户</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const getVerifiedBadge = (isVerified: boolean) => {
|
||||
return isVerified ? (
|
||||
<Badge className="bg-green-100 text-green-700">已验证</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">未验证</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
const formatDate = (dateString: 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;
|
||||
}
|
||||
};
|
||||
|
||||
if (!user) return null;
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="w-[80vw] max-w-4xl max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<div className="flex items-center justify-between pr-8">
|
||||
<DialogTitle>用户详情</DialogTitle>
|
||||
<div className="flex gap-2">
|
||||
{getRoleBadge(user.isSuperuser)}
|
||||
{getVerifiedBadge(user.isVerified)}
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogDescription className="sr-only">
|
||||
查看用户的详细信息和权限
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
|
||||
<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">
|
||||
<User className="w-4 h-4 mr-2" />
|
||||
基本信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="permissions">
|
||||
<Shield className="w-4 h-4 mr-2" />
|
||||
权限信息
|
||||
</TabsTrigger>
|
||||
<TabsTrigger value="activity">
|
||||
<Clock className="w-4 h-4 mr-2" />
|
||||
活动信息
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
{/* 基本信息 */}
|
||||
<TabsContent value="basic" className="space-y-4">
|
||||
<div className="flex items-center gap-4 mb-6">
|
||||
{user.avatarUrl ? (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.username}
|
||||
className="w-16 h-16 rounded-full"
|
||||
/>
|
||||
) : (
|
||||
<div className="w-16 h-16 rounded-full bg-gray-200 flex items-center justify-center">
|
||||
<User className="w-8 h-8 text-gray-500" />
|
||||
</div>
|
||||
)}
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{user.displayName || user.fullName || user.username}</h3>
|
||||
<p className="text-sm text-muted-foreground">@{user.username}</p>
|
||||
<div className="flex gap-2 mt-2">
|
||||
{getRoleBadge(user.isSuperuser)}
|
||||
{getVerifiedBadge(user.isVerified)}
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-6">
|
||||
<div>
|
||||
<Label>用户名</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{user.username}</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>显示名称</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
{user.displayName || user.fullName || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>邮箱</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||
<Mail className="w-4 h-4 text-muted-foreground" />
|
||||
{user.email}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>电话</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||
<Phone className="w-4 h-4 text-muted-foreground" />
|
||||
{user.phone || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>所属企业</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md flex items-center gap-2">
|
||||
<Building className="w-4 h-4 text-muted-foreground" />
|
||||
{user.companyName || '-'}
|
||||
</div>
|
||||
</div>
|
||||
<div>
|
||||
<Label>权限范围</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">
|
||||
{user.scope === 'tenant' ? '租户级' : user.scope || '-'}
|
||||
</div>
|
||||
</div>
|
||||
{user.departmentName && (
|
||||
<div>
|
||||
<Label>部门</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md">{user.departmentName}</div>
|
||||
</div>
|
||||
)}
|
||||
{user.bio && (
|
||||
<div className="col-span-2">
|
||||
<Label>个人简介</Label>
|
||||
<div className="mt-1.5 p-3 bg-gray-50 rounded-md min-h-[80px] whitespace-pre-wrap">
|
||||
{user.bio}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 权限信息 */}
|
||||
<TabsContent value="permissions" className="space-y-4">
|
||||
<div className="space-y-6">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">系统权限</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>超级管理员权限</span>
|
||||
{user.isSuperuser ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-red-600" />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>邮箱已验证</span>
|
||||
{user.isVerified ? (
|
||||
<CheckCircle className="w-5 h-5 text-green-600" />
|
||||
) : (
|
||||
<XCircle className="w-5 h-5 text-orange-600" />
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">访问状态</h4>
|
||||
<Card className="p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<span>账户状态</span>
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
{user.tenantId && (
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">关联信息</h4>
|
||||
<div className="space-y-4">
|
||||
<Card className="p-4">
|
||||
<Label>租户ID</Label>
|
||||
<div className="mt-1.5 text-sm font-mono bg-gray-100 p-2 rounded">
|
||||
{user.tenantId}
|
||||
</div>
|
||||
</Card>
|
||||
{user.departmentId && (
|
||||
<Card className="p-4">
|
||||
<Label>部门ID</Label>
|
||||
<div className="mt-1.5 text-sm font-mono bg-gray-100 p-2 rounded">
|
||||
{user.departmentId}
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</TabsContent>
|
||||
|
||||
{/* 活动信息 */}
|
||||
<TabsContent value="activity" className="space-y-4">
|
||||
<div className="space-y-4">
|
||||
<div>
|
||||
<h4 className="font-semibold mb-4">时间信息</h4>
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">创建时间</Label>
|
||||
<div className="mt-1.5 text-sm">{formatDate(user.createdAt)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">更新时间</Label>
|
||||
<div className="mt-1.5 text-sm">{formatDate(user.updatedAt)}</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">最后登录时间</Label>
|
||||
<div className="mt-1.5 text-sm">
|
||||
{user.lastLoginAt ? formatDate(user.lastLoginAt) : '从未登录'}
|
||||
</div>
|
||||
</Card>
|
||||
<Card className="p-4">
|
||||
<Label className="text-xs">账户状态</Label>
|
||||
<div className="mt-1.5">
|
||||
{getStatusBadge(user.isActive)}
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</ScrollArea>
|
||||
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
关闭
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogFooter } from '@/components/ui/dialog';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Label } from '@/components/ui/label';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { User, Enterprise, UserFormData } from '../types';
|
||||
|
||||
interface UserFormDialogProps {
|
||||
open: boolean;
|
||||
onOpenChange: (open: boolean) => void;
|
||||
editingUser: User | null;
|
||||
formData: UserFormData;
|
||||
onFormDataChange: (data: UserFormData) => void;
|
||||
onSave: () => void;
|
||||
enterprises: Enterprise[];
|
||||
}
|
||||
|
||||
export function UserFormDialog({
|
||||
open,
|
||||
onOpenChange,
|
||||
editingUser,
|
||||
formData,
|
||||
onFormDataChange,
|
||||
onSave,
|
||||
enterprises
|
||||
}: UserFormDialogProps) {
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle>{editingUser ? '编辑用户' : '添加用户'}</DialogTitle>
|
||||
<DialogDescription className="sr-only">
|
||||
{editingUser ? '编辑用户信息' : '添加新用户'}
|
||||
</DialogDescription>
|
||||
</DialogHeader>
|
||||
<div className="space-y-4">
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<Label htmlFor="username">用户名 *</Label>
|
||||
<Input
|
||||
id="username"
|
||||
value={formData.username || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, username: e.target.value })}
|
||||
placeholder="登录用户名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="name">姓名 *</Label>
|
||||
<Input
|
||||
id="name"
|
||||
value={formData.name || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, name: e.target.value })}
|
||||
placeholder="真实姓名"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="phone">电话 *</Label>
|
||||
<Input
|
||||
id="phone"
|
||||
value={formData.phone || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, phone: e.target.value })}
|
||||
placeholder="手机号码"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="email">邮箱</Label>
|
||||
<Input
|
||||
id="email"
|
||||
type="email"
|
||||
value={formData.email || ''}
|
||||
onChange={(e) => onFormDataChange({ ...formData, email: e.target.value })}
|
||||
placeholder="电子邮箱"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<Label htmlFor="userType">用户类型 *</Label>
|
||||
<Select
|
||||
value={formData.userType || 'enterprise_admin'}
|
||||
onValueChange={(value) => {
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
userType: value,
|
||||
// 如果切换为超级管理员,清除企业信息
|
||||
...(value === 'super_admin' ? { enterpriseId: undefined, enterpriseName: undefined } : {})
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="super_admin">超级管理员</SelectItem>
|
||||
<SelectItem value="enterprise_admin">企业管理员</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
{formData.userType === 'enterprise_admin' && (
|
||||
<div>
|
||||
<Label htmlFor="enterpriseId">所属企业 *</Label>
|
||||
<Select
|
||||
value={formData.enterpriseId || ''}
|
||||
onValueChange={(value: string) => {
|
||||
const selectedEnterprise = enterprises.find(e => e.id === value);
|
||||
onFormDataChange({
|
||||
...formData,
|
||||
enterpriseId: value,
|
||||
enterpriseName: selectedEnterprise?.name
|
||||
});
|
||||
}}
|
||||
>
|
||||
<SelectTrigger>
|
||||
<SelectValue placeholder="请选择企业" />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{enterprises.map((enterprise) => (
|
||||
<SelectItem key={enterprise.id} value={enterprise.id}>
|
||||
{enterprise.name}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
<DialogFooter>
|
||||
<Button variant="outline" onClick={() => onOpenChange(false)}>
|
||||
取消
|
||||
</Button>
|
||||
<Button onClick={onSave}>保存</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,245 @@
|
||||
/**
|
||||
* filekorolheader: 用户列表组件 - 用户数据表格展示界面
|
||||
* 功能:用户数据表格展示、状态徽章、操作按钮、分页功能
|
||||
* 路径:/central-config/tenant/user-management/components/UserList
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { User, PaginationState } from '../types';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Badge } from '@/components/ui/badge';
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
|
||||
import { Eye, Edit, Trash2, Lock, UserX, UserCheck } from 'lucide-react';
|
||||
|
||||
interface UserListProps {
|
||||
users: User[];
|
||||
loading: boolean;
|
||||
pagination: PaginationState;
|
||||
onPageChange: (page: number) => void;
|
||||
onViewDetail: (user: User) => void;
|
||||
onEdit?: (user: User) => void;
|
||||
onDelete?: (user: User) => void;
|
||||
onToggleStatus?: (user: User) => void;
|
||||
onResetPassword?: (user: User) => void;
|
||||
}
|
||||
|
||||
export function UserList({
|
||||
users,
|
||||
loading,
|
||||
pagination,
|
||||
onPageChange,
|
||||
onViewDetail,
|
||||
onEdit,
|
||||
onDelete,
|
||||
onToggleStatus,
|
||||
onResetPassword
|
||||
}: UserListProps) {
|
||||
const getStatusBadge = (user: User) => {
|
||||
// 根据isSuperuser和isActive判断状态
|
||||
if (user.isSuperuser) {
|
||||
return user.isActive ? (
|
||||
<Badge className="bg-green-100 text-green-700">正常</Badge>
|
||||
) : (
|
||||
<Badge className="bg-gray-100 text-gray-700">已冻结</Badge>
|
||||
);
|
||||
}
|
||||
|
||||
if (user.isActive) {
|
||||
return <Badge className="bg-green-100 text-green-700">正常</Badge>;
|
||||
} else {
|
||||
return <Badge className="bg-red-100 text-red-700">停用</Badge>;
|
||||
}
|
||||
};
|
||||
|
||||
const getUserTypeBadge = (user: User) => {
|
||||
if (user.isSuperuser) {
|
||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
||||
}
|
||||
// 根据scope或其他字段判断用户类型
|
||||
if (user.scope === 'enterprise' || user.companyName) {
|
||||
return <Badge className="bg-blue-100 text-blue-700">企业管理员</Badge>;
|
||||
}
|
||||
return <Badge className="bg-green-100 text-green-700">员工</Badge>;
|
||||
};
|
||||
|
||||
const getRoleBadge = (user: User) => {
|
||||
if (user.isSuperuser) {
|
||||
return <Badge className="bg-purple-100 text-purple-700">超级管理员</Badge>;
|
||||
}
|
||||
return <Badge className="bg-blue-100 text-blue-700">普通用户</Badge>;
|
||||
};
|
||||
|
||||
const getVerifiedBadge = (isVerified: boolean) => {
|
||||
return isVerified ? (
|
||||
<Badge className="bg-green-100 text-green-700">已验证</Badge>
|
||||
) : (
|
||||
<Badge variant="outline">未验证</Badge>
|
||||
);
|
||||
};
|
||||
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card>
|
||||
<div className="flex items-center justify-center h-96">
|
||||
<div className="text-muted-foreground">加载中...</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="space-y-4">
|
||||
<Card>
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow>
|
||||
<TableHead>用户名</TableHead>
|
||||
<TableHead>姓名</TableHead>
|
||||
<TableHead>电话</TableHead>
|
||||
<TableHead>所属企业</TableHead>
|
||||
<TableHead>用户类型</TableHead>
|
||||
<TableHead>角色</TableHead>
|
||||
<TableHead>状态</TableHead>
|
||||
<TableHead>操作</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{users.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={8} className="text-center text-muted-foreground py-8">
|
||||
暂无用户数据
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
users.map((user) => (
|
||||
<TableRow key={user.id}>
|
||||
<TableCell className="font-medium">
|
||||
<div className="flex items-center gap-2">
|
||||
{user.avatarUrl && (
|
||||
<img
|
||||
src={user.avatarUrl}
|
||||
alt={user.username}
|
||||
className="w-8 h-8 rounded-full"
|
||||
/>
|
||||
)}
|
||||
<span>{user.username}</span>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell>
|
||||
{user.fullName || user.displayName || '-'}
|
||||
</TableCell>
|
||||
<TableCell>{user.phone || '-'}</TableCell>
|
||||
<TableCell className="text-muted-foreground">{user.companyName || '-'}</TableCell>
|
||||
<TableCell>{getUserTypeBadge(user)}</TableCell>
|
||||
<TableCell>{getRoleBadge(user)}</TableCell>
|
||||
<TableCell>{getStatusBadge(user)}</TableCell>
|
||||
<TableCell>
|
||||
<div className="flex gap-1">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onViewDetail(user)}
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
{onEdit && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onEdit(user)}
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{onResetPassword && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onResetPassword(user)}
|
||||
>
|
||||
<Lock className="w-4 h-4" />
|
||||
</Button>
|
||||
)}
|
||||
{onToggleStatus && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onToggleStatus(user)}
|
||||
>
|
||||
{user.isActive ? (
|
||||
<UserX className="w-4 h-4 text-orange-600" />
|
||||
) : (
|
||||
<UserCheck className="w-4 h-4 text-green-600" />
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
{onDelete && !user.isSuperuser && (
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => onDelete(user)}
|
||||
>
|
||||
<Trash2 className="w-4 h-4 text-destructive" />
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</Card>
|
||||
|
||||
{/* 分页 */}
|
||||
{pagination.total > 0 && (
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="text-sm text-muted-foreground">
|
||||
显示第 {(pagination.page - 1) * pagination.size + 1} - {Math.min(pagination.page * pagination.size, pagination.total)} 条,共 {pagination.total} 条记录
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(pagination.page - 1)}
|
||||
disabled={!pagination.hasPrev}
|
||||
>
|
||||
上一页
|
||||
</Button>
|
||||
|
||||
<div className="flex items-center gap-1">
|
||||
<span className="text-sm text-muted-foreground">第</span>
|
||||
<div className="flex items-center">
|
||||
<input
|
||||
type="number"
|
||||
min={1}
|
||||
max={pagination.totalPages}
|
||||
value={pagination.page}
|
||||
onChange={(e) => {
|
||||
const newPage = parseInt(e.target.value);
|
||||
if (!isNaN(newPage)) {
|
||||
onPageChange(newPage);
|
||||
}
|
||||
}}
|
||||
className="w-16 h-8 text-center border rounded-md"
|
||||
/>
|
||||
</div>
|
||||
<span className="text-sm text-muted-foreground">/ {pagination.totalPages} 页</span>
|
||||
</div>
|
||||
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => onPageChange(pagination.page + 1)}
|
||||
disabled={!pagination.hasNext}
|
||||
>
|
||||
下一页
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,62 @@
|
||||
'use client';
|
||||
|
||||
import React from 'react';
|
||||
import { Card } from '@/components/ui/card';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||
import { Search } from 'lucide-react';
|
||||
import { UserFilters } from '../types';
|
||||
|
||||
interface UserManagementFiltersProps {
|
||||
filters: UserFilters;
|
||||
onSearchChange: (value: string) => void;
|
||||
onStatusFilterChange: (value: string) => void;
|
||||
onTypeFilterChange: (value: string) => void;
|
||||
}
|
||||
|
||||
export function UserManagementFilters({
|
||||
filters,
|
||||
onSearchChange,
|
||||
onStatusFilterChange,
|
||||
onTypeFilterChange
|
||||
}: UserManagementFiltersProps) {
|
||||
|
||||
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={filters.searchKeyword}
|
||||
onChange={(e) => onSearchChange(e.target.value)}
|
||||
className="pl-10"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<Select value={filters.typeFilter} onValueChange={onTypeFilterChange}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部类型</SelectItem>
|
||||
<SelectItem value="super_admin">超级管理员</SelectItem>
|
||||
<SelectItem value="enterprise_admin">企业管理员</SelectItem>
|
||||
<SelectItem value="employee">员工</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Select value={filters.statusFilter} onValueChange={onStatusFilterChange}>
|
||||
<SelectTrigger className="w-40">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="all">全部状态</SelectItem>
|
||||
<SelectItem value="active">正常</SelectItem>
|
||||
<SelectItem value="frozen">已冻结</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理页面头部组件 - 页面标题显示
|
||||
* 功能:页面标题显示、统计数据展示
|
||||
* 路径:/central-config/tenant/user-management/components/UserManagementHeader
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
interface UserManagementHeaderProps {
|
||||
stats: Array<{
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}>;
|
||||
}
|
||||
|
||||
export function UserManagementHeader({ stats }: UserManagementHeaderProps) {
|
||||
return (
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h2 className="text-green-800 dark:text-green-400">用户管理</h2>
|
||||
<p className="text-muted-foreground">平台所有用户账户的集中管理</p>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理统计卡片组件 - 用户统计数据展示界面
|
||||
* 功能:总用户数、活跃用户、管理员、已验证用户统计展示
|
||||
* 路径:/central-config/tenant/user-management/components/UserManagementStatsCards
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用shadcn/ui组件,TypeScript类型安全
|
||||
*/
|
||||
|
||||
'use client';
|
||||
|
||||
import { Card } from '@/components/ui/card';
|
||||
|
||||
interface UserManagementStatsCardsProps {
|
||||
stats: Array<{
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}>;
|
||||
loading?: boolean;
|
||||
}
|
||||
|
||||
export function UserManagementStatsCards({
|
||||
stats,
|
||||
loading = false
|
||||
}: UserManagementStatsCardsProps) {
|
||||
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 (
|
||||
<div className="grid grid-cols-2 md:grid-cols-4 gap-4">
|
||||
{stats.map((stat, index) => (
|
||||
<Card key={index} className="p-4">
|
||||
<div className="text-sm text-muted-foreground">{stat.label}</div>
|
||||
<div className={`mt-2 text-2xl font-bold ${stat.color}`}>{stat.value}</div>
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理API接口 - 用户数据查询接口服务
|
||||
* 功能:API请求封装、数据转换、错误处理、分页查询
|
||||
* 路径:/central-config/tenant/user-management/components/userManagementApi
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SDK API调用,TypeScript类型安全
|
||||
*/
|
||||
|
||||
import { getAuthToken } from "@/utils/token";
|
||||
import {
|
||||
listSystemUsersApiV1UsersSystemUsersGet,
|
||||
} from "@/lib/api/sdk.gen";
|
||||
|
||||
// API返回的用户数据类型
|
||||
export interface UserData {
|
||||
id: string;
|
||||
tenant_id: string;
|
||||
email: string;
|
||||
username: string;
|
||||
full_name: string | null;
|
||||
phone: string | null;
|
||||
is_active?: boolean;
|
||||
status?: string;
|
||||
is_superuser: boolean;
|
||||
is_verified: boolean;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
last_login_at: string | null;
|
||||
avatar_url: string | null;
|
||||
bio: string | null;
|
||||
display_name: string | null;
|
||||
department_id: string | null;
|
||||
department_name: string | null;
|
||||
scope: string;
|
||||
company_name: string | null;
|
||||
}
|
||||
|
||||
// API响应接口
|
||||
export interface UsersApiResponse {
|
||||
data: UserData[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
// 查询参数接口
|
||||
export interface UsersQueryParams {
|
||||
search?: string;
|
||||
is_active?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
order_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 页面使用的用户数据类型(转换后的)
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string | null;
|
||||
phone: string | null;
|
||||
isActive: boolean;
|
||||
isSuperuser: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastLoginAt: string | null;
|
||||
avatarUrl: string | null;
|
||||
bio: string | null;
|
||||
displayName: string | null;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
scope: string;
|
||||
companyName: string | null;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取用户列表数据
|
||||
*/
|
||||
export async function fetchUsers(params: UsersQueryParams = {}): Promise<UsersApiResponse> {
|
||||
try {
|
||||
// 构建查询参数对象
|
||||
const queryParams: any = {};
|
||||
|
||||
if (params.search) queryParams.search = params.search;
|
||||
if (params.is_active !== undefined) queryParams.is_active = params.is_active;
|
||||
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 listSystemUsersApiV1UsersSystemUsersGet({
|
||||
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 users:', error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 将API数据转换为页面所需的用户数据格式
|
||||
* 优先显示display_name,其次full_name,最后username
|
||||
*/
|
||||
export function transformUserData(user: UserData): User {
|
||||
return {
|
||||
id: user.id,
|
||||
username: user.username,
|
||||
email: user.email,
|
||||
fullName: user.full_name,
|
||||
phone: user.phone,
|
||||
isActive: user.is_active !== undefined ? user.is_active : user.status === 'active',
|
||||
isSuperuser: user.is_superuser,
|
||||
isVerified: user.is_verified,
|
||||
createdAt: formatDate(user.created_at),
|
||||
updatedAt: formatDate(user.updated_at),
|
||||
lastLoginAt: user.last_login_at ? formatDate(user.last_login_at) : null,
|
||||
avatarUrl: user.avatar_url,
|
||||
bio: user.bio,
|
||||
displayName: user.display_name || user.full_name || user.username,
|
||||
departmentId: user.department_id,
|
||||
departmentName: user.department_name,
|
||||
scope: user.scope,
|
||||
companyName: user.company_name,
|
||||
tenantId: user.tenant_id,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期
|
||||
*/
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
// Pagination state interface for page components
|
||||
export interface PaginationState {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
456
src/app/(app)/central-config/tenant/user-management/page.tsx
Normal file
456
src/app/(app)/central-config/tenant/user-management/page.tsx
Normal file
@@ -0,0 +1,456 @@
|
||||
/**
|
||||
* filekorolheader: 用户管理页面 - 用户查询和管理页面
|
||||
* 功能:用户列表查询、搜索筛选、详情查看、用户管理
|
||||
* 路径:/central-config/tenant/user-management
|
||||
* 规范:遵循crop-x/docs/开发项目规范.md,使用SearchFormPagination公共组件,shadcn语义化样式
|
||||
*/
|
||||
'use client';
|
||||
|
||||
import { useReducer, useEffect, useState, useCallback, useMemo } from 'react';
|
||||
import { toast } from 'sonner';
|
||||
import { Button } from '@/components/ui/button';
|
||||
import { Eye, Edit, Lock, UserX, UserCheck } from 'lucide-react';
|
||||
import { UserDetailDialog } from './components/UserDetailDialog';
|
||||
import { SearchFormPagination, SearchFieldConfig, TableColumnConfig } from '@/components/common/searchFormPagination';
|
||||
|
||||
import { fetchUsers, transformUserData, UsersQueryParams, User, UsersApiResponse, PaginationState } from './components/userManagementApi';
|
||||
import { UserManagementHeader } from './components/UserManagementHeader';
|
||||
import { UserManagementStatsCards } from './components/UserManagementStatsCards';
|
||||
import { UserFilters } from './types';
|
||||
|
||||
// 移除了Enterprise的引用,因为新实现中不再需要
|
||||
|
||||
// 用户管理状态管理
|
||||
interface UserManagementState {
|
||||
users: User[];
|
||||
loading: boolean;
|
||||
error: string | null;
|
||||
pagination: PaginationState;
|
||||
filters: UserFilters;
|
||||
sortBy?: string;
|
||||
sortOrder: 'asc' | 'desc';
|
||||
selectedUser: User | null;
|
||||
showDetailDialog: boolean;
|
||||
}
|
||||
|
||||
type UserManagementAction =
|
||||
| { type: 'SET_USERS'; payload: { data: User[]; pagination: PaginationState } }
|
||||
| { type: 'SET_LOADING'; payload: boolean }
|
||||
| { type: 'SET_ERROR'; payload: string | null }
|
||||
| { type: 'SET_FILTERS'; payload: Partial<UserFilters> }
|
||||
| { type: 'SET_SORT'; payload: { sortBy?: string; sortOrder: 'asc' | 'desc' } }
|
||||
| { type: 'SET_PAGINATION'; payload: Partial<PaginationState> }
|
||||
| { type: 'SET_SELECTED_USER'; payload: User | null }
|
||||
| { type: 'TOGGLE_DETAIL_DIALOG'; payload: boolean }
|
||||
| { type: 'REFRESH_DATA' };
|
||||
|
||||
const userManagementReducer = (state: UserManagementState, action: UserManagementAction): UserManagementState => {
|
||||
switch (action.type) {
|
||||
case 'SET_USERS':
|
||||
return {
|
||||
...state,
|
||||
users: 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_USER':
|
||||
return { ...state, selectedUser: action.payload };
|
||||
case 'TOGGLE_DETAIL_DIALOG':
|
||||
return { ...state, showDetailDialog: !state.showDetailDialog };
|
||||
case 'REFRESH_DATA':
|
||||
return { ...state, error: null };
|
||||
default:
|
||||
return state;
|
||||
}
|
||||
};
|
||||
|
||||
const initialState: UserManagementState = {
|
||||
users: [],
|
||||
loading: false,
|
||||
error: null,
|
||||
pagination: {
|
||||
page: 1,
|
||||
size: 10,
|
||||
total: 0,
|
||||
totalPages: 0,
|
||||
hasNext: false,
|
||||
hasPrev: false,
|
||||
},
|
||||
filters: {
|
||||
searchKeyword: '',
|
||||
statusFilter: 'all',
|
||||
typeFilter: 'all'
|
||||
},
|
||||
sortBy: 'created_at',
|
||||
sortOrder: 'desc',
|
||||
selectedUser: null,
|
||||
showDetailDialog: false,
|
||||
};
|
||||
|
||||
export default function TenantUserManagementPage() {
|
||||
const [state, dispatch] = useReducer(userManagementReducer, initialState);
|
||||
const [searchFilters, setSearchFilters] = useState<Record<string, string>>({});
|
||||
|
||||
// 搜索字段配置
|
||||
const searchFields: SearchFieldConfig[] = useMemo(() => [
|
||||
{
|
||||
key: 'search',
|
||||
label: '搜索',
|
||||
type: 'text',
|
||||
placeholder: '搜索用户名、姓名、邮箱...',
|
||||
},
|
||||
{
|
||||
key: 'status',
|
||||
label: '用户状态',
|
||||
type: 'select',
|
||||
defaultValue: 'all',
|
||||
options: [
|
||||
{ value: 'all', label: '全部状态' },
|
||||
{ value: 'active', label: '活跃' },
|
||||
{ value: 'inactive', label: '未激活' },
|
||||
],
|
||||
},
|
||||
{
|
||||
key: 'type',
|
||||
label: '用户类型',
|
||||
type: 'select',
|
||||
defaultValue: 'all',
|
||||
options: [
|
||||
{ value: 'all', label: '全部类型' },
|
||||
{ value: 'admin', label: '管理员' },
|
||||
{ value: 'user', label: '普通用户' },
|
||||
{ value: 'staff', label: '员工' },
|
||||
],
|
||||
},
|
||||
], []);
|
||||
|
||||
// 表格列配置
|
||||
const columns: TableColumnConfig[] = useMemo(() => [
|
||||
{
|
||||
key: 'username',
|
||||
label: '用户名',
|
||||
sortable: true,
|
||||
render: (value: string, user: User) => (
|
||||
<div className="font-medium">{value}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'fullName',
|
||||
label: '姓名',
|
||||
sortable: true,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
key: 'email',
|
||||
label: '邮箱',
|
||||
sortable: true,
|
||||
render: (value: string) => value || '-',
|
||||
},
|
||||
{
|
||||
key: 'isActive',
|
||||
label: '状态',
|
||||
sortable: true,
|
||||
render: (value: boolean) => (
|
||||
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
|
||||
value
|
||||
? 'bg-green-50 dark:bg-green-950 text-green-600 dark:text-green-400'
|
||||
: 'bg-red-50 dark:bg-red-950 text-red-600 dark:text-red-400'
|
||||
}`}>
|
||||
<div className={`w-2 h-2 rounded-full ${value ? 'bg-green-500' : 'bg-red-500'}`} />
|
||||
{value ? '活跃' : '未激活'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'isSuperuser',
|
||||
label: '角色',
|
||||
sortable: true,
|
||||
render: (value: boolean, user: User) => {
|
||||
if (value) {
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-purple-50 dark:bg-purple-950 text-purple-600 dark:text-purple-400">
|
||||
<div className="w-2 h-2 rounded-full bg-purple-500" />
|
||||
超级管理员
|
||||
</div>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium bg-blue-50 dark:bg-blue-950 text-blue-600 dark:text-blue-400">
|
||||
<div className="w-2 h-2 rounded-full bg-blue-500" />
|
||||
普通用户
|
||||
</div>
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'isVerified',
|
||||
label: '验证',
|
||||
sortable: true,
|
||||
render: (value: boolean) => (
|
||||
<div className={`inline-flex items-center gap-1 px-2 py-1 rounded-full text-xs font-medium ${
|
||||
value
|
||||
? 'bg-orange-50 dark:bg-orange-950 text-orange-600 dark:text-orange-400'
|
||||
: 'bg-gray-50 dark:bg-gray-950 text-gray-600 dark:text-gray-400'
|
||||
}`}>
|
||||
{value ? '已验证' : '未验证'}
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'lastLoginAt',
|
||||
label: '最后登录',
|
||||
sortable: true,
|
||||
render: (value: string) => {
|
||||
if (!value) return '-';
|
||||
try {
|
||||
const date = new Date(value);
|
||||
return date.toLocaleDateString('zh-CN', {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
} catch {
|
||||
return value;
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
key: 'actions',
|
||||
label: '操作',
|
||||
render: (_, user: User) => (
|
||||
<div className="flex items-center gap-2">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleViewDetail(user)}
|
||||
title="查看"
|
||||
>
|
||||
<Eye className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleEdit(user)}
|
||||
title="编辑"
|
||||
>
|
||||
<Edit className="w-4 h-4" />
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleToggleStatus(user)}
|
||||
title={user.isActive ? "冻结用户" : "激活用户"}
|
||||
>
|
||||
{user.isActive ? (
|
||||
<UserX className="w-4 h-4 text-orange-600" />
|
||||
) : (
|
||||
<UserCheck className="w-4 h-4 text-green-600" />
|
||||
)}
|
||||
</Button>
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="sm"
|
||||
onClick={() => handleResetPassword(user)}
|
||||
title="重置密码"
|
||||
>
|
||||
<Lock className="w-4 h-4" />
|
||||
</Button>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
], []);
|
||||
|
||||
// 加载用户数据
|
||||
const loadUsers = useCallback(async (resetPage = false) => {
|
||||
try {
|
||||
dispatch({ type: 'SET_LOADING', payload: true });
|
||||
debugger
|
||||
const params: UsersQueryParams = {
|
||||
page: resetPage ? 1 : state.pagination.page,
|
||||
size: state.pagination.size,
|
||||
is_active: true,
|
||||
};
|
||||
|
||||
// 添加搜索条件
|
||||
if (searchFilters.search) {
|
||||
params.search = searchFilters.search;
|
||||
}
|
||||
|
||||
if (searchFilters.status && searchFilters.status !== 'all') {
|
||||
params.is_active = searchFilters.status === 'active';
|
||||
}
|
||||
|
||||
if (searchFilters.type && searchFilters.type !== 'all') {
|
||||
// For user type filtering, we'll need to handle this differently based on the API
|
||||
// For now, we'll filter on the client side if needed
|
||||
}
|
||||
|
||||
if (state.sortBy) {
|
||||
params.order_by = state.sortBy;
|
||||
params.sort_order = state.sortOrder;
|
||||
}
|
||||
|
||||
const response: UsersApiResponse = await fetchUsers(params);
|
||||
const transformedUsers = response.data.map(transformUserData);
|
||||
|
||||
dispatch({
|
||||
type: 'SET_USERS',
|
||||
payload: {
|
||||
data: transformedUsers,
|
||||
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 users:', error);
|
||||
dispatch({
|
||||
type: 'SET_ERROR',
|
||||
payload: error instanceof Error ? error.message : '加载用户数据失败'
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 搜索处理
|
||||
const handleSearch = useCallback((filters: Record<string, string>) => {
|
||||
setSearchFilters(filters);
|
||||
dispatch({ type: 'SET_PAGINATION', payload: { page: 1 } });
|
||||
}, []);
|
||||
|
||||
// 排序处理
|
||||
const handleSort = useCallback((sortBy: string, sortOrder: 'asc' | 'desc') => {
|
||||
dispatch({ type: 'SET_SORT', payload: { sortBy, sortOrder } });
|
||||
}, []);
|
||||
|
||||
// 分页处理
|
||||
const handlePageChange = useCallback((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 } });
|
||||
}, [state.pagination.totalPages]);
|
||||
|
||||
// 每页条数变化处理
|
||||
const handleSizeChange = useCallback((size: number) => {
|
||||
dispatch({ type: 'SET_PAGINATION', payload: { size, page: 1 } });
|
||||
}, []);
|
||||
|
||||
// 查看详情
|
||||
const handleViewDetail = (user: User) => {
|
||||
dispatch({ type: 'SET_SELECTED_USER', payload: user });
|
||||
dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: true });
|
||||
};
|
||||
|
||||
// 编辑用户
|
||||
const handleEdit = (user: User) => {
|
||||
toast.info('编辑功能开发中...');
|
||||
};
|
||||
|
||||
// 切换用户状态
|
||||
const handleToggleStatus = (user: User) => {
|
||||
const newStatus = !user.isActive;
|
||||
const statusText = newStatus ? '激活' : '停用';
|
||||
if (!confirm(`确定要${statusText}用户 ${user.fullName || user.username} 吗?`)) return;
|
||||
toast.info(`${statusText}功能开发中...`);
|
||||
};
|
||||
|
||||
// 重置密码
|
||||
const handleResetPassword = (user: User) => {
|
||||
if (!confirm(`确定要重置用户 ${user.fullName || user.username} 的密码吗?`)) return;
|
||||
toast.info('重置密码功能开发中...');
|
||||
};
|
||||
|
||||
// 统计数据计算
|
||||
const stats = useMemo(() => [
|
||||
{
|
||||
label: '总用户数',
|
||||
value: state.pagination.total,
|
||||
color: 'text-blue-600 dark:text-blue-400',
|
||||
bg: 'bg-blue-50 dark:bg-blue-950',
|
||||
},
|
||||
{
|
||||
label: '活跃用户',
|
||||
value: state.users.filter(u => u.isActive).length,
|
||||
color: 'text-green-600 dark:text-green-400',
|
||||
bg: 'bg-green-50 dark:bg-green-950',
|
||||
},
|
||||
{
|
||||
label: '管理员',
|
||||
value: state.users.filter(u => u.isSuperuser).length,
|
||||
color: 'text-purple-600 dark:text-purple-400',
|
||||
bg: 'bg-purple-50 dark:bg-purple-950',
|
||||
},
|
||||
{
|
||||
label: '已验证',
|
||||
value: state.users.filter(u => u.isVerified).length,
|
||||
color: 'text-orange-600 dark:text-orange-400',
|
||||
bg: 'bg-orange-50 dark:bg-orange-950',
|
||||
},
|
||||
], [state.users, state.pagination.total]);
|
||||
|
||||
// 加载数据
|
||||
useEffect(() => {
|
||||
loadUsers();
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* 页面标题 */}
|
||||
<UserManagementHeader stats={stats} />
|
||||
|
||||
{/* 统计卡片 */}
|
||||
<UserManagementStatsCards stats={stats} />
|
||||
|
||||
{/* 搜索表单、数据表格和分页 */}
|
||||
<SearchFormPagination
|
||||
formTitle="用户列表"
|
||||
formRightContent={
|
||||
<Button onClick={() => toast.info('新建用户功能开发中...')}>
|
||||
新建用户
|
||||
</Button>
|
||||
}
|
||||
searchFields={searchFields}
|
||||
columns={columns}
|
||||
data={state.users}
|
||||
loading={state.loading}
|
||||
error={state.error}
|
||||
pagination={state.pagination}
|
||||
sortBy={state.sortBy}
|
||||
sortOrder={state.sortOrder}
|
||||
onPageChange={handlePageChange}
|
||||
onSizeChange={handleSizeChange}
|
||||
onSearch={handleSearch}
|
||||
onSort={handleSort}
|
||||
emptyText="暂无用户数据"
|
||||
sizeOptions={[10, 20, 50, 100]}
|
||||
/>
|
||||
|
||||
{/* 用户详情对话框 */}
|
||||
<UserDetailDialog
|
||||
open={state.showDetailDialog}
|
||||
onOpenChange={(open) => dispatch({ type: 'TOGGLE_DETAIL_DIALOG', payload: open })}
|
||||
user={state.selectedUser}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
104
src/app/(app)/central-config/tenant/user-management/types.ts
Normal file
104
src/app/(app)/central-config/tenant/user-management/types.ts
Normal file
@@ -0,0 +1,104 @@
|
||||
// 用户管理相关类型定义
|
||||
|
||||
export interface User {
|
||||
id: string;
|
||||
username: string;
|
||||
email: string;
|
||||
fullName: string | null;
|
||||
phone: string | null;
|
||||
isActive: boolean;
|
||||
isSuperuser: boolean;
|
||||
isVerified: boolean;
|
||||
createdAt: string;
|
||||
updatedAt: string;
|
||||
lastLoginAt: string | null;
|
||||
avatarUrl: string | null;
|
||||
bio: string | null;
|
||||
displayName: string | null;
|
||||
departmentId: string | null;
|
||||
departmentName: string | null;
|
||||
scope: string;
|
||||
companyName: string | null;
|
||||
tenantId: string;
|
||||
}
|
||||
|
||||
// 为了兼容现有代码,保留一些映射属性
|
||||
export interface UserWithLegacyFields extends User {
|
||||
// 向后兼容的属性
|
||||
name: string;
|
||||
phone: string;
|
||||
enterpriseId?: string;
|
||||
enterpriseName?: string;
|
||||
userType: UserType;
|
||||
status: UserStatus;
|
||||
lastLoginTime?: string;
|
||||
roleIds: string[];
|
||||
roles?: string[];
|
||||
}
|
||||
|
||||
export type UserType = 'super_admin' | 'enterprise_admin' | 'employee';
|
||||
export type UserStatus = 'active' | 'inactive' | 'frozen';
|
||||
|
||||
export interface Enterprise {
|
||||
id: string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
// 统计数据
|
||||
export interface UserManagementStats {
|
||||
label: string;
|
||||
value: number;
|
||||
color: string;
|
||||
bg: string;
|
||||
}
|
||||
|
||||
// 筛选条件
|
||||
export interface UserFilters {
|
||||
searchKeyword: string;
|
||||
statusFilter: string;
|
||||
typeFilter: string;
|
||||
}
|
||||
|
||||
// API响应数据类型
|
||||
export interface UsersApiResponse {
|
||||
data: User[];
|
||||
total: number;
|
||||
page: number;
|
||||
size: number;
|
||||
total_pages: number;
|
||||
has_next: boolean;
|
||||
has_prev: boolean;
|
||||
}
|
||||
|
||||
// 分页状态
|
||||
export interface PaginationState {
|
||||
page: number;
|
||||
size: number;
|
||||
total: number;
|
||||
totalPages: number;
|
||||
hasNext: boolean;
|
||||
hasPrev: boolean;
|
||||
}
|
||||
|
||||
// API查询参数
|
||||
export interface UsersQueryParams {
|
||||
search?: string;
|
||||
is_active?: boolean;
|
||||
page?: number;
|
||||
size?: number;
|
||||
order_by?: string;
|
||||
sort_order?: 'asc' | 'desc';
|
||||
}
|
||||
|
||||
// 表单数据
|
||||
export interface UserFormData {
|
||||
username?: string;
|
||||
name?: string;
|
||||
phone?: string;
|
||||
email?: string;
|
||||
userType?: UserType;
|
||||
status?: UserStatus;
|
||||
enterpriseId?: string;
|
||||
enterpriseName?: string;
|
||||
roleIds?: string[];
|
||||
}
|
||||
Reference in New Issue
Block a user