| ¶Ô±ÈÐÂÎļþ |
| | |
| | | -- 项ç®ç®¡çæéãå类主é®åæµç¨å®å
¨å级ã |
| | | -- æ§è¡å请å¤ä»½æ°æ®åºï¼æ¬èæ¬åªå建æé项ï¼ä¸èªå¨æ©å¤§æ®éè§è²æéã |
| | | SET NAMES utf8mb4; |
| | | |
| | | DROP PROCEDURE IF EXISTS `add_column_if_missing`; |
| | | DELIMITER $$ |
| | | CREATE PROCEDURE `add_column_if_missing`(IN p_table varchar(64), IN p_column varchar(64), IN p_definition varchar(1000)) |
| | | BEGIN |
| | | IF NOT EXISTS (SELECT 1 FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME=p_table AND COLUMN_NAME=p_column) THEN |
| | | SET @ddl=CONCAT('ALTER TABLE `',p_table,'` ADD COLUMN `',p_column,'` ',p_definition); |
| | | PREPARE stmt FROM @ddl; |
| | | EXECUTE stmt; |
| | | DEALLOCATE PREPARE stmt; |
| | | END IF; |
| | | END$$ |
| | | DELIMITER ; |
| | | CALL add_column_if_missing('project_management_info','project_category_id',"bigint DEFAULT NULL COMMENT '项ç®åç±»ID' AFTER project_classification"); |
| | | DROP PROCEDURE IF EXISTS `add_column_if_missing`; |
| | | |
| | | -- åç§°å¯ä¸çåå²é¡¹ç®å¯å®å
¨åå¡«ï¼éååç±»çç»ç¨æ·éæ°éæ©ã |
| | | UPDATE project_management_info p |
| | | JOIN (SELECT category_name,MIN(id) id FROM project_management_category WHERE is_deleted=0 GROUP BY category_name HAVING COUNT(*)=1) c |
| | | ON c.category_name=p.project_classification |
| | | SET p.project_category_id=c.id |
| | | WHERE p.project_category_id IS NULL AND p.is_delete=0; |
| | | |
| | | SET @project_list_menu=2285; |
| | | UPDATE sys_menu SET perms='project:info:list',visible='0',status='0',update_time=NOW() WHERE menu_id=@project_list_menu; |
| | | |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'æ¥ç项ç®',@project_list_menu,11,'#',NULL,'',1,0,'F','0','0','project:info:query','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:query'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'æ°å¢é¡¹ç®',@project_list_menu,12,'#',NULL,'',1,0,'F','0','0','project:info:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:add'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'ä¿®æ¹é¡¹ç®',@project_list_menu,13,'#',NULL,'',1,0,'F','0','0','project:info:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:edit'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'å é¤é¡¹ç®',@project_list_menu,14,'#',NULL,'',1,0,'F','0','0','project:info:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:info:remove'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'å¼å§é¡¹ç®',@project_list_menu,15,'#',NULL,'',1,0,'F','0','0','project:start','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:start'); |
| | | |
| | | |
| | | -- Ensure the category menu exists even when this hardening script is applied independently. |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT '项ç®åç±»',2276,2,'projectCategory','projectManagement/projectCategory/index','ProjectCategory',1,0,'C','0','0','project:category:list','tree','admin',NOW(),'å¤å±æ 形项ç®å类维æ¤' |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE parent_id=2276 AND path='projectCategory'); |
| | | |
| | | -- Workflow and execution button permissions. |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT '模åç»´æ¤',@project_list_menu,21,'#',NULL,'',1,0,'F','0','0','project:module:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:module:edit'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT '模åå é¤',@project_list_menu,22,'#',NULL,'',1,0,'F','0','0','project:module:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:module:remove'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'æ°å¢æä¿®æ¹è·è¿',@project_list_menu,23,'#',NULL,'',1,0,'F','0','0','project:follow:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:follow:add'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'å é¤è·è¿',@project_list_menu,24,'#',NULL,'',1,0,'F','0','0','project:follow:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:follow:remove'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'æ°å¢æä¿®æ¹æ±æ¥',@project_list_menu,25,'#',NULL,'',1,0,'F','0','0','project:report:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:report:add'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'å 餿±æ¥',@project_list_menu,26,'#',NULL,'',1,0,'F','0','0','project:report:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:report:remove'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'æ°å¢æä¿®æ¹èµæ',@project_list_menu,27,'#',NULL,'',1,0,'F','0','0','project:material:add','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:material:add'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'å é¤èµæ',@project_list_menu,28,'#',NULL,'',1,0,'F','0','0','project:material:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:material:remove'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'ç»´æ¤é¡¹ç®é¶æ®µ',@project_list_menu,29,'#',NULL,'',1,0,'F','0','0','project:phase:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:phase:edit'); |
| | | |
| | | SET @project_category_menu=(SELECT menu_id FROM sys_menu WHERE parent_id=2276 AND path='projectCategory' ORDER BY menu_id LIMIT 1); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'ç»´æ¤é¡¹ç®åç±»',@project_category_menu,1,'#',NULL,'',1,0,'F','0','0','project:category:edit','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:category:edit'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,component,route_name,is_frame,is_cache,menu_type,visible,status,perms,icon,create_by,create_time,remark) |
| | | SELECT 'å é¤é¡¹ç®åç±»',@project_category_menu,2,'#',NULL,'',1,0,'F','0','0','project:category:remove','#','admin',NOW(),'' WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='project:category:remove'); |
| | | |
| | | |
| | | -- è¶
级管çåè§è²è¡¥é½æé®ï¼å
¶ä»è§è²ç±âè§è²ç®¡çâæå²ä½å¾éã |
| | | INSERT IGNORE INTO sys_role_menu(role_id,menu_id) |
| | | SELECT 1,menu_id FROM sys_menu |
| | | WHERE status='0' AND perms IN ( |
| | | 'project:info:list','project:info:query','project:info:add','project:info:edit','project:info:remove','project:start', |
| | | 'project:module:edit','project:module:remove','project:follow:add','project:follow:remove', |
| | | 'project:report:add','project:report:remove','project:material:add','project:material:remove', |
| | | 'project:phase:edit','project:category:list','project:category:edit','project:category:remove' |
| | | ); |
| | |
| | | path: ":id", |
| | | component: () => import("@/views/projectManagement/Management/projectDetail.vue"), |
| | | name: "ProjectManagementDetail", |
| | | meta: { title: "项ç®è¯¦æ
", activeMenu: "/projectManagement/Management" }, |
| | | meta: { title: "项ç®è¯¦æ
", activeMenu: "/projectManagement/projectList" }, |
| | | }, |
| | | ], |
| | | }, |
| | |
| | | <span v-if="data.followWindow?.dueDate">æ¬æ¬¡åºè·è¿æ¥ {{ data.followWindow.dueDate }}ï¼å¯å¡«å {{ data.followWindow.windowStart }} è³ {{ data.followWindow.windowEnd }}</span> |
| | | <span v-else>馿¬¡è·è¿å¯ç´æ¥å¡«å</span> |
| | | </div> |
| | | <el-button v-if="!readonly" v-hasPermi="['project:follow:add']" type="primary" @click="openFollow()">å¡«åè·è¿</el-button> |
| | | <el-button v-if="!readonly && canManageFollow" v-hasPermi="['project:follow:add']" type="primary" @click="openFollow()">å¡«åè·è¿</el-button> |
| | | </div> |
| | | <el-table :data="data.followUps" border> |
| | | <el-table-column prop="followDate" label="è·è¿æ¥æ" width="130" /> |
| | |
| | | <span v-else class="empty-file">æ éä»¶</span> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="!readonly" label="æä½" width="140" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:follow:add']" link type="primary" @click="openFollow(row)">ç¼è¾</el-button><el-button v-hasPermi="['project:follow:remove']" link type="danger" @click="removeRecord('follow', row)">å é¤</el-button></template></el-table-column> |
| | | <el-table-column v-if="!readonly && canManageFollow" label="æä½" width="140" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:follow:add']" link type="primary" @click="openFollow(row)">ç¼è¾</el-button><el-button v-hasPermi="['project:follow:remove']" link type="danger" @click="removeRecord('follow', row)">å é¤</el-button></template></el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | |
| | | </el-button> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column v-if="!readonly" label="æä½" width="90" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:material:remove']" link type="danger" @click="removeRecord('material', row)">å é¤</el-button></template></el-table-column> |
| | | <el-table-column v-if="!readonly" label="æä½" width="140" align="center"><template #default="{ row }"><el-button v-hasPermi="['project:material:add']" link type="primary" @click="openMaterial(row)">ç¼è¾</el-button><el-button v-hasPermi="['project:material:remove']" link type="danger" @click="removeRecord('material', row)">å é¤</el-button></template></el-table-column> |
| | | </el-table> |
| | | </el-tab-pane> |
| | | |
| | |
| | | </div> |
| | | <div class="module-grid"> |
| | | <el-form-item label="模ååç§°" required><el-input v-model="module.moduleName" :disabled="module.locked" /></el-form-item> |
| | | <el-form-item label="é¢è®¡å¼å§" required><el-date-picker v-model="module.plannedStartDate" :disabled="module.locked" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="é¢è®¡å¼å§" required><el-date-picker v-model="module.plannedStartDate" :disabled="module.locked" :disabled-date="moduleDateDisabled" type="date" value-format="YYYY-MM-DD" style="width:100%" /></el-form-item> |
| | | <el-form-item label="å·¥ä½éå æ¯" required><el-input-number v-model="module.projectWeight" :disabled="module.locked" :min="0.01" :max="draftWeightMax(moduleIndex)" :precision="2" /><span class="unit">%</span><div class="percent-hint">å
¶ä»æ¨¡å {{ otherDraftWeight(moduleIndex) }}%ï¼æ¬æ¨¡åæå¤ {{ draftWeightMax(moduleIndex) }}%</div></el-form-item> |
| | | <el-form-item label="æ¨¡åæ¦è¦" required class="summary-item"><el-input v-model="module.summary" :disabled="module.locked" type="textarea" :rows="2" maxlength="1000" show-word-limit /></el-form-item> |
| | | <el-form-item label="æ¨¡åæ§è¡äºº" required class="members-item"> |
| | |
| | | |
| | | <el-dialog v-model="followVisible" :title="followForm.id ? 'ç¼è¾é¡¹ç®è·è¿' : 'å¡«å项ç®è·è¿'" width="650px" destroy-on-close> |
| | | <el-form ref="followFormRef" :model="followForm" :rules="followRules" label-width="90px"> |
| | | <el-form-item label="è·è¿æ¥æ" prop="followDate"><el-date-picker v-model="followForm.followDate" type="date" value-format="YYYY-MM-DD" style="width: 100%" /></el-form-item> |
| | | <el-form-item label="è·è¿æ¥æ" prop="followDate"><el-date-picker v-model="followForm.followDate" :disabled="!!followForm.id" :disabled-date="date => dayjs(date).isAfter(dayjs(), 'day')" type="date" value-format="YYYY-MM-DD" style="width: 100%" /><div v-if="followForm.id" class="percent-hint">åå²è·è¿æ¥æä¸å¯ä¿®æ¹ï¼é¿å
ç ´ååç»è·è¿å¨æã</div></el-form-item> |
| | | <el-form-item label="è·è¿è¯¦æ
" prop="detail"><el-input v-model="followForm.detail" type="textarea" :rows="5" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-form-item label="éä»¶"><AttachmentUploadFile v-model:file-list="followFiles" :limit="10" /></el-form-item> |
| | | </el-form> |
| | |
| | | </div> |
| | | </el-form-item> |
| | | <el-form-item label="æ¬æ¬¡è¿åº¦" prop="incrementProgress"><el-input-number v-model="reportForm.incrementProgress" :min="0.01" :max="reportIncrementMax" :precision="2" /><span class="unit">%</span><span class="current-progress-hint">{{ reportProgressHint }}</span></el-form-item> |
| | | <el-form-item label="æ±æ¥æ¶é´" prop="reportDate"><el-date-picker v-model="reportForm.reportDate" :disabled="!!reportForm.id" type="date" value-format="YYYY-MM-DD" style="width: 100%" /><div v-if="reportForm.id" class="percent-hint">ç¼è¾å·²ææ±æ¥æ¶ä¸å
è®¸è°æ´æ¶é´ï¼ä»¥ä¿è¯åå²è¿åº¦é¡ºåºæ£ç¡®ã</div></el-form-item> |
| | | <el-form-item label="æ±æ¥æ¶é´" prop="reportDate"><el-date-picker v-model="reportForm.reportDate" :disabled="!!reportForm.id" :disabled-date="date => dayjs(date).isAfter(dayjs(), 'day')" type="date" value-format="YYYY-MM-DD" style="width: 100%" /><div v-if="reportForm.id" class="percent-hint">ç¼è¾å·²ææ±æ¥æ¶ä¸å
è®¸è°æ´æ¶é´ï¼ä»¥ä¿è¯åå²è¿åº¦é¡ºåºæ£ç¡®ã</div></el-form-item> |
| | | <el-form-item label="夿³¨"><el-input v-model="reportForm.remark" type="textarea" :rows="3" maxlength="2000" show-word-limit /></el-form-item> |
| | | <el-form-item label="æä»¶"><AttachmentUploadFile v-model:file-list="reportFiles" :limit="10" /></el-form-item> |
| | | </el-form> |
| | | <template #footer><el-button @click="reportVisible = false">åæ¶</el-button><el-button type="primary" :loading="saving" @click="submitReport">{{ reportForm.id ? 'ä¿åä¿®æ¹' : 'æäº¤æ±æ¥' }}</el-button></template> |
| | | </el-dialog> |
| | | |
| | | <el-dialog v-model="materialVisible" title="ä¸ä¼ 项ç®èµæ" width="650px" destroy-on-close> |
| | | <el-dialog v-model="materialVisible" :title="materialForm.id ? 'ç¼è¾é¡¹ç®èµæ' : 'ä¸ä¼ 项ç®èµæ'" width="650px" destroy-on-close> |
| | | <el-form ref="materialFormRef" :model="materialForm" :rules="materialRules" label-width="90px"> |
| | | <el-form-item label="èµæåç§°" prop="materialName"><el-input v-model="materialForm.materialName" /></el-form-item> |
| | | <el-form-item label="èµæç±»å"><el-select v-model="materialForm.materialType" allow-create filterable clearable style="width: 100%"><el-option v-for="type in ['ç«é¡¹èµæ','å¼åèµæ','ç³æ¥èµæ','ç»æèµæ','éªæ¶èµæ','å
¶ä»']" :key="type" :label="type" :value="type" /></el-select></el-form-item> |
| | |
| | | const reportShareRemaining = computed(() => Math.max(0, 100 - Number(reportShareTotal.value)).toFixed(2)) |
| | | const followCycleLabel = computed(() => ({ MONTH: 'æ¯æ', QUARTER: 'æ¯å£åº¦', YEAR: 'æ¯å¹´' }[props.projectInfo?.followCycle] || props.projectInfo?.followCycle || 'æ¯æ')) |
| | | const isStarted = computed(() => !!props.projectInfo?.currentPhase && props.projectInfo.currentPhase !== 'æªå¼å§') |
| | | const canManageFollow = computed(() => String(props.projectInfo?.projectLeaderId ?? '') === String(userStore.id ?? '')) |
| | | const memberReportModules = computed(() => data.modules.filter(module => |
| | | (module.members || []).some(member => String(member.userId) === String(userStore.id)) |
| | | )) |
| | |
| | | const reportRules = { moduleId: [{ required: true, message: 'è¯·éæ©é¡¹ç®æ¨¡å', trigger: 'change' }], reportDate: [{ required: true, message: 'è¯·éæ©æ±æ¥æ¶é´', trigger: 'change' }] } |
| | | const materialVisible = ref(false) |
| | | const materialFormRef = ref() |
| | | const materialForm = reactive({ materialName: '', materialType: '', remark: '' }) |
| | | const materialForm = reactive({ id: null, materialName: '', materialType: '', remark: '' }) |
| | | const materialFiles = ref([]) |
| | | const materialRules = { materialName: [{ required: true, message: '请è¾å
¥èµæåç§°', trigger: 'blur' }] } |
| | | async function loadUsers() { |
| | |
| | | } |
| | | |
| | | function canEditReport(row) { |
| | | return memberReportModules.value.some(module => String(module.id) === String(row.moduleId)) |
| | | return (row.reporterIdList || []).some(userId => String(userId) === String(userStore.id)) |
| | | } |
| | | |
| | | function moduleDateDisabled(date) { |
| | | const start = props.projectInfo?.planStartTime |
| | | const end = props.projectInfo?.planEndTime |
| | | return (start && dayjs(date).isBefore(dayjs(start), 'day')) || (end && dayjs(date).isAfter(dayjs(end), 'day')) |
| | | } |
| | | |
| | | function reporterShareLimit(index) { |
| | |
| | | finally { saving.value = false } |
| | | } |
| | | |
| | | function openMaterial() { Object.assign(materialForm, { materialName: '', materialType: '', remark: '' }); materialFiles.value = []; materialVisible.value = true } |
| | | function openMaterial(row = null) { |
| | | Object.assign(materialForm, row |
| | | ? { id: row.id, materialName: row.materialName || '', materialType: row.materialType || '', remark: row.remark || '' } |
| | | : { id: null, materialName: '', materialType: '', remark: '' }) |
| | | materialFiles.value = row ? [...(row.attachmentList || [])] : [] |
| | | materialVisible.value = true |
| | | } |
| | | async function submitMaterial() { |
| | | await materialFormRef.value?.validate() |
| | | if (!materialFiles.value.length) return ElMessage.warning('请ä¸ä¼ è³å°ä¸ä¸ªæä»¶') |
| | |
| | | <el-col :span="12"><el-form-item label="客æ·åç§°" prop="clientName"><el-select v-model="form.clientName" filterable clearable style="width:100%" @change="syncCustomer"><el-option v-for="item in customers" :key="item.id" :label="item.customerName" :value="item.customerName" /></el-select></el-form-item></el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"><el-form-item label="项ç®åç±»" prop="projectClassification"><el-tree-select v-model="form.projectClassification" :data="categoryTree" :props="{ label:'categoryName',value:'categoryName',children:'children' }" check-strictly clearable :filterable="false" placeholder="è¯·éæ©é¡¹ç®åç±»" style="width:100%" /></el-form-item></el-col> |
| | | <el-col :span="12"><el-form-item label="项ç®åç±»" prop="projectCategoryId"><el-tree-select v-model="form.projectCategoryId" :data="categoryTree" :props="{ label:'categoryName',value:'id',children:'children' }" check-strictly clearable filterable placeholder="è¯·éæ©é¡¹ç®åç±»" style="width:100%" @change="syncCategory" /></el-form-item></el-col> |
| | | <el-col :span="12"><el-form-item label="é¡¹ç®æ¥æº"><el-input v-model="form.source" disabled /></el-form-item></el-col> |
| | | </el-row> |
| | | <el-row :gutter="20"> |
| | |
| | | <script setup> |
| | | import { computed, reactive, ref } from 'vue' |
| | | import { ElMessage } from 'element-plus' |
| | | import dayjs from 'dayjs' |
| | | import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue' |
| | | import { addProject, getProject, updateProject } from '@/api/projectManagement/project' |
| | | import { listCustomer } from '@/api/basicData/customer' |
| | |
| | | const users = ref([]) |
| | | const categoryTree = ref([]) |
| | | const files = ref([]) |
| | | const form = reactive({ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'æªå¼å§',remark:'' }) |
| | | const form = reactive({ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectCategoryId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'æªå¼å§',remark:'' }) |
| | | const rules = { |
| | | title:[{ required:true,message:'请è¾å
¥é¡¹ç®åç§°',trigger:'blur' }], clientName:[{ required:true,message:'è¯·éæ©å®¢æ·',trigger:'change' }], |
| | | projectClassification:[{ required:true,message:'è¯·éæ©é¡¹ç®åç±»',trigger:'change' }], planStartTime:[{ required:true,message:'è¯·éæ©é¢è®¡å¼å§æ¶é´',trigger:'change' }], |
| | | projectCategoryId:[{ required:true,message:'è¯·éæ©é¡¹ç®åç±»',trigger:'change' }], planStartTime:[{ required:true,message:'è¯·éæ©é¢è®¡å¼å§æ¶é´',trigger:'change' }], |
| | | planEndTime:[{ required:true,message:'è¯·éæ©é¢è®¡ç»ææ¶é´',trigger:'change' }], projectLeaderId:[{ required:true,message:'è¯·éæ©é¡¹ç®è´è´£äºº',trigger:'change' }], |
| | | projectManagerIds:[{ required:true,type:'array',min:1,message:'è¯·éæ©è³å°ä¸å项ç®ç»ç',trigger:'change' }], followCycle:[{ required:true,message:'è¯·éæ©è·è¿å¨æ',trigger:'change' }], |
| | | followDetail:[{ required:true,message:'请è¾å
¥è·è¿è¦æ±',trigger:'blur' }] |
| | |
| | | const userData=userRes?.data||userRes?.rows||userRes||[]; users.value=(Array.isArray(userData)?userData:[]).map(u=>({ label:u.nickName||u.userName||u.name,value:u.userId||u.id })) |
| | | categoryTree.value=buildTree(categoryRes?.data||[]) |
| | | } |
| | | function reset() { Object.assign(form,{ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'æªå¼å§',remark:'' }); files.value=[] } |
| | | function reset() { Object.assign(form,{ id:null,no:'',title:'',clientId:null,clientName:'',source:'æå¨å建',sourceBusinessId:null,projectCategoryId:null,projectClassification:'',planStartTime:'',planEndTime:'',projectLeaderId:null,projectLeaderName:'',projectManagerIds:[],projectManagerNames:[],followCycle:'MONTH',followDetail:'',currentPhase:'æªå¼å§',remark:'' }); files.value=[] } |
| | | function syncCustomer(name) { const item=customers.value.find(x=>x.customerName===name); form.clientId=item?.id||item?.customerId||null } |
| | | function syncLeader(id) { form.projectLeaderName=users.value.find(u=>String(u.value)===String(id))?.label||'' } |
| | | function findCategory(nodes,id) { for(const node of nodes||[]){ if(String(node.id)===String(id)) return node; const found=findCategory(node.children,id); if(found) return found } } |
| | | function syncCategory(id) { form.projectClassification=findCategory(categoryTree.value,id)?.categoryName||'' } |
| | | function normalizeFiles(list) { return (list||[]).map(f=>({ id:f.id??f.fileId??f.rawData?.id,name:f.name??f.fileName??f.rawData?.originalFilename,url:f.url??f.fileUrl??f.rawData?.url })).filter(f=>f.id||f.url) } |
| | | |
| | | async function openDialog(options={}) { |
| | |
| | | await formRef.value?.validate() |
| | | if(form.planEndTime < form.planStartTime) return ElMessage.warning('é¢è®¡ç»ææ¶é´ä¸è½æ©äºé¢è®¡å¼å§æ¶é´') |
| | | form.projectManagerNames=form.projectManagerIds.map(id=>users.value.find(u=>String(u.value)===String(id))?.label||'').filter(Boolean) |
| | | const info={ ...form, currentPhase:startProject.value?'æªå¼å§':form.currentPhase, establishTime:form.establishTime||new Date().toISOString().slice(0,10), managerId:form.projectLeaderId,managerName:form.projectLeaderName,attachmentIds:normalizeFiles(files.value).map(f=>f.id).filter(Boolean),projectManagementPlanId:null } |
| | | const info={ ...form, currentPhase:startProject.value?'æªå¼å§':form.currentPhase, establishTime:form.establishTime||dayjs().format('YYYY-MM-DD'), managerId:form.projectLeaderId,managerName:form.projectLeaderName,attachmentIds:normalizeFiles(files.value).map(f=>f.id).filter(Boolean),projectManagementPlanId:null } |
| | | saving.value=true |
| | | try { |
| | | await (form.id?updateProject:addProject)({ info,shippingAddress:null,contractInfo:null,salesLedgerProductList:null }) |
| | |
| | | </el-form> |
| | | </el-card> |
| | | <el-card shadow="never"> |
| | | <div class="actions"><el-button type="primary" @click="add">æå¨å建项ç®</el-button></div> |
| | | <div class="actions"><el-button v-hasPermi="['project:info:add']" type="primary" @click="add">æå¨å建项ç®</el-button></div> |
| | | <el-table :data="rows" v-loading="loading" border> |
| | | <el-table-column prop="no" label="项ç®ç¼å·" width="170" /> |
| | | <el-table-column prop="title" label="项ç®åç§°" min-width="180" /> |
| | |
| | | <el-table-column prop="actualEndTime" label="å®é
ç»æ" width="120"><template #default="{row}">{{ row.actualEndTime || '-' }}</template></el-table-column> |
| | | <el-table-column label="æä½" width="520" fixed="right" align="center"> |
| | | <template #default="{row}"> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" link type="success" @click="start(row)">å¼å§é¡¹ç®</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" link type="primary" @click="edit(row)">ç¼è¾</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" link type="primary" @click="maintain(row, 'modules')">模å管ç</el-button> |
| | | <el-dropdown v-if="row.currentPhase!=='æªå¼å§'" class="operation-dropdown" @command="tab => maintain(row, tab)"> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" v-hasPermi="['project:start']" link type="success" @click="start(row)">å¼å§é¡¹ç®</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" v-hasPermi="['project:info:edit']" link type="primary" @click="edit(row)">ç¼è¾</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" v-hasPermi="['project:module:edit']" link type="primary" @click="maintain(row, 'modules')">模å管ç</el-button> |
| | | <el-dropdown v-if="row.currentPhase!=='æªå¼å§'" v-hasPermi="['project:module:edit','project:follow:add','project:report:add','project:material:add']" class="operation-dropdown" @command="tab => maintain(row, tab)"> |
| | | <el-button link type="primary">项ç®ç»´æ¤<el-icon class="el-icon--right"><ArrowDown /></el-icon></el-button> |
| | | <template #dropdown> |
| | | <el-dropdown-menu> |
| | | <el-dropdown-item command="modules">模å管ç</el-dropdown-item> |
| | | <el-dropdown-item command="follow">项ç®è·è¿</el-dropdown-item> |
| | | <el-dropdown-item command="reports">é¡¹ç®æ±æ¥</el-dropdown-item> |
| | | <el-dropdown-item command="materials">项ç®èµæ</el-dropdown-item> |
| | | <el-dropdown-item v-hasPermi="['project:module:edit']" command="modules">模å管ç</el-dropdown-item> |
| | | <el-dropdown-item v-hasPermi="['project:follow:add']" command="follow">项ç®è·è¿</el-dropdown-item> |
| | | <el-dropdown-item v-hasPermi="['project:report:add']" command="reports">é¡¹ç®æ±æ¥</el-dropdown-item> |
| | | <el-dropdown-item v-hasPermi="['project:material:add']" command="materials">项ç®èµæ</el-dropdown-item> |
| | | </el-dropdown-menu> |
| | | </template> |
| | | </el-dropdown> |
| | | <el-button link type="primary" @click="detail(row)">详æ
</el-button> |
| | | <el-button v-if="canAdvanceStage(row)" link type="success" @click="advanceStage(row)">{{ nextStageAction(row) }}</el-button> |
| | | <el-button link type="primary" @click="openLifecycle(row)">项ç®çå½å¨æ</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" link type="danger" @click="remove(row)">å é¤</el-button> |
| | | <el-button v-hasPermi="['project:info:query']" link type="primary" @click="detail(row)">详æ
</el-button> |
| | | <el-button v-if="canAdvanceStage(row)" v-hasPermi="['project:phase:edit']" link type="success" @click="advanceStage(row)">{{ nextStageAction(row) }}</el-button> |
| | | <el-button v-hasPermi="['project:info:query']" link type="primary" @click="openLifecycle(row)">项ç®çå½å¨æ</el-button> |
| | | <el-button v-if="row.currentPhase==='æªå¼å§'" v-hasPermi="['project:info:remove']" link type="danger" @click="remove(row)">å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | |
| | | import { computed, onMounted, reactive, ref } from 'vue' |
| | | import { useRouter } from 'vue-router' |
| | | import { ElMessage, ElMessageBox } from 'element-plus' |
| | | import dayjs from 'dayjs' |
| | | import { listProject, delProject } from '@/api/projectManagement/project' |
| | | import { getProjectLifecycle, saveProjectPhase, transitionProjectStage } from '@/api/projectManagement/workflow' |
| | | import { getProjectLifecycle, saveProjectPhase, startProjectWorkflow, transitionProjectStage } from '@/api/projectManagement/workflow' |
| | | import AttachmentUploadFile from '@/components/AttachmentUpload/file/index.vue' |
| | | import FormDia from './components/formDia.vue' |
| | | import ProjectExecutionPanel from './components/ProjectExecutionPanel.vue' |
| | |
| | | const stageVisible=ref(false); const stageSaving=ref(false); const stageProject=ref({}); const stageFiles=ref([]) |
| | | const stageForm=reactive({applicationDate:'',applicationType:'客æ·äº¤ä»',applicationName:'',expectedFeedbackDate:'',description:'',resultDate:'',resultStatus:'PASS',resultDescription:'',acceptancePlanDate:'',acceptor:'',acceptanceStandard:'',acceptanceDate:'',acceptanceResult:'PASS',acceptanceDescription:''}) |
| | | const query=reactive({ keyword:'',customerName:'',source:'',pageNum:1,pageSize:10 }) |
| | | async function load(){ loading.value=true; try{ const res=await listProject({ noOrName:query.keyword,clientName:query.customerName,current:query.pageNum,size:query.pageSize }); const data=res?.data?.records||res?.rows||[]; rows.value=(data||[]).filter(r=>!query.source||r.source===query.source); total.value=res?.data?.total||res?.total||0 }finally{loading.value=false} } |
| | | async function load(){ loading.value=true; try{ const res=await listProject({ noOrName:query.keyword,clientName:query.customerName,source:query.source,current:query.pageNum,size:query.pageSize }); const data=res?.data?.records||res?.rows||[]; rows.value=data||[]; total.value=res?.data?.total||res?.total||0 }finally{loading.value=false} } |
| | | function search(){query.pageNum=1;load()} function reset(){Object.assign(query,{keyword:'',customerName:'',source:'',pageNum:1,pageSize:10});load()} |
| | | function add(){formRef.value?.openDialog({operationType:'add'})} function edit(row){formRef.value?.openDialog({operationType:'edit',row})} function start(row){formRef.value?.openDialog({operationType:'edit',row,startProject:true})} |
| | | function add(){formRef.value?.openDialog({operationType:'add'})} function edit(row){formRef.value?.openDialog({operationType:'edit',row})} |
| | | async function start(row){ |
| | | await ElMessageBox.confirm(`确认å¼å§é¡¹ç®â${row.title}âåï¼å¼å§åä¸è½éåæªå¼å§ç¶æã`,'å¼å§é¡¹ç®',{type:'warning'}) |
| | | await startProjectWorkflow(row.id) |
| | | ElMessage.success('项ç®å·²å¼å§') |
| | | await load() |
| | | } |
| | | const maintenanceNames={modules:'模å管ç',follow:'项ç®è·è¿',reports:'é¡¹ç®æ±æ¥',materials:'项ç®èµæ'} |
| | | const nextStageMap={å¼å:'ç³æ¥',ç³æ¥:'ç³æ¥ç»æ',ç³æ¥ç»æ:'éªæ¶',éªæ¶:'已宿'} |
| | | const maintenanceReadonly=computed(()=>['ç³æ¥','ç³æ¥ç»æ','éªæ¶','已宿'].includes(selectedProject.value.currentPhase)) |
| | |
| | | const stageDialogTitle=computed(()=>nextStageAction(stageProject.value)) |
| | | const stageSubmitText=computed(()=>stageProject.value.currentPhase==='éªæ¶'&&stageForm.acceptanceResult==='PASS'?'éªæ¶éè¿å¹¶å®æé¡¹ç®':'ä¿å') |
| | | function advanceStage(row){ |
| | | const today=new Date().toISOString().slice(0,10) |
| | | const today=dayjs().format('YYYY-MM-DD') |
| | | stageProject.value={...row} |
| | | Object.assign(stageForm,{applicationDate:today,applicationType:'客æ·äº¤ä»',applicationName:row.title||'',expectedFeedbackDate:'',description:'',resultDate:today,resultStatus:'PASS',resultDescription:'',acceptancePlanDate:today,acceptor:row.clientName||'',acceptanceStandard:'',acceptanceDate:today,acceptanceResult:'PASS',acceptanceDescription:''}) |
| | | stageFiles.value=[] |
| | |
| | | const form = reactive({ id: null, parentId: null, categoryName: '', sort: 0, remark: '' }) |
| | | const rules = { categoryName: [{ required: true, message: '请è¾å
¥åç±»åç§°', trigger: 'blur' }] } |
| | | |
| | | function buildTree(list, excludedId = null) { |
| | | const source = list.filter(item => String(item.id) !== String(excludedId ?? '')) |
| | | function buildTree(list, excludedIds = new Set()) { |
| | | const source = list.filter(item => !excludedIds.has(String(item.id))) |
| | | const map = new Map(source.map(item => [String(item.id), { ...item, children: [] }])) |
| | | const roots = [] |
| | | map.forEach(item => { |
| | |
| | | return sortRows(roots) |
| | | } |
| | | |
| | | function categoryBranchIds(id) { |
| | | const excluded = new Set(id == null ? [] : [String(id)]) |
| | | let changed = true |
| | | while (changed) { |
| | | changed = false |
| | | rows.value.forEach(item => { |
| | | if (excluded.has(String(item.parentId)) && !excluded.has(String(item.id))) { |
| | | excluded.add(String(item.id)); changed = true |
| | | } |
| | | }) |
| | | } |
| | | return excluded |
| | | } |
| | | const treeRows = computed(() => buildTree(rows.value)) |
| | | const parentOptions = computed(() => buildTree(rows.value, form.id)) |
| | | const parentOptions = computed(() => buildTree(rows.value, categoryBranchIds(form.id))) |
| | | |
| | | async function loadData() { |
| | | loading.value = true |