378 lines
13 KiB
TypeScript
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; |