- Login page: tenantId mandatory, ruby logo (木羽清風), brand rename to 木羽清風 - Tenant session: localStorage tenantId + X-Tenant-Id header on all requests - CRM Relations page: "我的供应商" / "我的客户" tabs with Chinese relation type mapping - CRM Graph page: Terminal CLI aesthetic force-directed supply chain visualization (admin only) - Menu filtering: empty menuPaths hides all menus (security fix) - Error handler: 403 no longer triggers logout (security fix) - API layer: added CRM endpoints (upstream/downstream/relations/history/full graph) - Route config: added /crm/relations and /crm/graph routes - app.ts renamed to app.tsx for JSX support in layout config Made-with: Cursor
113 lines
4.0 KiB
TypeScript
113 lines
4.0 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { PageContainer } from '@ant-design/pro-components';
|
|
import { Card, Col, Row, Table, Spin } from 'antd';
|
|
import {
|
|
ShoppingOutlined,
|
|
InboxOutlined,
|
|
DatabaseOutlined,
|
|
DollarOutlined,
|
|
RiseOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getStockSummary, getStockByColor, getStockByLocation } from '@/services/api';
|
|
import type { StockSummary, StockGroup } from '@/types/api';
|
|
import type { ColumnsType } from 'antd/es/table';
|
|
|
|
const groupColumns: ColumnsType<StockGroup> = [
|
|
{ title: '名称', dataIndex: 'name' },
|
|
{ title: '产品数', dataIndex: 'count' },
|
|
{ title: '库存量', dataIndex: 'quantity' },
|
|
];
|
|
|
|
const StatsPage: React.FC = () => {
|
|
const [summary, setSummary] = useState<StockSummary | null>(null);
|
|
const [colorData, setColorData] = useState<StockGroup[]>([]);
|
|
const [locationData, setLocationData] = useState<StockGroup[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
Promise.all([
|
|
getStockSummary(),
|
|
getStockByColor(),
|
|
getStockByLocation(),
|
|
]).then(([sumRes, colorRes, locRes]) => {
|
|
if (sumRes?.code === 0) setSummary(sumRes.data);
|
|
if (colorRes?.code === 0) setColorData(colorRes.data?.list || []);
|
|
if (locRes?.code === 0) setLocationData(locRes.data?.list || []);
|
|
}).finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<PageContainer>
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 120 }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
const stats = [
|
|
{ icon: <ShoppingOutlined />, title: '产品总数', value: summary?.productCount ?? 0 },
|
|
{ icon: <InboxOutlined />, title: '总匹数', value: summary?.totalPieces ?? 0 },
|
|
{ icon: <DatabaseOutlined />, title: '总盘数', value: summary?.totalRolls ?? 0 },
|
|
{ icon: <DollarOutlined />, title: '库存总值(成本)', value: `¥${summary?.totalCostValue ?? '0'}` },
|
|
{ icon: <RiseOutlined />, title: '库存总值(销售)', value: `¥${summary?.totalSalesValue ?? '0'}` },
|
|
];
|
|
|
|
return (
|
|
<PageContainer>
|
|
<Row gutter={[16, 16]} style={{ marginBottom: 24 }}>
|
|
{stats.map((s) => (
|
|
<Col xs={24} sm={12} lg={4} xl={4} key={s.title} style={{ flex: 1 }}>
|
|
<Card bordered={false} styles={{ body: { padding: '20px 18px' } }}>
|
|
<div style={{ display: 'flex', alignItems: 'center', gap: 10, marginBottom: 10 }}>
|
|
<span style={{
|
|
display: 'flex', justifyContent: 'center', alignItems: 'center',
|
|
width: 34, height: 34, borderRadius: 8,
|
|
background: 'rgba(0,82,255,0.06)', color: '#0052FF', fontSize: 16,
|
|
}}>
|
|
{s.icon}
|
|
</span>
|
|
<span style={{ fontSize: 12, color: '#64748B' }}>{s.title}</span>
|
|
</div>
|
|
<div style={{ fontSize: 22, fontWeight: 700, color: '#0F172A' }}>{s.value}</div>
|
|
</Card>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
<Row gutter={16}>
|
|
<Col xs={24} lg={12}>
|
|
<Card title="按颜色分类统计" bordered={false}>
|
|
<Table<StockGroup>
|
|
dataSource={colorData}
|
|
rowKey="name"
|
|
columns={[
|
|
{ title: '颜色', dataIndex: 'name' },
|
|
...groupColumns.slice(1),
|
|
]}
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col xs={24} lg={12}>
|
|
<Card title="按位置分布统计" bordered={false}>
|
|
<Table<StockGroup>
|
|
dataSource={locationData}
|
|
rowKey="name"
|
|
columns={[
|
|
{ title: '存放位置', dataIndex: 'name' },
|
|
...groupColumns.slice(1),
|
|
]}
|
|
pagination={false}
|
|
size="small"
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
export default StatsPage;
|