Files
yunhaifinance/frontend/src/pages/construction/ConstructionList.tsx
T
a273825743 706dcc24eb 备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
2026-06-13 12:44:48 +08:00

270 lines
9.0 KiB
TypeScript

import React, { useState, useEffect } from 'react';
import { Card, Typography, Button, Space, Tag, Progress, Empty, Spin, message, Row, Col, Divider } from 'antd';
import { FileTextOutlined, CameraOutlined, ScheduleOutlined, PlusOutlined, ReloadOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import apiClient from '../../utils/request';
import dayjs from 'dayjs';
import { useAuthStore } from '../../store/authStore';
import { useLanguageStore } from '../../store/languageStore';
const { Title, Paragraph, Text } = Typography;
const WEATHER_ICONS: Record<string, string> = {
sunny: '☀️',
cloudy: '⛅',
rainy: '🌧️',
stormy: '⛈️',
windy: '💨',
};
interface Project {
id: number;
project_code: string;
name: string;
customer_name: string;
status: string;
start_date: string;
expected_end_date: string;
contract_amount: number;
currency: string;
manager_name: string;
progress_percentage: number;
latest_log?: {
id: number;
log_date: string;
weather: string;
work_content: string;
};
}
const ConstructionList: React.FC = () => {
const [isMobile, setIsMobile] = useState(false);
const [projects, setProjects] = useState<Project[]>([]);
const [loading, setLoading] = useState(true);
const navigate = useNavigate();
const { user } = useAuthStore();
const { t, currentLanguage } = useLanguageStore();
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
pending: { color: 'default', text: t('construction.pendingStart') },
active: { color: 'processing', text: t('construction.underConstruction') },
completed: { color: 'success', text: t('construction.completed') },
suspended: { color: 'warning', text: t('construction.paused') },
cancelled: { color: 'error', text: t('construction.cancelled') },
};
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
fetchProjects();
}, []);
const fetchProjects = async () => {
setLoading(true);
try {
const res = await apiClient.get('/construction/my-projects');
if (res.data.success) {
setProjects(res.data.data);
}
} catch (error) {
console.error('获取项目列表失败:', error);
message.error(t('construction.getListFailed'));
} finally {
setLoading(false);
}
};
const formatCurrency = (amount: number, currency: string = 'CNY') => {
const symbols: Record<string, string> = {
CNY: '¥',
USD: '$',
LAK: '₭',
THB: '฿',
};
const symbol = symbols[currency] || '¥';
return `${symbol}${(amount || 0).toLocaleString('zh-CN', { minimumFractionDigits: 0 })}`;
};
const isToday = (dateStr: string) => {
return dayjs(dateStr).isSame(dayjs(), 'day');
};
const getWeatherLabel = (weather: string) => {
const icon = WEATHER_ICONS[weather] || '';
const textMap: Record<string, string> = {
sunny: t('construction.sunny'),
cloudy: t('construction.cloudy'),
rainy: t('construction.rain'),
stormy: t('construction.thunderstorm'),
windy: t('construction.windy'),
};
return `${icon} ${textMap[weather] || weather}`;
};
const renderProjectCard = (project: Project) => {
const statusConfig = STATUS_CONFIG[project.status] || STATUS_CONFIG.pending;
const hasTodayLog = project.latest_log && isToday(project.latest_log.log_date);
return (
<Card
key={project.id}
style={{
marginBottom: isMobile ? 12 : 16,
borderRadius: 12,
boxShadow: '0 2px 8px rgba(0,0,0,0.08)',
}}
styles={{ body: { padding: isMobile ? 16 : 20 } }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'flex-start', marginBottom: 12 }}>
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 8, marginBottom: 4 }}>
<span style={{ fontSize: 20 }}>🎯</span>
<Text strong style={{ fontSize: isMobile ? 15 : 16 }}>{project.name}</Text>
</div>
<Text type="secondary" style={{ fontSize: 13 }}>
{t('construction.customerLabel')}{project.customer_name || t('common.notSet')}
</Text>
</div>
<Tag color={statusConfig.color} style={{ marginLeft: 8 }}>
{statusConfig.text}
</Tag>
</div>
<div style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 4 }}>
<Text type="secondary" style={{ fontSize: 12 }}>{t('construction.progress')}</Text>
<Text strong style={{ fontSize: 12 }}>{Math.round((project.progress_percentage || 0))}%</Text>
</div>
<Progress
percent={Math.round((project.progress_percentage || 0))}
showInfo={false}
strokeColor={{
'0%': '#108ee9',
'100%': '#87d068',
}}
trailColor="#f0f0f0"
/>
</div>
{project.status === 'active' && (
<div style={{
padding: '8px 12px',
background: hasTodayLog ? '#f6ffed' : '#fff7e6',
borderRadius: 8,
marginBottom: 12,
display: 'flex',
alignItems: 'center',
gap: 8
}}>
{hasTodayLog ? (
<>
<span></span>
<Text style={{ fontSize: 13 }}>
{t('construction.todayLog')}{project.latest_log?.work_content?.substring(0, 30)}...
</Text>
</>
) : (
<>
<span>⚠️</span>
<Text type="warning" style={{ fontSize: 13 }}>{t('construction.todayLogEmpty')}</Text>
</>
)}
</div>
)}
<Divider style={{ margin: '12px 0' }} />
<Row gutter={[8, 8]}>
<Col xs={24} sm={8}>
<Button
type={project.status === 'active' && !hasTodayLog ? 'primary' : 'default'}
icon={<FileTextOutlined />}
onClick={() => navigate(`/construction/${project.id}/logs`)}
block
size={isMobile ? 'large' : 'middle'}
style={{ borderRadius: 8 }}
>
{project.status === 'active' && !hasTodayLog ? `📝 ${t('construction.writeLog')}` : `📝 ${t('construction.constructionLog')}`}
</Button>
</Col>
<Col xs={24} sm={8}>
<Button
icon={<CameraOutlined />}
onClick={() => navigate(`/construction/${project.id}/logs`)}
block
size={isMobile ? 'large' : 'middle'}
style={{ borderRadius: 8 }}
>
{`📷 ${t('construction.uploadPhoto')}`}
</Button>
</Col>
<Col xs={24} sm={8}>
<Button
icon={<ScheduleOutlined />}
onClick={() => navigate(`/construction/${project.id}/milestones`)}
block
size={isMobile ? 'large' : 'middle'}
style={{ borderRadius: 8 }}
>
{`📋 ${t('construction.milestoneProgress')}`}
</Button>
</Col>
</Row>
</Card>
);
};
return (
<div style={{
padding: isMobile ? 12 : 24,
maxWidth: 1200,
margin: '0 auto'
}}>
<div style={{ marginBottom: isMobile ? 16 : 24 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Title level={isMobile ? 4 : 3} style={{ marginBottom: 0 }}>{t('construction.management')}</Title>
<Button
icon={<ReloadOutlined />}
onClick={fetchProjects}
loading={loading}
>
{t('common.refresh')}
</Button>
</div>
<Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
{t('construction.description')}
</Paragraph>
</div>
{loading ? (
<div style={{ textAlign: 'center', padding: 60 }}>
<Spin size="large" />
<Paragraph type="secondary" style={{ marginTop: 16 }}>{t('common.loading')}</Paragraph>
</div>
) : projects.length === 0 ? (
<Card style={{ borderRadius: 12 }}>
<Empty
description={t('construction.noConstructionProjects')}
image={Empty.PRESENTED_IMAGE_SIMPLE}
>
<Text type="secondary">{t('construction.contactAdmin')}</Text>
</Empty>
</Card>
) : (
<div>
<Text type="secondary" style={{ marginBottom: 12, display: 'block' }}>
{t('construction.myProjects')} ({projects.length})
</Text>
{projects.map(project => renderProjectCard(project))}
</div>
)}
</div>
);
};
export default ConstructionList;