250 lines
10 KiB
TypeScript
250 lines
10 KiB
TypeScript
import React, { useState, useEffect } from 'react'
|
|||
|
|
import { useNavigate } from 'react-router-dom'
|
||
|
|
import { Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card, Row, Col, Statistic } from 'antd'
|
||
|
|
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, ShopOutlined } from '@ant-design/icons'
|
||
|
|
import type { ColumnsType } from 'antd/es/table'
|
||
|
|
|
||
|
|
interface Contact {
|
||
|
|
name: string
|
||
|
|
position: string
|
||
|
|
phone: string
|
||
|
|
is_primary?: boolean
|
||
|
|
}
|
||
|
|
|
||
|
|
interface Supplier {
|
||
|
|
id: number
|
||
|
|
code: string
|
||
|
|
name: string
|
||
|
|
supply_category: string
|
||
|
|
country: string
|
||
|
|
contacts: Contact[]
|
||
|
|
remark: string
|
||
|
|
total_purchase_amount: number
|
||
|
|
total_paid: number
|
||
|
|
total_payable: number
|
||
|
|
created_at: string
|
||
|
|
}
|
||
|
|
|
||
|
|
const SupplierPage: React.FC = () => {
|
||
|
|
const navigate = useNavigate()
|
||
|
|
const [suppliers, setSuppliers] = useState<Supplier[]>([])
|
||
|
|
const [loading, setLoading] = useState(false)
|
||
|
|
const [modalVisible, setModalVisible] = useState(false)
|
||
|
|
const [editingSupplier, setEditingSupplier] = useState<Supplier | null>(null)
|
||
|
|
const [searchText, setSearchText] = useState('')
|
||
|
|
const [form] = Form.useForm()
|
||
|
|
|
||
|
|
const fetchSuppliers = async () => {
|
||
|
|
setLoading(true)
|
||
|
|
try {
|
||
|
|
const response = await fetch('/api/suppliers')
|
||
|
|
const data = await response.json()
|
||
|
|
if (data.success) setSuppliers(data.data || [])
|
||
|
|
} catch (error) {
|
||
|
|
message.error('获取供应商列表失败')
|
||
|
|
} finally {
|
||
|
|
setLoading(false)
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
useEffect(() => { fetchSuppliers() }, [])
|
||
|
|
|
||
|
|
const stats = {
|
||
|
|
total: suppliers.length,
|
||
|
|
totalPurchase: suppliers.reduce((sum, s) => sum + (s.total_purchase_amount || 0), 0),
|
||
|
|
totalPayable: suppliers.reduce((sum, s) => sum + (s.total_payable || 0), 0)
|
||
|
|
}
|
||
|
|
|
||
|
|
const getPrimaryContact = (contacts: Contact[]) => {
|
||
|
|
const primary = contacts?.find(c => c.is_primary)
|
||
|
|
return primary?.name || '-'
|
||
|
|
}
|
||
|
|
|
||
|
|
const columns: ColumnsType<Supplier> = [
|
||
|
|
{ title: '编号', dataIndex: 'code', key: 'code', width: 120 },
|
||
|
|
{
|
||
|
|
title: '名称',
|
||
|
|
dataIndex: 'name',
|
||
|
|
key: 'name',
|
||
|
|
render: (text, record) => (
|
||
|
|
<Button type="link" style={{ padding: 0, fontWeight: 'bold' }} onClick={() => navigate(`/suppliers/${record.id}`)}>
|
||
|
|
{text}
|
||
|
|
</Button>
|
||
|
|
)
|
||
|
|
},
|
||
|
|
{ title: '供应类别', dataIndex: 'supply_category', key: 'supply_category', width: 120 },
|
||
|
|
{ title: '主联系人', key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) },
|
||
|
|
{ title: '国家', dataIndex: 'country', key: 'country', width: 80, render: (country) => <Tag>{country || '-'}</Tag> },
|
||
|
|
{ title: '采购金额', dataIndex: 'total_purchase_amount', key: 'total_purchase_amount', width: 100, render: (amount) => `¥${(amount || 0).toLocaleString()}` },
|
||
|
|
{ title: '应付金额', dataIndex: 'total_payable', key: 'total_payable', width: 100, render: (amount) => <span style={{ color: amount > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(amount || 0).toLocaleString()}</span> },
|
||
|
|
{
|
||
|
|
title: '操作',
|
||
|
|
key: 'actions',
|
||
|
|
width: 100,
|
||
|
|
render: (_, record) => (
|
||
|
|
<Space>
|
||
|
|
<Button type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} size="small" />
|
||
|
|
<Button type="text" danger icon={<DeleteOutlined />} onClick={() => handleDelete(record.id)} size="small" />
|
||
|
|
</Space>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
]
|
||
|
|
|
||
|
|
const filteredSuppliers = suppliers.filter(s =>
|
||
|
|
s.code?.toLowerCase().includes(searchText.toLowerCase()) ||
|
||
|
|
s.name?.toLowerCase().includes(searchText.toLowerCase()) ||
|
||
|
|
s.supply_category?.toLowerCase().includes(searchText.toLowerCase())
|
||
|
|
)
|
||
|
|
|
||
|
|
const handleContactChange = (index: number, field: string, value: any) => {
|
||
|
|
form.setFieldsValue({
|
||
|
|
contacts: form.getFieldValue('contacts').map((contact: any, i: number) => {
|
||
|
|
if (field === 'is_primary' && value) {
|
||
|
|
// 如果勾选了主联系人,取消其他联系人的主联系人选项
|
||
|
|
return i === index ? { ...contact, [field]: value } : { ...contact, is_primary: false }
|
||
|
|
}
|
||
|
|
return i === index ? { ...contact, [field]: value } : contact
|
||
|
|
})
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleSubmit = async (values: any) => {
|
||
|
|
try {
|
||
|
|
let contacts = values.contacts || [{ name: '', position: '', phone: '', is_primary: true }]
|
||
|
|
const hasPrimary = contacts.some(c => c.is_primary)
|
||
|
|
if (!hasPrimary && contacts[0].name) contacts[0].is_primary = true
|
||
|
|
|
||
|
|
const url = editingSupplier ? `/api/suppliers/${editingSupplier.id}` : '/api/suppliers'
|
||
|
|
const method = editingSupplier ? 'PUT' : 'POST'
|
||
|
|
|
||
|
|
const response = await fetch(url, {
|
||
|
|
method,
|
||
|
|
headers: { 'Content-Type': 'application/json' },
|
||
|
|
body: JSON.stringify({ ...values, contacts })
|
||
|
|
})
|
||
|
|
|
||
|
|
const data = await response.json()
|
||
|
|
if (data.success) {
|
||
|
|
message.success(editingSupplier ? '更新成功' : '创建成功')
|
||
|
|
setModalVisible(false)
|
||
|
|
form.resetFields()
|
||
|
|
setEditingSupplier(null)
|
||
|
|
fetchSuppliers()
|
||
|
|
} else {
|
||
|
|
message.error(data.message || '操作失败')
|
||
|
|
}
|
||
|
|
} catch (error) {
|
||
|
|
message.error('操作失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleEdit = (supplier: Supplier) => {
|
||
|
|
setEditingSupplier(supplier)
|
||
|
|
form.setFieldsValue({
|
||
|
|
name: supplier.name,
|
||
|
|
supply_category: supplier.supply_category,
|
||
|
|
country: supplier.country,
|
||
|
|
remark: supplier.remark,
|
||
|
|
contacts: supplier.contacts?.length ? supplier.contacts : [{ name: '', position: '', phone: '', is_primary: true }]
|
||
|
|
})
|
||
|
|
setModalVisible(true)
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleDelete = async (id: number) => {
|
||
|
|
Modal.confirm({
|
||
|
|
title: '确认删除',
|
||
|
|
content: '确定要删除此供应商吗?',
|
||
|
|
okText: '确定',
|
||
|
|
cancelText: '取消',
|
||
|
|
onOk: async () => {
|
||
|
|
try {
|
||
|
|
const response = await fetch(`/api/suppliers/${id}`, { method: 'DELETE' })
|
||
|
|
const data = await response.json()
|
||
|
|
if (data.success) { message.success('删除成功'); fetchSuppliers() }
|
||
|
|
else message.error(data.message || '删除失败')
|
||
|
|
} catch (error) {
|
||
|
|
message.error('删除失败')
|
||
|
|
}
|
||
|
|
}
|
||
|
|
})
|
||
|
|
}
|
||
|
|
|
||
|
|
const handleAdd = () => {
|
||
|
|
setEditingSupplier(null)
|
||
|
|
form.resetFields()
|
||
|
|
form.setFieldsValue({ country: 'Laos', contacts: [{ name: '', position: '', phone: '', is_primary: true }] })
|
||
|
|
setModalVisible(true)
|
||
|
|
}
|
||
|
|
|
||
|
|
return (
|
||
|
|
<div style={{ padding: 24 }}>
|
||
|
|
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||
|
|
<Col span={8}><Card><Statistic title="供应商总数" value={stats.total} prefix={<ShopOutlined />} /></Card></Col>
|
||
|
|
<Col span={8}><Card><Statistic title="采购总金额" value={stats.totalPurchase} prefix="¥" valueStyle={{ color: '#1890ff' }} /></Card></Col>
|
||
|
|
<Col span={8}><Card><Statistic title="应付总金额" value={stats.totalPayable} prefix="¥" valueStyle={{ color: stats.totalPayable > 0 ? '#ff4d4f' : '#52c41a' }} /></Card></Col>
|
||
|
|
</Row>
|
||
|
|
|
||
|
|
<Card style={{ marginBottom: 16 }}>
|
||
|
|
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||
|
|
<Input placeholder="搜索供应商编号、名称或供应类别" prefix={<SearchOutlined />} value={searchText} onChange={(e) => setSearchText(e.target.value)} allowClear style={{ width: 350 }} />
|
||
|
|
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>新增供应商</Button>
|
||
|
|
</div>
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Card>
|
||
|
|
<Table columns={columns} dataSource={filteredSuppliers} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 900 }} />
|
||
|
|
</Card>
|
||
|
|
|
||
|
|
<Modal title={editingSupplier ? '编辑供应商' : '新增供应商'} open={modalVisible} onCancel={() => { setModalVisible(false); form.resetFields(); setEditingSupplier(null) }} onOk={() => form.submit()} width={700}>
|
||
|
|
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||
|
|
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||
|
|
<Input placeholder="供应商名称" />
|
||
|
|
</Form.Item>
|
||
|
|
<Row gutter={16}>
|
||
|
|
<Col span={12}>
|
||
|
|
<Form.Item name="supply_category" label="供应类别">
|
||
|
|
<Input placeholder="手填:如电力设备、建筑材料" />
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
<Col span={12}>
|
||
|
|
<Form.Item name="country" label="国家" initialValue="Laos">
|
||
|
|
<Select>
|
||
|
|
<Select.Option value="China">中国</Select.Option>
|
||
|
|
<Select.Option value="Laos">老挝</Select.Option>
|
||
|
|
</Select>
|
||
|
|
</Form.Item>
|
||
|
|
</Col>
|
||
|
|
</Row>
|
||
|
|
<Form.Item name="remark" label="备注">
|
||
|
|
<Input.TextArea rows={2} placeholder="备注信息" />
|
||
|
|
</Form.Item>
|
||
|
|
<h4>联系人</h4>
|
||
|
|
<Form.List name="contacts" initialValue={[{ name: '', position: '', phone: '', is_primary: true }]}>
|
||
|
|
{(fields, { add, remove }) => (
|
||
|
|
<div>
|
||
|
|
{fields.map(({ key, name, ...restField }) => (
|
||
|
|
<div key={key} style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
|
||
|
|
<Form.Item {...restField} name={[name, 'name']} style={{ marginBottom: 0, flex: 1 }}><Input placeholder="姓名" /></Form.Item>
|
||
|
|
<Form.Item {...restField} name={[name, 'position']} style={{ marginBottom: 0, flex: 1 }}><Input placeholder="职位" /></Form.Item>
|
||
|
|
<Form.Item {...restField} name={[name, 'phone']} style={{ marginBottom: 0, flex: 1 }}><Input placeholder="电话" /></Form.Item>
|
||
|
|
<Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked" style={{ marginBottom: 0 }}>
|
||
|
|
<input
|
||
|
|
type="checkbox"
|
||
|
|
onChange={(e) => handleContactChange(name, 'is_primary', e.target.checked)}
|
||
|
|
/> 主联系人
|
||
|
|
</Form.Item>
|
||
|
|
{fields.length > 1 && <Button type="link" danger onClick={() => remove(name)}>删除</Button>}
|
||
|
|
</div>
|
||
|
|
))}
|
||
|
|
<Button type="dashed" onClick={() => add()} style={{ width: '100%' }}>+ 添加联系人</Button>
|
||
|
|
</div>
|
||
|
|
)}
|
||
|
|
</Form.List>
|
||
|
|
</Form>
|
||
|
|
</Modal>
|
||
|
|
</div>
|
||
|
|
)
|
||
|
|
}
|
||
|
|
|
||
|
|
export default SupplierPage
|