Files
yunhaifinance/frontend/src/pages/ProjectCostPage.tsx
T

252 lines
8.9 KiB
TypeScript

import React, { useState, useEffect } from 'react'
import {
Table, Button, Card, Row, Col, Statistic, Select, message, Spin, Progress
} from 'antd'
import { BarChartOutlined, DollarOutlined, ShoppingOutlined } from '@ant-design/icons'
import type { ColumnsType } from 'antd/es/table'
// ==================== 类型定义 ====================
interface Project {
id: number
name: string
code: string
contract_amount: number
}
interface CostSummary {
project_name: string
contract_amount: number
purchase_cost: {
total: number
by_category: Record<string, number>
}
payment_cost: number
total_cost: number
profit: number
}
// ==================== 组件 ====================
const ProjectCostPage: React.FC = () => {
// 状态
const [projects, setProjects] = useState<Project[]>([])
const [selectedProjectId, setSelectedProjectId] = useState<number | null>(null)
const [costSummary, setCostSummary] = useState<CostSummary | null>(null)
const [loading, setLoading] = useState(false)
// ==================== 数据加载 ====================
const fetchProjects = async () => {
try {
const response = await fetch('/api/projects')
const data = await response.json()
if (data.success) {
setProjects(data.data)
}
} catch (error) {
console.error('获取项目列表失败:', error)
}
}
const fetchCostSummary = async (projectId: number) => {
setLoading(true)
try {
const response = await fetch(`/api/projects/${projectId}/cost-summary`)
const data = await response.json()
if (data.success) {
setCostSummary(data.data)
} else {
message.error('获取成本统计失败')
setCostSummary(null)
}
} catch (error) {
console.error('获取成本统计失败:', error)
message.error('获取成本统计失败')
setCostSummary(null)
} finally {
setLoading(false)
}
}
useEffect(() => {
fetchProjects()
}, [])
useEffect(() => {
if (selectedProjectId) {
fetchCostSummary(selectedProjectId)
} else {
setCostSummary(null)
}
}, [selectedProjectId])
// ==================== 渲染 ====================
return (
<div style={{ padding: 24 }}>
<Card>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={12}>
<Select
placeholder="请选择项目查看成本统计"
style={{ width: '100%' }}
value={selectedProjectId}
onChange={(value) => setSelectedProjectId(value)}
>
{projects.map(project => (
<Select.Option key={project.id} value={project.id}>
{project.name}
</Select.Option>
))}
</Select>
</Col>
</Row>
{loading ? (
<div style={{ textAlign: 'center', padding: 40 }}>
<Spin size="large" />
</div>
) : costSummary ? (
<>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={6}>
<Card>
<Statistic
title="合同金额"
value={costSummary.contract_amount}
precision={2}
prefix={<DollarOutlined />}
valueStyle={{ color: '#1890ff' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="采购成本"
value={costSummary.purchase_cost.total}
precision={2}
prefix={<ShoppingOutlined />}
valueStyle={{ color: '#faad14' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="付款支出"
value={costSummary.payment_cost}
precision={2}
prefix={<DollarOutlined />}
valueStyle={{ color: '#fa8c16' }}
/>
</Card>
</Col>
<Col span={6}>
<Card>
<Statistic
title="利润"
value={costSummary.profit}
precision={2}
prefix={<BarChartOutlined />}
valueStyle={{ color: costSummary.profit >= 0 ? '#52c41a' : '#ff4d4f' }}
/>
</Card>
</Col>
</Row>
<Row gutter={16}>
<Col span={12}>
<Card title="总成本构成" style={{ marginBottom: 16 }}>
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span>合同金额</span>
<span>{(costSummary.contract_amount || 0).toFixed(2)}</span>
</div>
<Progress
percent={100}
status="active"
strokeColor="#1890ff"
/>
</div>
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span>采购成本</span>
<span>{(costSummary.purchase_cost.total || 0).toFixed(2)}</span>
</div>
<Progress
percent={costSummary.contract_amount > 0 ? Math.min((costSummary.purchase_cost.total / costSummary.contract_amount) * 100, 100) : 0}
status="normal"
strokeColor="#faad14"
/>
</div>
<div style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span>付款支出</span>
<span>{(costSummary.payment_cost || 0).toFixed(2)}</span>
</div>
<Progress
percent={costSummary.contract_amount > 0 ? Math.min((costSummary.payment_cost / costSummary.contract_amount) * 100, 100) : 0}
status="normal"
strokeColor="#fa8c16"
/>
</div>
<div>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span>总成本</span>
<span>{costSummary.total_cost.toFixed(2)}</span>
</div>
<Progress
percent={costSummary.contract_amount > 0 ? Math.min((costSummary.total_cost / costSummary.contract_amount) * 100, 100) : 0}
status="normal"
strokeColor={costSummary.profit >= 0 ? '#52c41a' : '#ff4d4f'}
/>
</div>
</Card>
</Col>
<Col span={12}>
<Card title="采购成本分类">
{Object.keys(costSummary.purchase_cost.by_category).length > 0 ? (
<div>
{Object.entries(costSummary.purchase_cost.by_category).map(([category, amount]) => {
const categoryMap: Record<string, string> = {
material: '材料',
equipment: '设备',
pole: '电杆',
other: '其他'
}
return (
<div key={category} style={{ marginBottom: 16 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<span>{categoryMap[category] || category}</span>
<span>{(amount || 0).toFixed(2)}</span>
</div>
<Progress
percent={costSummary.purchase_cost.total > 0 ? (amount / costSummary.purchase_cost.total) * 100 : 0}
status="normal"
/>
</div>
)
})}
</div>
) : (
<div style={{ textAlign: 'center', padding: 20, color: '#999' }}>
暂无采购成本数据
</div>
)}
</Card>
</Col>
</Row>
</>
) : (
<div style={{ textAlign: 'center', padding: 40, color: '#999' }}>
请选择项目查看成本统计
</div>
)}
</Card>
</div>
)
}
export default ProjectCostPage