大改造:施工进度系统+模板管理+物流收货+分包评分
- 新增5张数据库表:logistics_records, receiving_confirms, project_type_templates, project_phases, subcontractor_events - 修改3张表:projects(增加type_template_id/current_phase/phase_progress), purchase_orders(增加expected_delivery_date/receiving_status), subcontractors(增加score/initial_score) - 插入5套预设工程模板(配电安装/架空线路/电缆敷设/变电站/小型工程) - 后端新增:模板管理API、项目阶段管理API、收货确认API、分包商事件API - 后端修改:项目创建/预算签约支持模板选择,自动初始化阶段 - 前端新增:工程模板设计器(后台管理)、施工总览页、施工进度页(手机友好) - 前端修改:项目详情页重构为7个Tab,增加施工进度预览条和进入施工管理按钮 - 菜单更新:施工管理增加施工总览子菜单,后台管理增加工程模板管理
This commit is contained in:
@@ -0,0 +1,289 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Table, Space, Modal, Form, Input, Select, InputNumber, Tag, Steps, message, Popconfirm, Drawer, List, Checkbox, Radio, Empty, Spin, Typography, Row, Col } from 'antd';
|
||||
import { PlusOutlined, CopyOutlined, EditOutlined, DeleteOutlined, EyeOutlined, ArrowUpOutlined, ArrowDownOutlined } from '@ant-design/icons';
|
||||
import apiClient from '../../utils/request';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { Option } = Select;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface PhaseItem {
|
||||
order: number;
|
||||
name: string;
|
||||
key?: string;
|
||||
type: 'serial' | 'parallel';
|
||||
depends: number[];
|
||||
sub_items: string[];
|
||||
}
|
||||
|
||||
interface Template {
|
||||
id: number;
|
||||
name: string;
|
||||
description: string;
|
||||
phases: PhaseItem[];
|
||||
is_system: boolean;
|
||||
created_at: string;
|
||||
}
|
||||
|
||||
const ProcessTemplates: React.FC = () => {
|
||||
const [templates, setTemplates] = useState<Template[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [createModalVisible, setCreateModalVisible] = useState(false);
|
||||
const [viewDrawerVisible, setViewDrawerVisible] = useState(false);
|
||||
const [currentTemplate, setCurrentTemplate] = useState<Template | null>(null);
|
||||
const [currentStep, setCurrentStep] = useState(0);
|
||||
const [form] = Form.useForm();
|
||||
const [phases, setPhases] = useState<PhaseItem[]>([]);
|
||||
const [phaseDrawerVisible, setPhaseDrawerVisible] = useState(false);
|
||||
const [editingPhaseIndex, setEditingPhaseIndex] = useState<number>(-1);
|
||||
const [phaseForm] = Form.useForm();
|
||||
|
||||
useEffect(() => { fetchTemplates(); }, []);
|
||||
|
||||
const fetchTemplates = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get('/process-templates');
|
||||
if (res.data.success) setTemplates(res.data.data);
|
||||
} catch (e) { message.error('获取模板列表失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handleCreate = () => {
|
||||
form.resetFields();
|
||||
setPhases([]);
|
||||
setCurrentStep(0);
|
||||
setCreateModalVisible(true);
|
||||
};
|
||||
|
||||
const handleCopy = async (t: Template) => {
|
||||
try {
|
||||
await apiClient.post(`/process-templates/${t.id}/copy`, { name: `${t.name} (副本)` });
|
||||
message.success('复制成功');
|
||||
fetchTemplates();
|
||||
} catch (e) { message.error('复制失败'); }
|
||||
};
|
||||
|
||||
const handleDelete = async (id: number) => {
|
||||
try {
|
||||
await apiClient.delete(`/process-templates/${id}`);
|
||||
message.success('删除成功');
|
||||
fetchTemplates();
|
||||
} catch (e) { message.error('删除失败'); }
|
||||
};
|
||||
|
||||
const handleView = (t: Template) => {
|
||||
setCurrentTemplate(t);
|
||||
setViewDrawerVisible(true);
|
||||
};
|
||||
|
||||
const handleSaveTemplate = async () => {
|
||||
try {
|
||||
const name = form.getFieldValue('name');
|
||||
const description = form.getFieldValue('description');
|
||||
if (!name) { message.warning('请输入模板名称'); setCurrentStep(0); return; }
|
||||
if (phases.length === 0) { message.warning('请至少添加一个阶段'); setCurrentStep(1); return; }
|
||||
await apiClient.post('/process-templates', { name, description, phases });
|
||||
message.success('模板创建成功');
|
||||
setCreateModalVisible(false);
|
||||
fetchTemplates();
|
||||
} catch (e) { message.error('创建失败'); }
|
||||
};
|
||||
|
||||
const addPhase = () => {
|
||||
setEditingPhaseIndex(-1);
|
||||
phaseForm.resetFields();
|
||||
phaseForm.setFieldsValue({ name: '', type: 'serial', sub_items_text: '', depends: [] });
|
||||
setPhaseDrawerVisible(true);
|
||||
};
|
||||
|
||||
const editPhase = (index: number) => {
|
||||
setEditingPhaseIndex(index);
|
||||
const p = phases[index];
|
||||
phaseForm.setFieldsValue({
|
||||
name: p.name,
|
||||
type: p.type,
|
||||
sub_items_text: (p.sub_items || []).join('\n'),
|
||||
depends: p.depends.map(d => d),
|
||||
});
|
||||
setPhaseDrawerVisible(true);
|
||||
};
|
||||
|
||||
const savePhase = () => {
|
||||
const values = phaseForm.getFieldsValue();
|
||||
const subItems = (values.sub_items_text || '').split('\n').map((s: string) => s.trim()).filter(Boolean);
|
||||
const phaseData: PhaseItem = {
|
||||
order: 0,
|
||||
name: values.name,
|
||||
type: values.type || 'serial',
|
||||
depends: values.depends || [],
|
||||
sub_items: subItems,
|
||||
};
|
||||
if (!phaseData.name) { message.warning('阶段名称不能为空'); return; }
|
||||
const newPhases = [...phases];
|
||||
if (editingPhaseIndex >= 0) {
|
||||
phaseData.order = newPhases[editingPhaseIndex].order;
|
||||
newPhases[editingPhaseIndex] = phaseData;
|
||||
} else {
|
||||
phaseData.order = newPhases.length + 1;
|
||||
newPhases.push(phaseData);
|
||||
}
|
||||
setPhases(newPhases);
|
||||
setPhaseDrawerVisible(false);
|
||||
};
|
||||
|
||||
const removePhase = (index: number) => {
|
||||
const newPhases = phases.filter((_, i) => i !== index).map((p, i) => ({ ...p, order: i + 1 }));
|
||||
setPhases(newPhases);
|
||||
};
|
||||
|
||||
const movePhase = (index: number, direction: 'up' | 'down') => {
|
||||
const newPhases = [...phases];
|
||||
const targetIndex = direction === 'up' ? index - 1 : index + 1;
|
||||
if (targetIndex < 0 || targetIndex >= newPhases.length) return;
|
||||
[newPhases[index], newPhases[targetIndex]] = [newPhases[targetIndex], newPhases[index]];
|
||||
newPhases.forEach((p, i) => p.order = i + 1);
|
||||
setPhases(newPhases);
|
||||
};
|
||||
|
||||
const getTypeTag = (type: string) => type === 'parallel' ? <Tag color="blue">并行</Tag> : <Tag color="green">串行</Tag>;
|
||||
|
||||
const getDependsNames = (depends: number[]) => {
|
||||
return depends.map(d => {
|
||||
const p = phases.find(ph => ph.order === d);
|
||||
return p ? p.name : `阶段${d}`;
|
||||
}).join('、') || '无';
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{ title: '模板名称', dataIndex: 'name', key: 'name', render: (text: string, r: Template) => <Space>{text}{r.is_system && <Tag color="gold">系统预设</Tag>}</Space> },
|
||||
{ title: '阶段数', key: 'phases', render: (_: unknown, r: Template) => r.phases?.length || 0 },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
|
||||
{ title: '操作', key: 'action', render: (_: unknown, r: Template) => (
|
||||
<Space>
|
||||
<Button size="small" icon={<EyeOutlined />} onClick={() => handleView(r)}>查看</Button>
|
||||
<Button size="small" icon={<CopyOutlined />} onClick={() => handleCopy(r)}>复制</Button>
|
||||
{!r.is_system && <Popconfirm title="确定删除?" onConfirm={() => handleDelete(r.id)}><Button size="small" danger icon={<DeleteOutlined />}>删除</Button></Popconfirm>}
|
||||
</Space>
|
||||
)},
|
||||
];
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card title="工程模板管理" extra={<Button type="primary" icon={<PlusOutlined />} onClick={handleCreate}>新建模板</Button>}>
|
||||
<Table columns={columns} dataSource={templates} rowKey="id" loading={loading} pagination={false} />
|
||||
</Card>
|
||||
|
||||
<Modal title="新建工程模板" open={createModalVisible} onCancel={() => setCreateModalVisible(false)} width={800} footer={[
|
||||
<Button key="cancel" onClick={() => setCreateModalVisible(false)}>取消</Button>,
|
||||
currentStep > 0 && <Button key="prev" onClick={() => setCurrentStep(currentStep - 1)}>上一步</Button>,
|
||||
currentStep < 2 && <Button key="next" type="primary" onClick={() => {
|
||||
if (currentStep === 0 && !form.getFieldValue('name')) { message.warning('请输入模板名称'); return; }
|
||||
setCurrentStep(currentStep + 1);
|
||||
}}>下一步</Button>,
|
||||
currentStep === 2 && <Button key="save" type="primary" onClick={handleSaveTemplate}>确认创建</Button>,
|
||||
]}>
|
||||
<Steps current={currentStep} size="small" style={{ marginBottom: 24 }}>
|
||||
<Steps.Step title="基本信息" />
|
||||
<Steps.Step title="设计阶段" />
|
||||
<Steps.Step title="预览确认" />
|
||||
</Steps>
|
||||
|
||||
{currentStep === 0 && (
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item name="name" label="模板名称" rules={[{ required: true }]}>
|
||||
<Input placeholder="例如:配电安装工程" />
|
||||
</Form.Item>
|
||||
<Form.Item name="description" label="模板描述">
|
||||
<TextArea rows={3} placeholder="描述该模板适用的工程类型" />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)}
|
||||
|
||||
{currentStep === 1 && (
|
||||
<div>
|
||||
{phases.length === 0 ? <Empty description="暂无阶段,请点击下方添加" /> : (
|
||||
<List bordered dataSource={phases} renderItem={(phase, index) => (
|
||||
<List.Item actions={[
|
||||
<Button size="small" icon={<EditOutlined />} onClick={() => editPhase(index)}>编辑</Button>,
|
||||
<Button size="small" icon={<ArrowUpOutlined />} onClick={() => movePhase(index, 'up')} disabled={index === 0} />,
|
||||
<Button size="small" icon={<ArrowDownOutlined />} onClick={() => movePhase(index, 'down')} disabled={index === phases.length - 1} />,
|
||||
<Popconfirm title="确定删除?" onConfirm={() => removePhase(index)}><Button size="small" danger icon={<DeleteOutlined />} /></Popconfirm>,
|
||||
]}>
|
||||
<List.Item.Meta
|
||||
title={<Space>{phase.order}. {phase.name} {getTypeTag(phase.type)} <Text type="secondary">依赖:{getDependsNames(phase.depends)}</Text></Space>}
|
||||
description={phase.sub_items?.length > 0 ? `子项:${phase.sub_items.join('、')}` : '无子项'}
|
||||
/>
|
||||
</List.Item>
|
||||
)} />
|
||||
)}
|
||||
<Button type="dashed" block icon={<PlusOutlined />} style={{ marginTop: 16 }} onClick={addPhase}>添加阶段</Button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{currentStep === 2 && (
|
||||
<div>
|
||||
<Title level={5}>{form.getFieldValue('name')}</Title>
|
||||
<Paragraph type="secondary">{form.getFieldValue('description')}</Paragraph>
|
||||
<List bordered dataSource={phases} renderItem={(phase) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={<Space>{phase.order}. {phase.name} {getTypeTag(phase.type)}</Space>}
|
||||
description={<>
|
||||
<Text type="secondary">依赖:{getDependsNames(phase.depends)}</Text><br />
|
||||
{phase.sub_items?.length > 0 && <Text>子项:{phase.sub_items.join('、')}</Text>}
|
||||
</>}
|
||||
/>
|
||||
</List.Item>
|
||||
)} />
|
||||
</div>
|
||||
)}
|
||||
</Modal>
|
||||
|
||||
<Drawer title="阶段编辑" open={phaseDrawerVisible} onClose={() => setPhaseDrawerVisible(false)} width={400} extra={<Button type="primary" onClick={savePhase}>保存</Button>}>
|
||||
<Form form={phaseForm} layout="vertical">
|
||||
<Form.Item name="name" label="阶段名称" rules={[{ required: true }]}>
|
||||
<Input placeholder="例如:物资采购" />
|
||||
</Form.Item>
|
||||
<Form.Item name="type" label="阶段类型">
|
||||
<Radio.Group>
|
||||
<Radio value="serial">串行(必须等依赖项完成)</Radio>
|
||||
<Radio value="parallel">并行(可与相邻阶段同时进行)</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
<Form.Item name="depends" label="依赖关系(哪些阶段完成后才能开始)">
|
||||
<Checkbox.Group>
|
||||
{phases.filter((_, i) => i !== editingPhaseIndex).map(p => (
|
||||
<Checkbox key={p.order} value={p.order} style={{ display: 'block' }}>{p.order}. {p.name}</Checkbox>
|
||||
))}
|
||||
</Checkbox.Group>
|
||||
</Form.Item>
|
||||
<Form.Item name="sub_items_text" label="子项列表(每行一个)">
|
||||
<TextArea rows={6} placeholder={"电杆采购\n变压器采购\n电缆采购"} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Drawer>
|
||||
|
||||
<Drawer title={currentTemplate?.name} open={viewDrawerVisible} onClose={() => setViewDrawerVisible(false)} width={500}>
|
||||
{currentTemplate && (
|
||||
<div>
|
||||
<Paragraph type="secondary">{currentTemplate.description}</Paragraph>
|
||||
<List bordered dataSource={currentTemplate.phases || []} renderItem={(phase: PhaseItem) => (
|
||||
<List.Item>
|
||||
<List.Item.Meta
|
||||
title={<Space>{phase.order}. {phase.name} {phase.type === 'parallel' ? <Tag color="blue">并行</Tag> : <Tag color="green">串行</Tag>}</Space>}
|
||||
description={<>
|
||||
<Text type="secondary">依赖:阶段{phase.depends?.join('、') || '无'}</Text><br />
|
||||
{phase.sub_items?.length > 0 && <Text>子项:{phase.sub_items.join('、')}</Text>}
|
||||
</>}
|
||||
/>
|
||||
</List.Item>
|
||||
)} />
|
||||
</div>
|
||||
)}
|
||||
</Drawer>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ProcessTemplates;
|
||||
@@ -0,0 +1,127 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Progress, Tag, List, Spin, message, Empty, Space, Typography } from 'antd';
|
||||
import { ArrowLeftOutlined, RightOutlined } from '@ant-design/icons';
|
||||
import { useNavigate } from 'react-router-dom';
|
||||
import apiClient from '../../utils/request';
|
||||
|
||||
const { Title, Text } = Typography;
|
||||
|
||||
interface ProjectPhase {
|
||||
id: number;
|
||||
phase_name: string;
|
||||
phase_order: number;
|
||||
phase_type: string;
|
||||
status: string;
|
||||
}
|
||||
|
||||
interface Project {
|
||||
id: number;
|
||||
name: string;
|
||||
status: string;
|
||||
current_phase: string;
|
||||
phase_progress: number;
|
||||
contract_amount: number;
|
||||
phases: ProjectPhase[];
|
||||
}
|
||||
|
||||
const ConstructionOverview: React.FC = () => {
|
||||
const [projects, setProjects] = useState<Project[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => { fetchProjects(); }, []);
|
||||
|
||||
const fetchProjects = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get('/construction/my-projects');
|
||||
if (res.data.success) {
|
||||
const projectsData = res.data.data || [];
|
||||
const enriched = await Promise.all(projectsData.map(async (p: Project) => {
|
||||
try {
|
||||
const phaseRes = await apiClient.get(`/projects/${p.id}/phases`);
|
||||
return { ...p, phases: phaseRes.data.data || [] };
|
||||
} catch { return { ...p, phases: [] }; }
|
||||
}));
|
||||
setProjects(enriched);
|
||||
}
|
||||
} catch (e) { message.error('获取项目列表失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const getStatusColor = (status: string) => {
|
||||
const map: Record<string, string> = { active: 'green', planning: 'blue', completed: 'default', suspended: 'orange' };
|
||||
return map[status] || 'default';
|
||||
};
|
||||
|
||||
const getStatusText = (status: string) => {
|
||||
const map: Record<string, string> = { active: '施工中', planning: '待开工', completed: '已完工', suspended: '暂停' };
|
||||
return map[status] || status;
|
||||
};
|
||||
|
||||
const activeProjects = projects.filter(p => p.status !== 'completed');
|
||||
const completedProjects = projects.filter(p => p.status === 'completed');
|
||||
|
||||
return (
|
||||
<div style={{ padding: 24 }}>
|
||||
<Card title={<Title level={4} style={{ margin: 0 }}>施工总览</Title>}>
|
||||
<Spin spinning={loading}>
|
||||
{activeProjects.length === 0 && completedProjects.length === 0 ? (
|
||||
<Empty description="暂无施工项目" />
|
||||
) : (
|
||||
<>
|
||||
{activeProjects.length > 0 && (
|
||||
<>
|
||||
<Text strong style={{ fontSize: 16 }}>施工中项目:{activeProjects.length}个</Text>
|
||||
<List
|
||||
style={{ marginTop: 16 }}
|
||||
dataSource={activeProjects}
|
||||
renderItem={(project) => (
|
||||
<List.Item
|
||||
actions={[<Button type="primary" icon={<RightOutlined />} onClick={() => navigate(`/construction/progress/${project.id}`)}>进入</Button>]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={<Space><Text strong>{project.name}</Text><Tag color={getStatusColor(project.status)}>{getStatusText(project.status)}</Tag></Space>}
|
||||
description={
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Text type="secondary">当前阶段:{project.current_phase || '未设置'}</Text>
|
||||
{project.phases.length > 0 && <Text type="secondary" style={{ marginLeft: 16 }}>({project.phases.filter(p => p.status === 'completed').length}/{project.phases.length})</Text>}
|
||||
</div>
|
||||
<Progress percent={project.phase_progress || 0} size="small" strokeColor="#1890ff" />
|
||||
</div>
|
||||
}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
{completedProjects.length > 0 && (
|
||||
<>
|
||||
<Text strong style={{ fontSize: 16, marginTop: 24, display: 'block' }}>已完工项目:{completedProjects.length}个</Text>
|
||||
<List
|
||||
style={{ marginTop: 16 }}
|
||||
dataSource={completedProjects}
|
||||
renderItem={(project) => (
|
||||
<List.Item
|
||||
actions={[<Button icon={<RightOutlined />} onClick={() => navigate(`/construction/progress/${project.id}`)}>查看</Button>]}
|
||||
>
|
||||
<List.Item.Meta
|
||||
title={<Space><Text>{project.name}</Text><Tag>已完工</Tag></Space>}
|
||||
description={<Progress percent={100} size="small" />}
|
||||
/>
|
||||
</List.Item>
|
||||
)}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
)}
|
||||
</Spin>
|
||||
</Card>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConstructionOverview;
|
||||
@@ -0,0 +1,248 @@
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import { Card, Button, Progress, Tag, Checkbox, Input, Upload, Space, Spin, message, List, Typography, Modal, Image, Divider } from 'antd';
|
||||
import { ArrowLeftOutlined, CameraOutlined, CheckCircleOutlined, ClockCircleOutlined, MinusCircleOutlined, UndoOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import apiClient from '../../utils/request';
|
||||
|
||||
const { Title, Text, Paragraph } = Typography;
|
||||
const { TextArea } = Input;
|
||||
|
||||
interface Phase {
|
||||
id: number;
|
||||
phase_name: string;
|
||||
phase_order: number;
|
||||
phase_type: string;
|
||||
depends_on: number[];
|
||||
status: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
remark: string | null;
|
||||
photos: string[];
|
||||
sub_items: { name: string; completed: boolean }[];
|
||||
}
|
||||
|
||||
const ConstructionProgress: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [project, setProject] = useState<any>(null);
|
||||
const [phases, setPhases] = useState<Phase[]>([]);
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [completingPhaseId, setCompletingPhaseId] = useState<number | null>(null);
|
||||
const [remark, setRemark] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => { if (id) { fetchProject(); fetchPhases(); } }, [id]);
|
||||
|
||||
const fetchProject = async () => {
|
||||
try {
|
||||
const res = await apiClient.get(`/projects/${id}`);
|
||||
if (res.data.success) setProject(res.data.data);
|
||||
} catch (e) { message.error('获取项目信息失败'); }
|
||||
};
|
||||
|
||||
const fetchPhases = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await apiClient.get(`/projects/${id}/phases`);
|
||||
if (res.data.success) setPhases(res.data.data || []);
|
||||
} catch (e) { message.error('获取阶段信息失败'); }
|
||||
finally { setLoading(false); }
|
||||
};
|
||||
|
||||
const handleCompletePhase = (phaseId: number) => {
|
||||
setCompletingPhaseId(phaseId);
|
||||
setRemark('');
|
||||
};
|
||||
|
||||
const confirmComplete = async () => {
|
||||
if (!completingPhaseId || !id) return;
|
||||
try {
|
||||
const res = await apiClient.put(`/projects/${id}/phases/${completingPhaseId}/complete`, {
|
||||
remark,
|
||||
completed_by: 1,
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success(`阶段完成!进度: ${res.data.data.progress}%`);
|
||||
if (res.data.data.nextPhase) {
|
||||
message.info(`已推进到: ${res.data.data.nextPhase}`);
|
||||
}
|
||||
fetchPhases();
|
||||
fetchProject();
|
||||
}
|
||||
} catch (e) { message.error('操作失败'); }
|
||||
finally { setCompletingPhaseId(null); }
|
||||
};
|
||||
|
||||
const handleReopen = async (phaseId: number) => {
|
||||
if (!id) return;
|
||||
Modal.confirm({
|
||||
title: '重新打开阶段',
|
||||
content: '确定要重新打开此阶段吗?项目进度将回退。',
|
||||
onOk: async () => {
|
||||
try {
|
||||
const res = await apiClient.put(`/projects/${id}/phases/${phaseId}/reopen`, {});
|
||||
if (res.data.success) {
|
||||
message.success('阶段已重新打开');
|
||||
fetchPhases();
|
||||
fetchProject();
|
||||
}
|
||||
} catch (e) { message.error('操作失败'); }
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
const handleSubItemToggle = async (phaseId: number, subItemIndex: number, completed: boolean) => {
|
||||
if (!id) return;
|
||||
try {
|
||||
await apiClient.put(`/projects/${id}/phases/${phaseId}/sub-item`, {
|
||||
sub_item_index: subItemIndex,
|
||||
completed,
|
||||
});
|
||||
fetchPhases();
|
||||
} catch (e) { message.error('更新子项失败'); }
|
||||
};
|
||||
|
||||
const getStatusIcon = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return <CheckCircleOutlined style={{ color: '#52c41a', fontSize: 20 }} />;
|
||||
case 'in_progress': return <ClockCircleOutlined style={{ color: '#1890ff', fontSize: 20 }} />;
|
||||
default: return <MinusCircleOutlined style={{ color: '#d9d9d9', fontSize: 20 }} />;
|
||||
}
|
||||
};
|
||||
|
||||
const getStatusTag = (status: string) => {
|
||||
switch (status) {
|
||||
case 'completed': return <Tag color="green">已完成</Tag>;
|
||||
case 'in_progress': return <Tag color="blue">进行中</Tag>;
|
||||
default: return <Tag>待开始</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
const currentPhase = phases.find(p => p.status === 'in_progress');
|
||||
const completedPhases = phases.filter(p => p.status === 'completed').reverse();
|
||||
const pendingPhases = phases.filter(p => p.status === 'pending');
|
||||
const progress = project?.phase_progress || 0;
|
||||
|
||||
return (
|
||||
<div style={{ padding: '16px', maxWidth: 600, margin: '0 auto' }}>
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Button icon={<ArrowLeftOutlined />} onClick={() => navigate('/construction')}>返回总览</Button>
|
||||
</div>
|
||||
|
||||
<Card style={{ marginBottom: 16 }}>
|
||||
<Title level={4} style={{ margin: 0 }}>{project?.name || '加载中...'}</Title>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
<Progress percent={progress} strokeColor="#1890ff" />
|
||||
<Space style={{ marginTop: 4 }}>
|
||||
{project?.current_phase && <Tag color="blue">当前: {project.current_phase}</Tag>}
|
||||
{getStatusTag(project?.status)}
|
||||
</Space>
|
||||
</div>
|
||||
</Card>
|
||||
|
||||
<Spin spinning={loading}>
|
||||
{currentPhase && (
|
||||
<Card
|
||||
title={<Space>{getStatusIcon('in_progress')} 📍 当前阶段:{currentPhase.phase_name}</Space>}
|
||||
style={{ marginBottom: 16, borderColor: '#1890ff', borderWidth: 2 }}
|
||||
>
|
||||
{currentPhase.phase_type === 'parallel' && (
|
||||
<Tag color="blue" style={{ marginBottom: 8 }}>并行阶段(可与其他阶段同时进行)</Tag>
|
||||
)}
|
||||
|
||||
{currentPhase.sub_items && currentPhase.sub_items.length > 0 && (
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>完工标准:</Text>
|
||||
<div style={{ marginTop: 8 }}>
|
||||
{currentPhase.sub_items.map((item, idx) => (
|
||||
<div key={idx} style={{ marginBottom: 4 }}>
|
||||
<Checkbox
|
||||
checked={item.completed}
|
||||
onChange={(e) => handleSubItemToggle(currentPhase.id, idx, e.target.checked)}
|
||||
>
|
||||
<Text delete={item.completed} type={item.completed ? 'secondary' : undefined}>{item.name}</Text>
|
||||
</Checkbox>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div style={{ marginBottom: 16 }}>
|
||||
<Text strong>备注(可选):</Text>
|
||||
<TextArea rows={2} value={remark} onChange={e => setRemark(e.target.value)} placeholder="填写完成备注..." style={{ marginTop: 4 }} />
|
||||
</div>
|
||||
|
||||
<Button
|
||||
type="primary"
|
||||
size="large"
|
||||
block
|
||||
icon={<CheckCircleOutlined />}
|
||||
onClick={() => handleCompletePhase(currentPhase.id)}
|
||||
>
|
||||
确认完成此阶段
|
||||
</Button>
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{pendingPhases.length > 0 && (
|
||||
<Card title="后续阶段" style={{ marginBottom: 16 }} size="small">
|
||||
{pendingPhases.map(phase => (
|
||||
<div key={phase.id} style={{ padding: '8px 0', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<Space>
|
||||
{getStatusIcon('pending')}
|
||||
<Text type="secondary">{phase.phase_order}. {phase.phase_name}</Text>
|
||||
{phase.phase_type === 'parallel' && <Tag color="blue">并行</Tag>}
|
||||
</Space>
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{completedPhases.length > 0 && (
|
||||
<Card title="阶段历史" size="small">
|
||||
{completedPhases.map(phase => (
|
||||
<div key={phase.id} style={{ padding: '8px 0', borderBottom: '1px solid #f0f0f0' }}>
|
||||
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
|
||||
<Space>
|
||||
{getStatusIcon('completed')}
|
||||
<Text>{phase.phase_order}. {phase.phase_name}</Text>
|
||||
</Space>
|
||||
<Space>
|
||||
{phase.completed_at && <Text type="secondary" style={{ fontSize: 12 }}>{new Date(phase.completed_at).toLocaleDateString()}</Text>}
|
||||
<Button size="small" type="link" icon={<UndoOutlined />} onClick={() => handleReopen(phase.id)}>回退</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{phase.remark && <Paragraph type="secondary" style={{ margin: '4px 0 0 28px', fontSize: 12 }}>{phase.remark}</Paragraph>}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
|
||||
{!currentPhase && pendingPhases.length === 0 && completedPhases.length === 0 && (
|
||||
<Card>
|
||||
<div style={{ textAlign: 'center', padding: 24 }}>
|
||||
<Text type="secondary">此项目尚未初始化施工阶段</Text>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
<Button type="primary" onClick={() => navigate(`/projects/${id}`)}>返回项目详情</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
)}
|
||||
</Spin>
|
||||
|
||||
<Modal
|
||||
title="确认完成阶段"
|
||||
open={completingPhaseId !== null}
|
||||
onOk={confirmComplete}
|
||||
onCancel={() => setCompletingPhaseId(null)}
|
||||
okText="确认完成"
|
||||
cancelText="取消"
|
||||
>
|
||||
<Paragraph>确认此阶段已完成?系统将自动推进到下一阶段。</Paragraph>
|
||||
{remark && <Paragraph type="secondary">备注:{remark}</Paragraph>}
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ConstructionProgress;
|
||||
@@ -2,7 +2,7 @@ import React, { useState, useEffect } from 'react'
|
||||
import { useParams, useNavigate } from 'react-router-dom'
|
||||
import { Tabs, Card, Descriptions, Button, Table, Tag, Progress, Space, message, Spin, Select, Menu, Dropdown, Modal, Form, Input, InputNumber, Switch, Upload, DatePicker } from 'antd'
|
||||
import dayjs from 'dayjs'
|
||||
import { DownOutlined, UploadOutlined } from '@ant-design/icons'
|
||||
import { DownOutlined, UploadOutlined, ToolOutlined } from '@ant-design/icons'
|
||||
import {
|
||||
ArrowLeftOutlined,
|
||||
EditOutlined,
|
||||
@@ -945,13 +945,19 @@ const ProjectDetail: React.FC = () => {
|
||||
<h2 style={{ margin: 0 }}>{project.name}</h2>
|
||||
<Space style={{ marginTop: 8 }}>
|
||||
{getStatusTag(project.status)}
|
||||
<span>进度: 60%</span>
|
||||
{/* 从付款节点中查找质保金节点,显示质保金金额 */}
|
||||
{project.current_phase && <Tag color="blue">当前: {project.current_phase}</Tag>}
|
||||
{project.phase_progress > 0 && <span>进度: {project.phase_progress}%</span>}
|
||||
<span>质保金: ¥{(milestones.find(m => m.milestone_name === '质保金')?.amount || 0).toLocaleString()}</span>
|
||||
</Space>
|
||||
</div>
|
||||
<Button icon={<EditOutlined />}>编辑项目</Button>
|
||||
<Space>
|
||||
<Button icon={<ToolOutlined />} type="primary" onClick={() => navigate(`/construction/progress/${id}`)}>进入施工管理</Button>
|
||||
<Button icon={<EditOutlined />} onClick={() => setBasicInfoEditModalVisible(true)}>编辑项目</Button>
|
||||
</Space>
|
||||
</div>
|
||||
{project.phase_progress > 0 && (
|
||||
<Progress percent={project.phase_progress} style={{ marginTop: 12 }} strokeColor="#1890ff" />
|
||||
)}
|
||||
</Card>
|
||||
|
||||
{isMobile ? (
|
||||
@@ -962,13 +968,12 @@ const ProjectDetail: React.FC = () => {
|
||||
onChange={setActiveTab}
|
||||
options={[
|
||||
{ value: 'basic', label: '基本信息' },
|
||||
{ value: 'contract', label: '合同详情' },
|
||||
{ value: 'contract', label: '合同与收款' },
|
||||
{ value: 'subcontract', label: '分包管理' },
|
||||
{ value: 'material', label: '材料管理' },
|
||||
{ value: 'milestone', label: '施工节点' },
|
||||
{ value: 'log', label: '施工日志' },
|
||||
{ value: 'finance', label: '财务信息' },
|
||||
{ value: 'warranty', label: '质保金' }
|
||||
{ value: 'finance', label: '财务收支' },
|
||||
{ value: 'warranty', label: '质保金' },
|
||||
{ value: 'log', label: '施工日志' }
|
||||
]}
|
||||
/>
|
||||
<div style={{ marginTop: 16 }}>
|
||||
@@ -976,10 +981,9 @@ const ProjectDetail: React.FC = () => {
|
||||
{activeTab === 'contract' && <ContractTab />}
|
||||
{activeTab === 'subcontract' && <SubcontractTab />}
|
||||
{activeTab === 'material' && <MaterialTab />}
|
||||
{activeTab === 'milestone' && <MilestoneTab />}
|
||||
{activeTab === 'log' && <LogTab />}
|
||||
{activeTab === 'finance' && <FinanceTab />}
|
||||
{activeTab === 'warranty' && <WarrantyTab />}
|
||||
{activeTab === 'log' && <LogTab />}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
@@ -995,7 +999,7 @@ const ProjectDetail: React.FC = () => {
|
||||
},
|
||||
{
|
||||
key: 'contract',
|
||||
label: <span><FileTextOutlined /> 合同详情</span>,
|
||||
label: <span><FileTextOutlined /> 合同与收款</span>,
|
||||
children: <ContractTab />
|
||||
},
|
||||
{
|
||||
@@ -1008,25 +1012,20 @@ const ProjectDetail: React.FC = () => {
|
||||
label: <span><DatabaseOutlined /> 材料管理</span>,
|
||||
children: <MaterialTab />
|
||||
},
|
||||
{
|
||||
key: 'milestone',
|
||||
label: <span><CheckCircleOutlined /> 施工节点</span>,
|
||||
children: <MilestoneTab />
|
||||
},
|
||||
{
|
||||
key: 'log',
|
||||
label: <span><FileSearchOutlined /> 施工日志</span>,
|
||||
children: <LogTab />
|
||||
},
|
||||
{
|
||||
key: 'finance',
|
||||
label: <span><DollarOutlined /> 财务信息</span>,
|
||||
label: <span><DollarOutlined /> 财务收支</span>,
|
||||
children: <FinanceTab />
|
||||
},
|
||||
{
|
||||
key: 'warranty',
|
||||
label: <span><SafetyOutlined /> 质保金</span>,
|
||||
children: <WarrantyTab />
|
||||
},
|
||||
{
|
||||
key: 'log',
|
||||
label: <span><FileSearchOutlined /> 施工日志</span>,
|
||||
children: <LogTab />
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
Reference in New Issue
Block a user