备份:大改造前的完整版本 - 修复合同细节/付款节点/文件上传/施工管理/项目保存等BUG

This commit is contained in:
root
2026-05-15 12:02:24 +08:00
parent 1cad1e438d
commit fad28741bd
6157 changed files with 5147 additions and 877912 deletions
+110 -15
View File
@@ -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,