财务分类体系+统一账本+Excel导入+报表改造
This commit is contained in:
@@ -1,196 +1,185 @@
|
||||
import React from 'react';
|
||||
import { Card, Typography, Table, Statistic, Row, Col, Tag } from 'antd';
|
||||
import { DollarOutlined, FileTextOutlined, CheckCircleOutlined } from '@ant-design/icons';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
|
||||
const FinancePage: React.FC = () => {
|
||||
// 统计数据 - 从API获取真实数据
|
||||
const [stats, setStats] = React.useState([
|
||||
{
|
||||
title: '本月总收入',
|
||||
value: 0,
|
||||
prefix: '¥',
|
||||
icon: <DollarOutlined />,
|
||||
trend: '',
|
||||
color: '#3f8600',
|
||||
},
|
||||
{
|
||||
title: '本月总支出',
|
||||
value: 0,
|
||||
prefix: '¥',
|
||||
icon: <FileTextOutlined />,
|
||||
trend: '',
|
||||
color: '#cf1322',
|
||||
},
|
||||
{
|
||||
title: '待审批报销',
|
||||
value: 0,
|
||||
prefix: '¥',
|
||||
icon: <CheckCircleOutlined />,
|
||||
trend: '',
|
||||
color: '#1890ff',
|
||||
},
|
||||
]);
|
||||
|
||||
// 财务记录数据 - 从API获取真实数据
|
||||
const [dataSource, setDataSource] = React.useState([]);
|
||||
|
||||
// 从API获取财务数据
|
||||
React.useEffect(() => {
|
||||
// 这里可以添加从API获取真实数据的逻辑
|
||||
// 例如:fetch('/api/finance-stats') 等
|
||||
}, []);
|
||||
|
||||
// 桌面端表格列
|
||||
const desktopColumns = [
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'date',
|
||||
key: 'date',
|
||||
sorter: (a: any, b: any) => a.date.localeCompare(b.date),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
render: (type: string) => (
|
||||
<span style={{ color: type === '收入' ? 'green' : 'red' }}>
|
||||
{type === '收入' ? '↑ 收入' : '↓ 支出'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类别',
|
||||
dataIndex: 'category',
|
||||
key: 'category',
|
||||
},
|
||||
{
|
||||
title: '项目',
|
||||
dataIndex: 'project',
|
||||
key: 'project',
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
render: (amount: number) => `¥${amount.toLocaleString()}`,
|
||||
sorter: (a: any, b: any) => a.amount - b.amount,
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => {
|
||||
const colorMap: Record<string, string> = {
|
||||
'已入账': 'green',
|
||||
'已付款': 'blue',
|
||||
'处理中': 'orange',
|
||||
};
|
||||
return <Tag color={colorMap[status] || 'default'}>{status}</Tag>;
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
// 移动端简化表格列
|
||||
const mobileColumns = [
|
||||
{
|
||||
title: '日期',
|
||||
dataIndex: 'date',
|
||||
key: 'date',
|
||||
width: 100,
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
key: 'type',
|
||||
width: 60,
|
||||
render: (type: string) => (
|
||||
<span style={{ color: type === '收入' ? 'green' : 'red', fontSize: 12 }}>
|
||||
{type === '收入' ? '↑' : '↓'}
|
||||
</span>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '金额',
|
||||
dataIndex: 'amount',
|
||||
key: 'amount',
|
||||
render: (amount: number) => (
|
||||
<div style={{ fontWeight: 'bold' }}>¥{amount.toLocaleString()}</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
dataIndex: 'status',
|
||||
key: 'status',
|
||||
render: (status: string) => (
|
||||
<Tag color={status === '已入账' ? 'green' : 'blue'} style={{ fontSize: 10 }}>
|
||||
{status}
|
||||
</Tag>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const [isMobile, setIsMobile] = React.useState(false);
|
||||
|
||||
React.useEffect(() => {
|
||||
const checkMobile = () => {
|
||||
setIsMobile(window.innerWidth <= 768);
|
||||
};
|
||||
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Title level={3} style={{ marginBottom: 8 }}>财务管理</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>
|
||||
查看公司财务状况、收支明细
|
||||
</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 统计卡片 - 移动端优化 */}
|
||||
<Row gutter={[8, 8]} style={{ marginBottom: 16 }}>
|
||||
{stats.map((stat, index) => (
|
||||
<Col xs={24} sm={8} key={index}>
|
||||
<Card size="small" style={{ textAlign: 'center' }}>
|
||||
<Statistic
|
||||
title={<span style={{ fontSize: 12 }}>{stat.title}</span>}
|
||||
value={stat.value}
|
||||
prefix={stat.prefix}
|
||||
suffix={stat.trend}
|
||||
valueStyle={{
|
||||
color: stat.color,
|
||||
fontSize: isMobile ? 18 : undefined
|
||||
}}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
|
||||
{/* 财务明细表 */}
|
||||
<Card
|
||||
title="财务明细"
|
||||
size="small"
|
||||
styles={{ body: { padding: isMobile ? 8 : 24 } }}
|
||||
>
|
||||
<Table
|
||||
dataSource={dataSource}
|
||||
columns={isMobile ? mobileColumns : desktopColumns}
|
||||
pagination={{
|
||||
pageSize: 5,
|
||||
size: isMobile ? 'small' : 'default'
|
||||
}}
|
||||
scroll={isMobile ? { x: 500 } : undefined}
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
/>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinancePage;
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Table, Statistic, Row, Col, Tag, Select, DatePicker, Space, Spin } from 'antd';
|
||||
import { DollarOutlined, FileTextOutlined, RiseOutlined, FallOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { RangePicker } = DatePicker;
|
||||
|
||||
const LEVEL1_LABELS: Record<string, string> = { income: '收入', project: '项目支出', company: '公司支出' };
|
||||
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 FinancePage: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [summary, setSummary] = useState<any>({ total_income: 0, total_expense: 0, net_profit: 0 });
|
||||
const [byCategory, setByCategory] = useState<any[]>([]);
|
||||
const [records, setRecords] = useState<any[]>([]);
|
||||
const [pagination, setPagination] = useState({ page: 1, pageSize: 20, total: 0 });
|
||||
const [dateRange, setDateRange] = useState<[dayjs.Dayjs | null, dayjs.Dayjs | null] | null>(null);
|
||||
const [filterType, setFilterType] = useState<string | undefined>(undefined);
|
||||
const [isMobile, setIsMobile] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
|
||||
checkMobile();
|
||||
window.addEventListener('resize', checkMobile);
|
||||
return () => window.removeEventListener('resize', checkMobile);
|
||||
}, []);
|
||||
|
||||
const fetchSummary = async () => {
|
||||
try {
|
||||
const params: any = {};
|
||||
if (dateRange && dateRange[0]) {
|
||||
params.date_from = dateRange[0].format('YYYY-MM-DD');
|
||||
params.date_to = dateRange[1]?.format('YYYY-MM-DD');
|
||||
}
|
||||
const res = await apiClient.get('/financial-records/summary', { params });
|
||||
if (res.data.success) {
|
||||
setSummary(res.data.data.totals);
|
||||
setByCategory(res.data.data.byCategory);
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
};
|
||||
|
||||
const fetchRecords = async (page = 1) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const params: any = { page, pageSize: pagination.pageSize };
|
||||
if (dateRange && dateRange[0]) {
|
||||
params.date_from = dateRange[0].format('YYYY-MM-DD');
|
||||
params.date_to = dateRange[1]?.format('YYYY-MM-DD');
|
||||
}
|
||||
if (filterType) params.txn_type = filterType;
|
||||
const res = await apiClient.get('/financial-records', { params });
|
||||
if (res.data.success) {
|
||||
setRecords(res.data.data);
|
||||
setPagination(res.data.pagination);
|
||||
}
|
||||
} catch (e) { console.error(e); }
|
||||
setLoading(false);
|
||||
};
|
||||
|
||||
useEffect(() => { fetchSummary(); fetchRecords(1); }, [dateRange, filterType]);
|
||||
|
||||
const desktopColumns = [
|
||||
{ title: '日期', dataIndex: 'record_date', width: 100, sorter: (a: any, b: any) => a.record_date?.localeCompare(b.record_date) },
|
||||
{
|
||||
title: '收支', dataIndex: 'txn_type', width: 60,
|
||||
render: (v: string) => <Tag color={v === 'income' ? 'green' : 'red'}>{v === 'income' ? '收入' : '支出'}</Tag>
|
||||
},
|
||||
{
|
||||
title: '一级分类', dataIndex: 'category_level1', width: 90,
|
||||
render: (v: string) => LEVEL1_LABELS[v] || v
|
||||
},
|
||||
{
|
||||
title: '二级分类', dataIndex: 'category_level2', width: 100,
|
||||
render: (v: string) => LEVEL2_LABELS[v] || v
|
||||
},
|
||||
{ title: '项目', dataIndex: 'project_name', width: 140, ellipsis: true },
|
||||
{ title: '原始金额', dataIndex: 'amount_original', width: 100, render: (v: number) => v?.toLocaleString(), align: 'right' as const },
|
||||
{ title: '币种', dataIndex: 'currency', width: 50 },
|
||||
{ title: '等效人民币', dataIndex: 'amount_cny', width: 110, render: (v: number) => `¥${v?.toLocaleString()}`, align: 'right' as const, sorter: (a: any, b: any) => a.amount_cny - b.amount_cny },
|
||||
{ title: '人员', dataIndex: 'user_name', width: 70 },
|
||||
{ title: '描述', dataIndex: 'description', ellipsis: true },
|
||||
{
|
||||
title: '来源', dataIndex: 'source', width: 80,
|
||||
render: (v: string) => {
|
||||
const m: Record<string, string> = { manual: '手动导入', advance: '预支', reimbursement: '报销', payment_request: '付款', material: '材料', primary_freight: '运费', secondary_freight: '运费' };
|
||||
return <Tag>{m[v] || v}</Tag>;
|
||||
}
|
||||
}
|
||||
];
|
||||
|
||||
const mobileColumns = [
|
||||
{ title: '日期', dataIndex: 'record_date', width: 80 },
|
||||
{ title: '收支', dataIndex: 'txn_type', width: 40, render: (v: string) => <span style={{ color: v === 'income' ? 'green' : 'red' }}>{v === 'income' ? '↑' : '↓'}</span> },
|
||||
{ title: '分类', dataIndex: 'category_level2', width: 80, render: (v: string) => LEVEL2_LABELS[v] || v },
|
||||
{ title: '金额', dataIndex: 'amount_cny', render: (v: number) => <b>¥{v?.toLocaleString()}</b>, align: 'right' as const },
|
||||
];
|
||||
|
||||
return (
|
||||
<div>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Title level={3} style={{ marginBottom: 8 }}>财务管理</Title>
|
||||
<Paragraph type="secondary" style={{ marginBottom: 0 }}>查看公司财务状况、收支明细</Paragraph>
|
||||
</div>
|
||||
|
||||
<Row gutter={[8, 8]} style={{ marginBottom: 16 }}>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card size="small" style={{ textAlign: 'center' }}>
|
||||
<Statistic title={<span style={{ fontSize: 12 }}>总收入</span>} value={summary.total_income} prefix="¥"
|
||||
valueStyle={{ color: '#3f8600', fontSize: isMobile ? 18 : undefined }}
|
||||
icon={<RiseOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card size="small" style={{ textAlign: 'center' }}>
|
||||
<Statistic title={<span style={{ fontSize: 12 }}>总支出</span>} value={summary.total_expense} prefix="¥"
|
||||
valueStyle={{ color: '#cf1322', fontSize: isMobile ? 18 : undefined }}
|
||||
icon={<FallOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
<Col xs={24} sm={8}>
|
||||
<Card size="small" style={{ textAlign: 'center' }}>
|
||||
<Statistic title={<span style={{ fontSize: 12 }}>净利润</span>} value={summary.net_profit} prefix="¥"
|
||||
valueStyle={{ color: summary.net_profit >= 0 ? '#3f8600' : '#cf1322', fontSize: isMobile ? 18 : undefined }}
|
||||
icon={<DollarOutlined />} />
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
{byCategory.length > 0 && (
|
||||
<Card title="支出分类汇总" size="small" style={{ marginBottom: 16 }}>
|
||||
<Row gutter={[8, 8]}>
|
||||
{byCategory.filter(c => c.category_level1 !== 'income').map((c, i) => (
|
||||
<Col xs={12} sm={8} md={6} key={i}>
|
||||
<div style={{ fontSize: 12, color: '#666' }}>{LEVEL2_LABELS[c.category_level2] || c.category_level2}</div>
|
||||
<div style={{ fontWeight: 'bold' }}>¥{parseFloat(c.total_amount).toLocaleString()}</div>
|
||||
</Col>
|
||||
))}
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
<Card title="财务明细" size="small" styles={{ body: { padding: isMobile ? 8 : 24 } }}
|
||||
extra={
|
||||
<Space size="small" wrap>
|
||||
<RangePicker size="small" onChange={(dates) => setDateRange(dates as any)} />
|
||||
<Select size="small" allowClear placeholder="筛选类型" style={{ width: 100 }}
|
||||
onChange={setFilterType} value={filterType}
|
||||
options={[{ value: 'income', label: '收入' }, { value: 'expense', label: '支出' }]} />
|
||||
</Space>
|
||||
}
|
||||
>
|
||||
<Spin spinning={loading}>
|
||||
<Table
|
||||
dataSource={records}
|
||||
columns={isMobile ? mobileColumns : desktopColumns}
|
||||
rowKey="id"
|
||||
size={isMobile ? 'small' : 'middle'}
|
||||
scroll={isMobile ? { x: 400 } : { x: 1100 }}
|
||||
pagination={{
|
||||
current: pagination.page,
|
||||
pageSize: pagination.pageSize,
|
||||
total: pagination.total,
|
||||
onChange: (page) => fetchRecords(page),
|
||||
size: isMobile ? 'small' : 'default',
|
||||
showTotal: (total) => `共 ${total} 条`
|
||||
}}
|
||||
/>
|
||||
</Spin>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default FinancePage;
|
||||
|
||||
Reference in New Issue
Block a user