| | |
| | | import { useMemo, useState } from 'react'; |
| | | import { DownloadOutlined, ReloadOutlined, SearchOutlined } from '@ant-design/icons'; |
| | | import { |
| | | App as AntdApp, |
| | | Button, |
| | | Card, |
| | | Input, |
| | | Select, |
| | | Table, |
| | | Tag, |
| | | } from 'antd'; |
| | | import type { TableProps } from 'antd'; |
| | | import { useCallback, useMemo, useState } from 'react'; |
| | | import { DownloadOutlined } from '@ant-design/icons'; |
| | | import { ProTable } from '@ant-design/pro-components'; |
| | | import { App as AntdApp, Button } from 'antd'; |
| | | import { createOrderColumns } from './columns'; |
| | | import type { OrderRecord, OrderStatus } from './types'; |
| | | import './Orders.scss'; |
| | | |
| | | type OrderStatus = 'pending' | 'processing' | 'completed' | 'cancelled'; |
| | | |
| | | interface OrderRecord { |
| | | key: string; |
| | | id: string; |
| | | customer: string; |
| | | contact: string; |
| | | amount: number; |
| | | channel: string; |
| | | status: OrderStatus; |
| | | createdAt: string; |
| | | interface OrderQuery { |
| | | id?: string; |
| | | customer?: string; |
| | | status?: OrderStatus; |
| | | orders: OrderRecord[]; |
| | | } |
| | | |
| | | const statusMeta: Record<OrderStatus, { label: string; color: string }> = { |
| | | pending: { label: '待付款', color: 'orange' }, |
| | | processing: { label: '处理中', color: 'processing' }, |
| | | completed: { label: '已完成', color: 'success' }, |
| | | cancelled: { label: '已取消', color: 'default' }, |
| | | }; |
| | | |
| | | const initialOrders: OrderRecord[] = [ |
| | | { |
| | |
| | | |
| | | const Orders = () => { |
| | | const [orders, setOrders] = useState<OrderRecord[]>(initialOrders); |
| | | const [keyword, setKeyword] = useState(''); |
| | | const [status, setStatus] = useState<OrderStatus | 'all'>('all'); |
| | | const [summary, setSummary] = useState(() => ({ |
| | | count: initialOrders.length, |
| | | amount: initialOrders.reduce((sum, order) => sum + order.amount, 0), |
| | | })); |
| | | const { message, modal } = AntdApp.useApp(); |
| | | |
| | | const filteredOrders = useMemo(() => { |
| | | const normalizedKeyword = keyword.trim().toLowerCase(); |
| | | return orders.filter((order) => { |
| | | const matchesKeyword = |
| | | !normalizedKeyword || |
| | | order.id.toLowerCase().includes(normalizedKeyword) || |
| | | order.customer.toLowerCase().includes(normalizedKeyword); |
| | | const matchesStatus = status === 'all' || order.status === status; |
| | | return matchesKeyword && matchesStatus; |
| | | }); |
| | | }, [orders, keyword, status]); |
| | | const tableParams = useMemo(() => ({ orders }), [orders]); |
| | | |
| | | const handleProcess = (record: OrderRecord) => { |
| | | const handleProcess = useCallback( |
| | | (record: OrderRecord) => { |
| | | modal.confirm({ |
| | | title: '处理订单', |
| | | content: `确定将订单「${record.id}」标记为已完成吗?`, |
| | |
| | | message.success('订单状态已更新'); |
| | | }, |
| | | }); |
| | | }; |
| | | }, |
| | | [message, modal], |
| | | ); |
| | | |
| | | const columns = useMemo( |
| | | () => createOrderColumns({ onProcess: handleProcess }), |
| | | [handleProcess], |
| | | ); |
| | | |
| | | const handleExport = () => { |
| | | message.success(`已导出 ${filteredOrders.length} 条订单记录(演示)`); |
| | | message.success(`已导出 ${summary.count} 条订单记录(演示)`); |
| | | }; |
| | | |
| | | const columns: TableProps<OrderRecord>['columns'] = [ |
| | | { |
| | | title: '订单号', |
| | | dataIndex: 'id', |
| | | width: 160, |
| | | render: (value: string) => <code className="order-id">{value}</code>, |
| | | }, |
| | | { |
| | | title: '客户', |
| | | dataIndex: 'customer', |
| | | width: 240, |
| | | render: (_value, record) => ( |
| | | <div className="order-customer"> |
| | | <strong>{record.customer}</strong> |
| | | <span>{record.contact}</span> |
| | | </div> |
| | | ), |
| | | }, |
| | | { |
| | | title: '订单金额', |
| | | dataIndex: 'amount', |
| | | width: 120, |
| | | align: 'right', |
| | | render: (value: number) => <strong className="order-amount">¥{value.toLocaleString()}</strong>, |
| | | }, |
| | | { |
| | | title: '来源渠道', |
| | | dataIndex: 'channel', |
| | | width: 110, |
| | | render: (value: string) => ( |
| | | <Tag bordered={false} color="geekblue"> |
| | | {value} |
| | | </Tag> |
| | | ), |
| | | }, |
| | | { |
| | | title: '状态', |
| | | dataIndex: 'status', |
| | | width: 100, |
| | | render: (value: OrderStatus) => ( |
| | | <Tag color={statusMeta[value].color} bordered={false}> |
| | | {statusMeta[value].label} |
| | | </Tag> |
| | | ), |
| | | }, |
| | | { title: '创建时间', dataIndex: 'createdAt', width: 160 }, |
| | | { |
| | | title: '操作', |
| | | key: 'action', |
| | | width: 100, |
| | | render: (_value, record) => |
| | | record.status === 'completed' ? ( |
| | | <span className="text-muted">已完成</span> |
| | | ) : ( |
| | | <Button type="link" size="small" onClick={() => handleProcess(record)}> |
| | | 处理 |
| | | </Button> |
| | | ), |
| | | }, |
| | | ]; |
| | | |
| | | return ( |
| | | <div className="orders-page"> |
| | |
| | | 跟踪销售订单的处理进度,支持筛选与导出。 |
| | | </p> |
| | | </div> |
| | | <div className="page-header__actions"> |
| | | <Button |
| | | icon={<ReloadOutlined />} |
| | | onClick={() => message.success('订单数据已刷新')} |
| | | > |
| | | 刷新 |
| | | </Button> |
| | | <Button type="primary" icon={<DownloadOutlined />} onClick={handleExport}> |
| | | 导出订单 |
| | | </Button> |
| | | </div> |
| | | </div> |
| | | |
| | | <Card className="filter-card" variant="borderless"> |
| | | <div className="page-toolbar"> |
| | | <Input |
| | | allowClear |
| | | prefix={<SearchOutlined />} |
| | | placeholder="搜索订单号 / 客户名称" |
| | | value={keyword} |
| | | onChange={(event) => setKeyword(event.target.value)} |
| | | style={{ width: 280 }} |
| | | /> |
| | | <Select<OrderStatus | 'all'> |
| | | value={status} |
| | | onChange={setStatus} |
| | | style={{ width: 150 }} |
| | | options={[ |
| | | { value: 'all', label: '全部状态' }, |
| | | { value: 'pending', label: '待付款' }, |
| | | { value: 'processing', label: '处理中' }, |
| | | { value: 'completed', label: '已完成' }, |
| | | { value: 'cancelled', label: '已取消' }, |
| | | ]} |
| | | /> |
| | | <span className="text-muted"> |
| | | 共 {filteredOrders.length} 条订单 · 合计 ¥ |
| | | {filteredOrders.reduce((sum, order) => sum + order.amount, 0).toLocaleString()} |
| | | </span> |
| | | </div> |
| | | </Card> |
| | | |
| | | <Card className="table-card" variant="borderless"> |
| | | <Table<OrderRecord> |
| | | <ProTable<OrderRecord, OrderQuery> |
| | | rowKey="key" |
| | | columns={columns} |
| | | dataSource={filteredOrders} |
| | | params={tableParams} |
| | | cardProps={{ className: 'orders-table-card' }} |
| | | search={{ labelWidth: 'auto', defaultCollapsed: false }} |
| | | scroll={{ x: 1000 }} |
| | | options={false} |
| | | pagination={{ |
| | | pageSize: 8, |
| | | showSizeChanger: false, |
| | | defaultPageSize: 8, |
| | | showSizeChanger: true, |
| | | hideOnSinglePage: false, |
| | | showTotal: (total) => `共 ${total} 条`, |
| | | }} |
| | | toolBarRender={() => [ |
| | | <span key="summary" className="text-muted"> |
| | | 共 {summary.count} 条订单 · 合计 ¥{summary.amount.toLocaleString()} |
| | | </span>, |
| | | <Button key="export" type="primary" icon={<DownloadOutlined />} onClick={handleExport}> |
| | | 导出订单 |
| | | </Button>, |
| | | ]} |
| | | request={async (params) => { |
| | | const { current = 1, pageSize = 8, id, customer, status, orders: dataSource } = |
| | | params; |
| | | const normalizedId = id?.trim().toLowerCase(); |
| | | const normalizedCustomer = customer?.trim().toLowerCase(); |
| | | |
| | | const filteredOrders = dataSource.filter((order) => { |
| | | const matchesId = |
| | | !normalizedId || order.id.toLowerCase().includes(normalizedId); |
| | | const matchesCustomer = |
| | | !normalizedCustomer || |
| | | order.customer.toLowerCase().includes(normalizedCustomer); |
| | | const matchesStatus = !status || order.status === status; |
| | | return matchesId && matchesCustomer && matchesStatus; |
| | | }); |
| | | |
| | | setSummary({ |
| | | count: filteredOrders.length, |
| | | amount: filteredOrders.reduce((sum, order) => sum + order.amount, 0), |
| | | }); |
| | | |
| | | const start = (current - 1) * pageSize; |
| | | return { |
| | | data: filteredOrders.slice(start, start + pageSize), |
| | | total: filteredOrders.length, |
| | | success: true, |
| | | }; |
| | | }} |
| | | /> |
| | | </Card> |
| | | </div> |
| | | ); |
| | | }; |