252 lines
8.9 KiB
TypeScript
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
|