大改造:施工进度系统+模板管理+物流收货+分包评分

- 新增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:
root
2026-05-15 12:25:08 +08:00
parent fad28741bd
commit cc5d96e226
13 changed files with 1761 additions and 648 deletions
+2
View File
@@ -65,6 +65,8 @@ app.use('/api/payment-execution', require('./routes/payment-execution'));
app.use('/api/verifications-new', require('./routes/verifications-new'));
app.use('/api/returns', require('./routes/returns'));
app.use('/api/project-materials', require('./routes/project-materials'));
app.use('/api/process-templates', require('./routes/process-templates'));
app.use('/api/receiving', require('./routes/receiving'));
app.get('*', (req, res) => {
res.sendFile(path.join(__dirname, '../frontend/dist/index.html'));
+18
View File
@@ -255,6 +255,7 @@ router.put('/:id/sign', authenticate, async (req, res) => {
);
let project_id = projectResult.rows[0].project_id;
const { type_template_id } = req.body;
if (!project_id) {
const projResult = await db.query(
`INSERT INTO projects (name, customer_id, project_manager_id, status, created_at, updated_at)
@@ -265,6 +266,23 @@ router.put('/:id/sign', authenticate, async (req, res) => {
);
project_id = projResult.rows[0].id;
await db.query('UPDATE budget_projects SET project_id = $1 WHERE id = $2', [project_id, id]);
if (type_template_id) {
const tplResult = await db.query('SELECT phases FROM project_type_templates WHERE id = $1', [type_template_id]);
if (tplResult.rows.length > 0) {
const phases = tplResult.rows[0].phases;
if (Array.isArray(phases)) {
for (const phase of phases) {
const subItems = (phase.sub_items || []).map(item => ({ name: item, completed: false }));
await db.query(
`INSERT INTO project_phases (project_id, template_key, phase_name, phase_order, phase_type, depends_on, sub_items, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[project_id, phase.key || `phase_${phase.order}`, phase.name, phase.order, phase.type || 'serial', phase.depends || [], JSON.stringify(subItems), phase.order === 1 ? 'in_progress' : 'pending']
);
}
const firstPhase = phases.find(p => p.order === 1);
await db.query('UPDATE projects SET type_template_id = $1, current_phase = $2 WHERE id = $3', [type_template_id, firstPhase ? firstPhase.name : '', project_id]);
}
}
}
}
res.json({
+121
View File
@@ -0,0 +1,121 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
router.get('/', async (req, res) => {
try {
const result = await db.query(
'SELECT id, name, description, phases, is_system, created_at, updated_at FROM project_type_templates ORDER BY is_system DESC, created_at ASC'
);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('获取模板列表失败:', error);
res.status(500).json({ success: false, message: '获取模板列表失败' });
}
});
router.get('/:id', async (req, res) => {
try {
const result = await db.query(
'SELECT id, name, description, phases, is_system, created_at, updated_at FROM project_type_templates WHERE id = $1',
[req.params.id]
);
if (result.rows.length === 0) {
return res.status(404).json({ success: false, message: '模板不存在' });
}
res.json({ success: true, data: result.rows[0] });
} catch (error) {
console.error('获取模板详情失败:', error);
res.status(500).json({ success: false, message: '获取模板详情失败' });
}
});
router.post('/', async (req, res) => {
try {
const { name, description, phases } = req.body;
if (!name) {
return res.status(400).json({ success: false, message: '模板名称不能为空' });
}
if (!phases || !Array.isArray(phases) || phases.length === 0) {
return res.status(400).json({ success: false, message: '模板阶段不能为空' });
}
const result = await db.query(
'INSERT INTO project_type_templates (name, description, phases, is_system) VALUES ($1, $2, $3, false) RETURNING id',
[name, description || '', JSON.stringify(phases)]
);
res.json({ success: true, message: '模板创建成功', data: { id: result.rows[0].id } });
} catch (error) {
console.error('创建模板失败:', error);
res.status(500).json({ success: false, message: '创建模板失败' });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const existing = await db.query('SELECT is_system FROM project_type_templates WHERE id = $1', [id]);
if (existing.rows.length === 0) {
return res.status(404).json({ success: false, message: '模板不存在' });
}
if (existing.rows[0].is_system) {
return res.status(403).json({ success: false, message: '系统预设模板不可修改' });
}
const { name, description, phases } = req.body;
const updates = [];
const values = [];
let paramIndex = 1;
if (name !== undefined) { updates.push(`name = $${paramIndex++}`); values.push(name); }
if (description !== undefined) { updates.push(`description = $${paramIndex++}`); values.push(description); }
if (phases !== undefined) { updates.push(`phases = $${paramIndex++}`); values.push(JSON.stringify(phases)); }
if (updates.length === 0) {
return res.status(400).json({ success: false, message: '没有更新数据' });
}
updates.push('updated_at = NOW()');
values.push(id);
await db.query(`UPDATE project_type_templates SET ${updates.join(', ')} WHERE id = $${paramIndex}`, values);
res.json({ success: true, message: '模板更新成功' });
} catch (error) {
console.error('更新模板失败:', error);
res.status(500).json({ success: false, message: '更新模板失败' });
}
});
router.delete('/:id', async (req, res) => {
try {
const { id } = req.params;
const existing = await db.query('SELECT is_system FROM project_type_templates WHERE id = $1', [id]);
if (existing.rows.length === 0) {
return res.status(404).json({ success: false, message: '模板不存在' });
}
if (existing.rows[0].is_system) {
return res.status(403).json({ success: false, message: '系统预设模板不可删除' });
}
await db.query('DELETE FROM project_type_templates WHERE id = $1', [id]);
res.json({ success: true, message: '模板删除成功' });
} catch (error) {
console.error('删除模板失败:', error);
res.status(500).json({ success: false, message: '删除模板失败' });
}
});
router.post('/:id/copy', async (req, res) => {
try {
const { id } = req.params;
const { name } = req.body;
const existing = await db.query('SELECT name, description, phases FROM project_type_templates WHERE id = $1', [id]);
if (existing.rows.length === 0) {
return res.status(404).json({ success: false, message: '模板不存在' });
}
const src = existing.rows[0];
const result = await db.query(
'INSERT INTO project_type_templates (name, description, phases, is_system) VALUES ($1, $2, $3, false) RETURNING id',
[name || `${src.name} (副本)`, src.description, src.phases]
);
res.json({ success: true, message: '模板复制成功', data: { id: result.rows[0].id } });
} catch (error) {
console.error('复制模板失败:', error);
res.status(500).json({ success: false, message: '复制模板失败' });
}
});
module.exports = router;
+169 -5
View File
@@ -362,22 +362,42 @@ router.get('/:id/construction-logs', async (req, res) => {
router.post('/', async (req, res) => {
try {
const { name, code, customer_id, manager_id, contract_amount, start_date, end_date, description, status, location } = req.body;
const { name, code, customer_id, manager_id, contract_amount, start_date, end_date, description, status, location, type_template_id } = req.body;
const projectCode = code || 'PROJ' + new Date().toISOString().slice(0, 10).replace(/-/g, '') +
String(Math.floor(Math.random() * 10000)).padStart(4, '0');
const result = await db.query(
`INSERT INTO projects (name, code, customer_id, project_manager_id, contract_amount, start_date, end_date, description, status, location, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
`INSERT INTO projects (name, code, customer_id, project_manager_id, contract_amount, start_date, end_date, description, status, location, type_template_id, created_at, updated_at)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, CURRENT_TIMESTAMP, CURRENT_TIMESTAMP)
RETURNING id`,
[name, projectCode, customer_id || null, manager_id || null, contract_amount || 0, start_date || '', end_date || '', description || '', status || 'planning', location || '']
[name, projectCode, customer_id || null, manager_id || null, contract_amount || 0, start_date || '', end_date || '', description || '', status || 'planning', location || '', type_template_id || null]
);
const projectId = result.rows[0]?.id || result.rows?.[0]?.id;
if (type_template_id && projectId) {
const tplResult = await db.query('SELECT phases FROM project_type_templates WHERE id = $1', [type_template_id]);
if (tplResult.rows.length > 0) {
const phases = tplResult.rows[0].phases;
if (Array.isArray(phases)) {
for (const phase of phases) {
const subItems = (phase.sub_items || []).map(item => ({ name: item, completed: false }));
await db.query(
`INSERT INTO project_phases (project_id, template_key, phase_name, phase_order, phase_type, depends_on, sub_items, status) VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[projectId, phase.key || `phase_${phase.order}`, phase.name, phase.order, phase.type || 'serial', phase.depends || [], JSON.stringify(subItems), phase.order === 1 ? 'in_progress' : 'pending']
);
}
const firstPhase = phases.find(p => p.order === 1);
await db.query('UPDATE projects SET current_phase = $1 WHERE id = $2', [firstPhase ? firstPhase.name : '', projectId]);
}
}
}
res.json({
success: true,
message: '项目创建成功',
data: { id: (result.rows[0]?.id || result.rows?.[0]?.id), code: projectCode }
data: { id: projectId, code: projectCode }
});
} catch (error) {
console.error('创建项目失败:', error);
@@ -624,3 +644,147 @@ router.get('/:id/cost-summary', async (req, res) => {
module.exports = router;
router.get('/:id/phases', async (req, res) => {
try {
const { id } = req.params;
const result = await db.query(
'SELECT * FROM project_phases WHERE project_id = $1 ORDER BY phase_order',
[id]
);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('获取项目阶段失败:', error);
res.status(500).json({ success: false, message: '获取项目阶段失败' });
}
});
router.post('/:id/phases/init', async (req, res) => {
try {
const { id } = req.params;
const { template_id } = req.body;
const templateResult = await db.query('SELECT phases FROM project_type_templates WHERE id = $1', [template_id]);
if (templateResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '模板不存在' });
}
const phases = templateResult.rows[0].phases;
if (!Array.isArray(phases)) {
return res.status(400).json({ success: false, message: '模板阶段数据格式错误' });
}
const existing = await db.query('SELECT id FROM project_phases WHERE project_id = $1', [id]);
if (existing.rows.length > 0) {
return res.status(400).json({ success: false, message: '该项目已有阶段数据' });
}
for (const phase of phases) {
const subItems = (phase.sub_items || []).map(item => ({ name: item, completed: false }));
await db.query(
`INSERT INTO project_phases (project_id, template_key, phase_name, phase_order, phase_type, depends_on, sub_items, status)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
[id, phase.key || `phase_${phase.order}`, phase.name, phase.order, phase.type || 'serial', phase.depends || [], JSON.stringify(subItems), phase.order === 1 ? 'in_progress' : 'pending']
);
}
const firstPhase = phases.find(p => p.order === 1);
await db.query(
'UPDATE projects SET type_template_id = $1, current_phase = $2, phase_progress = 0, updated_at = CURRENT_TIMESTAMP WHERE id = $3',
[template_id, firstPhase ? firstPhase.name : '', id]
);
res.json({ success: true, message: '阶段初始化成功' });
} catch (error) {
console.error('初始化项目阶段失败:', error);
res.status(500).json({ success: false, message: '初始化项目阶段失败' });
}
});
router.put('/:id/phases/:phaseId/complete', async (req, res) => {
try {
const { id, phaseId } = req.params;
const { remark, photos, completed_by } = 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];
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]
);
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;
const totalCount = allPhases.rows.length;
const progress = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
let nextPhase = null;
for (const p of allPhases.rows) {
if (p.status === 'pending') {
const deps = p.depends_on || [];
const depsMet = deps.length === 0 || deps.every(depId => {
const depPhase = allPhases.rows.find(ap => ap.id === depId || ap.phase_order === depId);
return depPhase && depPhase.status === 'completed';
});
if (depsMet) {
nextPhase = p;
break;
}
}
}
if (nextPhase) {
await db.query("UPDATE project_phases SET status = 'in_progress', started_at = NOW() WHERE id = $1", [nextPhase.id]);
const newStatus = progress === 100 ? 'completed' : 'active';
await db.query(
'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4',
[nextPhase.phase_name, progress, newStatus, id]
);
} else {
const newStatus = progress === 100 ? 'completed' : 'active';
await db.query(
'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4',
['', progress, newStatus, id]
);
}
res.json({ success: true, message: '阶段完成', data: { progress, nextPhase: nextPhase ? nextPhase.phase_name : null } });
} catch (error) {
console.error('完成阶段失败:', error);
res.status(500).json({ success: false, message: '完成阶段失败' });
}
});
router.put('/:id/phases/:phaseId/reopen', async (req, res) => {
try {
const { id, phaseId } = req.params;
await db.query(
"UPDATE project_phases SET status = 'in_progress', completed_at = NULL, completed_by = NULL WHERE id = $1 AND project_id = $2",
[phaseId, id]
);
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;
const totalCount = allPhases.rows.length;
const progress = totalCount > 0 ? Math.round((completedCount / totalCount) * 100) : 0;
const reopenedPhase = allPhases.rows.find(p => p.id === parseInt(phaseId));
await db.query(
'UPDATE projects SET current_phase = $1, phase_progress = $2, status = $3, updated_at = CURRENT_TIMESTAMP WHERE id = $4',
[reopenedPhase ? reopenedPhase.phase_name : '', progress, 'active', id]
);
res.json({ success: true, message: '阶段已重新打开', data: { progress } });
} catch (error) {
console.error('重新打开阶段失败:', error);
res.status(500).json({ success: false, message: '重新打开阶段失败' });
}
});
router.put('/:id/phases/:phaseId/sub-item', async (req, res) => {
try {
const { id, phaseId } = req.params;
const { sub_item_index, completed } = req.body;
const phaseResult = await db.query('SELECT sub_items 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 subItems = phaseResult.rows[0].sub_items || [];
if (sub_item_index >= 0 && sub_item_index < subItems.length) {
subItems[sub_item_index].completed = completed;
await db.query('UPDATE project_phases SET sub_items = $1 WHERE id = $2', [JSON.stringify(subItems), phaseId]);
}
res.json({ success: true, message: '子项状态更新成功' });
} catch (error) {
console.error('更新子项状态失败:', error);
res.status(500).json({ success: false, message: '更新子项状态失败' });
}
});
+69
View File
@@ -0,0 +1,69 @@
const express = require('express');
const db = require('../db');
const router = express.Router();
router.post('/', async (req, res) => {
try {
const { purchase_order_id, logistics_record_id, confirm_date, receiver_name, receiver_phone, received_location, is_complete, missing_items, damage_items, photo_urls, remark, confirmed_by } = req.body;
if (!purchase_order_id) {
return res.status(400).json({ success: false, message: '采购订单ID不能为空' });
}
const result = await db.query(
`INSERT INTO receiving_confirms (purchase_order_id, logistics_record_id, confirm_date, receiver_name, receiver_phone, received_location, is_complete, missing_items, damage_items, photo_urls, remark, confirmed_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id`,
[purchase_order_id, logistics_record_id || null, confirm_date || new Date().toISOString().slice(0, 10), receiver_name || '', receiver_phone || '', received_location || '', is_complete !== false, missing_items || '', damage_items || '', photo_urls || [], remark || '', confirmed_by || null]
);
if (is_complete) {
await db.query("UPDATE purchase_orders SET receiving_status = 'received' WHERE id = $1", [purchase_order_id]);
} else {
await db.query("UPDATE purchase_orders SET receiving_status = 'partial_received' WHERE id = $1", [purchase_order_id]);
}
if (logistics_record_id) {
await db.query("UPDATE logistics_records SET status = 'arrived', final_arrival_date = CURRENT_DATE WHERE id = $1", [logistics_record_id]);
}
res.json({ success: true, message: '收货确认成功', data: { id: result.rows[0].id } });
} catch (error) {
console.error('收货确认失败:', error);
res.status(500).json({ success: false, message: '收货确认失败' });
}
});
router.get('/:purchase_order_id', async (req, res) => {
try {
const result = await db.query(
'SELECT * FROM receiving_confirms WHERE purchase_order_id = $1 ORDER BY created_at DESC',
[req.params.purchase_order_id]
);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('获取收货记录失败:', error);
res.status(500).json({ success: false, message: '获取收货记录失败' });
}
});
router.put('/:id', async (req, res) => {
try {
const { id } = req.params;
const fields = ['confirm_date', 'receiver_name', 'receiver_phone', 'received_location', 'is_complete', 'missing_items', 'damage_items', 'photo_urls', 'remark'];
const updates = [];
const values = [];
let paramIndex = 1;
for (const field of fields) {
if (req.body[field] !== undefined) {
updates.push(`${field} = $${paramIndex++}`);
values.push(req.body[field]);
}
}
if (updates.length === 0) {
return res.status(400).json({ success: false, message: '没有更新数据' });
}
values.push(id);
await db.query(`UPDATE receiving_confirms SET ${updates.join(', ')} WHERE id = $${paramIndex}`, values);
res.json({ success: true, message: '收货记录更新成功' });
} catch (error) {
console.error('更新收货记录失败:', error);
res.status(500).json({ success: false, message: '更新收货记录失败' });
}
});
module.exports = router;
+59
View File
@@ -515,3 +515,62 @@ router.delete('/payment-infos/:infoId', async (req, res) => {
});
module.exports = router;
router.get('/:id/events', async (req, res) => {
try {
const result = await db.query(
'SELECT * FROM subcontractor_events WHERE subcontractor_id = $1 ORDER BY event_date DESC, created_at DESC',
[req.params.id]
);
res.json({ success: true, data: result.rows });
} catch (error) {
console.error('获取分包商事件失败:', error);
res.status(500).json({ success: false, message: '获取分包商事件失败' });
}
});
router.post('/:id/events', async (req, res) => {
try {
const { id } = req.params;
const { project_id, event_type, score_change, event_date, title, description, created_by } = req.body;
if (!event_type || !score_change || !event_date || !title) {
return res.status(400).json({ success: false, message: '缺少必填字段' });
}
if (!['positive', 'negative'].includes(event_type)) {
return res.status(400).json({ success: false, message: '事件类型必须是positive或negative' });
}
const actualChange = event_type === 'positive' ? Math.abs(score_change) : -Math.abs(score_change);
const result = await db.query(
`INSERT INTO subcontractor_events (subcontractor_id, project_id, event_type, score_change, event_date, title, description, created_by)
VALUES ($1, $2, $3, $4, $5, $6, $7, $8) RETURNING id`,
[id, project_id || null, event_type, actualChange, event_date, title, description || '', created_by || null]
);
await db.query(
'UPDATE subcontractors SET score = GREATEST(0, score + $1) WHERE id = $2',
[actualChange, id]
);
res.json({ success: true, message: '事件添加成功', data: { id: result.rows[0].id } });
} catch (error) {
console.error('添加分包商事件失败:', error);
res.status(500).json({ success: false, message: '添加分包商事件失败' });
}
});
router.delete('/:id/events/:eventId', async (req, res) => {
try {
const { id, eventId } = req.params;
const eventResult = await db.query('SELECT score_change FROM subcontractor_events WHERE id = $1 AND subcontractor_id = $2', [eventId, id]);
if (eventResult.rows.length === 0) {
return res.status(404).json({ success: false, message: '事件不存在' });
}
const scoreChange = eventResult.rows[0].score_change;
await db.query('DELETE FROM subcontractor_events WHERE id = $1', [eventId]);
await db.query(
'UPDATE subcontractors SET score = GREATEST(0, LEAST(200, score - $1)) WHERE id = $2',
[scoreChange, id]
);
res.json({ success: true, message: '事件删除成功' });
} catch (error) {
console.error('删除分包商事件失败:', error);
res.status(500).json({ success: false, message: '删除分包商事件失败' });
}
});
+6 -1
View File
@@ -50,11 +50,14 @@ import BudgetProjectDetail from './pages/budget/BudgetProjectDetail'
import ConstructionList from './pages/construction'
import ConstructionLog from './pages/construction/ConstructionLog'
import ConstructionMilestones from './pages/construction/ConstructionMilestones'
import ConstructionOverview from './pages/construction/ConstructionOverview'
import ConstructionProgress from './pages/construction/ConstructionProgress'
// 后台管理
import AdminLayout from './layouts/AdminLayout'
import BackupPage from './pages/admin/BackupPage'
import ProcessManagement from './pages/admin/ProcessManagement'
import ProcessTemplates from './pages/admin/ProcessTemplates'
import AboutPage from './pages/admin/AboutPage'
// 布局组件
@@ -137,9 +140,10 @@ function App() {
<Route path="budget-projects" element={<BudgetProjectList />} />
<Route path="budget-projects/create" element={<BudgetProjectCreate />} />
<Route path="budget-projects/:id" element={<BudgetProjectDetail />} />
<Route path="construction" element={<ConstructionList />} />
<Route path="construction" element={<ConstructionOverview />} />
<Route path="construction/:id/logs" element={<ConstructionLog />} />
<Route path="construction/:id/milestones" element={<ConstructionMilestones />} />
<Route path="construction/progress/:id" element={<ConstructionProgress />} />
<Route path="approval" element={<ApprovalManagement />} />
<Route path="execution" element={<ExecutionManagement />} />
<Route path="advances" element={<AdvancesPage />} />
@@ -173,6 +177,7 @@ function App() {
<Route path="users" element={<UsersPage />} />
<Route path="roles" element={<RolesPage />} />
<Route path="process" element={<ProcessManagement />} />
<Route path="process-templates" element={<ProcessTemplates />} />
<Route path="logs" element={<SystemLogsPage />} />
<Route path="backup" element={<BackupPage />} />
<Route path="about" element={<AboutPage />} />
@@ -68,7 +68,13 @@ const menuItems = [
{
key: '/construction',
icon: <ToolOutlined />,
label: '施工管理'
label: '施工管理',
children: [
{
key: '/construction',
label: '施工总览'
}
]
},
{
key: 'approval',
+7 -1
View File
@@ -8,7 +8,8 @@ import {
DatabaseOutlined,
InfoCircleOutlined,
ArrowLeftOutlined,
SettingOutlined
SettingOutlined,
AppstoreOutlined
} from '@ant-design/icons';
import { useNavigate } from 'react-router-dom';
@@ -34,6 +35,11 @@ const AdminLayout: React.FC = () => {
icon: <SettingOutlined />,
label: '流程管理'
},
{
key: '/admin/process-templates',
icon: <AppstoreOutlined />,
label: '工程模板管理'
},
{
key: '/admin/logs',
icon: <FileTextOutlined />,
@@ -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;
+22 -23
View File
@@ -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 />
}
]}
/>