备份:大改造前的完整版本 - 修复合同细节/付款节点/文件上传/施工管理/项目保存等BUG
This commit is contained in:
@@ -1,11 +1,12 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Button, Form, Input, Select, DatePicker, InputNumber, Radio, Space, message, Divider, Row, Col } from 'antd';
|
||||
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 axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import FileUpload from '../../components/FileUpload';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
const { Title, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
@@ -31,9 +32,71 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
const [attachments, setAttachments] = useState<string[]>([]);
|
||||
const [surveyPhotos, setSurveyPhotos] = useState<string[]>([]);
|
||||
const navigate = useNavigate();
|
||||
|
||||
|
||||
const { user: currentUser } = useAuthStore();
|
||||
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: '发现未完成的草稿',
|
||||
content: '检测到上次未提交的商谈项目,是否恢复?',
|
||||
okText: '恢复草稿',
|
||||
cancelText: '重新填写',
|
||||
onOk: () => {
|
||||
restoreDraft();
|
||||
},
|
||||
onCancel: () => {
|
||||
clearDraft();
|
||||
},
|
||||
});
|
||||
}
|
||||
}, []);
|
||||
|
||||
// 检查权限,如果不是管理员,重定向到列表页面
|
||||
useEffect(() => {
|
||||
@@ -62,7 +125,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
|
||||
const fetchCustomers = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/customers');
|
||||
const res = await apiClient.get('/customers');
|
||||
if (res.data.success) setCustomers(res.data.data);
|
||||
} catch (error) {
|
||||
console.error('获取客户列表失败:', error);
|
||||
@@ -71,7 +134,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
|
||||
const fetchUsers = async () => {
|
||||
try {
|
||||
const res = await axios.get('/api/users');
|
||||
const res = await apiClient.get('/users');
|
||||
if (res.data.success) setUsers(res.data.data);
|
||||
} catch (error) {
|
||||
console.error('获取用户列表失败:', error);
|
||||
@@ -91,13 +154,14 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
status: 'negotiating',
|
||||
};
|
||||
|
||||
const res = await axios.post('/api/budget-projects', projectData, {
|
||||
const res = await apiClient.post('/budget-projects', projectData, {
|
||||
headers: {
|
||||
'x-user-role': 'admin' // 创建预算项目需要管理员权限
|
||||
'x-user-role': 'admin'
|
||||
}
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success('创建成功');
|
||||
clearDraft();
|
||||
navigate('/budget-projects');
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -117,7 +181,22 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
<div style={{ display: 'flex', alignItems: 'center', gap: 16, marginBottom: 8 }}>
|
||||
<Button
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/budget-projects')}
|
||||
onClick={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认离开',
|
||||
content: '表单数据尚未保存,离开后可通过草稿恢复。确定离开吗?',
|
||||
okText: '离开',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ attachments, surveyPhotos });
|
||||
navigate('/budget-projects');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate('/budget-projects');
|
||||
}
|
||||
}}
|
||||
>
|
||||
返回
|
||||
</Button>
|
||||
@@ -127,10 +206,11 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
</div>
|
||||
|
||||
<Card>
|
||||
<Form
|
||||
form={form}
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
onValuesChange={handleFormChange}
|
||||
initialValues={{
|
||||
intermediary_fee_type: 'fixed',
|
||||
survey_date: dayjs(), // 勘察日期默认为当天
|
||||
attachments: [],
|
||||
@@ -256,7 +336,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
<Form.Item label="附件上传">
|
||||
<FileUpload
|
||||
value={attachments}
|
||||
onChange={setAttachments}
|
||||
onChange={(urls) => { setAttachments(urls); saveDraft({ attachments: urls, surveyPhotos }); }}
|
||||
accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xlsx,.xls"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -265,7 +345,7 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
<Form.Item label="勘察照片">
|
||||
<FileUpload
|
||||
value={surveyPhotos}
|
||||
onChange={setSurveyPhotos}
|
||||
onChange={(urls) => { setSurveyPhotos(urls); saveDraft({ attachments, surveyPhotos: urls }); }}
|
||||
accept="image/*"
|
||||
/>
|
||||
</Form.Item>
|
||||
@@ -275,7 +355,22 @@ const BudgetProjectCreate: React.FC = () => {
|
||||
{/* 提交按钮 */}
|
||||
<div style={{ marginTop: 24, textAlign: 'right' }}>
|
||||
<Space>
|
||||
<Button onClick={() => navigate('/budget-projects')}>取消</Button>
|
||||
<Button onClick={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认离开',
|
||||
content: '表单数据尚未保存,离开后可通过草稿恢复。确定离开吗?',
|
||||
okText: '离开',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ attachments, surveyPhotos });
|
||||
navigate('/budget-projects');
|
||||
},
|
||||
});
|
||||
} else {
|
||||
navigate('/budget-projects');
|
||||
}
|
||||
}}>取消</Button>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<SaveOutlined />}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Button, Space, Tag, message, Empty, Divider, Row, Col, List, Descriptions, Avatar, Badge, Modal, Input } from 'antd';
|
||||
import { ArrowLeftOutlined, EyeOutlined, FileAddOutlined, FileOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import QuotationCreateModal from './QuotationCreateModal';
|
||||
import ContractCreateModal from './ContractCreateModal';
|
||||
@@ -78,7 +78,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get(`/api/budget-projects/${id}`);
|
||||
const res = await apiClient.get(`/budget-projects/${id}`);
|
||||
if (res.data.success) {
|
||||
const projectData = res.data.data;
|
||||
// 后端已经解析了数据,直接使用
|
||||
@@ -136,7 +136,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
if (!project) return;
|
||||
|
||||
try {
|
||||
const res = await axios.put(`/api/budget-projects/${project.id}/unsigned`, {}, {
|
||||
const res = await apiClient.put(`/budget-projects/${project.id}/unsigned`, {}, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
@@ -167,7 +167,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await axios.delete(`/api/budget-projects/${project.id}/quotations/${quotationDeleteId}`, {
|
||||
const res = await apiClient.delete(`/budget-projects/${project.id}/quotations/${quotationDeleteId}`, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
@@ -218,7 +218,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await axios.delete(`/api/budget-projects/${project.id}`, {
|
||||
const res = await apiClient.delete(`/budget-projects/${project.id}`, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react';
|
||||
import { Card, Typography, Button, Space, Tag, message, Empty, Radio, Modal, Input } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import dayjs from 'dayjs';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
|
||||
@@ -79,7 +79,7 @@ const BudgetProjectList: React.FC = () => {
|
||||
const fetchProjects = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await axios.get('/api/budget-projects');
|
||||
const res = await apiClient.get('/budget-projects');
|
||||
if (res.data.success) {
|
||||
// 后端已经解析了数据,直接使用
|
||||
const projectsWithParsedData = res.data.data.map((project: any) => {
|
||||
@@ -148,7 +148,7 @@ const BudgetProjectList: React.FC = () => {
|
||||
|
||||
setDeleteLoading(true);
|
||||
try {
|
||||
const res = await axios.delete(`/api/budget-projects/${deleteProjectId}`, {
|
||||
const res = await apiClient.delete(`/budget-projects/${deleteProjectId}`, {
|
||||
headers: {
|
||||
'x-user-role': currentUser?.role || 'employee'
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Form, Input, DatePicker, InputNumber, Select, Space, message } from 'antd';
|
||||
import dayjs from 'dayjs';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
interface ContractCreateModalProps {
|
||||
visible: boolean;
|
||||
@@ -20,6 +21,16 @@ const ContractCreateModal: React.FC<ContractCreateModalProps> = ({
|
||||
}) => {
|
||||
const [form] = Form.useForm();
|
||||
const [contractAmount, setContractAmount] = useState(0);
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'contract_create',
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft()
|
||||
}, [saveDraft])
|
||||
|
||||
// 生成默认的合同编号(包含时间戳确保唯一性)
|
||||
const today = dayjs();
|
||||
@@ -66,19 +77,18 @@ const ContractCreateModal: React.FC<ContractCreateModalProps> = ({
|
||||
unit_price_items: []
|
||||
};
|
||||
|
||||
console.log('提交的合同信息:', submitData);
|
||||
|
||||
try {
|
||||
const res = await axios.put(`/api/budget-projects/${projectId}/sign`, submitData, {
|
||||
const res = await apiClient.put(`/budget-projects/${projectId}/sign`, submitData, {
|
||||
headers: {
|
||||
'x-user-role': 'admin' // 签约操作需要管理员权限
|
||||
}
|
||||
});
|
||||
|
||||
console.log('API响应:', res);
|
||||
|
||||
if (res.data.success) {
|
||||
message.success('签约成功,项目已自动创建');
|
||||
clearDraft()
|
||||
onSuccess();
|
||||
onCancel();
|
||||
} else {
|
||||
@@ -97,15 +107,32 @@ const ContractCreateModal: React.FC<ContractCreateModalProps> = ({
|
||||
title="快速签约"
|
||||
open={visible}
|
||||
onOk={() => form.submit()}
|
||||
onCancel={onCancel}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft()
|
||||
onCancel()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
}}
|
||||
width={600}
|
||||
okText="确认签约"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
onFinish={handleSubmit}
|
||||
onValuesChange={handleFormChange}
|
||||
>
|
||||
{/* 基本信息 */}
|
||||
<Form.Item
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useCallback } from 'react';
|
||||
import { Modal, Form, Input, DatePicker, InputNumber, Select, Upload, Button, message } from 'antd';
|
||||
import { UploadOutlined, DeleteOutlined, FileOutlined } from '@ant-design/icons';
|
||||
import dayjs from 'dayjs';
|
||||
import axios from 'axios';
|
||||
import apiClient from '../../utils/request';
|
||||
import useFormDraft from '../../hooks/useFormDraft';
|
||||
|
||||
const { Option } = Select;
|
||||
|
||||
@@ -47,6 +48,19 @@ const QuotationCreateModal: React.FC<QuotationCreateModalProps> = ({
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [uploadedFile, setUploadedFile] = useState<{ url: string; name: string } | null>(null);
|
||||
|
||||
// 表单草稿保护
|
||||
const { save: saveDraft, restore: restoreDraft, clear: clearDraft, hasDraft } = useFormDraft({
|
||||
form,
|
||||
storageKey: 'quotation_create',
|
||||
onRestore: (data) => {
|
||||
if (data.uploadedFile) setUploadedFile(data.uploadedFile)
|
||||
},
|
||||
})
|
||||
|
||||
const handleFormChange = useCallback(() => {
|
||||
saveDraft({ uploadedFile })
|
||||
}, [saveDraft, uploadedFile])
|
||||
|
||||
// 计算下一个版本号
|
||||
const nextVersion = project?.quotations && Array.isArray(project.quotations) && project.quotations.length > 0
|
||||
? Math.max(...project.quotations.map(q => q.version || 0)) + 1
|
||||
@@ -110,13 +124,14 @@ const QuotationCreateModal: React.FC<QuotationCreateModalProps> = ({
|
||||
version: nextVersion,
|
||||
};
|
||||
|
||||
const res = await axios.post(`/api/budget-projects/${project.id}/quotations`, quotationData, {
|
||||
const res = await apiClient.post(`/budget-projects/${project.id}/quotations`, quotationData, {
|
||||
headers: {
|
||||
'x-user-role': 'admin' // 创建报价版本需要管理员权限
|
||||
}
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success('新增报价版本成功');
|
||||
clearDraft()
|
||||
onSuccess();
|
||||
}
|
||||
} catch (error: any) {
|
||||
@@ -152,13 +167,29 @@ const QuotationCreateModal: React.FC<QuotationCreateModalProps> = ({
|
||||
title="新增报价版本"
|
||||
open={visible}
|
||||
onOk={handleSubmit}
|
||||
onCancel={onCancel}
|
||||
onCancel={() => {
|
||||
if (form.isFieldsTouched()) {
|
||||
Modal.confirm({
|
||||
title: '确认关闭',
|
||||
content: '表单数据尚未保存,关闭后可通过草稿恢复。确定关闭吗?',
|
||||
okText: '关闭',
|
||||
cancelText: '继续编辑',
|
||||
onOk: () => {
|
||||
saveDraft({ uploadedFile })
|
||||
onCancel()
|
||||
},
|
||||
})
|
||||
} else {
|
||||
onCancel()
|
||||
}
|
||||
}}
|
||||
width={600}
|
||||
confirmLoading={loading}
|
||||
okText="保存"
|
||||
cancelText="取消"
|
||||
maskClosable={false}
|
||||
>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form form={form} layout="vertical" onValuesChange={handleFormChange}>
|
||||
{/* 项目信息展示 */}
|
||||
<div style={{
|
||||
padding: 16,
|
||||
|
||||
Reference in New Issue
Block a user