2026-04-19 19:15:01 +08:00
|
|
|
/**
|
|
|
|
|
* 物流管理页面
|
|
|
|
|
* 遵循设计方案:采购-付款-物流-退库一体化流程设计方案.md
|
|
|
|
|
* 章节:四、物流管理
|
|
|
|
|
*
|
|
|
|
|
* 统一合作伙伴界面规范:
|
|
|
|
|
* - 基本信息:公司名称、地址、联系方式、报价描述
|
|
|
|
|
* - 联系人:支持多个联系人,标记主联系人
|
|
|
|
|
* - 收款信息:支持多个银行账户,标记默认账户
|
|
|
|
|
* - 业务台账:订单列表、运费总额、已付/未付金额
|
|
|
|
|
*/
|
2026-05-15 12:02:24 +08:00
|
|
|
import React, { useState, useEffect, useCallback } from 'react'
|
2026-04-19 19:15:01 +08:00
|
|
|
import {
|
|
|
|
|
Table, Button, Modal, Form, Input, Select, message, Space, Tag, Card,
|
2026-05-15 12:02:24 +08:00
|
|
|
Row, Col, Popconfirm, Tabs, Descriptions, Upload, Image, Checkbox
|
2026-04-19 19:15:01 +08:00
|
|
|
} from 'antd'
|
|
|
|
|
import {
|
|
|
|
|
PlusOutlined, EditOutlined, DeleteOutlined, EyeOutlined,
|
|
|
|
|
PhoneOutlined, BankOutlined, FileTextOutlined, CarOutlined, DollarOutlined
|
|
|
|
|
} from '@ant-design/icons'
|
|
|
|
|
import type { ColumnsType } from 'antd/es/table'
|
|
|
|
|
import dayjs from 'dayjs'
|
|
|
|
|
import BusinessLedgerTab from '../components/BusinessLedgerTab'
|
2026-05-15 12:02:24 +08:00
|
|
|
import useFormDraft from '../hooks/useFormDraft'
|
2026-04-19 19:15:01 +08:00
|
|
|
|
|
|
|
|
interface LogisticsCompany {
|
|
|
|
|
id: number
|
|
|
|
|
name: string
|
|
|
|
|
address: string
|
|
|
|
|
phone: string
|
|
|
|
|
quotation_description: string
|
|
|
|
|
remark: string
|
|
|
|
|
created_at: string
|
|
|
|
|
contacts: Contact[]
|
|
|
|
|
payment_infos: PaymentInfo[]
|
|
|
|
|
orders: OrderRecord[]
|
|
|
|
|
total_primary_freight: number
|
|
|
|
|
total_secondary_freight: number
|
|
|
|
|
paid_primary_freight: number
|
|
|
|
|
paid_secondary_freight: number
|
|
|
|
|
ledger?: {
|
|
|
|
|
summary: {
|
|
|
|
|
item_count: number
|
|
|
|
|
total_primary_freight: number
|
|
|
|
|
total_secondary_freight: number
|
|
|
|
|
total_freight: number
|
|
|
|
|
paid_primary_freight: number
|
|
|
|
|
paid_secondary_freight: number
|
|
|
|
|
paid_amount: number
|
|
|
|
|
unpaid_amount: number
|
|
|
|
|
}
|
|
|
|
|
items: any[]
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface Contact {
|
|
|
|
|
id: number
|
|
|
|
|
name: string
|
|
|
|
|
phone: string
|
|
|
|
|
position: string
|
|
|
|
|
is_primary: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface PaymentInfo {
|
|
|
|
|
id: number
|
|
|
|
|
account_name: string
|
|
|
|
|
account_number: string
|
|
|
|
|
bank_name: string
|
|
|
|
|
qr_code: string
|
|
|
|
|
is_default: number
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface OrderRecord {
|
|
|
|
|
id: number
|
|
|
|
|
code: string
|
|
|
|
|
order_code: string
|
|
|
|
|
ship_date: string
|
|
|
|
|
status: string
|
|
|
|
|
primary_freight: number
|
|
|
|
|
primary_freight_currency: string
|
|
|
|
|
primary_freight_status: string
|
|
|
|
|
secondary_freight: number
|
|
|
|
|
secondary_freight_currency: string
|
|
|
|
|
secondary_freight_status: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const LogisticsCompaniesPage: React.FC = () => {
|
|
|
|
|
const [companies, setCompanies] = useState<LogisticsCompany[]>([])
|
|
|
|
|
const [loading, setLoading] = useState(false)
|
|
|
|
|
|
|
|
|
|
const [modalVisible, setModalVisible] = useState(false)
|
|
|
|
|
const [detailModalVisible, setDetailModalVisible] = useState(false)
|
|
|
|
|
const [editingCompany, setEditingCompany] = useState<LogisticsCompany | null>(null)
|
|
|
|
|
const [currentCompany, setCurrentCompany] = useState<LogisticsCompany | null>(null)
|
|
|
|
|
const [activeDetailTab, setActiveDetailTab] = useState('basic')
|
|
|
|
|
|
|
|
|
|
const [contactModalVisible, setContactModalVisible] = useState(false)
|
|
|
|
|
const [editingContact, setEditingContact] = useState<Contact | null>(null)
|
|
|
|
|
const [contactForm] = Form.useForm()
|
|
|
|
|
|
|
|
|
|
const [paymentModalVisible, setPaymentModalVisible] = useState(false)
|
|
|
|
|
const [editingPayment, setEditingPayment] = useState<PaymentInfo | null>(null)
|
|
|
|
|
const [paymentForm] = Form.useForm()
|
|
|
|
|
|
|
|
|
|
const [form] = Form.useForm()
|
|
|
|
|
|
2026-05-15 12:02:24 +08:00
|
|
|
// 表单草稿保护
|
|
|
|
|
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
|
|
|
|
form,
|
|
|
|
|
storageKey: 'logistics_company_create',
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
const handleFormChange = useCallback(() => {
|
|
|
|
|
saveDraft()
|
|
|
|
|
}, [saveDraft])
|
|
|
|
|
|
2026-04-19 19:15:01 +08:00
|
|
|
const fetchCompanies = async () => {
|
|
|
|
|
setLoading(true)
|
|
|
|
|
try {
|
2026-05-15 12:02:24 +08:00
|
|
|
const response = await fetch('/api/logistics-companies')
|
2026-04-19 19:15:01 +08:00
|
|
|
const data = await response.json()
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
setCompanies(data.data)
|
|
|
|
|
} else {
|
|
|
|
|
message.error('获取物流公司列表失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取物流公司列表失败:', error)
|
|
|
|
|
message.error('获取物流公司列表失败')
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const fetchCompanyDetail = async (id: number) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/logistics-companies/${id}`)
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
if (data.success) {
|
|
|
|
|
setCurrentCompany(data.data)
|
|
|
|
|
setDetailModalVisible(true)
|
|
|
|
|
setActiveDetailTab('basic')
|
|
|
|
|
} else {
|
|
|
|
|
message.error('获取物流公司详情失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取物流公司详情失败:', error)
|
|
|
|
|
message.error('获取物流公司详情失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
fetchCompanies()
|
2026-05-15 12:02:24 +08:00
|
|
|
}, [])
|
2026-04-19 19:15:01 +08:00
|
|
|
|
|
|
|
|
const handleCreate = () => {
|
|
|
|
|
setEditingCompany(null)
|
|
|
|
|
form.resetFields()
|
|
|
|
|
setModalVisible(true)
|
2026-05-15 12:02:24 +08:00
|
|
|
// 检查是否有草稿,提示用户是否恢复
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
if (hasDraft()) {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: '发现未完成的草稿',
|
|
|
|
|
content: '检测到上次未提交的物流公司信息,是否恢复?',
|
|
|
|
|
okText: '恢复草稿',
|
|
|
|
|
cancelText: '重新填写',
|
|
|
|
|
onOk: () => {
|
|
|
|
|
restoreDraft()
|
|
|
|
|
},
|
|
|
|
|
onCancel: () => {
|
|
|
|
|
clearDraft()
|
|
|
|
|
form.resetFields()
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}, 0)
|
2026-04-19 19:15:01 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleEdit = (company: LogisticsCompany) => {
|
|
|
|
|
setEditingCompany(company)
|
|
|
|
|
form.setFieldsValue(company)
|
|
|
|
|
setModalVisible(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDelete = async (id: number) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/logistics-companies/${id}`, { method: 'DELETE' })
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
if (data.success) {
|
|
|
|
|
message.success('删除成功')
|
|
|
|
|
fetchCompanies()
|
|
|
|
|
} else {
|
|
|
|
|
message.error(data.message || '删除失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('删除失败:', error)
|
|
|
|
|
message.error('删除失败')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSave = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const values = await form.validateFields()
|
|
|
|
|
const url = editingCompany
|
|
|
|
|
? `/api/logistics-companies/${editingCompany.id}`
|
|
|
|
|
: '/api/logistics-companies'
|
|
|
|
|
const method = editingCompany ? 'PUT' : 'POST'
|
|
|
|
|
|
2026-05-15 12:02:24 +08:00
|
|
|
const { contacts, ...companyData } = values
|
|
|
|
|
|
2026-04-19 19:15:01 +08:00
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method,
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
2026-05-15 12:02:24 +08:00
|
|
|
body: JSON.stringify(companyData)
|
2026-04-19 19:15:01 +08:00
|
|
|
})
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
2026-05-15 12:02:24 +08:00
|
|
|
if (!editingCompany && contacts && contacts.length > 0) {
|
|
|
|
|
const companyId = data.data.id
|
|
|
|
|
for (const contact of contacts) {
|
|
|
|
|
if (contact.name) {
|
|
|
|
|
await fetch(`/api/logistics-companies/${companyId}/contacts`, {
|
|
|
|
|
method: 'POST',
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(contact)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
2026-04-19 19:15:01 +08:00
|
|
|
message.success(editingCompany ? '更新成功' : '创建成功')
|
2026-05-15 12:02:24 +08:00
|
|
|
clearDraft()
|
2026-04-19 19:15:01 +08:00
|
|
|
setModalVisible(false)
|
|
|
|
|
fetchCompanies()
|
|
|
|
|
} else {
|
|
|
|
|
message.error('保存失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存失败:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleAddContact = () => {
|
|
|
|
|
setEditingContact(null)
|
|
|
|
|
contactForm.resetFields()
|
|
|
|
|
setContactModalVisible(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleEditContact = (contact: Contact) => {
|
|
|
|
|
setEditingContact(contact)
|
|
|
|
|
contactForm.setFieldsValue(contact)
|
|
|
|
|
setContactModalVisible(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSaveContact = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const values = await contactForm.validateFields()
|
|
|
|
|
const url = editingContact
|
|
|
|
|
? `/api/logistics-companies/${currentCompany?.id}/contacts/${editingContact.id}`
|
|
|
|
|
: `/api/logistics-companies/${currentCompany?.id}/contacts`
|
|
|
|
|
const method = editingContact ? 'PUT' : 'POST'
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method,
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(values)
|
|
|
|
|
})
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
message.success(editingContact ? '联系人更新成功' : '联系人添加成功')
|
|
|
|
|
setContactModalVisible(false)
|
|
|
|
|
fetchCompanyDetail(currentCompany!.id)
|
|
|
|
|
} else {
|
|
|
|
|
message.error('操作失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存联系人失败:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeleteContact = async (contactId: number) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/logistics-companies/${currentCompany?.id}/contacts/${contactId}`, {
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
})
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
if (data.success) {
|
|
|
|
|
message.success('联系人删除成功')
|
|
|
|
|
fetchCompanyDetail(currentCompany!.id)
|
|
|
|
|
} else {
|
|
|
|
|
message.error('删除失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('删除联系人失败:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleAddPayment = () => {
|
|
|
|
|
setEditingPayment(null)
|
|
|
|
|
paymentForm.resetFields()
|
|
|
|
|
setPaymentModalVisible(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleEditPayment = (payment: PaymentInfo) => {
|
|
|
|
|
setEditingPayment(payment)
|
|
|
|
|
paymentForm.setFieldsValue(payment)
|
|
|
|
|
setPaymentModalVisible(true)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleSavePayment = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const values = await paymentForm.validateFields()
|
|
|
|
|
const url = editingPayment
|
|
|
|
|
? `/api/logistics-companies/${currentCompany?.id}/payment-infos/${editingPayment.id}`
|
|
|
|
|
: `/api/logistics-companies/${currentCompany?.id}/payment-infos`
|
|
|
|
|
const method = editingPayment ? 'PUT' : 'POST'
|
|
|
|
|
|
|
|
|
|
const response = await fetch(url, {
|
|
|
|
|
method,
|
|
|
|
|
headers: { 'Content-Type': 'application/json' },
|
|
|
|
|
body: JSON.stringify(values)
|
|
|
|
|
})
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
|
|
|
|
|
if (data.success) {
|
|
|
|
|
message.success(editingPayment ? '收款信息更新成功' : '收款信息添加成功')
|
|
|
|
|
setPaymentModalVisible(false)
|
|
|
|
|
fetchCompanyDetail(currentCompany!.id)
|
|
|
|
|
} else {
|
|
|
|
|
message.error('操作失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('保存收款信息失败:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const handleDeletePayment = async (paymentId: number) => {
|
|
|
|
|
try {
|
|
|
|
|
const response = await fetch(`/api/logistics-companies/${currentCompany?.id}/payment-infos/${paymentId}`, {
|
|
|
|
|
method: 'DELETE'
|
|
|
|
|
})
|
|
|
|
|
const data = await response.json()
|
|
|
|
|
if (data.success) {
|
|
|
|
|
message.success('收款信息删除成功')
|
|
|
|
|
fetchCompanyDetail(currentCompany!.id)
|
|
|
|
|
} else {
|
|
|
|
|
message.error('删除失败')
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('删除收款信息失败:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const getFreightStatusTag = (status: string) => {
|
|
|
|
|
const statusMap: Record<string, { color: string; text: string }> = {
|
|
|
|
|
pending: { color: 'default', text: '待付款' },
|
|
|
|
|
requested: { color: 'blue', text: '已申请' },
|
|
|
|
|
paid: { color: 'green', text: '已支付' }
|
|
|
|
|
}
|
|
|
|
|
const info = statusMap[status] || { color: 'default', text: status }
|
|
|
|
|
return <Tag color={info.color}>{info.text}</Tag>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const columns: ColumnsType<LogisticsCompany> = [
|
|
|
|
|
{
|
|
|
|
|
title: '公司名称',
|
|
|
|
|
dataIndex: 'name',
|
|
|
|
|
key: 'name',
|
|
|
|
|
width: 180,
|
|
|
|
|
render: (v: string, r: LogisticsCompany) => (
|
|
|
|
|
<a onClick={() => fetchCompanyDetail(r.id)} style={{ fontWeight: 500 }}>{v}</a>
|
|
|
|
|
)
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '联系电话',
|
|
|
|
|
dataIndex: 'phone',
|
|
|
|
|
key: 'phone',
|
|
|
|
|
width: 120
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '报价描述',
|
|
|
|
|
dataIndex: 'quotation_description',
|
|
|
|
|
key: 'quotation_description',
|
|
|
|
|
width: 200,
|
|
|
|
|
ellipsis: true,
|
|
|
|
|
render: (v: string) => v || '-'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '创建时间',
|
|
|
|
|
dataIndex: 'created_at',
|
|
|
|
|
key: 'created_at',
|
|
|
|
|
width: 100,
|
|
|
|
|
render: (v: string) => v ? dayjs(v).format('MM-DD') : '-'
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
title: '操作',
|
|
|
|
|
key: 'actions',
|
|
|
|
|
width: 150,
|
|
|
|
|
fixed: 'right',
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
<Button size="small" type="text" icon={<EyeOutlined />} onClick={() => fetchCompanyDetail(record.id)} />
|
|
|
|
|
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEdit(record)} />
|
|
|
|
|
<Popconfirm title="确定要删除吗?" onConfirm={() => handleDelete(record.id)}>
|
|
|
|
|
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const contactColumns: ColumnsType<Contact> = [
|
|
|
|
|
{ title: '姓名', dataIndex: 'name', key: 'name', width: 100 },
|
|
|
|
|
{ title: '职位', dataIndex: 'position', key: 'position', width: 80 },
|
|
|
|
|
{ title: '电话', dataIndex: 'phone', key: 'phone', width: 120 },
|
2026-05-15 12:02:24 +08:00
|
|
|
{ title: '主联系人', dataIndex: 'is_primary', key: 'is_primary', width: 80, render: (v: boolean | number) => v ? <Tag color="blue">主联系人</Tag> : null },
|
2026-04-19 19:15:01 +08:00
|
|
|
{
|
|
|
|
|
title: '操作',
|
|
|
|
|
key: 'actions',
|
|
|
|
|
width: 100,
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEditContact(record)} />
|
|
|
|
|
<Popconfirm title="确定删除?" onConfirm={() => handleDeleteContact(record.id)}>
|
|
|
|
|
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const paymentColumns: ColumnsType<PaymentInfo> = [
|
|
|
|
|
{ title: '收款户名', dataIndex: 'account_name', key: 'account_name', width: 120 },
|
|
|
|
|
{ title: '银行账号', dataIndex: 'account_number', key: 'account_number', width: 150 },
|
|
|
|
|
{ title: '开户银行', dataIndex: 'bank_name', key: 'bank_name', width: 120 },
|
2026-05-15 12:02:24 +08:00
|
|
|
{ title: '默认', dataIndex: 'is_default', key: 'is_default', width: 60, render: (v: boolean | number) => v ? <Tag color="green">默认</Tag> : null },
|
2026-04-19 19:15:01 +08:00
|
|
|
{
|
|
|
|
|
title: '操作',
|
|
|
|
|
key: 'actions',
|
|
|
|
|
width: 100,
|
|
|
|
|
render: (_, record) => (
|
|
|
|
|
<Space size={4}>
|
|
|
|
|
<Button size="small" type="text" icon={<EditOutlined />} onClick={() => handleEditPayment(record)} />
|
|
|
|
|
<Popconfirm title="确定删除?" onConfirm={() => handleDeletePayment(record.id)}>
|
|
|
|
|
<Button size="small" type="text" danger icon={<DeleteOutlined />} />
|
|
|
|
|
</Popconfirm>
|
|
|
|
|
</Space>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const orderColumns: ColumnsType<OrderRecord> = [
|
|
|
|
|
{ title: '物流单号', dataIndex: 'code', key: 'code', width: 120 },
|
|
|
|
|
{ title: '采购订单', dataIndex: 'order_code', key: 'order_code', width: 120 },
|
|
|
|
|
{ title: '发货日期', dataIndex: 'ship_date', key: 'ship_date', width: 100 },
|
|
|
|
|
{ title: '一次运费', dataIndex: 'primary_freight', key: 'primary_freight', width: 100, align: 'right', render: (v: number, r: OrderRecord) => `${r.primary_freight_currency || 'CNY'} ${v?.toFixed(2) || '0.00'}` },
|
|
|
|
|
{ title: '一次运费状态', dataIndex: 'primary_freight_status', key: 'primary_freight_status', width: 100, render: getFreightStatusTag },
|
|
|
|
|
{ title: '二次运费', dataIndex: 'secondary_freight', key: 'secondary_freight', width: 100, align: 'right', render: (v: number, r: OrderRecord) => `${r.secondary_freight_currency || 'LAK'} ${v?.toFixed(2) || '0.00'}` },
|
|
|
|
|
{ title: '二次运费状态', dataIndex: 'secondary_freight_status', key: 'secondary_freight_status', width: 100, render: getFreightStatusTag },
|
|
|
|
|
{ title: '状态', dataIndex: 'status', key: 'status', width: 80, render: (s: string) => <Tag>{s}</Tag> }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div style={{ padding: 24 }}>
|
|
|
|
|
<div style={{ marginBottom: 24 }}>
|
|
|
|
|
<h2 style={{ marginBottom: 8 }}>物流管理</h2>
|
|
|
|
|
<p style={{ color: '#888', marginBottom: 0 }}>管理物流合作伙伴(统一合作伙伴界面规范)</p>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>新建物流公司</Button>}>
|
|
|
|
|
<Table columns={columns} dataSource={companies} rowKey="id" loading={loading} pagination={{ pageSize: 20 }} size="small" scroll={{ x: 1100 }} />
|
|
|
|
|
</Card>
|
|
|
|
|
|
|
|
|
|
{/* 编辑/新建弹窗 */}
|
|
|
|
|
<Modal
|
|
|
|
|
title={editingCompany ? '编辑物流公司' : '新建物流公司'}
|
|
|
|
|
open={modalVisible}
|
|
|
|
|
onOk={handleSave}
|
2026-05-15 12:02:24 +08:00
|
|
|
onCancel={() => {
|
|
|
|
|
if (form.isFieldsTouched()) {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: '确认关闭',
|
|
|
|
|
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
|
|
|
|
okText: '关闭',
|
|
|
|
|
cancelText: '继续编辑',
|
|
|
|
|
onOk: () => {
|
|
|
|
|
saveDraft()
|
|
|
|
|
setModalVisible(false)
|
|
|
|
|
},
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
setModalVisible(false)
|
|
|
|
|
}
|
|
|
|
|
}}
|
|
|
|
|
width={700}
|
|
|
|
|
maskClosable={false}
|
2026-04-19 19:15:01 +08:00
|
|
|
>
|
2026-05-15 12:02:24 +08:00
|
|
|
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
|
|
|
|
<Form.Item name="name" label="公司名称" rules={[{ required: true }]}>
|
|
|
|
|
<Input placeholder="请输入公司名称" />
|
|
|
|
|
</Form.Item>
|
2026-04-19 19:15:01 +08:00
|
|
|
<Form.Item name="address" label="地址">
|
|
|
|
|
<Input placeholder="请输入地址" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="quotation_description" label="报价描述">
|
|
|
|
|
<Input.TextArea rows={3} placeholder="请输入报价描述(如:中国-老挝陆运报价、时效等)" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="remark" label="备注">
|
|
|
|
|
<Input.TextArea rows={2} placeholder="请输入备注" />
|
|
|
|
|
</Form.Item>
|
2026-05-15 12:02:24 +08:00
|
|
|
{!editingCompany && (
|
|
|
|
|
<Form.List name="contacts">
|
|
|
|
|
{(fields, { add, remove }) => (
|
|
|
|
|
<>
|
|
|
|
|
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
|
|
|
|
<span style={{ fontWeight: 500 }}>联系人</span>
|
|
|
|
|
<Button type="dashed" size="small" icon={<PlusOutlined />} onClick={() => add()}>添加联系人</Button>
|
|
|
|
|
</div>
|
|
|
|
|
{fields.map(({ key, name, ...restField }) => (
|
|
|
|
|
<Row key={key} gutter={8} style={{ marginBottom: 8 }}>
|
|
|
|
|
<Col span={6}>
|
|
|
|
|
<Form.Item {...restField} name={[name, 'name']} rules={[{ required: true, message: '必填' }]}>
|
|
|
|
|
<Input placeholder="姓名" size="small" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={5}>
|
|
|
|
|
<Form.Item {...restField} name={[name, 'phone']}>
|
|
|
|
|
<Input placeholder="电话" size="small" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={5}>
|
|
|
|
|
<Form.Item {...restField} name={[name, 'position']}>
|
|
|
|
|
<Input placeholder="职位" size="small" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={5}>
|
|
|
|
|
<Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked">
|
|
|
|
|
<Checkbox>主联系人</Checkbox>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={3}>
|
|
|
|
|
<Button type="text" danger size="small" icon={<DeleteOutlined />} onClick={() => remove(name)} />
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
))}
|
|
|
|
|
</>
|
|
|
|
|
)}
|
|
|
|
|
</Form.List>
|
|
|
|
|
)}
|
2026-04-19 19:15:01 +08:00
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
{/* 详情弹窗 - 多TAB */}
|
|
|
|
|
<Modal
|
|
|
|
|
title={`物流公司详情 - ${currentCompany?.name || ''}`}
|
|
|
|
|
open={detailModalVisible}
|
|
|
|
|
onCancel={() => setDetailModalVisible(false)}
|
|
|
|
|
footer={null}
|
|
|
|
|
width={1000}
|
|
|
|
|
>
|
|
|
|
|
{currentCompany && (
|
|
|
|
|
<Tabs activeKey={activeDetailTab} onChange={setActiveDetailTab}>
|
|
|
|
|
{/* TAB1: 基本信息 */}
|
|
|
|
|
<Tabs.TabPane tab={<span><FileTextOutlined /> 基本信息</span>} key="basic">
|
|
|
|
|
<Descriptions bordered column={2}>
|
|
|
|
|
<Descriptions.Item label="公司名称">{currentCompany.name}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="联系电话">{currentCompany.phone || '-'}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="邮箱">{currentCompany.email || '-'}</Descriptions.Item>
|
2026-05-15 12:02:24 +08:00
|
|
|
<Descriptions.Item label="创建时间">{currentCompany.created_at}</Descriptions.Item>
|
2026-04-19 19:15:01 +08:00
|
|
|
<Descriptions.Item label="地址" span={2}>{currentCompany.address || '-'}</Descriptions.Item>
|
|
|
|
|
<Descriptions.Item label="报价描述" span={2}>{currentCompany.quotation_description || '-'}</Descriptions.Item>
|
|
|
|
|
{currentCompany.remark && <Descriptions.Item label="备注" span={2}>{currentCompany.remark}</Descriptions.Item>}
|
|
|
|
|
</Descriptions>
|
|
|
|
|
</Tabs.TabPane>
|
|
|
|
|
|
|
|
|
|
{/* TAB2: 联系人 */}
|
|
|
|
|
<Tabs.TabPane tab={<span><PhoneOutlined /> 联系人</span>} key="contacts">
|
|
|
|
|
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddContact} style={{ marginBottom: 16 }}>添加联系人</Button>
|
|
|
|
|
<Table columns={contactColumns} dataSource={currentCompany.contacts || []} rowKey="id" pagination={false} size="small" />
|
|
|
|
|
</Tabs.TabPane>
|
|
|
|
|
|
|
|
|
|
{/* TAB3: 收款信息 */}
|
|
|
|
|
<Tabs.TabPane tab={<span><BankOutlined /> 收款信息</span>} key="payment">
|
|
|
|
|
<Button type="dashed" icon={<PlusOutlined />} onClick={handleAddPayment} style={{ marginBottom: 16 }}>添加收款信息</Button>
|
|
|
|
|
<Table columns={paymentColumns} dataSource={currentCompany.payment_infos || []} rowKey="id" pagination={false} size="small" />
|
|
|
|
|
</Tabs.TabPane>
|
|
|
|
|
|
|
|
|
|
{/* TAB4: 业务台账 */}
|
|
|
|
|
<Tabs.TabPane tab={<span><DollarOutlined /> 业务台账</span>} key="orders">
|
|
|
|
|
<BusinessLedgerTab
|
|
|
|
|
partnerType="logistics"
|
|
|
|
|
summary={currentCompany.ledger?.summary || { item_count: 0, total_primary_freight: 0, total_secondary_freight: 0, total_freight: 0, paid_primary_freight: 0, paid_secondary_freight: 0, paid_amount: 0, unpaid_amount: 0 }}
|
|
|
|
|
items={currentCompany.ledger?.items || []}
|
|
|
|
|
/>
|
|
|
|
|
</Tabs.TabPane>
|
|
|
|
|
</Tabs>
|
|
|
|
|
)}
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
{/* 联系人编辑弹窗 */}
|
|
|
|
|
<Modal
|
|
|
|
|
title={editingContact ? '编辑联系人' : '添加联系人'}
|
|
|
|
|
open={contactModalVisible}
|
|
|
|
|
onOk={handleSaveContact}
|
|
|
|
|
onCancel={() => setContactModalVisible(false)}
|
|
|
|
|
width={500}
|
|
|
|
|
>
|
|
|
|
|
<Form form={contactForm} layout="vertical">
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="name" label="姓名" rules={[{ required: true }]}>
|
|
|
|
|
<Input placeholder="请输入姓名" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="position" label="职位">
|
|
|
|
|
<Input placeholder="请输入职位" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="phone" label="电话">
|
|
|
|
|
<Input placeholder="请输入电话" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
2026-05-15 12:02:24 +08:00
|
|
|
<Form.Item name="is_primary" label="主联系人">
|
2026-04-19 19:15:01 +08:00
|
|
|
<Select placeholder="是否为主联系人">
|
2026-05-15 12:02:24 +08:00
|
|
|
<Select.Option value={true}>是</Select.Option>
|
|
|
|
|
<Select.Option value={false}>否</Select.Option>
|
2026-04-19 19:15:01 +08:00
|
|
|
</Select>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
|
|
|
|
|
{/* 收款信息编辑弹窗 */}
|
|
|
|
|
<Modal
|
|
|
|
|
title={editingPayment ? '编辑收款信息' : '添加收款信息'}
|
|
|
|
|
open={paymentModalVisible}
|
|
|
|
|
onOk={handleSavePayment}
|
|
|
|
|
onCancel={() => setPaymentModalVisible(false)}
|
|
|
|
|
width={500}
|
|
|
|
|
>
|
|
|
|
|
<Form form={paymentForm} layout="vertical">
|
|
|
|
|
<Form.Item name="account_name" label="收款户名" rules={[{ required: true }]}>
|
|
|
|
|
<Input placeholder="请输入收款户名" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="account_number" label="银行账号" rules={[{ required: true }]}>
|
|
|
|
|
<Input placeholder="请输入银行账号" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col span={12}>
|
|
|
|
|
<Form.Item name="bank_name" label="开户银行" rules={[{ required: true }]}>
|
|
|
|
|
<Input placeholder="请输入开户银行" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
<Form.Item name="qr_code" label="收款码">
|
|
|
|
|
<Input placeholder="请输入收款码图片URL" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
<Form.Item name="is_default" label="默认账户">
|
|
|
|
|
<Select placeholder="是否为默认账户">
|
2026-05-15 12:02:24 +08:00
|
|
|
<Select.Option value={true}>是</Select.Option>
|
|
|
|
|
<Select.Option value={false}>否</Select.Option>
|
2026-04-19 19:15:01 +08:00
|
|
|
</Select>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Form>
|
|
|
|
|
</Modal>
|
|
|
|
|
</div>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default LogisticsCompaniesPage
|