备份:修复前完整项目快照 2026-04-19
This commit is contained in:
@@ -0,0 +1,251 @@
|
||||
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
|
||||
Reference in New Issue
Block a user