683 lines
29 KiB
TypeScript
Raw Normal View History

import React, { useState, useEffect } from 'react';
import DashboardLayout from '../../components/Layout/DashboardLayout';
import {
BuildingOfficeIcon,
MagnifyingGlassIcon,
PlusIcon,
EyeIcon,
PencilIcon,
TrashIcon,
ClockIcon,
CheckCircleIcon,
XCircleIcon,
ChevronLeftIcon,
ChevronRightIcon,
UserGroupIcon,
CalendarIcon,
CurrencyDollarIcon,
DocumentTextIcon,
StarIcon,
PhoneIcon,
EnvelopeIcon
} from '@heroicons/react/24/outline';
interface Enterprise {
id: string;
companyName: string;
contactPerson: string;
contactPhone: string;
contactEmail: string;
industry: string;
companySize: 'small' | 'medium' | 'large' | 'enterprise';
servicePackage: 'basic' | 'standard' | 'premium' | 'custom';
contractStatus: 'active' | 'expired' | 'pending' | 'cancelled';
contractValue: number;
contractStart: string;
contractEnd: string;
monthlyUsage: number;
totalOrders: number;
rating: number;
address: string;
website?: string;
notes?: string;
createdAt: string;
lastActivity: string;
}
export default function Enterprise() {
const [enterprises, setEnterprises] = useState<Enterprise[]>([]);
const [loading, setLoading] = useState(true);
const [searchTerm, setSearchTerm] = useState('');
const [filterPackage, setFilterPackage] = useState<'all' | 'basic' | 'standard' | 'premium' | 'custom'>('all');
const [filterStatus, setFilterStatus] = useState<'all' | 'active' | 'expired' | 'pending' | 'cancelled'>('all');
const [currentPage, setCurrentPage] = useState(1);
const [selectedEnterprises, setSelectedEnterprises] = useState<string[]>([]);
const enterprisesPerPage = 10;
useEffect(() => {
loadEnterprises();
}, [searchTerm, filterPackage, filterStatus, currentPage]);
const loadEnterprises = async () => {
try {
setLoading(true);
// 模拟API调用
setTimeout(() => {
const mockData: Enterprise[] = [
{
id: '1',
companyName: '科技创新有限公司',
contactPerson: '张总',
contactPhone: '+86 138-0000-0001',
contactEmail: 'zhang@tech-innovation.com',
industry: '科技',
companySize: 'large',
servicePackage: 'premium',
contractStatus: 'active',
contractValue: 500000,
contractStart: '2024-01-01',
contractEnd: '2024-12-31',
monthlyUsage: 45000,
totalOrders: 156,
rating: 4.8,
address: '北京市朝阳区科技园区A座',
website: 'https://tech-innovation.com',
notes: '重要客户,需要优先服务',
createdAt: '2023-12-15',
lastActivity: '2024-01-20 15:30'
},
{
id: '2',
companyName: '国际贸易集团',
contactPerson: '李经理',
contactPhone: '+86 139-0000-0002',
contactEmail: 'li@international-trade.com',
industry: '贸易',
companySize: 'enterprise',
servicePackage: 'custom',
contractStatus: 'active',
contractValue: 800000,
contractStart: '2023-06-01',
contractEnd: '2025-05-31',
monthlyUsage: 68000,
totalOrders: 289,
rating: 4.9,
address: '上海市浦东新区金融中心B座',
website: 'https://international-trade.com',
notes: '多语言需求,主要涉及欧洲市场',
createdAt: '2023-05-20',
lastActivity: '2024-01-21 09:15'
},
{
id: '3',
companyName: '医疗设备公司',
contactPerson: '王主任',
contactPhone: '+86 136-0000-0003',
contactEmail: 'wang@medical-device.com',
industry: '医疗',
companySize: 'medium',
servicePackage: 'standard',
contractStatus: 'active',
contractValue: 200000,
contractStart: '2024-01-15',
contractEnd: '2024-07-14',
monthlyUsage: 15000,
totalOrders: 67,
rating: 4.5,
address: '广州市天河区医疗产业园',
notes: '专业医疗术语翻译需求',
createdAt: '2024-01-10',
lastActivity: '2024-01-19 14:20'
},
{
id: '4',
companyName: '教育培训机构',
contactPerson: '陈校长',
contactPhone: '+86 135-0000-0004',
contactEmail: 'chen@education.com',
industry: '教育',
companySize: 'small',
servicePackage: 'basic',
contractStatus: 'expired',
contractValue: 50000,
contractStart: '2023-09-01',
contractEnd: '2023-12-31',
monthlyUsage: 8000,
totalOrders: 34,
rating: 4.2,
address: '深圳市南山区教育城',
notes: '合同已到期,需要续约',
createdAt: '2023-08-25',
lastActivity: '2024-01-05 10:45'
},
{
id: '5',
companyName: '新兴科技公司',
contactPerson: '刘总监',
contactPhone: '+86 137-0000-0005',
contactEmail: 'liu@emerging-tech.com',
industry: '科技',
companySize: 'medium',
servicePackage: 'standard',
contractStatus: 'pending',
contractValue: 150000,
contractStart: '2024-02-01',
contractEnd: '2024-08-31',
monthlyUsage: 0,
totalOrders: 0,
rating: 0,
address: '杭州市西湖区创新园',
website: 'https://emerging-tech.com',
notes: '新客户,正在洽谈合同',
createdAt: '2024-01-18',
lastActivity: '2024-01-21 16:00'
}
];
// 应用过滤器
let filteredData = mockData;
if (searchTerm) {
const term = searchTerm.toLowerCase();
filteredData = filteredData.filter(enterprise =>
enterprise.companyName.toLowerCase().includes(term) ||
enterprise.contactPerson.toLowerCase().includes(term) ||
enterprise.contactEmail.toLowerCase().includes(term) ||
enterprise.industry.toLowerCase().includes(term)
);
}
if (filterPackage !== 'all') {
filteredData = filteredData.filter(enterprise => enterprise.servicePackage === filterPackage);
}
if (filterStatus !== 'all') {
filteredData = filteredData.filter(enterprise => enterprise.contractStatus === filterStatus);
}
setEnterprises(filteredData);
setLoading(false);
}, 1000);
} catch (error) {
console.error('加载企业数据失败:', error);
setLoading(false);
}
};
const handleSelectEnterprise = (enterpriseId: string) => {
setSelectedEnterprises(prev =>
prev.includes(enterpriseId)
? prev.filter(id => id !== enterpriseId)
: [...prev, enterpriseId]
);
};
const handleSelectAll = () => {
if (selectedEnterprises.length === enterprises.length) {
setSelectedEnterprises([]);
} else {
setSelectedEnterprises(enterprises.map(enterprise => enterprise.id));
}
};
const getPackageText = (packageType: string) => {
switch (packageType) {
case 'basic': return '基础版';
case 'standard': return '标准版';
case 'premium': return '高级版';
case 'custom': return '定制版';
default: return packageType;
}
};
const getPackageColor = (packageType: string) => {
switch (packageType) {
case 'basic': return 'bg-gray-100 text-gray-800';
case 'standard': return 'bg-blue-100 text-blue-800';
case 'premium': return 'bg-purple-100 text-purple-800';
case 'custom': return 'bg-green-100 text-green-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getStatusText = (status: string) => {
switch (status) {
case 'active': return '生效中';
case 'expired': return '已过期';
case 'pending': return '待生效';
case 'cancelled': return '已取消';
default: return status;
}
};
const getStatusColor = (status: string) => {
switch (status) {
case 'active': return 'bg-green-100 text-green-800';
case 'expired': return 'bg-red-100 text-red-800';
case 'pending': return 'bg-yellow-100 text-yellow-800';
case 'cancelled': return 'bg-gray-100 text-gray-800';
default: return 'bg-gray-100 text-gray-800';
}
};
const getStatusIcon = (status: string) => {
switch (status) {
case 'active': return <CheckCircleIcon className="h-4 w-4" />;
case 'expired': return <XCircleIcon className="h-4 w-4" />;
case 'pending': return <ClockIcon className="h-4 w-4" />;
case 'cancelled': return <XCircleIcon className="h-4 w-4" />;
default: return <ClockIcon className="h-4 w-4" />;
}
};
const getCompanySizeText = (size: string) => {
switch (size) {
case 'small': return '小型企业';
case 'medium': return '中型企业';
case 'large': return '大型企业';
case 'enterprise': return '集团企业';
default: return size;
}
};
const formatCurrency = (amount: number) => {
return new Intl.NumberFormat('zh-CN', {
style: 'currency',
currency: 'CNY'
}).format(amount);
};
const totalPages = Math.ceil(enterprises.length / enterprisesPerPage);
const currentEnterprises = enterprises.slice(
(currentPage - 1) * enterprisesPerPage,
currentPage * enterprisesPerPage
);
if (loading) {
return (
<DashboardLayout title="企业服务">
<div className="flex items-center justify-center h-64">
<div className="text-center">
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-600 mx-auto"></div>
<div className="mt-4 text-lg text-gray-600">...</div>
</div>
</div>
</DashboardLayout>
);
}
return (
<DashboardLayout title="企业服务">
<div className="space-y-6">
{/* 页面标题 */}
<div className="sm:flex sm:items-center sm:justify-between">
<div>
<h1 className="text-2xl font-bold text-gray-900"></h1>
<p className="mt-1 text-sm text-gray-600">
</p>
</div>
<div className="mt-4 sm:mt-0">
<button
type="button"
className="inline-flex items-center px-4 py-2 border border-transparent rounded-md shadow-sm text-sm font-medium text-white bg-indigo-600 hover:bg-indigo-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-indigo-500"
>
<PlusIcon className="h-4 w-4 mr-2" />
</button>
</div>
</div>
{/* 统计卡片 */}
<div className="grid grid-cols-1 gap-5 sm:grid-cols-2 lg:grid-cols-4">
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<BuildingOfficeIcon className="h-6 w-6 text-gray-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">{enterprises.length}</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<CheckCircleIcon className="h-6 w-6 text-green-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">
{enterprises.filter(e => e.contractStatus === 'active').length}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<CurrencyDollarIcon className="h-6 w-6 text-green-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">
{formatCurrency(enterprises.reduce((sum, e) => sum + e.contractValue, 0))}
</dd>
</dl>
</div>
</div>
</div>
</div>
<div className="bg-white overflow-hidden shadow rounded-lg">
<div className="p-5">
<div className="flex items-center">
<div className="flex-shrink-0">
<DocumentTextIcon className="h-6 w-6 text-blue-400" />
</div>
<div className="ml-5 w-0 flex-1">
<dl>
<dt className="text-sm font-medium text-gray-500 truncate"></dt>
<dd className="text-lg font-medium text-gray-900">
{enterprises.reduce((sum, e) => sum + e.totalOrders, 0)}
</dd>
</dl>
</div>
</div>
</div>
</div>
</div>
{/* 搜索和过滤 */}
<div className="bg-white shadow rounded-lg p-6">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-3">
{/* 搜索框 */}
<div className="relative">
<div className="absolute inset-y-0 left-0 pl-3 flex items-center pointer-events-none">
<MagnifyingGlassIcon className="h-5 w-5 text-gray-400" />
</div>
<input
type="text"
placeholder="搜索企业名称、联系人或行业..."
className="block w-full pl-10 pr-3 py-2 border border-gray-300 rounded-md leading-5 bg-white placeholder-gray-500 focus:outline-none focus:placeholder-gray-400 focus:ring-1 focus:ring-indigo-500 focus:border-indigo-500"
value={searchTerm}
onChange={(e) => setSearchTerm(e.target.value)}
/>
</div>
{/* 服务套餐过滤 */}
<div className="relative">
<select
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
value={filterPackage}
onChange={(e) => setFilterPackage(e.target.value as any)}
>
<option value="all"></option>
<option value="basic"></option>
<option value="standard"></option>
<option value="premium"></option>
<option value="custom"></option>
</select>
</div>
{/* 合同状态过滤 */}
<div className="relative">
<select
className="block w-full pl-3 pr-10 py-2 text-base border border-gray-300 focus:outline-none focus:ring-indigo-500 focus:border-indigo-500 rounded-md"
value={filterStatus}
onChange={(e) => setFilterStatus(e.target.value as any)}
>
<option value="all"></option>
<option value="active"></option>
<option value="expired"></option>
<option value="pending"></option>
<option value="cancelled"></option>
</select>
</div>
</div>
{/* 批量操作 */}
{selectedEnterprises.length > 0 && (
<div className="mt-4 flex items-center justify-between bg-gray-50 p-3 rounded-md">
<span className="text-sm text-gray-700">
{selectedEnterprises.length}
</span>
<div className="flex space-x-2">
<button className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
</button>
<button className="inline-flex items-center px-3 py-1 border border-transparent text-sm leading-4 font-medium rounded-md text-white bg-green-600 hover:bg-green-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500">
</button>
</div>
</div>
)}
</div>
{/* 企业列表 */}
<div className="bg-white shadow rounded-lg overflow-hidden">
<div className="overflow-x-auto">
<table className="min-w-full divide-y divide-gray-200">
<thead className="bg-gray-50">
<tr>
<th scope="col" className="relative px-6 py-3">
<input
type="checkbox"
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
checked={enterprises.length > 0 && selectedEnterprises.length === enterprises.length}
onChange={handleSelectAll}
/>
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
使
</th>
<th scope="col" className="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
</th>
<th scope="col" className="relative px-6 py-3">
<span className="sr-only"></span>
</th>
</tr>
</thead>
<tbody className="bg-white divide-y divide-gray-200">
{currentEnterprises.map((enterprise) => (
<tr key={enterprise.id} className="hover:bg-gray-50">
<td className="relative px-6 py-4">
<input
type="checkbox"
className="absolute left-4 top-1/2 -mt-2 h-4 w-4 rounded border-gray-300 text-indigo-600 focus:ring-indigo-500"
checked={selectedEnterprises.includes(enterprise.id)}
onChange={() => handleSelectEnterprise(enterprise.id)}
/>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="flex items-center">
<BuildingOfficeIcon className="h-5 w-5 text-gray-400 mr-3" />
<div>
<div className="text-sm font-medium text-gray-900">{enterprise.companyName}</div>
<div className="text-sm text-gray-500">{enterprise.industry} · {getCompanySizeText(enterprise.companySize)}</div>
{enterprise.website && (
<div className="text-xs text-blue-500">{enterprise.website}</div>
)}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="flex items-center text-sm font-medium text-gray-900">
<UserGroupIcon className="h-4 w-4 mr-1" />
{enterprise.contactPerson}
</div>
<div className="flex items-center text-sm text-gray-500 mt-1">
<PhoneIcon className="h-4 w-4 mr-1" />
{enterprise.contactPhone}
</div>
<div className="flex items-center text-sm text-gray-500 mt-1">
<EnvelopeIcon className="h-4 w-4 mr-1" />
{enterprise.contactEmail}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<span className={`inline-flex px-2 py-1 text-xs font-semibold rounded-full ${getPackageColor(enterprise.servicePackage)}`}>
{getPackageText(enterprise.servicePackage)}
</span>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="space-y-1">
<span className={`inline-flex items-center px-2 py-1 text-xs font-semibold rounded-full ${getStatusColor(enterprise.contractStatus)}`}>
{getStatusIcon(enterprise.contractStatus)}
<span className="ml-1">{getStatusText(enterprise.contractStatus)}</span>
</span>
<div className="text-xs text-gray-500">
{enterprise.contractStart} ~ {enterprise.contractEnd}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div className="text-sm font-medium text-gray-900">
{formatCurrency(enterprise.contractValue)}
</div>
<div className="text-xs text-gray-500">
使{formatCurrency(enterprise.monthlyUsage)}
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
<div>
<div className="text-sm text-gray-900">{enterprise.totalOrders}</div>
<div className="text-xs text-gray-500">
{enterprise.lastActivity}
</div>
</div>
</td>
<td className="px-6 py-4 whitespace-nowrap">
{enterprise.rating > 0 && (
<div className="flex items-center">
<div className="flex items-center">
{[...Array(5)].map((_, i) => (
<StarIcon
key={i}
className={`h-4 w-4 ${
i < enterprise.rating ? 'text-yellow-400 fill-current' : 'text-gray-300'
}`}
/>
))}
</div>
<span className="ml-2 text-sm text-gray-500">{enterprise.rating}</span>
</div>
)}
</td>
<td className="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<div className="flex items-center space-x-2">
<button className="text-indigo-600 hover:text-indigo-900" title="查看详情">
<EyeIcon className="h-4 w-4" />
</button>
<button className="text-green-600 hover:text-green-900" title="编辑信息">
<PencilIcon className="h-4 w-4" />
</button>
<button className="text-blue-600 hover:text-blue-900" title="查看合同">
<DocumentTextIcon className="h-4 w-4" />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{/* 分页 */}
{totalPages > 1 && (
<div className="bg-white px-4 py-3 flex items-center justify-between border-t border-gray-200 sm:px-6">
<div className="flex-1 flex justify-between sm:hidden">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="ml-3 relative inline-flex items-center px-4 py-2 border border-gray-300 text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
</button>
</div>
<div className="hidden sm:flex-1 sm:flex sm:items-center sm:justify-between">
<div>
<p className="text-sm text-gray-700">
<span className="font-medium">{(currentPage - 1) * enterprisesPerPage + 1}</span> {' '}
<span className="font-medium">{Math.min(currentPage * enterprisesPerPage, enterprises.length)}</span>
<span className="font-medium">{enterprises.length}</span>
</p>
</div>
<div>
<nav className="relative z-0 inline-flex rounded-md shadow-sm -space-x-px" aria-label="Pagination">
<button
onClick={() => setCurrentPage(Math.max(1, currentPage - 1))}
disabled={currentPage === 1}
className="relative inline-flex items-center px-2 py-2 rounded-l-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronLeftIcon className="h-5 w-5" />
</button>
{Array.from({ length: totalPages }, (_, i) => i + 1).map((page) => (
<button
key={page}
onClick={() => setCurrentPage(page)}
className={`relative inline-flex items-center px-4 py-2 border text-sm font-medium ${
page === currentPage
? 'z-10 bg-indigo-50 border-indigo-500 text-indigo-600'
: 'bg-white border-gray-300 text-gray-500 hover:bg-gray-50'
}`}
>
{page}
</button>
))}
<button
onClick={() => setCurrentPage(Math.min(totalPages, currentPage + 1))}
disabled={currentPage === totalPages}
className="relative inline-flex items-center px-2 py-2 rounded-r-md border border-gray-300 bg-white text-sm font-medium text-gray-500 hover:bg-gray-50 disabled:opacity-50 disabled:cursor-not-allowed"
>
<ChevronRightIcon className="h-5 w-5" />
</button>
</nav>
</div>
</div>
</div>
)}
</div>
</div>
</DashboardLayout>
);
}