修复进度条假数据+施工确认添加附件上传+后端支持attachments
This commit is contained in:
@@ -697,15 +697,16 @@ router.post('/:id/phases/init', async (req, res) => {
|
||||
router.put('/:id/phases/:phaseId/complete', async (req, res) => {
|
||||
try {
|
||||
const { id, phaseId } = req.params;
|
||||
const { remark, photos, completed_by } = req.body;
|
||||
const { remark, photos, completed_by, attachments } = req.body;
|
||||
const phaseResult = await db.query('SELECT * FROM project_phases WHERE id = $1 AND project_id = $2', [phaseId, id]);
|
||||
if (phaseResult.rows.length === 0) {
|
||||
return res.status(404).json({ success: false, message: '阶段不存在' });
|
||||
}
|
||||
const phase = phaseResult.rows[0];
|
||||
const allAttachments = attachments || photos || phase.photos || [];
|
||||
await db.query(
|
||||
`UPDATE project_phases SET status = 'completed', completed_at = NOW(), completed_by = $1, remark = $2, photos = $3 WHERE id = $4`,
|
||||
[completed_by || null, remark || phase.remark, photos || phase.photos, phaseId]
|
||||
[completed_by || null, remark || phase.remark, JSON.stringify(allAttachments), phaseId]
|
||||
);
|
||||
const allPhases = await db.query('SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order', [id]);
|
||||
const completedCount = allPhases.rows.filter(p => p.status === 'completed').length;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { ArrowLeftOutlined, CameraOutlined, CheckCircleOutlined, ClockCircleOutlined, MinusCircleOutlined, UndoOutlined, UploadOutlined, PaperClipOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { useNavigate, useParams } from 'react-router-dom';
|
||||
import apiClient from '../../utils/request';
|
||||
|
||||
@@ -15,12 +15,28 @@ interface Phase {
|
||||
depends_on: number[];
|
||||
status: string;
|
||||
started_at: string | null;
|
||||
completed_at: string | null;
|
||||
completed_at: string | string | null;
|
||||
remark: string | null;
|
||||
photos: string[];
|
||||
attachments: any[];
|
||||
sub_items: { name: string; completed: boolean }[];
|
||||
}
|
||||
|
||||
const isImageUrl = (url: string) => {
|
||||
if (!url) return false;
|
||||
const lower = url.toLowerCase();
|
||||
return lower.match(/\.(jpg|jpeg|png|gif|bmp|webp|svg)(\?|$)/) !== null;
|
||||
};
|
||||
|
||||
const getFileName = (url: string) => {
|
||||
if (!url) return '附件';
|
||||
const parts = url.split('/');
|
||||
const last = parts[parts.length - 1];
|
||||
const decoded = decodeURIComponent(last);
|
||||
const noTimestamp = decoded.replace(/^\d+_/, '');
|
||||
return noTimestamp;
|
||||
};
|
||||
|
||||
const ConstructionProgress: React.FC = () => {
|
||||
const { id } = useParams<{ id: string }>();
|
||||
const [project, setProject] = useState<any>(null);
|
||||
@@ -28,6 +44,9 @@ const ConstructionProgress: React.FC = () => {
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [completingPhaseId, setCompletingPhaseId] = useState<number | null>(null);
|
||||
const [remark, setRemark] = useState('');
|
||||
const [confirmAttachments, setConfirmAttachments] = useState<any[]>([]);
|
||||
const [previewVisible, setPreviewVisible] = useState(false);
|
||||
const [previewUrl, setPreviewUrl] = useState('');
|
||||
const navigate = useNavigate();
|
||||
|
||||
useEffect(() => { if (id) { fetchProject(); fetchPhases(); } }, [id]);
|
||||
@@ -51,14 +70,42 @@ const ConstructionProgress: React.FC = () => {
|
||||
const handleCompletePhase = (phaseId: number) => {
|
||||
setCompletingPhaseId(phaseId);
|
||||
setRemark('');
|
||||
setConfirmAttachments([]);
|
||||
};
|
||||
|
||||
const handleUpload = async (file: File) => {
|
||||
try {
|
||||
const formData = new FormData();
|
||||
formData.append('file', file);
|
||||
const res = await apiClient.post('/upload/single', formData);
|
||||
if (res.data.success) {
|
||||
const uploaded = res.data.data;
|
||||
setConfirmAttachments(prev => [...prev, {
|
||||
uid: String(Date.now()),
|
||||
name: uploaded.name || file.name,
|
||||
url: uploaded.url,
|
||||
isImage: uploaded.isImage,
|
||||
status: 'done',
|
||||
}]);
|
||||
}
|
||||
} catch (e) {
|
||||
message.error('上传失败');
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
const handleRemoveAttachment = (uid: string) => {
|
||||
setConfirmAttachments(prev => prev.filter(a => a.uid !== uid));
|
||||
};
|
||||
|
||||
const confirmComplete = async () => {
|
||||
if (!completingPhaseId || !id) return;
|
||||
try {
|
||||
const attachmentUrls = confirmAttachments.map(a => a.url);
|
||||
const res = await apiClient.put(`/projects/${id}/phases/${completingPhaseId}/complete`, {
|
||||
remark,
|
||||
completed_by: 1,
|
||||
attachments: attachmentUrls,
|
||||
});
|
||||
if (res.data.success) {
|
||||
message.success(`阶段完成!进度: ${res.data.data.progress}%`);
|
||||
@@ -117,6 +164,50 @@ const ConstructionProgress: React.FC = () => {
|
||||
}
|
||||
};
|
||||
|
||||
const renderAttachments = (attachments: any[]) => {
|
||||
if (!attachments || attachments.length === 0) return null;
|
||||
const images = attachments.filter(a => isImageUrl(typeof a === 'string' ? a : a.url));
|
||||
const files = attachments.filter(a => !isImageUrl(typeof a === 'string' ? a : a.url));
|
||||
|
||||
return (
|
||||
<div style={{ marginTop: 8, marginLeft: 28 }}>
|
||||
{images.length > 0 && (
|
||||
<div style={{ marginBottom: 4 }}>
|
||||
<Image.PreviewGroup>
|
||||
{images.map((a, i) => {
|
||||
const url = typeof a === 'string' ? a : a.url;
|
||||
return (
|
||||
<Image
|
||||
key={i}
|
||||
src={url}
|
||||
width={48}
|
||||
height={48}
|
||||
style={{ borderRadius: 4, objectFit: 'cover', marginRight: 4, cursor: 'pointer' }}
|
||||
preview={{ mask: <EyeOutlined /> }}
|
||||
fallback="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAYAAAAfFcSJAAAADUlEQVR42mN88P/BfwAJhAPk2iMa1AAAAABJRU5ErkJggg=="
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Image.PreviewGroup>
|
||||
</div>
|
||||
)}
|
||||
{files.length > 0 && (
|
||||
<div>
|
||||
{files.map((a, i) => {
|
||||
const url = typeof a === 'string' ? a : a.url;
|
||||
const name = typeof a === 'string' ? getFileName(a) : a.name || getFileName(a.url);
|
||||
return (
|
||||
<div key={i} style={{ fontSize: 12, color: '#1890ff', marginBottom: 2 }}>
|
||||
<PaperClipOutlined /> <a href={url} target="_blank" rel="noopener">{name}</a>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
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');
|
||||
@@ -213,6 +304,7 @@ const ConstructionProgress: React.FC = () => {
|
||||
</Space>
|
||||
</div>
|
||||
{phase.remark && <Paragraph type="secondary" style={{ margin: '4px 0 0 28px', fontSize: 12 }}>{phase.remark}</Paragraph>}
|
||||
{renderAttachments(phase.attachments || phase.photos)}
|
||||
</div>
|
||||
))}
|
||||
</Card>
|
||||
@@ -254,6 +346,39 @@ const ConstructionProgress: React.FC = () => {
|
||||
>
|
||||
<Paragraph>确认此阶段已完成?系统将自动推进到下一阶段。</Paragraph>
|
||||
{remark && <Paragraph type="secondary">备注:{remark}</Paragraph>}
|
||||
|
||||
<Divider style={{ margin: '12px 0' }} />
|
||||
|
||||
<div style={{ marginBottom: 8 }}>
|
||||
<Text strong>上传证明材料(照片/文件):</Text>
|
||||
</div>
|
||||
<Upload
|
||||
beforeUpload={(file) => { handleUpload(file); return false; }}
|
||||
onRemove={(file) => handleRemoveAttachment(file.uid)}
|
||||
fileList={confirmAttachments.map(a => ({
|
||||
uid: a.uid,
|
||||
name: a.name,
|
||||
status: 'done' as const,
|
||||
url: a.url,
|
||||
thumbUrl: a.isImage ? a.url : undefined,
|
||||
}))}
|
||||
listType="picture"
|
||||
accept="image/*,.pdf,.doc,.docx,.xls,.xlsx"
|
||||
multiple
|
||||
>
|
||||
<Button icon={<UploadOutlined />}>选择文件</Button>
|
||||
</Upload>
|
||||
<div style={{ marginTop: 4 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>支持照片、PDF、Word、Excel等文件</Text>
|
||||
</div>
|
||||
</Modal>
|
||||
|
||||
<Modal
|
||||
open={previewVisible}
|
||||
footer={null}
|
||||
onCancel={() => setPreviewVisible(false)}
|
||||
>
|
||||
<img src={previewUrl} style={{ width: '100%' }} />
|
||||
</Modal>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -61,7 +61,7 @@ const ProjectsPage: React.FC = () => {
|
||||
setProjects(response.data.data.map((p: Project) => ({
|
||||
...p,
|
||||
key: p.id.toString(),
|
||||
progress: Math.floor(Math.random() * 100),
|
||||
progress: p.status === 'completed' ? 100 : (p.phase_progress || 0),
|
||||
manager_name: p.manager_name || '未分配'
|
||||
})));
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user