修复进度条假数据+施工确认添加附件上传+后端支持attachments

This commit is contained in:
a273825743
2026-05-17 10:01:43 +08:00
parent 9fab49b3d5
commit e953e4250a
3 changed files with 131 additions and 5 deletions
+3 -2
View File
@@ -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 }}>PDFWordExcel等文件</Text>
</div>
</Modal>
<Modal
open={previewVisible}
footer={null}
onCancel={() => setPreviewVisible(false)}
>
<img src={previewUrl} style={{ width: '100%' }} />
</Modal>
</div>
);
+1 -1
View File
@@ -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 {