2026-05-15 12:02:24 +08:00
|
|
|
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';
|
2026-04-19 19:15:01 +08:00
|
|
|
import { SaveOutlined, ArrowLeftOutlined } from '@ant-design/icons';
|
|
|
|
|
import { useNavigate } from 'react-router-dom';
|
2026-05-15 12:02:24 +08:00
|
|
|
import apiClient from '../../utils/request';
|
2026-04-19 19:15:01 +08:00
|
|
|
import dayjs from 'dayjs';
|
|
|
|
|
import FileUpload from '../../components/FileUpload';
|
|
|
|
|
import { useAuthStore } from '../../store/authStore';
|
2026-05-15 12:02:24 +08:00
|
|
|
import useFormDraft from '../../hooks/useFormDraft';
|
2026-04-19 19:15:01 +08:00
|
|
|
|
|
|
|
|
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();
|
2026-05-15 12:02:24 +08:00
|
|
|
|
2026-04-19 19:15:01 +08:00
|
|
|
const { user: currentUser } = useAuthStore();
|
|
|
|
|
const isAdmin = currentUser?.role === 'admin';
|
2026-05-15 12:02:24 +08:00
|
|
|
|
|
|
|
|
// 表单草稿保护
|
|
|
|
|
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: '发现未完成的草稿',
|
|
|
|
|
content: '检测到上次未提交的商谈项目,是否恢复?',
|
|
|
|
|
okText: '恢复草稿',
|
|
|
|
|
cancelText: '重新填写',
|
|
|
|
|
onOk: () => {
|
|
|
|
|
restoreDraft();
|
|
|
|
|
},
|
|
|
|
|
onCancel: () => {
|
|
|
|
|
clearDraft();
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}, []);
|
2026-04-19 19:15:01 +08:00
|
|
|
|
|
|
|
|
// 检查权限,如果不是管理员,重定向到列表页面
|
|
|
|
|
useEffect(() => {
|
|
|
|
|
if (!isAdmin) {
|
|
|
|
|
message.error('您没有权限访问此页面');
|
|
|
|
|
navigate('/budget-projects');
|
|
|
|
|
}
|
|
|
|
|
}, [isAdmin, navigate]);
|
|
|
|
|
|
|
|
|
|
// const { user: currentUser } = useAuthStore();
|
|
|
|
|
|
|
|
|
|
// 表单监听值
|
|
|
|
|
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 {
|
2026-05-15 12:02:24 +08:00
|
|
|
const res = await apiClient.get('/customers');
|
2026-04-19 19:15:01 +08:00
|
|
|
if (res.data.success) setCustomers(res.data.data);
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取客户列表失败:', error);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const fetchUsers = async () => {
|
|
|
|
|
try {
|
2026-05-15 12:02:24 +08:00
|
|
|
const res = await apiClient.get('/users');
|
2026-04-19 19:15:01 +08:00
|
|
|
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',
|
|
|
|
|
};
|
|
|
|
|
|
2026-05-15 12:02:24 +08:00
|
|
|
const res = await apiClient.post('/budget-projects', projectData, {
|
2026-04-19 19:15:01 +08:00
|
|
|
headers: {
|
2026-05-15 12:02:24 +08:00
|
|
|
'x-user-role': 'admin'
|
2026-04-19 19:15:01 +08:00
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
if (res.data.success) {
|
|
|
|
|
message.success('创建成功');
|
2026-05-15 12:02:24 +08:00
|
|
|
clearDraft();
|
2026-04-19 19:15:01 +08:00
|
|
|
navigate('/budget-projects');
|
|
|
|
|
}
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
if (error.response?.data?.error) {
|
|
|
|
|
message.error(error.response.data.error);
|
|
|
|
|
} else {
|
|
|
|
|
message.error('创建失败');
|
|
|
|
|
}
|
|
|
|
|
} 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 />}
|
2026-05-15 12:02:24 +08:00
|
|
|
onClick={() => {
|
|
|
|
|
if (form.isFieldsTouched()) {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: '确认离开',
|
|
|
|
|
content: '表单数据尚未保存,离开后可通过草稿恢复。确定离开吗?',
|
|
|
|
|
okText: '离开',
|
|
|
|
|
cancelText: '继续编辑',
|
|
|
|
|
onOk: () => {
|
|
|
|
|
saveDraft({ attachments, surveyPhotos });
|
|
|
|
|
navigate('/budget-projects');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
navigate('/budget-projects');
|
|
|
|
|
}
|
|
|
|
|
}}
|
2026-04-19 19:15:01 +08:00
|
|
|
>
|
|
|
|
|
返回
|
|
|
|
|
</Button>
|
|
|
|
|
<Title level={isMobile ? 3 : 2} style={{ marginBottom: 0 }}>新建商谈项目</Title>
|
|
|
|
|
</div>
|
|
|
|
|
<Paragraph type="secondary">创建新的商谈项目,添加项目基本信息</Paragraph>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<Card>
|
2026-05-15 12:02:24 +08:00
|
|
|
<Form
|
|
|
|
|
form={form}
|
2026-04-19 19:15:01 +08:00
|
|
|
layout="vertical"
|
2026-05-15 12:02:24 +08:00
|
|
|
onValuesChange={handleFormChange}
|
|
|
|
|
initialValues={{
|
2026-04-19 19:15:01 +08:00
|
|
|
intermediary_fee_type: 'fixed',
|
|
|
|
|
survey_date: dayjs(), // 勘察日期默认为当天
|
|
|
|
|
attachments: [],
|
|
|
|
|
survey_photos: []
|
|
|
|
|
}}
|
|
|
|
|
>
|
|
|
|
|
{/* 基本信息 */}
|
|
|
|
|
<Divider orientation="left">基本信息</Divider>
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="name"
|
|
|
|
|
label="项目名称"
|
|
|
|
|
rules={[{ required: true, message: '请输入项目名称' }]}
|
|
|
|
|
>
|
|
|
|
|
<Input placeholder="请输入项目名称" size="large" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="customer_id"
|
|
|
|
|
label="客户"
|
|
|
|
|
rules={[{ required: true, message: '请选择客户' }]}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
placeholder="请选择客户"
|
|
|
|
|
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="业务经理"
|
|
|
|
|
rules={[{ required: true, message: '请选择业务经理' }]}
|
|
|
|
|
>
|
|
|
|
|
<Select
|
|
|
|
|
placeholder="请选择业务经理"
|
|
|
|
|
showSearch
|
|
|
|
|
optionFilterProp="children"
|
|
|
|
|
size="large"
|
|
|
|
|
>
|
|
|
|
|
{users.map((u) => (
|
|
|
|
|
<Option key={u.id} value={u.id}>{u.name} ({u.department || '未知部门'})</Option>
|
|
|
|
|
))}
|
|
|
|
|
</Select>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
<Form.Item name="location" label="项目地点">
|
|
|
|
|
<Input placeholder="请输入项目地点" size="large" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
<Form.Item name="survey_date" label="勘察日期">
|
|
|
|
|
<DatePicker style={{ width: '100%' }} size="large" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* 居间人信息 */}
|
|
|
|
|
<Divider orientation="left">居间人信息</Divider>
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col xs={24} md={8}>
|
|
|
|
|
<Form.Item name="intermediary" label="居间人">
|
|
|
|
|
<Input placeholder="请输入居间人姓名" size="large" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} md={8}>
|
|
|
|
|
<Form.Item name="intermediary_fee_type" label="居间费类型">
|
|
|
|
|
<Radio.Group>
|
|
|
|
|
<Radio value="fixed">固定金额</Radio>
|
|
|
|
|
<Radio value="percentage">百分比</Radio>
|
|
|
|
|
</Radio.Group>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} md={8}>
|
|
|
|
|
<Form.Item
|
|
|
|
|
name="intermediary_fee_value"
|
|
|
|
|
label={intermediaryFeeType === 'percentage' ? '居间费比例(%)' : '居间费金额'}
|
|
|
|
|
>
|
|
|
|
|
<InputNumber
|
|
|
|
|
style={{ width: '100%' }}
|
|
|
|
|
size="large"
|
|
|
|
|
min={0}
|
|
|
|
|
precision={intermediaryFeeType === 'percentage' ? 2 : 0}
|
|
|
|
|
placeholder={intermediaryFeeType === 'percentage' ? '输入比例,如:5' : '输入金额'}
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* 项目详情 */}
|
|
|
|
|
<Divider orientation="left">项目详情</Divider>
|
|
|
|
|
|
|
|
|
|
<Form.Item name="customer_requirements" label="客户要求">
|
|
|
|
|
<TextArea rows={4} placeholder="请输入客户的具体要求" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
<Form.Item name="project_overview" label="工程概况">
|
|
|
|
|
<TextArea rows={4} placeholder="请输入工程概况描述" />
|
|
|
|
|
</Form.Item>
|
|
|
|
|
|
|
|
|
|
{/* 附件上传 */}
|
|
|
|
|
<Divider orientation="left">附件</Divider>
|
|
|
|
|
|
|
|
|
|
<Row gutter={16}>
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
<Form.Item label="附件上传">
|
|
|
|
|
<FileUpload
|
|
|
|
|
value={attachments}
|
2026-05-15 12:02:24 +08:00
|
|
|
onChange={(urls) => { setAttachments(urls); saveDraft({ attachments: urls, surveyPhotos }); }}
|
2026-04-19 19:15:01 +08:00
|
|
|
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xlsx,.xls"
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
<Col xs={24} md={12}>
|
|
|
|
|
<Form.Item label="勘察照片">
|
|
|
|
|
<FileUpload
|
|
|
|
|
value={surveyPhotos}
|
2026-05-15 12:02:24 +08:00
|
|
|
onChange={(urls) => { setSurveyPhotos(urls); saveDraft({ attachments, surveyPhotos: urls }); }}
|
2026-04-19 19:15:01 +08:00
|
|
|
accept="image/*"
|
|
|
|
|
/>
|
|
|
|
|
</Form.Item>
|
|
|
|
|
</Col>
|
|
|
|
|
</Row>
|
|
|
|
|
|
|
|
|
|
{/* 提交按钮 */}
|
|
|
|
|
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
|
|
|
|
<Space>
|
2026-05-15 12:02:24 +08:00
|
|
|
<Button onClick={() => {
|
|
|
|
|
if (form.isFieldsTouched()) {
|
|
|
|
|
Modal.confirm({
|
|
|
|
|
title: '确认离开',
|
|
|
|
|
content: '表单数据尚未保存,离开后可通过草稿恢复。确定离开吗?',
|
|
|
|
|
okText: '离开',
|
|
|
|
|
cancelText: '继续编辑',
|
|
|
|
|
onOk: () => {
|
|
|
|
|
saveDraft({ attachments, surveyPhotos });
|
|
|
|
|
navigate('/budget-projects');
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
navigate('/budget-projects');
|
|
|
|
|
}
|
|
|
|
|
}}>取消</Button>
|
2026-04-19 19:15:01 +08:00
|
|
|
<Button
|
|
|
|
|
type="primary"
|
|
|
|
|
icon={<SaveOutlined />}
|
|
|
|
|
loading={loading}
|
|
|
|
|
onClick={handleSubmit}
|
|
|
|
|
>
|
|
|
|
|
保存
|
|
|
|
|
</Button>
|
|
|
|
|
</Space>
|
|
|
|
|
</div>
|
|
|
|
|
</Form>
|
|
|
|
|
</Card>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
export default BudgetProjectCreate;
|