mars f20988b90c feat: 完成所有页面的演示模式实现
- 更新 DashboardLayout 组件,统一使用演示模式布局
- 实现仪表盘页面的完整演示数据和功能
- 完成用户管理页面的演示模式,包含搜索、过滤、分页等功能
- 实现通话记录页面的演示数据和录音播放功能
- 完成翻译员管理页面的演示模式
- 实现订单管理页面的完整功能
- 完成发票管理页面的演示数据
- 更新文档管理页面
- 添加 utils.ts 工具函数库
- 完善 API 路由和数据库结构
- 修复各种 TypeScript 类型错误
- 统一界面风格和用户体验
2025-06-30 19:42:43 +08:00

683 lines
29 KiB
TypeScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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>
);
}