Files
yunhaifinance/frontend/src/pages/budget/BudgetProjectCreate.tsx
T
a273825743 706dcc24eb 备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
2026-06-13 12:44:48 +08:00

378 lines
13 KiB
TypeScript

import React, { useState, useEffect, useCallback } from 'react';
import { Card, Typography, Button, Form, Input, Select, DatePicker, InputNumber, Radio, Space, message, Divider, Row, Col, Modal } from 'antd';
import { SaveOutlined, ArrowLeftOutlined } from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
import apiClient from '../../utils/request';
import dayjs from 'dayjs';
import FileUpload from '../../components/FileUpload';
import { useAuthStore } from '../../store/authStore';
import { useLanguageStore } from '../../store/languageStore';
import useFormDraft from '../../hooks/useFormDraft';
const { Title, Paragraph } = Typography;
const { Option } = Select;
const { TextArea } = Input;
interface Customer {
id: number;
name: string;
}
interface User {
id: number;
name: string;
department?: string;
}
const BudgetProjectCreate: React.FC = () => {
const [isMobile, setIsMobile] = useState(false);
const [loading, setLoading] = useState(false);
const [customers, setCustomers] = useState<Customer[]>([]);
const [users, setUsers] = useState<User[]>([]);
const [form] = Form.useForm();
const [attachments, setAttachments] = useState<string[]>([]);
const [surveyPhotos, setSurveyPhotos] = useState<string[]>([]);
const navigate = useNavigate();
const { user: currentUser } = useAuthStore();
const { t, currentLanguage } = useLanguageStore();
const isAdmin = currentUser?.role === 'admin';
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
form,
storageKey: 'budget_project_create',
onRestore: (data) => {
if (data.attachments) setAttachments(data.attachments);
if (data.surveyPhotos) setSurveyPhotos(data.surveyPhotos);
},
});
const handleFormChange = useCallback(() => {
saveDraft({ attachments, surveyPhotos });
}, [saveDraft, attachments, surveyPhotos]);
useEffect(() => {
const handler = (e: BeforeUnloadEvent) => {
if (form.isFieldsTouched()) {
e.preventDefault();
}
};
window.addEventListener('beforeunload', handler);
return () => window.removeEventListener('beforeunload', handler);
}, [form]);
useEffect(() => {
const handler = () => {
if (document.visibilityState === 'hidden' && form.isFieldsTouched()) {
saveDraft({ attachments, surveyPhotos });
}
};
document.addEventListener('visibilitychange', handler);
const pageHideHandler = () => {
if (form.isFieldsTouched()) saveDraft({ attachments, surveyPhotos });
};
window.addEventListener('pagehide', pageHideHandler);
return () => {
document.removeEventListener('visibilitychange', handler);
window.removeEventListener('pagehide', pageHideHandler);
};
}, [form, saveDraft, attachments, surveyPhotos]);
useEffect(() => {
if (hasDraft()) {
Modal.confirm({
title: t('common.draftFound'),
content: t('common.draftRestore'),
okText: t('common.restoreDraft'),
cancelText: t('common.reFill'),
onOk: () => {
restoreDraft();
},
onCancel: () => {
clearDraft();
},
});
}
}, []);
useEffect(() => {
if (!isAdmin) {
message.error(t('budget.noAccess'));
navigate('/budget-projects');
}
}, [isAdmin, navigate]);
const intermediaryFeeType = Form.useWatch('intermediary_fee_type', form);
useEffect(() => {
const checkMobile = () => setIsMobile(window.innerWidth <= 768);
checkMobile();
window.addEventListener('resize', checkMobile);
return () => window.removeEventListener('resize', checkMobile);
}, []);
useEffect(() => {
fetchCustomers();
fetchUsers();
}, []);
const fetchCustomers = async () => {
try {
const res = await apiClient.get('/customers');
if (res.data.success) setCustomers(res.data.data);
} catch (error) {
console.error('获取客户列表失败:', error);
}
};
const fetchUsers = async () => {
try {
const res = await apiClient.get('/users');
if (res.data.success) setUsers(res.data.data);
} catch (error) {
console.error('获取用户列表失败:', error);
}
};
const handleSubmit = async () => {
try {
const values = await form.validateFields();
setLoading(true);
const projectData = {
...values,
attachments,
survey_photos: surveyPhotos,
survey_date: values.survey_date?.format('YYYY-MM-DD'),
status: 'negotiating',
};
const res = await apiClient.post('/budget-projects', projectData, {
headers: {
'x-user-role': 'admin'
}
});
if (res.data.success) {
message.success(t('budget.createSuccess'));
clearDraft();
navigate('/budget-projects');
}
} catch (error: any) {
if (error.response?.data?.error) {
message.error(error.response.data.error);
} else {
message.error(t('budget.createFailed'));
}
} finally {
setLoading(false);
}
};
return (
<div style={{ padding: isMobile ? 8 : 24 }}>
<div style={{ marginBottom: isMobile ? 12 : 24 }}>
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 8 }}>
<Button
icon={<ArrowLeftOutlined />}
onClick={() => {
if (form.isFieldsTouched()) {
Modal.confirm({
title: t('budget.leaveConfirm'),
content: t('budget.leaveConfirmMsg'),
okText: t('budget.leave'),
cancelText: t('budget.continueEdit'),
onOk: () => {
saveDraft({ attachments, surveyPhotos });
navigate('/budget-projects');
},
});
} else {
navigate('/budget-projects');
}
}}
>
{t('budget.return')}
</Button>
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 0 }}>{t('budget.createTitle')}</Title>
</div>
<Paragraph type="secondary">{t('budget.createDesc')}</Paragraph>
</div>
<Card>
<Form
form={form}
layout="vertical"
onValuesChange={handleFormChange}
initialValues={{
intermediary_fee_type: 'fixed',
survey_date: dayjs(),
attachments: [],
survey_photos: []
}}
>
<Divider orientation="left">{t('budget.basicInfo')}</Divider>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item
name="name"
label={t('budget.projectName')}
rules={[{ required: true, message: t('budget.projectNamePlaceholder') }]}
>
<Input placeholder={t('budget.projectNamePlaceholder')} size="large" />
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item
name="customer_id"
label={t('budget.customer')}
rules={[{ required: true, message: t('budget.selectCustomer') }]}
>
<Select
placeholder={t('budget.selectCustomer')}
showSearch
optionFilterProp="children"
size="large"
>
{customers.map((c) => (
<Option key={c.id} value={c.id}>{c.name}</Option>
))}
</Select>
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item
name="manager_id"
label={t('budget.businessManager')}
rules={[{ required: true, message: t('budget.selectManager') }]}
>
<Select
placeholder={t('budget.selectManager')}
showSearch
optionFilterProp="children"
size="large"
>
{users.map((u) => (
<Option key={u.id} value={u.id}>{u.name} ({u.department || t('budget.unknownDept')})</Option>
))}
</Select>
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item name="location" label={t('budget.projectLocation')}>
<Input placeholder={t('budget.locationPlaceholder')} size="large" />
</Form.Item>
</Col>
</Row>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item name="survey_date" label={t('budget.surveyDate')}>
<DatePicker style={{ width: '100%' }} size="large" />
</Form.Item>
</Col>
</Row>
<Divider orientation="left">{t('budget.intermediary')}</Divider>
<Row gutter={16}>
<Col xs={24} md={8}>
<Form.Item name="intermediary" label={t('budget.intermediaryName')}>
<Input placeholder={t('budget.intermediaryNamePlaceholder')} size="large" />
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item name="intermediary_fee_type" label={t('budget.intermediaryType')}>
<Radio.Group>
<Radio value="fixed">{t('budget.fixedAmount')}</Radio>
<Radio value="percentage">{t('budget.percentage')}</Radio>
</Radio.Group>
</Form.Item>
</Col>
<Col xs={24} md={8}>
<Form.Item
name="intermediary_fee_value"
label={intermediaryFeeType === 'percentage' ? t('budget.intermediaryRatio') : t('budget.intermediaryAmount')}
>
<InputNumber
style={{ width: '100%' }}
size="large"
min={0}
precision={intermediaryFeeType === 'percentage' ? 2 : 0}
placeholder={intermediaryFeeType === 'percentage' ? t('budget.intermediaryRatioPlaceholder') : t('budget.intermediaryAmountPlaceholder')}
/>
</Form.Item>
</Col>
</Row>
<Divider orientation="left">{t('budget.projectDetail')}</Divider>
<Form.Item name="customer_requirements" label={t('budget.customerRequirement')}>
<TextArea rows={4} placeholder={t('budget.requirementPlaceholder')} />
</Form.Item>
<Form.Item name="project_overview" label={t('budget.overview')}>
<TextArea rows={4} placeholder={t('budget.overviewPlaceholder')} />
</Form.Item>
<Divider orientation="left">{t('budget.attachment')}</Divider>
<Row gutter={16}>
<Col xs={24} md={12}>
<Form.Item label={t('budget.attachmentUpload')}>
<FileUpload
value={attachments}
onChange={(urls) => { setAttachments(urls); saveDraft({ attachments: urls, surveyPhotos }); }}
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xlsx,.xls"
/>
</Form.Item>
</Col>
<Col xs={24} md={12}>
<Form.Item label={t('budget.surveyPhoto')}>
<FileUpload
value={surveyPhotos}
onChange={(urls) => { setSurveyPhotos(urls); saveDraft({ attachments, surveyPhotos: urls }); }}
accept="image/*"
/>
</Form.Item>
</Col>
</Row>
<div style={{ marginTop: 24, textAlign: 'right' }}>
<Space>
<Button onClick={() => {
if (form.isFieldsTouched()) {
Modal.confirm({
title: t('budget.leaveConfirm'),
content: t('budget.leaveConfirmMsg'),
okText: t('budget.leave'),
cancelText: t('budget.continueEdit'),
onOk: () => {
saveDraft({ attachments, surveyPhotos });
navigate('/budget-projects');
},
});
} else {
navigate('/budget-projects');
}
}}>{t('common.cancel')}</Button>
<Button
type="primary"
icon={<SaveOutlined />}
loading={loading}
onClick={handleSubmit}
>
{t('common.save')}
</Button>
</Space>
</div>
</Form>
</Card>
</div>
);
};
export default BudgetProjectCreate;