- 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
253 lines
7.2 KiB
TypeScript
253 lines
7.2 KiB
TypeScript
import React, { useEffect, useState } from 'react';
|
|
import { PageContainer } from '@ant-design/pro-components';
|
|
import { Card, Col, Row, Spin } from 'antd';
|
|
import {
|
|
ShoppingOutlined,
|
|
InboxOutlined,
|
|
DatabaseOutlined,
|
|
DollarOutlined,
|
|
RiseOutlined,
|
|
} from '@ant-design/icons';
|
|
import { getStockSummary } from '@/services/api';
|
|
import type { StockSummary } from '@/types/api';
|
|
|
|
interface StatCardProps {
|
|
icon: React.ReactNode;
|
|
title: string;
|
|
value: string | number;
|
|
suffix?: string;
|
|
prefix?: string;
|
|
gradient?: boolean;
|
|
}
|
|
|
|
const StatCard: React.FC<StatCardProps> = ({ icon, title, value, suffix, prefix, gradient }) => (
|
|
<Card
|
|
bordered={false}
|
|
style={{
|
|
borderRadius: 16,
|
|
border: gradient ? '2px solid transparent' : '1px solid #E2E8F0',
|
|
background: gradient
|
|
? 'linear-gradient(135deg, #0052FF, #4D7CFF)'
|
|
: '#FFFFFF',
|
|
boxShadow: gradient
|
|
? '0 8px 24px rgba(0,82,255,0.25)'
|
|
: '0 1px 3px rgba(0,0,0,0.06)',
|
|
transition: 'all 0.3s ease',
|
|
cursor: 'default',
|
|
height: '100%',
|
|
}}
|
|
hoverable
|
|
styles={{
|
|
body: { padding: '28px 24px' },
|
|
}}
|
|
>
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 16,
|
|
marginBottom: 16,
|
|
}}>
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
width: 44,
|
|
height: 44,
|
|
borderRadius: 12,
|
|
background: gradient
|
|
? 'rgba(255,255,255,0.2)'
|
|
: 'rgba(0,82,255,0.06)',
|
|
fontSize: 20,
|
|
color: gradient ? '#FFFFFF' : '#0052FF',
|
|
}}>
|
|
{icon}
|
|
</div>
|
|
<span style={{
|
|
fontSize: 13,
|
|
color: gradient ? 'rgba(255,255,255,0.8)' : '#64748B',
|
|
fontWeight: 500,
|
|
}}>
|
|
{title}
|
|
</span>
|
|
</div>
|
|
<div style={{
|
|
fontSize: 32,
|
|
fontWeight: 700,
|
|
fontFamily: '"Inter", system-ui, sans-serif',
|
|
color: gradient ? '#FFFFFF' : '#0F172A',
|
|
letterSpacing: '-0.02em',
|
|
lineHeight: 1.1,
|
|
}}>
|
|
{prefix}{typeof value === 'number' ? value.toLocaleString() : value}
|
|
{suffix && (
|
|
<span style={{
|
|
fontSize: 14,
|
|
fontWeight: 500,
|
|
marginLeft: 4,
|
|
color: gradient ? 'rgba(255,255,255,0.7)' : '#94A3B8',
|
|
}}>
|
|
{suffix}
|
|
</span>
|
|
)}
|
|
</div>
|
|
</Card>
|
|
);
|
|
|
|
const DashboardPage: React.FC = () => {
|
|
const [summary, setSummary] = useState<StockSummary | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
getStockSummary()
|
|
.then((res) => {
|
|
if (res?.code === 0) setSummary(res.data);
|
|
})
|
|
.finally(() => setLoading(false));
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<PageContainer>
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 120 }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
</PageContainer>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<PageContainer
|
|
header={{
|
|
title: (
|
|
<span style={{ fontFamily: '"Calistoga", Georgia, serif', fontSize: 24 }}>
|
|
数据总览
|
|
</span>
|
|
),
|
|
}}
|
|
>
|
|
<Row gutter={[20, 20]}>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<StatCard
|
|
icon={<ShoppingOutlined />}
|
|
title="产品总数"
|
|
value={summary?.productCount ?? 0}
|
|
suffix="种"
|
|
gradient
|
|
/>
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<StatCard
|
|
icon={<InboxOutlined />}
|
|
title="总匹数"
|
|
value={summary?.totalPieces ?? 0}
|
|
suffix="匹"
|
|
/>
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={8}>
|
|
<StatCard
|
|
icon={<DatabaseOutlined />}
|
|
title="总盘数"
|
|
value={summary?.totalRolls ?? 0}
|
|
suffix="盘"
|
|
/>
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={12}>
|
|
<StatCard
|
|
icon={<DollarOutlined />}
|
|
title="库存总值(成本)"
|
|
value={summary?.totalCostValue ?? '0.00'}
|
|
prefix="¥"
|
|
/>
|
|
</Col>
|
|
<Col xs={24} sm={12} lg={12}>
|
|
<StatCard
|
|
icon={<RiseOutlined />}
|
|
title="库存总值(销售)"
|
|
value={summary?.totalSalesValue ?? '0.00'}
|
|
prefix="¥"
|
|
/>
|
|
</Col>
|
|
</Row>
|
|
|
|
{/* Quick navigation section */}
|
|
<div style={{ marginTop: 32 }}>
|
|
<div style={{
|
|
display: 'inline-flex',
|
|
alignItems: 'center',
|
|
gap: 10,
|
|
borderRadius: 24,
|
|
border: '1px solid rgba(0,82,255,0.2)',
|
|
background: 'rgba(0,82,255,0.04)',
|
|
padding: '8px 18px',
|
|
marginBottom: 20,
|
|
}}>
|
|
<span style={{
|
|
width: 7,
|
|
height: 7,
|
|
borderRadius: '50%',
|
|
background: '#0052FF',
|
|
}} />
|
|
<span style={{
|
|
fontFamily: '"JetBrains Mono", monospace',
|
|
fontSize: 11,
|
|
textTransform: 'uppercase' as const,
|
|
letterSpacing: '0.15em',
|
|
color: '#0052FF',
|
|
}}>
|
|
快捷入口
|
|
</span>
|
|
</div>
|
|
<Row gutter={[16, 16]}>
|
|
{[
|
|
{ title: '产品档案', desc: '管理产品信息', path: '/inventory/products', icon: <ShoppingOutlined /> },
|
|
{ title: '库存查询', desc: '实时库存数据', path: '/inventory/stocks', icon: <DatabaseOutlined /> },
|
|
{ title: '库存统计', desc: '分类统计分析', path: '/inventory/stats', icon: <RiseOutlined /> },
|
|
{ title: 'Excel导入', desc: '批量导入产品', path: '/inventory/import', icon: <InboxOutlined /> },
|
|
].map((item) => (
|
|
<Col xs={24} sm={12} lg={6} key={item.path}>
|
|
<Card
|
|
bordered={false}
|
|
hoverable
|
|
style={{
|
|
borderRadius: 14,
|
|
border: '1px solid #E2E8F0',
|
|
cursor: 'pointer',
|
|
transition: 'all 0.2s ease',
|
|
}}
|
|
styles={{ body: { padding: '20px 22px' } }}
|
|
onClick={() => window.location.assign(item.path)}
|
|
>
|
|
<div style={{
|
|
display: 'flex',
|
|
alignItems: 'center',
|
|
gap: 14,
|
|
}}>
|
|
<div style={{
|
|
display: 'flex',
|
|
justifyContent: 'center',
|
|
alignItems: 'center',
|
|
width: 40,
|
|
height: 40,
|
|
borderRadius: 10,
|
|
background: 'linear-gradient(135deg, #0052FF, #4D7CFF)',
|
|
color: '#FFFFFF',
|
|
fontSize: 18,
|
|
}}>
|
|
{item.icon}
|
|
</div>
|
|
<div>
|
|
<div style={{ fontSize: 15, fontWeight: 600, color: '#0F172A' }}>{item.title}</div>
|
|
<div style={{ fontSize: 12, color: '#94A3B8', marginTop: 2 }}>{item.desc}</div>
|
|
</div>
|
|
</div>
|
|
</Card>
|
|
</Col>
|
|
))}
|
|
</Row>
|
|
</div>
|
|
</PageContainer>
|
|
);
|
|
};
|
|
|
|
export default DashboardPage;
|