备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
@@ -5,6 +5,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, HomeOutline
|
||||
import type { ColumnsType } from 'antd/es/table'
|
||||
import FileUpload from '../components/FileUpload'
|
||||
import useFormDraft from '../hooks/useFormDraft'
|
||||
import { useLanguageStore } from '../store/languageStore'
|
||||
|
||||
interface Contact {
|
||||
name: string
|
||||
@@ -37,6 +38,7 @@ interface Customer {
|
||||
|
||||
const CustomerPage: React.FC = () => {
|
||||
const navigate = useNavigate()
|
||||
const { t, currentLanguage } = useLanguageStore()
|
||||
const [customers, setCustomers] = useState<Customer[]>([])
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [modalVisible, setModalVisible] = useState(false)
|
||||
@@ -61,7 +63,7 @@ const CustomerPage: React.FC = () => {
|
||||
const data = await response.json()
|
||||
if (data.success) setCustomers(data.data || [])
|
||||
} catch (error) {
|
||||
message.error('获取客户列表失败')
|
||||
message.error(t('customer.getListFailed'))
|
||||
} finally {
|
||||
setLoading(false)
|
||||
}
|
||||
@@ -87,32 +89,32 @@ const CustomerPage: React.FC = () => {
|
||||
|
||||
const columns: ColumnsType<Customer> = [
|
||||
{
|
||||
title: '名称', dataIndex: 'name', key: 'name',
|
||||
title: t('customer.name'), dataIndex: 'name', key: 'name',
|
||||
render: (text, record) => (
|
||||
<Button type="link" style={{ padding: 0, fontWeight: 'bold' }} onClick={() => navigate(`/customers/${record.id}`)}>{text}</Button>
|
||||
)
|
||||
},
|
||||
{ title: '地址', dataIndex: 'address', key: 'address', width: 150, render: (t) => t || '-' },
|
||||
{ title: '主联系人', key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) },
|
||||
{ title: t('customer.address'), dataIndex: 'address', key: 'address', width: 150, render: (t) => t || '-' },
|
||||
{ title: t('customer.mainContact'), key: 'primary_contact', width: 100, render: (_, record) => getPrimaryContact(record.contacts || []) },
|
||||
{
|
||||
title: '收款信息',
|
||||
title: t('customer.paymentInfo'),
|
||||
key: 'payment_info',
|
||||
width: 200,
|
||||
render: (_, record) => {
|
||||
const primary = getPrimaryPaymentInfo(record.payment_infos || [])
|
||||
if (!primary) return <Tag>未设置</Tag>
|
||||
if (!primary) return <Tag>{t('common.notSet')}</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>{t('customer.accountNameLabel')} {primary.account_name || '-'}</div>
|
||||
<div>{t('customer.accountNumLabel')} {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) => (
|
||||
{ title: t('customer.contractAmount'), dataIndex: 'total_contract_amount', key: 'total_contract_amount', width: 100, render: (v) => `¥${(v || 0).toLocaleString()}` },
|
||||
{ title: t('customer.receivableAmount'), dataIndex: 'total_receivable', key: 'total_receivable', width: 100, render: (v) => <span style={{ color: v > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(v || 0).toLocaleString()}</span> },
|
||||
{ title: t('common.action'), 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" />
|
||||
@@ -170,17 +172,17 @@ const CustomerPage: React.FC = () => {
|
||||
})
|
||||
const data = await response.json()
|
||||
if (data.success) {
|
||||
message.success(editingCustomer ? '更新成功' : '创建成功')
|
||||
message.success(editingCustomer ? t('common.updateSuccess') : t('common.createSuccess'))
|
||||
clearDraft()
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingCustomer(null)
|
||||
fetchCustomers()
|
||||
} else {
|
||||
message.error(data.message || '操作失败')
|
||||
message.error(data.message || t('common.operationFailed'))
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败')
|
||||
message.error(t('common.operationFailed'))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -198,14 +200,14 @@ const CustomerPage: React.FC = () => {
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
Modal.confirm({
|
||||
title: '确认删除', content: '确定要删除此客户吗?', okText: '确定', cancelText: '取消',
|
||||
title: t('common.confirmDelete'), content: t('customer.confirmDeleteMsg'), okText: t('common.confirm'), cancelText: t('common.cancel'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await fetch(`/api/customers/${id}`, { method: 'DELETE' })
|
||||
const data = await response.json()
|
||||
if (data.success) { message.success('删除成功'); fetchCustomers() }
|
||||
else message.error(data.message || '删除失败')
|
||||
} catch (error) { message.error('删除失败') }
|
||||
if (data.success) { message.success(t('common.deleteSuccess')); fetchCustomers() }
|
||||
else message.error(data.message || t('common.deleteFailed'))
|
||||
} catch (error) { message.error(t('common.deleteFailed')) }
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -222,10 +224,10 @@ const CustomerPage: React.FC = () => {
|
||||
setTimeout(() => {
|
||||
if (hasDraft()) {
|
||||
Modal.confirm({
|
||||
title: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的客户信息,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
title: t('common.draftFound'),
|
||||
content: t('common.draftRestore'),
|
||||
okText: t('common.restoreDraft'),
|
||||
cancelText: t('common.reFill'),
|
||||
onOk: () => {
|
||||
restoreDraft()
|
||||
},
|
||||
@@ -245,74 +247,75 @@ const CustomerPage: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Row gutter={16} style={{ marginBottom: 24 }}>
|
||||
<Col span={8}><Card><Statistic title="客户总数" value={stats.total} prefix={<HomeOutlined />} /></Card></Col>
|
||||
<Col span={8}><Card><Statistic title="合同总金额" value={stats.totalContract} prefix="¥" valueStyle={{ color: '#1890ff' }} /></Card></Col>
|
||||
<Col span={8}><Card><Statistic title="应收总金额" value={stats.totalReceivable} prefix="¥" valueStyle={{ color: stats.totalReceivable > 0 ? '#ff4d4f' : '#52c41a' }} /></Card></Col>
|
||||
<Col span={8}><Card><Statistic title={t('customer.totalCount')} value={stats.total} prefix={<HomeOutlined />} /></Card></Col>
|
||||
<Col span={8}><Card><Statistic title={t('customer.totalContract')} value={stats.totalContract} prefix="¥" valueStyle={{ color: '#1890ff' }} /></Card></Col>
|
||||
<Col span={8}><Card><Statistic title={t('customer.totalReceivable')} value={stats.totalReceivable} prefix="¥" valueStyle={{ color: stats.totalReceivable > 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>
|
||||
<Input placeholder={t('customer.searchPlaceholder')} prefix={<SearchOutlined />} value={searchText} onChange={(e) => setSearchText(e.target.value)} allowClear style={{ width: 350 }} />
|
||||
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>{t('customer.newCustomer')}</Button>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Card>
|
||||
<Table columns={columns} dataSource={filteredCustomers} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => `共 ${total} 条` }} scroll={{ x: 1100 }} />
|
||||
<Table columns={columns} dataSource={filteredCustomers} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => t('common.totalCount', { total }) }} scroll={{ x: 1100 }} />
|
||||
</Card>
|
||||
|
||||
<Modal
|
||||
title={editingCustomer ? '编辑客户' : '新增客户'}
|
||||
open={modalVisible}
|
||||
<Modal
|
||||
title={editingCustomer ? t('customer.editCustomer') : t('customer.newCustomer')}
|
||||
open={modalVisible}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
title: t('common.closeConfirm'),
|
||||
content: t('common.closeConfirmMsg'),
|
||||
okText: t('common.close'),
|
||||
cancelText: t('common.continueEdit'),
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
form.resetFields();
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingCustomer(null)
|
||||
},
|
||||
})
|
||||
} else {
|
||||
form.resetFields();
|
||||
setModalVisible(false)
|
||||
form.resetFields()
|
||||
setEditingCustomer(null)
|
||||
}
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
}}
|
||||
onOk={() => form.submit()}
|
||||
destroyOnClose
|
||||
width={800}
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical" onFinish={handleSubmit} onValuesChange={handleFormChange}>
|
||||
<Form.Item name="name" label="名称" rules={[{ required: true, message: '请输入名称' }]}>
|
||||
<Input placeholder="客户名称" />
|
||||
<Form.Item name="name" label={t('customer.name')} rules={[{ required: true, message: t('customer.nameRequired') }]}>
|
||||
<Input placeholder={t('customer.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item name="address" label="地址">
|
||||
<Input placeholder="客户地址" />
|
||||
<Form.Item name="address" label={t('customer.address')}>
|
||||
<Input placeholder={t('customer.addressPlaceholder')} />
|
||||
</Form.Item>
|
||||
<Form.Item name="remark" label="备注">
|
||||
<Input.TextArea rows={2} placeholder="备注信息" />
|
||||
<Form.Item name="remark" label={t('common.remark')}>
|
||||
<Input.TextArea rows={2} placeholder={t('customer.remarkPlaceholder')} />
|
||||
</Form.Item>
|
||||
|
||||
<h4>联系人</h4>
|
||||
<h4>{t('customer.contact')}</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="姓名" />
|
||||
<Input placeholder={t('logistics.name')} />
|
||||
</Form.Item>
|
||||
<Form.Item {...restField} name={[name, 'position']} style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder="职位" />
|
||||
<Input placeholder={t('common.position')} />
|
||||
</Form.Item>
|
||||
<Form.Item {...restField} name={[name, 'phone']} style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder="电话" />
|
||||
<Input placeholder={t('common.phone')} />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', alignItems: 'center' }}>
|
||||
<Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked" style={{ marginBottom: 0, marginRight: 8 }}>
|
||||
@@ -321,33 +324,33 @@ const CustomerPage: React.FC = () => {
|
||||
onChange={(e) => handleContactChange(name, 'is_primary', e.target.checked)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<span>主联系人</span>
|
||||
<span>{t('customer.mainContact')}</span>
|
||||
</div>
|
||||
{fields.length > 1 && <Button type="link" danger onClick={() => remove(name)}>删除</Button>}
|
||||
{fields.length > 1 && <Button type="link" danger onClick={() => remove(name)}>{t('common.delete')}</Button>}
|
||||
</div>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add()} style={{ width: '100%' }}>+ 添加联系人</Button>
|
||||
<Button type="dashed" onClick={() => add()} style={{ width: '100%' }}>{t('customer.addContact')}</Button>
|
||||
</div>
|
||||
)}
|
||||
</Form.List>
|
||||
|
||||
<h4 style={{ marginTop: 24 }}>收款信息</h4>
|
||||
<h4 style={{ marginTop: 24 }}>{t('customer.paymentInfo')}</h4>
|
||||
<Form.List name="payment_infos" initialValue={[]}>
|
||||
{(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 {...restField} name={[name, 'account_name']} label={t('customer.accountName')} style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder={t('customer.accountName')} />
|
||||
</Form.Item>
|
||||
<Form.Item {...restField} name={[name, 'bank_name']} label="开户银行" style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder="开户银行" />
|
||||
<Form.Item {...restField} name={[name, 'bank_name']} label={t('customer.bankName')} style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder={t('customer.bankName')} />
|
||||
</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 {...restField} name={[name, 'bank_account']} label={t('customer.bankAccount')} style={{ marginBottom: 0, flex: 1 }}>
|
||||
<Input placeholder={t('customer.bankAccount')} />
|
||||
</Form.Item>
|
||||
<div style={{ display: 'flex', alignItems: 'center', marginTop: 30 }}>
|
||||
<Form.Item {...restField} name={[name, 'is_primary']} valuePropName="checked" style={{ marginBottom: 0, marginRight: 8 }}>
|
||||
@@ -356,19 +359,19 @@ const CustomerPage: React.FC = () => {
|
||||
onChange={(e) => handlePaymentInfoChange(name, 'is_primary', e.target.checked)}
|
||||
/>
|
||||
</Form.Item>
|
||||
<span>主要收款账户</span>
|
||||
<span>{t('customer.mainAccount')}</span>
|
||||
</div>
|
||||
</div>
|
||||
<Form.Item {...restField} name={[name, 'qr_code']} label="收款码" style={{ marginBottom: 0 }}>
|
||||
<Form.Item {...restField} name={[name, 'qr_code']} label={t('customer.qrCode')} style={{ marginBottom: 0 }}>
|
||||
<FileUpload maxCount={1} accept="image/*" />
|
||||
</Form.Item>
|
||||
{fields.length > 0 && (
|
||||
<Button type="link" danger onClick={() => remove(name)} style={{ marginTop: 8 }}>删除此收款信息</Button>
|
||||
<Button type="link" danger onClick={() => remove(name)} style={{ marginTop: 8 }}>{t('customer.deletePaymentInfo')}</Button>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
<Button type="dashed" onClick={() => add({ account_name: '', bank_account: '', bank_name: '', is_primary: false })} style={{ width: '100%' }}>
|
||||
+ 添加收款信息
|
||||
{t('customer.addPaymentInfo')}
|
||||
</Button>
|
||||
</div>
|
||||
)}
|
||||
@@ -379,4 +382,4 @@ const CustomerPage: React.FC = () => {
|
||||
)
|
||||
}
|
||||
|
||||
export default CustomerPage
|
||||
export default CustomerPage
|
||||
Reference in New Issue
Block a user