Backup project on 2026-03-27
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { Table, Button, Modal, Form, Input, message, Space, Tag, Card, Row, Col, Statistic } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, HomeOutlined } from '@ant-design/icons'
|
||||
import { Table, Button, Modal, Form, Input, message, Space, Tag, Card, Row, Col, Statistic, Image } from 'antd'
|
||||
import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, HomeOutlined, BankOutlined } from '@ant-design/icons'
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import FileUpload from '../components/FileUpload'
|
||||
|
||||
interface Contact {
|
||||
name: string
|
||||
@@ -11,12 +12,21 @@ interface Contact {
|
||||
is_primary?: boolean
|
||||
}
|
||||
|
||||
interface PaymentInfo {
|
||||
account_name: string
|
||||
bank_account: string
|
||||
bank_name: string
|
||||
qr_code?: string
|
||||
is_primary: boolean
|
||||
}
|
||||
|
||||
interface Customer {
|
||||
id: number
|
||||
code: string
|
||||
name: string
|
||||
address: string
|
||||
contacts: Contact[]
|
||||
payment_infos: PaymentInfo[]
|
||||
remark: string
|
||||
total_contract_amount: number
|
||||
total_received: number
|
||||
@@ -59,6 +69,11 @@ const CustomerPage: React.FC = () => {
|
||||
return primary?.name || '-'
|
||||
}
|
||||
|
||||
const getPrimaryPaymentInfo = (paymentInfos: PaymentInfo[]) => {
|
||||
const primary = paymentInfos?.find(p => p.is_primary)
|
||||
return primary
|
||||
}
|
||||
|
||||
const columns: ColumnsType<Customer> = [
|
||||
{ title: '编号', dataIndex: 'code', key: 'code', width: 120 },
|
||||
{
|
||||
@@ -69,6 +84,22 @@ const CustomerPage: React.FC = () => {
|
||||
},
|
||||
{ title: '地址', dataIndex: 'address', key: 'address', width: 150, render: (t) => t || '-' },
|
||||
{ title: '主联系人', key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) },
|
||||
{
|
||||
title: '收款信息',
|
||||
key: 'payment_info',
|
||||
width: 200,
|
||||
render: (_, record) => {
|
||||
const primary = getPrimaryPaymentInfo(record.payment_infos || [])
|
||||
if (!primary) return <Tag>未设置</Tag>
|
||||
return (
|
||||
<div style={{ fontSize: 12 }}>
|
||||
<div><BankOutlined /> {primary.bank_name || '-'}</div>
|
||||
<div>户名: {primary.account_name || '-'}</div>
|
||||
<div>账号: {primary.bank_account ? primary.bank_account.slice(-4).padStart(primary.bank_account.length, '*') : '-'}</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
},
|
||||
{ title: '合同金额', dataIndex: 'total_contract_amount', key: 'total_contract_amount', width: 100, render: (v) => `¥${(v || 0).toLocaleString()}` },
|
||||
{ title: '应收金额', dataIndex: 'total_receivable', key: 'total_receivable', width: 100, render: (v) => <span style={{ color: v > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(v || 0).toLocaleString()}</span> },
|
||||
{ title: '操作', key: 'actions', width: 100, render: (_, record) => (
|
||||
@@ -89,7 +120,6 @@ const CustomerPage: React.FC = () => {
|
||||
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
|
||||
@@ -97,16 +127,37 @@ const CustomerPage: React.FC = () => {
|
||||
})
|
||||
}
|
||||
|
||||
const handlePaymentInfoChange = (index: number, field: string, value: any) => {
|
||||
form.setFieldsValue({
|
||||
payment_infos: form.getFieldValue('payment_infos').map((info: any, i: number) => {
|
||||
if (field === 'is_primary' && value) {
|
||||
return i === index ? { ...info, [field]: value } : { ...info, is_primary: false }
|
||||
}
|
||||
return i === index ? { ...info, [field]: value } : info
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
const handleSubmit = async (values: any) => {
|
||||
try {
|
||||
let contacts = values.contacts || [{ name: '', position: '', phone: '', is_primary: true }]
|
||||
const hasPrimary = contacts.some(c => c.is_primary)
|
||||
const hasPrimary = contacts.some((c: Contact) => c.is_primary)
|
||||
if (!hasPrimary && contacts[0].name) contacts[0].is_primary = true
|
||||
|
||||
let paymentInfos = values.payment_infos || []
|
||||
const hasPrimaryPayment = paymentInfos.some((p: PaymentInfo) => p.is_primary)
|
||||
if (!hasPrimaryPayment && paymentInfos.length > 0 && paymentInfos[0].account_name) {
|
||||
paymentInfos[0].is_primary = true
|
||||
}
|
||||
|
||||
const url = editingCustomer ? `/api/customers/${editingCustomer.id}` : '/api/customers'
|
||||
const method = editingCustomer ? 'PUT' : 'POST'
|
||||
|
||||
const response = await fetch(url, { method, headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ ...values, contacts }) })
|
||||
const response = await fetch(url, {
|
||||
method,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ ...values, contacts, payment_infos: paymentInfos })
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
message.success(editingCustomer ? '更新成功' : '创建成功')
|
||||
@@ -125,8 +176,11 @@ const CustomerPage: React.FC = () => {
|
||||
const handleEdit = (customer: Customer) => {
|
||||
setEditingCustomer(customer)
|
||||
form.setFieldsValue({
|
||||
name: customer.name, address: customer.address, remark: customer.remark,
|
||||
contacts: customer.contacts?.length ? customer.contacts : [{ name: '', position: '', phone: '', is_primary: true }]
|
||||
name: customer.name,
|
||||
address: customer.address,
|
||||
remark: customer.remark,
|
||||
contacts: customer.contacts?.length ? customer.contacts : [{ name: '', position: '', phone: '', is_primary: true }],
|
||||
payment_infos: customer.payment_infos?.length ? customer.payment_infos : []
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
@@ -148,7 +202,10 @@ const CustomerPage: React.FC = () => {
|
||||
const handleAdd = () => {
|
||||
setEditingCustomer(null)
|
||||
form.resetFields()
|
||||
form.setFieldsValue({ contacts: [{ name: '', position: '', phone: '', is_primary: true }] })
|
||||
form.setFieldsValue({
|
||||
contacts: [{ name: '', position: '', phone: '', is_primary: true }],
|
||||
payment_infos: []
|
||||
})
|
||||
setModalVisible(true)
|
||||
}
|
||||
|
||||
@@ -168,23 +225,42 @@ const CustomerPage: React.FC = () => {
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Table columns={columns} dataSource={filteredCustomers} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 900 }} />
|
||||
<Table columns={columns} dataSource={filteredCustomers} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 1100 }} />
|
||||
</Card>
|
||||
|
||||
<Modal title={editingCustomer ? '编辑客户' : '新增客户'} open={modalVisible} onCancel={() => { setModalVisible(false); form.resetFields(); setEditingCustomer(null) }} onOk={() => form.submit()} width={700}>
|
||||
<Modal
|
||||
title={editingCustomer ? '编辑客户' : '新增客户'}
|
||||
open={modalVisible}
|
||||
onCancel={() => { setModalVisible(false); form.resetFields(); setEditingCustomer(null) }}
|
||||
onOk={() => form.submit()}
|
||||
width={800}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}><Input placeholder="客户名称" /></Form.Item>
|
||||
<Form.Item name="address" label="地址"><Input placeholder="客户地址" /></Form.Item>
|
||||
<Form.Item name="remark" label="备注"><Input.TextArea rows={2} placeholder="备注信息" /></Form.Item>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="客户名称" />
|
||||
</Form.Item>
|
||||
<Form.Item name="address" label="地址">
|
||||
<Input placeholder="客户地址" />
|
||||
</Form.Item>
|
||||
<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, '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"
|
||||
@@ -198,6 +274,46 @@ const CustomerPage: React.FC = () => {
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<h4 style={{ marginTop: 24 }}>收款信息</h4>
|
||||
<Form.List name="payment_infos">
|
||||
{(fields, { add, remove }) => (
|
||||
<div>
|
||||
{fields.map(({ key, name, ...restField }) => (
|
||||
<div key={key} style={{ border: '1px solid #e8e8e8', padding: 16, marginBottom: 16, borderRadius: 4, backgroundColor: '#fafafa' }}>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
|
||||
<Form.Item {...restField} name={[name, 'account_name']} label="收款户名" style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder="收款户名" />
|
||||
</Form.Item>
|
||||
<Form.Item {...restField} name={[name, 'bank_name']} label="开户银行" style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder="开户银行" />
|
||||
</Form.Item>
|
||||
</div>
|
||||
<div style={{ display: 'flex', gap: 8, marginBottom: 8, alignItems: 'center' }}>
|
||||
<Form.Item {...restField} name={[name, 'bank_account']} label="银行账号" style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder="银行账号" />
|
||||
</Form.Item>
|
||||
<Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked" style={{ marginBottom: 0, marginTop: 30 }}>
|
||||
<input
|
||||
type="checkbox"
|
||||
onChange={(e) => handlePaymentInfoChange(name, 'is_primary', e.target.checked)}
|
||||
/> 主要收款账户
|
||||
</Form.Item>
|
||||
</div>
|
||||
<Form.Item {...restField} name={[name, 'qr_code']} label="收款码" style={{ marginBottom: 0 }}>
|
||||
<FileUpload maxCount={1} accept="image/*" />
|
||||
</Form.Item>
|
||||
{fields.length > 0 && (
|
||||
<Button type="link" danger onClick={() => remove(name)} style={{ marginTop: 8 }}>删除此收款信息</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add({ account_name: '', bank_account: '', bank_name: '', is_primary: false })} style={{ width: '100%' }}>
|
||||
+ 添加收款信息
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
</Form>
|
||||
</Modal>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user