财务分类体系+统一账本+Excel导入+报表改造
This commit is contained in:
@@ -1,177 +1,177 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Table, DatePicker, Button, Row, Col, Tag } from 'antd';
|
||||
import { DownloadOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const ReportsPage: React.FC = () => {
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
// 报表数据 - 从API获取或为空数组
|
||||
const dataSource: any[] = [];
|
||||
|
||||
// 桌面端表格列
|
||||
const desktopColumns = [
|
||||
{
|
||||
title: '月份',
|
||||
dataIndex: 'month',
|
||||
key: 'month',
|
||||
},
|
||||
{
|
||||
title: '总收入',
|
||||
dataIndex: 'income',
|
||||
key: 'income',
|
||||
render: (amount: number) => `¥${amount.toLocaleString()}`,
|
||||
},
|
||||
{
|
||||
title: '总支出',
|
||||
dataIndex: 'expense',
|
||||
key: 'expense',
|
||||
render: (amount: number) => `¥${amount.toLocaleString()}`,
|
||||
},
|
||||
{
|
||||
title: '净利润',
|
||||
dataIndex: 'profit',
|
||||
key: 'profit',
|
||||
render: (amount: number) => (
|
||||
<span style={{ color: amount > 0 ? 'green' : 'red' }}>
|
||||
¥{amount.toLocaleString()}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '项目数量',
|
||||
dataIndex: 'projects',
|
||||
key: 'projects',
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
key: 'action',
|
||||
render: () => (
|
||||
<Button size="small" icon={<DownloadOutlined />}>导出</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
// 移动端简化表格列
|
||||
const mobileColumns = [
|
||||
{
|
||||
title: '月份',
|
||||
dataIndex: 'month',
|
||||
key: 'month',
|
||||
render: (month: string) => month.replace('-', '/'),
|
||||
},
|
||||
{
|
||||
title: '收入',
|
||||
dataIndex: 'income',
|
||||
key: 'income',
|
||||
render: (amount: number) => (
|
||||
<div style={{ color: 'green' }}>¥{(amount / 10000).toFixed(0)}万</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '支出',
|
||||
dataIndex: 'expense',
|
||||
key: 'expense',
|
||||
render: (amount: number) => (
|
||||
<div style={{ color: 'red' }}>¥{(amount / 10000).toFixed(0)}万</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '利润',
|
||||
dataIndex: 'profit',
|
||||
key: 'profit',
|
||||
render: (amount: number) => (
|
||||
<div style={{ fontWeight: 'bold', color: amount > 0 ? 'green' : 'red' }}>
|
||||
¥{(amount / 10000).toFixed(0)}万
|
||||
</div>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? 8 : 24 }}>
|
||||
<div style={{ marginBottom: isMobile ? 12 : 24 }}>
|
||||
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 8 }}>统计报表</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
查看项目财务报表和统计分析数据
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
<Card
|
||||
title="月度财务报表"
|
||||
size="small"
|
||||
styles={{ body: { padding: isMobile ? 8 : 24 } }}
|
||||
extra={
|
||||
isMobile ? (
|
||||
<Button size="small" icon={<DownloadOutlined />} />
|
||||
) : (
|
||||
<DatePicker picker="month" style={{ marginRight: 8 }} />
|
||||
)
|
||||
}
|
||||
>
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
columns={isMobile ? mobileColumns : desktopColumns}
|
||||
pagination={false}
|
||||
scroll={isMobile ? { x: 350 } : undefined}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
summary={(pageData) => {
|
||||
let totalIncome = 0;
|
||||
let totalExpense = 0;
|
||||
let totalProfit = 0;
|
||||
let totalProjects = 0;
|
||||
|
||||
pageData.forEach(({ income, expense, profit, projects }) => {
|
||||
totalIncome += income;
|
||||
totalExpense += expense;
|
||||
totalProfit += profit;
|
||||
totalProjects += projects;
|
||||
});
|
||||
|
||||
return (
|
||||
<Table.Summary fixed>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0}>
|
||||
<strong>合计</strong>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={1}>
|
||||
<strong>¥{(totalIncome / 10000).toFixed(0)}万</strong>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={2}>
|
||||
<strong>¥{(totalExpense / 10000).toFixed(0)}万</strong>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={3}>
|
||||
<strong style={{ color: totalProfit > 0 ? 'green' : 'red' }}>
|
||||
¥{(totalProfit / 10000).toFixed(0)}万
|
||||
</strong>
|
||||
</Table.Summary.Cell>
|
||||
{!isMobile && (
|
||||
<>
|
||||
<Table.Summary.Cell index={4}>
|
||||
<strong>{totalProjects}</strong>
|
||||
</Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={5} />
|
||||
</>
|
||||
)}
|
||||
</Table.Summary.Row>
|
||||
</Table.Summary>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportsPage;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Table, DatePicker, Row, Col, Statistic, Tag, Spin } from 'antd';
|
||||
import { RiseOutlined, FallOutlined, DollarOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const LEVEL2_LABELS: Record<string, string> = {
|
||||
contract_payment: '项目合同收款', deposit_refund: '质保金退回', shareholder_investment: '股东投资入股', other_income: '其他收入',
|
||||
material: '材料采购', equipment: '设备采购', subcontract: '施工分包', labor: '人工工资',
|
||||
travel: '差旅交通', accommodation: '食宿费用', freight: '运输物流', design: '勘测设计',
|
||||
tools: '小型工具', client_relations: '客户/EDL关系', other_project: '其他项目支出',
|
||||
salary: '工资薪酬', rent: '房租物业', office: '办公费用', commute: '交通通勤',
|
||||
vehicle_maintenance: '车辆维保', assets: '固定资产', marketing: '营销拓展',
|
||||
entertainment: '招待费用', welfare: '员工福利', logistics: '快递物流', other_company: '其他公司支出'
|
||||
};
|
||||
|
||||
const ReportsPage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
const [summary, setSummary] = useState<any>({ total_income: 0, total_expense: 0, net_profit: 0 });
|
||||
const [byMonth, setByMonth] = useState<any[]>([]);
|
||||
const [byCategory, setByCategory] = useState<any[]>([]);
|
||||
const [selectedMonth, setSelectedMonth] = useState<dayjs.Dayjs | null>(null);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const fetchData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = {};
|
||||
if (selectedMonth) {
|
||||
params.date_from = selectedMonth.startOf('month').format('YYYY-MM-DD');
|
||||
params.date_to = selectedMonth.endOf('month').format('YYYY-MM-DD');
|
||||
}
|
||||
const res = await apiClient.get('/financial-records/summary', { params });
|
||||
if (res.data.success) {
|
||||
setSummary(res.data.data.totals);
|
||||
setByMonth(res.data.data.byMonth);
|
||||
setByCategory(res.data.data.byCategory);
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchData(); }, [selectedMonth]);
|
||||
|
||||
const monthData = byMonth.map(m => ({
|
||||
...m,
|
||||
profit: m.income - m.expense
|
||||
}));
|
||||
|
||||
const desktopColumns = [
|
||||
{ title: '月份', dataIndex: 'month', key: 'month' },
|
||||
{ title: '总收入', dataIndex: 'income', key: 'income', render: (v: number) => `¥${parseFloat(v).toLocaleString()}` },
|
||||
{ title: '总支出', dataIndex: 'expense', key: 'expense', render: (v: number) => `¥${parseFloat(v).toLocaleString()}` },
|
||||
{
|
||||
title: '净利润', dataIndex: 'profit', key: 'profit',
|
||||
render: (v: number) => <span style={{ color: v >= 0 ? 'green' : 'red', fontWeight: 'bold' }}>¥{v.toLocaleString()}</span>
|
||||
},
|
||||
];
|
||||
|
||||
const mobileColumns = [
|
||||
{ title: '月份', dataIndex: 'month', key: 'month', render: (v: string) => v.replace('-', '/') },
|
||||
{ title: '收入', dataIndex: 'income', key: 'income', render: (v: number) => <span style={{ color: 'green' }}>¥{(parseFloat(v) / 10000).toFixed(1)}万</span> },
|
||||
{ title: '支出', dataIndex: 'expense', key: 'expense', render: (v: number) => <span style={{ color: 'red' }}>¥{(parseFloat(v) / 10000).toFixed(1)}万</span> },
|
||||
{
|
||||
title: '利润', dataIndex: 'profit', key: 'profit',
|
||||
render: (v: number) => <b style={{ color: v >= 0 ? 'green' : 'red' }}>¥{(v / 10000).toFixed(1)}万</b>
|
||||
},
|
||||
];
|
||||
|
||||
const categoryColumns = [
|
||||
{ title: '分类', dataIndex: 'category_level2', render: (v: string) => LEVEL2_LABELS[v] || v },
|
||||
{ title: '金额(¥)', dataIndex: 'total_amount', render: (v: number) => parseFloat(v).toLocaleString(), align: 'right' as const },
|
||||
{ title: '笔数', dataIndex: 'count', align: 'center' as const },
|
||||
{
|
||||
title: '占比', render: (_: unknown, r: any) => {
|
||||
const total = byCategory.filter(c => c.category_level1 === r.category_level1).reduce((s, c) => s + parseFloat(c.total_amount), 0);
|
||||
return total > 0 ? `${(parseFloat(r.total_amount) / total * 100).toFixed(1)}%` : '-';
|
||||
}
|
||||
},
|
||||
];
|
||||
|
||||
const projectCategories = byCategory.filter(c => c.category_level1 === 'project');
|
||||
const companyCategories = byCategory.filter(c => c.category_level1 === 'company');
|
||||
const incomeCategories = byCategory.filter(c => c.category_level1 === 'income');
|
||||
|
||||
return (
|
||||
<div style={{ padding: isMobile ? 8 : 24 }}>
|
||||
<div style={{ marginBottom: isMobile ? 12 : 24 }}>
|
||||
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 8 }}>统计报表</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>查看项目财务报表和统计分析数据</Paragraph>
|
||||
</div>
|
||||
|
||||
<Row gutter={[8, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={8} sm={8}>
|
||||
<Card size="small" style={{ textAlign: 'center' }}>
|
||||
<Statistic title="总收入" value={summary.total_income} prefix="¥" valueStyle={{ color: '#3f8600', fontSize: isMobile ? 16 : undefined }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={8} sm={8}>
|
||||
<Card size="small" style={{ textAlign: 'center' }}>
|
||||
<Statistic title="总支出" value={summary.total_expense} prefix="¥" valueStyle={{ color: '#cf1322', fontSize: isMobile ? 16 : undefined }} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={8} sm={8}>
|
||||
<Card size="small" style={{ textAlign: 'center' }}>
|
||||
<Statistic title="净利润" value={summary.net_profit} prefix="¥" valueStyle={{ color: summary.net_profit >= 0 ? '#3f8600' : '#cf1322', fontSize: isMobile ? 16 : undefined }} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Card title="月度财务报表" size="small" style={{ marginBottom: 16 }}
|
||||
extra={<DatePicker picker="month" size="small" allowClear onChange={(d) => setSelectedMonth(d)} placeholder="选择月份" />}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Table
|
||||
dataSource={monthData}
|
||||
columns={isMobile ? mobileColumns : desktopColumns}
|
||||
rowKey="month"
|
||||
pagination={false}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
summary={(pageData) => {
|
||||
let ti = 0, te = 0;
|
||||
pageData.forEach(({ income, expense }) => { ti += parseFloat(income); te += parseFloat(expense); });
|
||||
return (
|
||||
<Table.Summary fixed>
|
||||
<Table.Summary.Row>
|
||||
<Table.Summary.Cell index={0}><strong>合计</strong></Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={1}><strong>¥{ti.toLocaleString()}</strong></Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={2}><strong>¥{te.toLocaleString()}</strong></Table.Summary.Cell>
|
||||
<Table.Summary.Cell index={3}>
|
||||
<strong style={{ color: ti - te >= 0 ? 'green' : 'red' }}>¥{(ti - te).toLocaleString()}</strong>
|
||||
</Table.Summary.Cell>
|
||||
</Table.Summary.Row>
|
||||
</Table.Summary>
|
||||
);
|
||||
}}
|
||||
/>
|
||||
</Spin>
|
||||
</Card>
|
||||
|
||||
<Row gutter={[16, 16]}>
|
||||
{incomeCategories.length > 0 && (
|
||||
<Col xs={24} md={8}>
|
||||
<Card title={<span><Tag color="green">收入</Tag>收入分类</span>} size="small">
|
||||
<Table dataSource={incomeCategories} columns={categoryColumns} rowKey="category_level2" pagination={false} size="small" />
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
{projectCategories.length > 0 && (
|
||||
<Col xs={24} md={8}>
|
||||
<Card title={<span><Tag color="blue">项目</Tag>项目支出分类</span>} size="small">
|
||||
<Table dataSource={projectCategories} columns={categoryColumns} rowKey="category_level2" pagination={false} size="small" />
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
{companyCategories.length > 0 && (
|
||||
<Col xs={24} md={8}>
|
||||
<Card title={<span><Tag color="orange">公司</Tag>公司支出分类</span>} size="small">
|
||||
<Table dataSource={companyCategories} columns={categoryColumns} rowKey="category_level2" pagination={false} size="small" />
|
||||
</Card>
|
||||
</Col>
|
||||
)}
|
||||
</Row>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ReportsPage;
|
||||
|
||||
Reference in New Issue
Block a user