Files
yunhaifinance/company-finance-system/frontend/src/pages/construction/ConstructionList.tsx
T

265 lines
8.4 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 axios from 'axios';
import dayjs from 'dayjs';
import { useAuthStore } from '../../store/authStore';
const { Title, Paragraph, Text } = Typography;
// 天气图标映射
const WEATHER_ICONS: Record<string, string> = {
sunny: '☀️ 晴',
cloudy: '⛅ 多云',
rainy: '🌧️ 雨',
stormy: '⛈️ 雷暴',
windy: '💨 大风',
};
// 项目状态映射
const STATUS_CONFIG: Record<string, { color: string; text: string }> = {
pending: { color: 'default', text: '待开始' },
active: { color: 'processing', text: '施工中' },
completed: { color: 'success', text: '完工' },
suspended: { color: 'warning', text: '暂停' },
cancelled: { color: 'error', text: '已取消' },
};
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();
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 axios.get('/api/construction/my-projects');
if (res.data.success) {
setProjects(res.data.data);
}
} catch (error) {
console.error('获取项目列表失败:', error);
message.error('获取项目列表失败');
} 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 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 }}>
客户: {project.customer_name || '未指定'}
</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 }}>施工进度</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 }}>
今日日志: {project.latest_log?.work_content?.substring(0, 30)}...
</Text>
</>
) : (
<>
<span>⚠️</span>
<Text type="warning" style={{ fontSize: 13 }}>今日日志: 未填写</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 ? '📝 写今日日志' : '📝 施工日志'}
</Button>
</Col>
<Col xs={24} sm={8}>
<Button
icon={<CameraOutlined />}
onClick={() => navigate(`/construction/${project.id}/logs`)}
block
size={isMobile ? 'large' : 'middle'}
style={{ borderRadius: 8 }}
>
📷 上传照片
</Button>
</Col>
<Col xs={24} sm={8}>
<Button
icon={<ScheduleOutlined />}
onClick={() => navigate(`/construction/${project.id}/milestones`)}
block
size={isMobile ? 'large' : 'middle'}
style={{ borderRadius: 8 }}
>
📋 节点进度
</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 }}>施工管理</Title>
<Button
icon={<ReloadOutlined />}
onClick={fetchProjects}
loading={loading}
>
刷新
</Button>
</div>
<Paragraph type="secondary" style={{ marginTop: 8, marginBottom: 0 }}>
查看和管理您的施工项目
</Paragraph>
</div>
{/* 项目列表 */}
{loading ? (
<div style={{ textAlign: 'center', padding: 60 }}>
<Spin size="large" />
<Paragraph type="secondary" style={{ marginTop: 16 }}>加载中...</Paragraph>
</div>
) : projects.length === 0 ? (
<Card style={{ borderRadius: 12 }}>
<Empty
description="暂无施工项目"
image={Empty.PRESENTED_IMAGE_SIMPLE}
>
<Text type="secondary">请联系管理员为您分配施工项目</Text>
</Empty>
</Card>
) : (
<div>
<Text type="secondary" style={{ marginBottom: 12, display: 'block' }}>
我的施工项目 ({projects.length})
</Text>
{projects.map(project => renderProjectCard(project))}
</div>
)}
</div>
);
};
export default ConstructionList;