备份:PWA配置前的完整版本

包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
a273825743
2026-06-13 12:44:48 +08:00
parent 5fd19822a2
commit 706dcc24eb
83 changed files with 26590 additions and 13379 deletions
+68 -65
View File
@@ -5,6 +5,7 @@ import { PlusOutlined, EditOutlined, DeleteOutlined, SearchOutlined, SolutionOut
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
@@ -39,6 +40,7 @@ interface Subcontractor {
const SubcontractorPage: React.FC = () => {
const navigate = useNavigate()
const { t, currentLanguage } = useLanguageStore()
const [subcontractors, setSubcontractors] = useState<Subcontractor[]>([])
const [loading, setLoading] = useState(false)
const [modalVisible, setModalVisible] = useState(false)
@@ -63,7 +65,7 @@ const SubcontractorPage: React.FC = () => {
const data = await response.json()
if (data.success) setSubcontractors(data.data || [])
} catch (error) {
message.error('获取分包商列表失败')
message.error(t('subcontractor.getListFailed'))
} finally {
setLoading(false)
}
@@ -89,16 +91,16 @@ const SubcontractorPage: React.FC = () => {
const columns: ColumnsType<Subcontractor> = [
{
title: '名称', dataIndex: 'name', key: 'name',
title: t('subcontractor.name'), dataIndex: 'name', key: 'name',
render: (text, record) => (
<Button type="link" style={{ padding: 0, fontWeight: 'bold' }} onClick={() => navigate(`/subcontractors/${record.id}`)}>{text}</Button>
)
},
{ title: '承包范围', dataIndex: 'scope', key: 'scope', width: 120 },
{ title: '国家', dataIndex: 'country', key: 'country', width: 80, render: (c) => <Tag>{c || '-'}</Tag> },
{ title: '合同金额', dataIndex: 'total_contract_amount', key: 'total_contract_amount', width: 100, render: (v) => `¥${(v || 0).toLocaleString()}` },
{ title: '应付金额', dataIndex: 'total_payable', key: 'total_payable', width: 100, render: (v) => <span style={{ color: v > 0 ? '#ff4d4f' : '#52c41a' }}>¥{(v || 0).toLocaleString()}</span> },
{ title: '操作', key: 'actions', width: 100, render: (_, record) => (
{ title: t('subcontractor.scope'), dataIndex: 'scope', key: 'scope', width: 120 },
{ title: t('subcontractor.country'), dataIndex: 'country', key: 'country', width: 80, render: (c) => <Tag>{c || '-'}</Tag> },
{ title: t('subcontractor.contractAmount'), dataIndex: 'total_contract_amount', key: 'total_contract_amount', width: 100, render: (v) => `¥${(v || 0).toLocaleString()}` },
{ title: t('subcontractor.payableAmount'), dataIndex: 'total_payable', key: 'total_payable', 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" />
@@ -156,17 +158,17 @@ const SubcontractorPage: React.FC = () => {
})
const data = await response.json()
if (data.success) {
message.success(editingSubcontractor ? '更新成功' : '创建成功')
message.success(editingSubcontractor ? t('common.updateSuccess') : t('common.createSuccess'))
clearDraft()
setModalVisible(false)
form.resetFields()
setEditingSubcontractor(null)
fetchSubcontractors()
} else {
message.error(data.message || '操作失败')
message.error(data.message || t('common.operationFailed'))
}
} catch (error) {
message.error('操作失败')
message.error(t('common.operationFailed'))
}
}
@@ -186,14 +188,14 @@ const SubcontractorPage: React.FC = () => {
const handleDelete = async (id: number) => {
Modal.confirm({
title: '确认删除', content: '确定要删除此分包商吗?', okText: '确定', cancelText: '取消',
title: t('common.confirmDelete'), content: t('subcontractor.confirmDeleteMsg'), okText: t('common.confirm'), cancelText: t('common.cancel'),
onOk: async () => {
try {
const response = await fetch(`/api/subcontractors/${id}`, { method: 'DELETE' })
const data = await response.json()
if (data.success) { message.success('删除成功'); fetchSubcontractors() }
else message.error(data.message || '删除失败')
} catch (error) { message.error('删除失败') }
if (data.success) { message.success(t('common.deleteSuccess')); fetchSubcontractors() }
else message.error(data.message || t('common.deleteFailed'))
} catch (error) { message.error(t('common.deleteFailed')) }
}
})
}
@@ -211,10 +213,10 @@ const SubcontractorPage: 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()
},
@@ -235,89 +237,90 @@ const SubcontractorPage: React.FC = () => {
return (
<div style={{ padding: 24 }}>
<Row gutter={16} style={{ marginBottom: 24 }}>
<Col span={8}><Card><Statistic title="分包商总数" value={stats.total} prefix={<SolutionOutlined />} /></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.totalPayable} prefix="¥" valueStyle={{ color: stats.totalPayable > 0 ? '#ff4d4f' : '#52c41a' }} /></Card></Col>
<Col span={8}><Card><Statistic title={t('subcontractor.totalCount')} value={stats.total} prefix={<SolutionOutlined />} /></Card></Col>
<Col span={8}><Card><Statistic title={t('subcontractor.totalContract')} value={stats.totalContract} prefix="¥" valueStyle={{ color: '#1890ff' }} /></Card></Col>
<Col span={8}><Card><Statistic title={t('subcontractor.totalPayable')} 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>
<Input placeholder={t('subcontractor.searchPlaceholder')} prefix={<SearchOutlined />} value={searchText} onChange={(e) => setSearchText(e.target.value)} allowClear style={{ width: 350 }} />
<Button type="primary" icon={<PlusOutlined />} onClick={handleAdd}>{t('subcontractor.newSubcontractor')}</Button>
</div>
</Card>
<Card>
<Table columns={columns} dataSource={filteredSubcontractors} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => `${total}` }} scroll={{ x: 1100 }} />
<Table columns={columns} dataSource={filteredSubcontractors} rowKey="id" loading={loading} pagination={{ pageSize: 10, showTotal: (total) => t('common.totalCount', { total }) }} scroll={{ x: 1100 }} />
</Card>
<Modal
title={editingSubcontractor ? '编辑分包商' : '新增分包商'}
open={modalVisible}
<Modal
title={editingSubcontractor ? t('subcontractor.editSubcontractor') : t('subcontractor.newSubcontractor')}
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()
setEditingSubcontractor(null)
},
})
} else {
form.resetFields();
setModalVisible(false)
form.resetFields()
setEditingSubcontractor(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('subcontractor.name')} rules={[{ required: true, message: t('subcontractor.nameRequired') }]}>
<Input placeholder={t('subcontractor.namePlaceholder')} />
</Form.Item>
<Row gutter={16}>
<Col span={12}>
<Form.Item name="scope" label="承包范围">
<Input placeholder="手填:如电力安装、土建工程" />
<Form.Item name="scope" label={t('subcontractor.scope')}>
<Input placeholder={t('subcontractor.scopePlaceholder')} />
</Form.Item>
</Col>
<Col span={12}>
<Form.Item name="country" label="国家" initialValue="Laos">
<Form.Item name="country" label={t('subcontractor.country')} initialValue="Laos">
<Select>
<Select.Option value="China"></Select.Option>
<Select.Option value="Laos"></Select.Option>
<Select.Option value="China">{t('subcontractor.china')}</Select.Option>
<Select.Option value="Laos">{t('subcontractor.laos')}</Select.Option>
</Select>
</Form.Item>
</Col>
</Row>
<Form.Item name="features" label="特点">
<Input.TextArea rows={2} placeholder="手填:如专业团队、设备齐全、价格合理等" />
<Form.Item name="features" label={t('subcontractor.feature')}>
<Input.TextArea rows={2} placeholder={t('subcontractor.featurePlaceholder')} />
</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('subcontractor.remarkPlaceholder')} />
</Form.Item>
<h4></h4>
<h4>{t('subcontractor.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 }}>
@@ -326,33 +329,33 @@ const SubcontractorPage: React.FC = () => {
onChange={(e) => handleContactChange(name, 'is_primary', e.target.checked)}
/>
</Form.Item>
<span></span>
<span>{t('subcontractor.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('subcontractor.addContact')}</Button>
</div>
)}
</Form.List>
<h4 style={{ marginTop: 24 }}></h4>
<h4 style={{ marginTop: 24 }}>{t('subcontractor.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('subcontractor.accountName')} style={{ marginBottom: 0, flex: 1 }}>
<Input placeholder={t('subcontractor.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('subcontractor.bankName')} style={{ marginBottom: 0, flex: 1 }}>
<Input placeholder={t('subcontractor.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('subcontractor.bankAccount')} style={{ marginBottom: 0, flex: 1 }}>
<Input placeholder={t('subcontractor.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 }}>
@@ -361,19 +364,19 @@ const SubcontractorPage: React.FC = () => {
onChange={(e) => handlePaymentInfoChange(name, 'is_primary', e.target.checked)}
/>
</Form.Item>
<span></span>
<span>{t('subcontractor.mainAccount')}</span>
</div>
</div>
<Form.Item {...restField} name={[name, 'qr_code']} label="收款码" style={{ marginBottom: 0 }}>
<Form.Item {...restField} name={[name, 'qr_code']} label={t('subcontractor.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('subcontractor.deletePaymentInfo')}</Button>
)}
</div>
))}
<Button type="dashed" onClick={() => add({ account_name: '', bank_account: '', bank_name: '', is_primary: false })} style={{ width: '100%' }}>
+
{t('subcontractor.addPaymentInfo')}
</Button>
</div>
)}
@@ -384,4 +387,4 @@ const SubcontractorPage: React.FC = () => {
)
}
export default SubcontractorPage
export default SubcontractorPage