'use client'; /** * 合同管理页面 * [2026-02-05] 新增:全局合同查询、筛选、批量下载功能 * 回滚方式:删除此目录即可 */ import { useState, useEffect, useCallback } from 'react'; import { Button, toast } from '@/components/common'; import { PageContainer } from '@/components/layout'; import { cn } from '@/utils/helpers'; import { formatNumber } from '@/utils/formatters'; import { contractService, CONTRACT_STATUS_LABELS, type ContractsListResponse, type ContractStatisticsResponse, type ContractQueryParams, } from '@/services/contractService'; import { PROVINCE_CODE_NAMES } from '@/types'; import styles from './contracts.module.scss'; // 获取合同状态对应的样式类 const getStatusStyleClass = (status: string): string => { switch (status) { case 'SIGNED': return styles.contracts__statusGreen; case 'PENDING': case 'SCROLLED': case 'ACKNOWLEDGED': return styles.contracts__statusYellow; case 'EXPIRED': case 'TIMEOUT': return styles.contracts__statusRed; default: return styles.contracts__statusGray; } }; // 省份列表(用于筛选)- 只使用 6 位代码(数据库中存的是 6 位) const PROVINCE_OPTIONS = Object.entries(PROVINCE_CODE_NAMES) .filter(([code]) => code.length === 6) .map(([code, name]) => ({ value: code, label: name, })); /** * 合同管理页面 */ export default function ContractsPage() { // 筛选状态 const [filters, setFilters] = useState({ page: 1, pageSize: 20, provinceCode: '', cityCode: '', status: '', signedAfter: '', signedBefore: '', orderBy: 'signedAt', orderDir: 'desc', }); // 数据状态 const [data, setData] = useState(null); const [statistics, setStatistics] = useState(null); const [loading, setLoading] = useState(true); const [statsLoading, setStatsLoading] = useState(true); // 增量下载状态 const [lastDownloadTime, setLastDownloadTime] = useState(() => { if (typeof window !== 'undefined') { return localStorage.getItem('contract_last_download_time'); } return null; }); // 加载合同列表 const loadContracts = useCallback(async () => { setLoading(true); try { const result = await contractService.getContracts({ ...filters, provinceCode: filters.provinceCode || undefined, cityCode: filters.cityCode || undefined, status: filters.status || undefined, signedAfter: filters.signedAfter || undefined, signedBefore: filters.signedBefore || undefined, }); setData(result); } catch (error) { console.error('获取合同列表失败:', error); toast.error('获取合同列表失败'); } finally { setLoading(false); } }, [filters]); // 加载统计信息 const loadStatistics = useCallback(async () => { setStatsLoading(true); try { const result = await contractService.getStatistics({ provinceCode: filters.provinceCode || undefined, cityCode: filters.cityCode || undefined, }); setStatistics(result); } catch (error) { console.error('获取统计信息失败:', error); } finally { setStatsLoading(false); } }, [filters.provinceCode, filters.cityCode]); useEffect(() => { loadContracts(); }, [loadContracts]); useEffect(() => { loadStatistics(); }, [loadStatistics]); // 格式化日期 const formatDate = (dateStr: string | null) => { if (!dateStr) return '-'; return new Date(dateStr).toLocaleString('zh-CN'); }; // 格式化金额 const formatAmount = (amount: number) => { return amount.toLocaleString('zh-CN', { minimumFractionDigits: 2, maximumFractionDigits: 2 }); }; // 下载单个合同 const handleDownloadContract = (orderNo: string) => { const downloadUrl = contractService.getDownloadUrl(orderNo); window.open(downloadUrl, '_blank'); }; // 批量下载状态 const [batchDownloading, setBatchDownloading] = useState(false); const [batchProgress, setBatchProgress] = useState(0); // 轮询任务状态直到完成 const pollTaskUntilComplete = async (taskNo: string): Promise => { const maxAttempts = 120; // 最多轮询2分钟 for (let i = 0; i < maxAttempts; i++) { try { const status = await contractService.getBatchDownloadStatus(taskNo); setBatchProgress(status.progress); if (status.status === 'COMPLETED' && status.resultFileUrl) { return status.resultFileUrl; } else if (status.status === 'FAILED') { throw new Error('任务处理失败'); } // 等待1秒后继续轮询 await new Promise(resolve => setTimeout(resolve, 1000)); } catch (error) { console.error('查询任务状态失败:', error); throw error; } } throw new Error('任务处理超时'); }; // 使用 File System Access API 让用户选择保存位置(Chrome/Edge 支持) const saveFileWithPicker = async (url: string, suggestedName: string): Promise => { try { // 检查浏览器是否支持 File System Access API if ('showSaveFilePicker' in window) { // 先下载文件内容 const response = await fetch(url); const blob = await response.blob(); // 弹出保存对话框让用户选择位置 const fileHandle = await (window as unknown as { showSaveFilePicker: (options: unknown) => Promise }).showSaveFilePicker({ suggestedName, types: [{ description: 'ZIP 压缩文件', accept: { 'application/zip': ['.zip'] }, }], }); // 写入文件 const writable = await fileHandle.createWritable(); await writable.write(blob); await writable.close(); return true; } } catch (error) { // 用户取消或浏览器不支持,回退到普通下载 console.log('File System Access API 不可用或用户取消,使用普通下载'); } return false; }; // 批量下载(创建下载任务并等待完成后自动下载) const handleBatchDownload = async () => { setBatchDownloading(true); setBatchProgress(0); try { toast.info('正在准备批量下载,请稍候...'); const result = await contractService.createBatchDownload({ filters: { signedAfter: filters.signedAfter || undefined, signedBefore: filters.signedBefore || undefined, provinceCode: filters.provinceCode || undefined, cityCode: filters.cityCode || undefined, }, }); // 轮询等待任务完成 const downloadUrl = await pollTaskUntilComplete(result.taskNo); if (downloadUrl) { // 生成文件名 const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); const suggestedName = `contracts_${today}.zip`; // 优先使用 File System Access API 让用户选择保存位置 toast.info('请选择保存位置...'); const saved = await saveFileWithPicker(downloadUrl, suggestedName); if (saved) { toast.success('文件保存成功!'); } else { // 浏览器不支持或用户取消,使用备用方法 const link = document.createElement('a'); link.href = downloadUrl; link.download = suggestedName; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success('下载已开始'); } // 保存当前时间作为最后下载时间 const now = new Date().toISOString(); localStorage.setItem('contract_last_download_time', now); setLastDownloadTime(now); } } catch (error) { console.error('批量下载失败:', error); toast.error('批量下载失败,请重试'); } finally { setBatchDownloading(false); setBatchProgress(0); } }; // 增量下载(只下载上次之后签署的合同) const handleIncrementalDownload = async () => { if (!lastDownloadTime) { toast.error('未找到上次下载记录,请先执行全量下载'); return; } setBatchDownloading(true); setBatchProgress(0); try { toast.info('正在准备增量下载,请稍候...'); const result = await contractService.createBatchDownload({ filters: { signedAfter: lastDownloadTime, provinceCode: filters.provinceCode || undefined, cityCode: filters.cityCode || undefined, }, }); // 轮询等待任务完成 const downloadUrl = await pollTaskUntilComplete(result.taskNo); if (downloadUrl) { // 生成文件名 const today = new Date().toISOString().split('T')[0].replace(/-/g, ''); const suggestedName = `contracts_incremental_${today}.zip`; // 优先使用 File System Access API 让用户选择保存位置 toast.info('请选择保存位置...'); const saved = await saveFileWithPicker(downloadUrl, suggestedName); if (saved) { toast.success('文件保存成功!'); } else { // 浏览器不支持或用户取消,使用备用方法 const link = document.createElement('a'); link.href = downloadUrl; link.download = suggestedName; document.body.appendChild(link); link.click(); document.body.removeChild(link); toast.success('下载已开始'); } // 更新最后下载时间 const now = new Date().toISOString(); localStorage.setItem('contract_last_download_time', now); setLastDownloadTime(now); } } catch (error) { console.error('增量下载失败:', error); toast.error('增量下载失败,请重试'); } finally { setBatchDownloading(false); setBatchProgress(0); } }; // 重置筛选 const handleResetFilters = () => { setFilters({ page: 1, pageSize: 20, provinceCode: '', cityCode: '', status: '', signedAfter: '', signedBefore: '', orderBy: 'signedAt', orderDir: 'desc', }); }; // 翻页 const handlePageChange = (newPage: number) => { setFilters(prev => ({ ...prev, page: newPage })); }; return (
{/* 统计卡片 */}
合同总数 {statsLoading ? '...' : formatNumber(statistics?.totalContracts || 0)}
已签署 {statsLoading ? '...' : formatNumber(statistics?.signedContracts || 0)}
待签署 {statsLoading ? '...' : formatNumber(statistics?.pendingContracts || 0)}
已超时 {statsLoading ? '...' : formatNumber(statistics?.expiredContracts || 0)}
{/* 筛选栏 */}
setFilters(prev => ({ ...prev, signedAfter: e.target.value ? `${e.target.value}T00:00:00.000Z` : '', page: 1 }))} />
setFilters(prev => ({ ...prev, signedBefore: e.target.value ? `${e.target.value}T23:59:59.999Z` : '', page: 1 }))} />
{lastDownloadTime && ( )}
{/* 合同列表 */}
合同编号
订单号
用户
认种数量
金额
省市
状态
签署时间
操作
{loading ? (
加载中...
) : data && data.items.length > 0 ? ( data.items.map((contract) => (
{contract.contractNo}
{contract.orderNo}
{contract.userRealName || '未实名'}
{contract.accountSequence}
{formatNumber(contract.treeCount)}
{formatAmount(contract.totalAmount)}
{contract.provinceName} / {contract.cityName}
{CONTRACT_STATUS_LABELS[contract.status] || contract.status}
{contract.signedAt ? formatDate(contract.signedAt) : '-'}
{contract.status === 'SIGNED' && contract.signedPdfUrl && ( )}
)) ) : (
暂无合同数据
)}
{/* 分页 */} {data && data.totalPages > 1 && (
第 {filters.page} / {data.totalPages} 页,共 {data.total} 条
)}
); }