备份:PWA配置前的完整版本
包含所有最新开发的功能和修复,作为PWA正确配置前的备份点。
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
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 { Card, Typography, Button, Space, Tag, message, Empty, Divider, Row, Col, List, Descriptions, Modal, Input } from 'antd';
|
||||
import { ArrowLeftOutlined, EyeOutlined, FileAddOutlined, FileOutlined, CheckCircleOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import apiClient from '../../utils/request';
|
||||
@@ -7,6 +7,7 @@ import dayjs from 'dayjs';
|
||||
import QuotationCreateModal from './QuotationCreateModal';
|
||||
import ContractCreateModal from './ContractCreateModal';
|
||||
import { useAuthStore } from '../../store/authStore';
|
||||
import { useLanguageStore } from '../../store/languageStore';
|
||||
|
||||
const { Title, Paragraph, Text } = Typography;
|
||||
|
||||
@@ -65,6 +66,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
|
||||
const { user: currentUser } = useAuthStore();
|
||||
const { t, currentLanguage } = useLanguageStore();
|
||||
const isAdmin = currentUser?.role === 'admin' || false;
|
||||
|
||||
useEffect(() => {
|
||||
@@ -81,7 +83,6 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
const res = await apiClient.get(`/budget-projects/${id}`);
|
||||
if (res.data.success) {
|
||||
const projectData = res.data.data;
|
||||
// 后端已经解析了数据,直接使用
|
||||
projectData.quotations = Array.isArray(projectData.quotations) ? projectData.quotations : [];
|
||||
projectData.attachments = Array.isArray(projectData.attachments) ? projectData.attachments : [];
|
||||
projectData.survey_photos = Array.isArray(projectData.survey_photos) ? projectData.survey_photos : [];
|
||||
@@ -89,7 +90,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取项目详情失败:', error);
|
||||
message.error('获取数据失败');
|
||||
message.error(t('budget.getDataFailed'));
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
@@ -97,9 +98,9 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
negotiating: { color: 'processing', text: '商谈中' },
|
||||
signed: { color: 'success', text: '已签约' },
|
||||
unsigned: { color: 'error', text: '未签约' },
|
||||
negotiating: { color: 'processing', text: t('budget.inNegotiation') },
|
||||
signed: { color: 'success', text: t('budget.signed') },
|
||||
unsigned: { color: 'error', text: t('budget.unsigned') },
|
||||
};
|
||||
const config = statusMap[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
@@ -107,10 +108,10 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
|
||||
const getQuotationStatusTag = (status: string) => {
|
||||
const statusMap: Record<string, { color: string; text: string }> = {
|
||||
draft: { color: 'default', text: '草稿' },
|
||||
sent: { color: 'processing', text: '已发送' },
|
||||
approved: { color: 'success', text: '已通过' },
|
||||
rejected: { color: 'error', text: '已拒绝' },
|
||||
draft: { color: 'default', text: t('budget.draft') },
|
||||
sent: { color: 'processing', text: t('budget.sent') },
|
||||
approved: { color: 'success', text: t('budget.approved') },
|
||||
rejected: { color: 'error', text: t('budget.rejected') },
|
||||
};
|
||||
const config = statusMap[status] || { color: 'default', text: status };
|
||||
return <Tag color={config.color}>{config.text}</Tag>;
|
||||
@@ -142,11 +143,11 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
}
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success('标记未签约成功');
|
||||
message.success(t('budget.signedSuccess'));
|
||||
fetchProjectDetail();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('操作失败');
|
||||
message.error(t('common.operationFailed'));
|
||||
}
|
||||
};
|
||||
|
||||
@@ -159,9 +160,8 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
const handleQuotationDeleteConfirm = async () => {
|
||||
if (!project || !quotationDeleteId) return;
|
||||
|
||||
// 验证密码(这里简单验证,实际项目中应该使用更安全的验证方式)
|
||||
if (deletePassword !== 'X123c321@') {
|
||||
message.error('密码错误');
|
||||
message.error(t('budget.passwordError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -173,12 +173,12 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
}
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success('删除成功');
|
||||
message.success(t('budget.deleteSuccess'));
|
||||
setQuotationDeleteModalVisible(false);
|
||||
fetchProjectDetail();
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
message.error(t('common.deleteFailed'));
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
@@ -210,9 +210,8 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
const handleProjectDeleteConfirm = async () => {
|
||||
if (!project) return;
|
||||
|
||||
// 验证密码(这里简单验证,实际项目中应该使用更安全的验证方式)
|
||||
if (deletePassword !== 'X123c321@') {
|
||||
message.error('密码错误');
|
||||
message.error(t('budget.passwordError'));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -224,12 +223,12 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
}
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success('删除成功');
|
||||
message.success(t('budget.deleteSuccess'));
|
||||
setDeleteModalVisible(false);
|
||||
navigate('/budget-projects');
|
||||
}
|
||||
} catch (error) {
|
||||
message.error('删除失败');
|
||||
message.error(t('common.deleteFailed'));
|
||||
} finally {
|
||||
setDeleteLoading(false);
|
||||
}
|
||||
@@ -247,9 +246,9 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card>
|
||||
<Empty description="项目不存在" />
|
||||
<Empty description={t('budget.notFound')} />
|
||||
<Button type="primary" onClick={() => navigate('/budget-projects')} style={{ marginTop: 16 }}>
|
||||
返回列表
|
||||
{t('budget.return')}
|
||||
</Button>
|
||||
</Card>
|
||||
</div>
|
||||
@@ -264,60 +263,58 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
icon={<ArrowLeftOutlined />}
|
||||
onClick={() => navigate('/budget-projects')}
|
||||
>
|
||||
返回列表
|
||||
{t('budget.return')}
|
||||
</Button>
|
||||
<Title level={2} style={{ marginBottom: 0 }}>预算项目详情</Title>
|
||||
<Title level={2} style={{ marginBottom: 0 }}>{t('budget.detailTitle')}</Title>
|
||||
</div>
|
||||
<Paragraph type="secondary">查看项目详细信息和报价版本</Paragraph>
|
||||
<Paragraph type="secondary">{t('budget.detailDesc')}</Paragraph>
|
||||
</div>
|
||||
|
||||
{/* 项目基本信息 */}
|
||||
<Card style={{ marginBottom: 24 }}>
|
||||
<Title level={4}>项目信息</Title>
|
||||
<Title level={4}>{t('budget.projectInfo')}</Title>
|
||||
<Divider />
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<Descriptions column={1} bordered>
|
||||
<Descriptions.Item label="项目名称">{project.name}</Descriptions.Item>
|
||||
<Descriptions.Item label="客户">{project.customer_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="业务经理">{project.manager_name}</Descriptions.Item>
|
||||
<Descriptions.Item label="项目地点">{project.location || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="勘察日期">{project.survey_date || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="状态">{getStatusTag(project.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label="创建时间">{dayjs(project.created_at).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.projectName')}>{project.name}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.customer')}>{project.customer_name}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.businessManager')}>{project.manager_name}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.projectLocation')}>{project.location || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.surveyDate')}>{project.survey_date || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('common.status')}>{getStatusTag(project.status)}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('common.create')}>{dayjs(project.created_at).format('YYYY-MM-DD HH:mm:ss')}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<Descriptions column={1} bordered>
|
||||
<Descriptions.Item label="居间人">{project.intermediary || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="居间费类型">
|
||||
{project.intermediary_fee_type === 'fixed' ? '固定金额' : project.intermediary_fee_type === 'percentage' ? '百分比' : '-'}
|
||||
<Descriptions.Item label={t('budget.intermediaryName')}>{project.intermediary || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.intermediaryType')}>
|
||||
{project.intermediary_fee_type === 'fixed' ? t('budget.fixedAmount') : project.intermediary_fee_type === 'percentage' ? t('budget.percentage') : '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="居间费">
|
||||
<Descriptions.Item label={t('budget.intermediaryAmount')}>
|
||||
{project.intermediary_fee_value ?
|
||||
project.intermediary_fee_type === 'percentage' ?
|
||||
`${project.intermediary_fee_value}%` :
|
||||
formatAmount(project.intermediary_fee_value, 'CNY')
|
||||
: '-'}
|
||||
</Descriptions.Item>
|
||||
<Descriptions.Item label="客户要求">{project.customer_requirements || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label="工程概况">{project.project_overview || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.customerRequirement')}>{project.customer_requirements || '-'}</Descriptions.Item>
|
||||
<Descriptions.Item label={t('budget.overview')}>{project.project_overview || '-'}</Descriptions.Item>
|
||||
</Descriptions>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 附件和照片 */}
|
||||
<Card style={{ marginBottom: 24 }}>
|
||||
<Title level={4}>附件和照片</Title>
|
||||
<Title level={4}>{t('budget.attachment')}</Title>
|
||||
<Divider />
|
||||
|
||||
<Row gutter={16}>
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>附件上传:</Text>
|
||||
<Text strong>{t('budget.attachmentUpload')}:</Text>
|
||||
{project.attachments && project.attachments.length > 0 ? (
|
||||
<List
|
||||
style={{ marginTop: 8 }}
|
||||
@@ -342,7 +339,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
icon={<EyeOutlined />}
|
||||
onClick={handleView}
|
||||
>
|
||||
查看
|
||||
{t('common.view')}
|
||||
</Button>
|
||||
</Space>
|
||||
</List.Item>
|
||||
@@ -350,14 +347,14 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Text type="secondary" style={{ display: 'block', marginTop: 8 }}>暂无附件</Text>
|
||||
<Text type="secondary" style={{ display: 'block', marginTop: 8 }}>{t('budget.noAttachment')}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
|
||||
<Col xs={24} md={12}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>勘察照片:</Text>
|
||||
<Text strong>{t('budget.surveyPhoto')}:</Text>
|
||||
{project.survey_photos && project.survey_photos.length > 0 ? (
|
||||
<div style={{ marginTop: 8, display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
||||
{project.survey_photos.map((url, index) => (
|
||||
@@ -369,30 +366,29 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
onClick={() => window.open(url, '_blank')}
|
||||
/>
|
||||
<div style={{ position: 'absolute', bottom: 0, left: 0, right: 0, background: 'rgba(0, 0, 0, 0.5)', color: '#fff', padding: 4, fontSize: 12, textAlign: 'center' }}>
|
||||
照片 {index + 1}
|
||||
{t('budget.photos')}{index + 1}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<Text type="secondary" style={{ display: 'block', marginTop: 8 }}>暂无勘察照片</Text>
|
||||
<Text type="secondary" style={{ display: 'block', marginTop: 8 }}>{t('budget.noPhotos')}</Text>
|
||||
)}
|
||||
</div>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* 报价版本列表 */}
|
||||
<Card style={{ marginBottom: 24 }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 16 }}>
|
||||
<Title level={4}>报价版本</Title>
|
||||
<Title level={4}>{t('budget.quotationVersions')}</Title>
|
||||
{isAdmin && project.status === 'negotiating' && (
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<FileAddOutlined />}
|
||||
onClick={openQuotationModal}
|
||||
>
|
||||
新增报价版本
|
||||
{t('budget.addVersion')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
@@ -424,7 +420,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
onClick={handleViewFile}
|
||||
disabled={!quotation.file_url}
|
||||
>
|
||||
查看
|
||||
{t('common.view')}
|
||||
</Button>,
|
||||
isAdmin && (
|
||||
<Button
|
||||
@@ -432,24 +428,24 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
danger
|
||||
onClick={() => handleDeleteQuotation(quotation.id)}
|
||||
>
|
||||
删除
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
)
|
||||
].filter(Boolean)}
|
||||
>
|
||||
<List.Item.Meta
|
||||
avatar={<Avatar style={{ backgroundColor: '#1890ff' }}>V{quotation.version}</Avatar>}
|
||||
avatar={<div style={{ width: 40, height: 40, borderRadius: '50%', backgroundColor: '#1890ff', display: 'flex', alignItems: 'center', justifyContent: 'center', color: '#fff', fontWeight: 'bold' }}>V{quotation.version}</div>}
|
||||
title={
|
||||
<Space>
|
||||
<Text strong>报价V{quotation.version}</Text>
|
||||
<Text strong>{t('budget.version')}V{quotation.version}</Text>
|
||||
{getQuotationStatusTag(quotation.status)}
|
||||
</Space>
|
||||
}
|
||||
description={
|
||||
<Space direction="vertical">
|
||||
<Text>报价日期: {dayjs(quotation.quotation_date).format('YYYY-MM-DD')}</Text>
|
||||
<Text>报价金额: {formatAmount(quotation.amount, quotation.currency)}</Text>
|
||||
{quotation.remark && <Text>备注: {quotation.remark}</Text>}
|
||||
<Text>{t('budget.versionDate')}{dayjs(quotation.quotation_date).format('YYYY-MM-DD')}</Text>
|
||||
<Text>{t('budget.versionAmount')}{formatAmount(quotation.amount, quotation.currency)}</Text>
|
||||
{quotation.remark && <Text>{t('budget.versionRemark')}{quotation.remark}</Text>}
|
||||
</Space>
|
||||
}
|
||||
/>
|
||||
@@ -458,13 +454,12 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<Empty description="暂无报价版本" image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
<Empty description={t('budget.noVersion')} image={Empty.PRESENTED_IMAGE_SIMPLE} />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{/* 操作按钮 */}
|
||||
<Card>
|
||||
<Title level={4}>操作</Title>
|
||||
<Title level={4}>{t('common.action')}</Title>
|
||||
<Divider />
|
||||
|
||||
<div style={{ display: 'flex', gap: 12, flexWrap: 'wrap' }}>
|
||||
@@ -475,14 +470,14 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={handleSign}
|
||||
>
|
||||
标记签约
|
||||
{t('budget.markSigned')}
|
||||
</Button>
|
||||
<Button
|
||||
danger
|
||||
icon={<CloseCircleOutlined />}
|
||||
onClick={handleUnsigned}
|
||||
>
|
||||
标记未签约
|
||||
{t('budget.markUnsigned')}
|
||||
</Button>
|
||||
</>
|
||||
)}
|
||||
@@ -492,7 +487,7 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
type="primary"
|
||||
onClick={goToProjectManagement}
|
||||
>
|
||||
进入项目管理
|
||||
{t('budget.enterProject')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
@@ -501,13 +496,12 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
danger
|
||||
onClick={handleDeleteProject}
|
||||
>
|
||||
删除项目
|
||||
{t('budget.deleteProject')}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
{/* 新增报价版本弹窗 */}
|
||||
<QuotationCreateModal
|
||||
visible={quotationModalVisible}
|
||||
project={project}
|
||||
@@ -515,7 +509,6 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
onSuccess={handleQuotationSuccess}
|
||||
/>
|
||||
|
||||
{/* 合同信息录入弹窗 */}
|
||||
<ContractCreateModal
|
||||
visible={contractModalVisible}
|
||||
projectId={project?.id || 0}
|
||||
@@ -524,44 +517,42 @@ const BudgetProjectDetail: React.FC = () => {
|
||||
onSuccess={handleContractSuccess}
|
||||
/>
|
||||
|
||||
{/* 删除项目确认模态框 */}
|
||||
<Modal
|
||||
title="删除确认"
|
||||
title={t('budget.deleteConfirm')}
|
||||
open={deleteModalVisible}
|
||||
onOk={handleProjectDeleteConfirm}
|
||||
onCancel={() => setDeleteModalVisible(false)}
|
||||
confirmLoading={deleteLoading}
|
||||
okText="确认删除"
|
||||
cancelText="取消"
|
||||
okText={t('common.deleteConfirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p>确定要删除这个预算项目吗?此操作不可恢复。</p>
|
||||
<p>请输入管理员密码确认删除操作:</p>
|
||||
<p>{t('budget.deleteConfirmMsg')}</p>
|
||||
<p>{t('budget.deletePassMsg')}</p>
|
||||
</div>
|
||||
<Input.Password
|
||||
placeholder="请输入管理员密码"
|
||||
placeholder={t('budget.deletePass')}
|
||||
value={deletePassword}
|
||||
onChange={(e) => setDeletePassword(e.target.value)}
|
||||
size="large"
|
||||
/>
|
||||
</Modal>
|
||||
|
||||
{/* 删除报价版本确认模态框 */}
|
||||
<Modal
|
||||
title="删除确认"
|
||||
title={t('budget.deleteConfirm')}
|
||||
open={quotationDeleteModalVisible}
|
||||
onOk={handleQuotationDeleteConfirm}
|
||||
onCancel={() => setQuotationDeleteModalVisible(false)}
|
||||
confirmLoading={deleteLoading}
|
||||
okText="确认删除"
|
||||
cancelText="取消"
|
||||
okText={t('common.deleteConfirm')}
|
||||
cancelText={t('common.cancel')}
|
||||
>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<p>确定要删除这个报价版本吗?此操作不可恢复。</p>
|
||||
<p>请输入管理员密码确认删除操作:</p>
|
||||
<p>{t('budget.versionDeleteConfirm')}</p>
|
||||
<p>{t('budget.deletePassMsg')}</p>
|
||||
</div>
|
||||
<Input.Password
|
||||
placeholder="请输入管理员密码"
|
||||
placeholder={t('budget.deletePass')}
|
||||
value={deletePassword}
|
||||
onChange={(e) => setDeletePassword(e.target.value)}
|
||||
size="large"
|
||||
|
||||
Reference in New Issue
Block a user