OA系统-项目任务协同。项目,项目阶段,项目阶段任务
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | import request from "@/utils/request"; |
| | | import { parseStrEmpty } from "@/utils/ruoyi"; |
| | | |
| | | // æ¥è¯¢é¡¹ç®å表 |
| | | export function listProject(query) { |
| | | return request({ |
| | | url: "/oA/project/listPage", |
| | | method: "get", |
| | | params: query |
| | | }); |
| | | } |
| | | |
| | | // æ¥è¯¢é¡¹ç®åè¡¨è¯¦ç» |
| | | export function getProject(query) { |
| | | return request({ |
| | | url: "oA/project/getList", |
| | | method: "get" |
| | | }); |
| | | } |
| | | |
| | | // æ°å¢é¡¹ç® |
| | | export function addProject(data) { |
| | | return request({ |
| | | url: "/oA/project/add", |
| | | method: "post", |
| | | data: data |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹é¡¹ç® |
| | | export function updateProject(data) { |
| | | return request({ |
| | | url: "/oA/project/update", |
| | | method: "post", |
| | | data: data |
| | | }); |
| | | } |
| | | |
| | | // å é¤é¡¹ç® |
| | | export function delProject(projectId) { |
| | | return request({ |
| | | url: "/oA/project/delete/" + projectId, |
| | | method: "delete" |
| | | }); |
| | | } |
| | | // 导åºé¡¹ç® |
| | | export function exportProject(data) { |
| | | return request({ |
| | | url: "/oA/project/export", |
| | | method: "post", |
| | | data: data |
| | | }); |
| | | } |
| | | // // æ¹éå é¤é¡¹ç® |
| | | // export function delProjectBatch(projectIds) { |
| | | // return request({ |
| | | // url: "/oaSystem/project/batch", |
| | | // method: "delete", |
| | | // data: projectIds |
| | | // }); |
| | | // } |
| | | |
| | | // æ ¹æ®é¡¹ç®é¶æ®µidæ¥è¯¢é¡¹ç®é¶æ®µä»»å¡å表 |
| | | export function listProjectTask(phaseId) { |
| | | return request({ |
| | | url: "/oA/projectPhaseTask/listByPhaseId/"+ phaseId, |
| | | method: "get" |
| | | }); |
| | | } |
| | | |
| | | // // æ¥è¯¢é¡¹ç®ä»»å¡è¯¦ç» |
| | | // export function getProjectTask(taskId) { |
| | | // return request({ |
| | | // url: "/oaSystem/project/task/" + taskId, |
| | | // method: "get" |
| | | // }); |
| | | // } |
| | | |
| | | // æ°å¢é¡¹ç®é¶æ®µä»»å¡ |
| | | export function addProjectTask(data) { |
| | | return request({ |
| | | url: "/oA/projectPhaseTask/add", |
| | | method: "post", |
| | | data: data |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹é¡¹ç®é¶æ®µä»»å¡ |
| | | export function updateProjectTask(data) { |
| | | return request({ |
| | | url: "/oA/projectPhaseTask/update", |
| | | method: "post", |
| | | data: data |
| | | }); |
| | | } |
| | | |
| | | // å é¤é¡¹ç®é¶æ®µä»»å¡ |
| | | export function delProjectTask(taskId) { |
| | | return request({ |
| | | url: "/oA/projectPhaseTask/delete/" + taskId, |
| | | method: "delete" |
| | | }); |
| | | } |
| | | |
| | | // 项ç®idæ¥è¯¢é¡¹ç®é¶æ®µå表 |
| | | export function listProjectPhase(projectId) { |
| | | return request({ |
| | | url: "/oA/projectPhase/listByProjectId/" + projectId, |
| | | method: "get" |
| | | }); |
| | | } |
| | | // æ°å¢é¡¹ç®é¶æ®µ |
| | | export function addProjectPhase(data) { |
| | | return request({ |
| | | url: "/oA/projectPhase/add", |
| | | method: "post", |
| | | data: data |
| | | }); |
| | | } |
| | | |
| | | // ä¿®æ¹é¡¹ç®é¶æ®µ |
| | | export function updateProjectPhase(data) { |
| | | return request({ |
| | | url: "/oA/projectPhase/update", |
| | | method: "post", |
| | | data: data |
| | | }); |
| | | |
| | | } |
| | | // å é¤é¡¹ç®é¶æ®µ |
| | | export function delProjectPhase(phaseId) { |
| | | return request({ |
| | | url: "/oA/projectPhase/delete/" + phaseId, |
| | | method: "delete" |
| | | }) |
| | | } |
| | | // |
| | | |
| | | // // æ¥è¯¢é¡¹ç®éç¨ç¢å表 |
| | | // export function listProjectMilestone(query) { |
| | | // return request({ |
| | | // url: "/oaSystem/project/milestone/list", |
| | | // method: "get", |
| | | // params: query |
| | | // }); |
| | | // } |
| | | |
| | | // // 项ç®ç»è®¡ä¿¡æ¯ |
| | | // export function getProjectStatistics() { |
| | | // return request({ |
| | | // url: "/oaSystem/project/statistics", |
| | | // method: "get" |
| | | // }); |
| | | // } |
| | |
| | | name: "DeviceInfo", |
| | | meta: { title: "设å¤ä¿¡æ¯", icon: "monitor" }, |
| | | }, |
| | | // æ·»å 项ç®è¯¦æ
页é¢è·¯ç±é
ç½® |
| | | { |
| | | path: "/oaSystem/projectManagement/projectDetail", |
| | | component: Layout, |
| | | hidden: true, |
| | | children: [ |
| | | { |
| | | path: ":projectId", |
| | | component: () => import("@/views/oaSystem/projectManagement/projectDetail.vue"), |
| | | name: "ProjectDetail", |
| | | meta: { title: "项ç®è¯¦æ
", activeMenu: "/oaSystem/projectManagement" }, |
| | | }, |
| | | ], |
| | | }, |
| | | ]; |
| | | |
| | | // å¨æè·¯ç±ï¼åºäºç¨æ·æé卿å»å è½½ |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // ... existing code ... |
| | | <template> |
| | | <div class="milestone-list-container"> |
| | | <el-timeline> |
| | | <el-timeline-item |
| | | v-for="milestone in milestoneList" |
| | | :key="milestone.phaseId" |
| | | :timestamp="milestone.endDate" |
| | | > |
| | | <el-card> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>{{ milestone.phaseName }}</span> |
| | | <div class="milestone-actions"> |
| | | <el-button type="text" size="small" @click="handleEdit(milestone)">ç¼è¾</el-button> |
| | | <el-button type="text" size="small" @click="handleDelete(milestone)" danger>å é¤</el-button> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | <div class="milestone-content"> |
| | | <p>{{ milestone.description }}</p> |
| | | <div class="milestone-status"> |
| | | <el-tag :type="getStatusType(milestone.status)">{{ getStatusText(milestone.status) }}</el-tag> |
| | | </div> |
| | | </div> |
| | | </el-card> |
| | | </el-timeline-item> |
| | | </el-timeline> |
| | | |
| | | <!-- æ éç¨ç¢æ¶çæç¤º --> |
| | | <div v-if="milestoneList.length === 0" class="empty-tip"> |
| | | <el-empty description="ææ éç¨ç¢æ°æ®" /> |
| | | </div> |
| | | |
| | | <!-- ç¼è¾éç¨ç¢å¯¹è¯æ¡ --> |
| | | <el-dialog |
| | | v-model="dialogVisible" |
| | | :title="'ç¼è¾éç¨ç¢: ' + (form.phaseName || '')" |
| | | width="600px" |
| | | :close-on-click-modal="false" |
| | | > |
| | | <el-form |
| | | ref="formRef" |
| | | :model="form" |
| | | :rules="rules" |
| | | label-width="100px" |
| | | > |
| | | <el-form-item label="éç¨ç¢åç§°" prop="phaseName"> |
| | | <el-input v-model="form.phaseName" placeholder="请è¾å
¥éç¨ç¢åç§°" /> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¥æ" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="form.startDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©å¼å§æ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¥æ" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="form.endDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©ç»ææ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select v-model="form.status" placeholder="è¯·éæ©ç¶æ"> |
| | | <el-option label="æªå¼å§" value="notStarted" /> |
| | | <el-option label="已宿" value="completed" /> |
| | | <el-option label="已延è¿" value="delayed" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="dialogVisible = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitEditForm">ç¡®å®</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, watch, reactive } from 'vue'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import { getProject, listProjectPhase, updateProjectPhase,delProjectPhase } from '@/api/oaSystem/projectManagement'; |
| | | |
| | | const props = defineProps({ |
| | | projectId: { |
| | | type: String, |
| | | required: true |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['refresh']); |
| | | |
| | | const milestoneList = ref([]); |
| | | const dialogVisible = ref(false); |
| | | const formRef = ref(null); |
| | | const form = reactive({ |
| | | phaseId: '', |
| | | phaseName: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | status: 'notStarted', |
| | | projectId: props.projectId |
| | | }); |
| | | |
| | | // 表åéªè¯è§å |
| | | const rules = { |
| | | phaseName: [ |
| | | { required: true, message: '请è¾å
¥éç¨ç¢åç§°', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: 'é¿åº¦å¨ 2 å° 50 个å符', trigger: 'blur' } |
| | | ], |
| | | status: [ |
| | | { required: true, message: 'è¯·éæ©ç¶æ', trigger: 'change' } |
| | | ] |
| | | }; |
| | | |
| | | // è·åéç¨ç¢å表 |
| | | const getMilestoneList = async () => { |
| | | try { |
| | | listProjectPhase(props.projectId).then(res => { |
| | | milestoneList.value = res.data.rows || res.data; |
| | | // æç®æ æ¥ææåº |
| | | // milestoneList.value.sort((a, b) => new Date(a.endDate) - new Date(b.endDate)); |
| | | }) |
| | | } catch (error) { |
| | | ElMessage.error('è·åéç¨ç¢å表失败'); |
| | | console.error('è·åéç¨ç¢å表失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // ç¼è¾éç¨ç¢ |
| | | const handleEdit = (milestone) => { |
| | | // å¤å¶éç¨ç¢æ°æ®å°è¡¨å |
| | | Object.assign(form, { |
| | | phaseId: milestone.phaseId, |
| | | phaseName: milestone.phaseName, |
| | | description: milestone.description, |
| | | endDate: milestone.endDate, |
| | | status: milestone.status, |
| | | projectId: props.projectId |
| | | }); |
| | | dialogVisible.value = true; |
| | | }; |
| | | |
| | | // æäº¤ç¼è¾è¡¨å |
| | | const submitEditForm = async () => { |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | // åéæ´æ°è¯·æ± |
| | | const res = await updateProjectPhase(form); |
| | | |
| | | if (res.code === 200) { |
| | | ElMessage.success('éç¨ç¢ç¼è¾æå'); |
| | | dialogVisible.value = false; |
| | | getMilestoneList(); // å·æ°å表 |
| | | emit('refresh'); // éç¥ç¶ç»ä»¶å·æ° |
| | | } else { |
| | | ElMessage.error(res.msg || 'éç¨ç¢ç¼è¾å¤±è´¥'); |
| | | } |
| | | } catch (error) { |
| | | if (error.name === 'ValidationError') { |
| | | // 表åéªè¯å¤±è´¥ï¼Element Plusä¼èªå¨æç¤º |
| | | return; |
| | | } |
| | | ElMessage.error('éç¨ç¢ç¼è¾å¤±è´¥'); |
| | | console.error('ç¼è¾éç¨ç¢å¤±è´¥:', error); |
| | | } |
| | | }; |
| | | |
| | | // å é¤éç¨ç¢ |
| | | const handleDelete = (milestone) => { |
| | | ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤éç¨ç¢ "${milestone.phaseName}" åï¼å é¤åå°æ æ³æ¢å¤ã`, |
| | | 'å é¤ç¡®è®¤', |
| | | { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | } |
| | | ) |
| | | .then(async () => { |
| | | try { |
| | | // è°ç¨å é¤API |
| | | const res = await delProjectPhase(milestone.phaseId); |
| | | |
| | | if (res.code === 200) { |
| | | ElMessage.success('éç¨ç¢å 餿å'); |
| | | getMilestoneList(); // å·æ°å表 |
| | | emit('refresh'); // éç¥ç¶ç»ä»¶å·æ° |
| | | } else { |
| | | ElMessage.error(res.msg || 'éç¨ç¢å é¤å¤±è´¥'); |
| | | } |
| | | } catch (error) { |
| | | ElMessage.error('éç¨ç¢å é¤å¤±è´¥'); |
| | | console.error('å é¤éç¨ç¢å¤±è´¥:', error); |
| | | } |
| | | }) |
| | | .catch(() => { |
| | | // ç¨æ·åæ¶å é¤ |
| | | ElMessage.info('已忶å é¤'); |
| | | }); |
| | | }; |
| | | |
| | | // è·åç¶ææ ç¾ç±»å |
| | | const getStatusType = (status) => { |
| | | const statusTypeMap = { |
| | | notStarted: 'info', |
| | | completed: 'success', |
| | | delayed: 'danger' |
| | | }; |
| | | return statusTypeMap[status] || 'default'; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusTextMap = { |
| | | notStarted: 'æªå¼å§', |
| | | completed: '已宿', |
| | | delayed: '已延è¿' |
| | | }; |
| | | return statusTextMap[status] || status; |
| | | }; |
| | | |
| | | // çå¬é¡¹ç®IDåå |
| | | watch(() => props.projectId, () => { |
| | | if (props.projectId) { |
| | | getMilestoneList(); |
| | | } |
| | | }); |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | if (props.projectId) { |
| | | getMilestoneList(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .milestone-list-container { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .milestone-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | } |
| | | |
| | | .milestone-content { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .milestone-status { |
| | | margin-top: 10px; |
| | | } |
| | | |
| | | .empty-tip { |
| | | margin-top: 40px; |
| | | text-align: center; |
| | | } |
| | | |
| | | .dialog-footer { |
| | | display: flex; |
| | | justify-content: flex-end; |
| | | gap: 10px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | |
| | | <template> |
| | | <div class="phase-goal-list-container"> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="phaseGoalList" |
| | | style="width: 100%" |
| | | > |
| | | <el-table-column prop="phaseName" label="æå±é¶æ®µ" width="180" /> |
| | | <el-table-column prop="taskName" label="ç®æ åç§°" min-width="200" /> |
| | | <el-table-column prop="targetValue" label="ç®æ å¼" width="120" /> |
| | | <el-table-column prop="currentValue" label="å½åå¼" width="120" /> |
| | | <el-table-column prop="unit" label="åä½" width="80" /> |
| | | <el-table-column prop="targetDate" label="ç®æ æ¥æ" width="120" /> |
| | | <el-table-column prop="status" label="ç¶æ" width="100"> |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column prop="completionRate" label="å®æåº¦" width="120"> |
| | | <template #default="scope"> |
| | | <el-progress :percentage="scope.row.completionRate" :stroke-width="6" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column label="æä½" width="150" fixed="right"> |
| | | <template #default="scope"> |
| | | <el-button type="text" size="small" @click="handleEdit(scope.row)">ç¼è¾</el-button> |
| | | <el-button type="text" size="small" @click="handleDelete(scope.row)" danger>å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- æ é¶æ®µç®æ æ¶çæç¤º --> |
| | | <div v-if="!loading && phaseGoalList.length === 0" class="empty-tip"> |
| | | <el-empty description="ææ é¶æ®µç®æ æ°æ®" /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, onMounted, watch } from 'vue'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import { listProjectPhase, updateProjectTask, delProjectTask } from '@/api/oaSystem/projectManagement'; |
| | | |
| | | const props = defineProps({ |
| | | projectId: { |
| | | type: String, |
| | | required: true |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['refresh']); |
| | | |
| | | const phaseGoalList = ref([]); |
| | | const loading = ref(true); |
| | | |
| | | // è·åé¶æ®µç®æ å表 |
| | | const getPhaseGoalList = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const { data } = await listProjectPhase(props.projectId); |
| | | // å设phaseæ°æ®ä¸å
å«äºç®æ ä¿¡æ¯ |
| | | // è¿éç®åå¤çï¼å®é
åºè¯¥è°ç¨ä¸é¨çè·åé¶æ®µç®æ çAPI |
| | | const phases = data.rows || data; |
| | | phaseGoalList.value = []; |
| | | |
| | | phases.forEach(phase => { |
| | | if (phase.oaProjectPhaseTasks && phase.oaProjectPhaseTasks.length > 0) { |
| | | phase.oaProjectPhaseTasks.forEach(goal => { |
| | | phaseGoalList.value.push({ |
| | | ...goal, |
| | | phaseName: phase.phaseName |
| | | }); |
| | | }); |
| | | } |
| | | }); |
| | | } catch (error) { |
| | | ElMessage.error('è·åé¶æ®µç®æ å表失败'); |
| | | console.error('è·åé¶æ®µç®æ å表失败:', error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // ç¼è¾é¶æ®µç®æ |
| | | const handleEdit = (goal) => { |
| | | // 触åç¶ç»ä»¶çç¼è¾äºä»¶ï¼ä¼ éç®æ æ°æ® |
| | | emit('editGoal', goal); |
| | | }; |
| | | |
| | | // å é¤é¶æ®µç®æ |
| | | const handleDelete = async (goal) => { |
| | | try { |
| | | await ElMessageBox.confirm( |
| | | `ç¡®å®è¦å é¤ç®æ ã${goal.taskName}ãåï¼`, |
| | | '确认å é¤', |
| | | { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | } |
| | | ); |
| | | |
| | | // è°ç¨å é¤API |
| | | await delProjectTask(goal.taskId); |
| | | ElMessage.success('å é¤é¶æ®µç®æ æå'); |
| | | |
| | | // å·æ°å表 |
| | | getPhaseGoalList(); |
| | | emit('refresh'); |
| | | } catch (error) { |
| | | if (error !== 'cancel') { |
| | | ElMessage.error('å é¤é¶æ®µç®æ 失败'); |
| | | console.error('å é¤é¶æ®µç®æ 失败:', error); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // è·åç¶ææ ç¾ç±»å |
| | | const getStatusType = (status) => { |
| | | const statusTypeMap = { |
| | | notStarted: 'info', |
| | | inProgress: 'primary', |
| | | completed: 'success', |
| | | delayed: 'danger' |
| | | }; |
| | | return statusTypeMap[status] || 'default'; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusTextMap = { |
| | | notStarted: 'æªå¼å§', |
| | | inProgress: 'è¿è¡ä¸', |
| | | completed: '已宿', |
| | | delayed: '已延è¿' |
| | | }; |
| | | return statusTextMap[status] || status; |
| | | }; |
| | | |
| | | // çå¬é¡¹ç®IDåå |
| | | watch(() => props.projectId, () => { |
| | | if (props.projectId) { |
| | | getPhaseGoalList(); |
| | | } |
| | | }); |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | if (props.projectId) { |
| | | getPhaseGoalList(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .phase-goal-list-container { |
| | | padding: 10px 0; |
| | | } |
| | | |
| | | .empty-tip { |
| | | margin-top: 40px; |
| | | text-align: center; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <el-form :model="form" :rules="rules" ref="formRef" label-width="100px"> |
| | | <el-form-item label="项ç®åç§°" prop="projectName"> |
| | | <el-input |
| | | v-model="form.projectName" |
| | | placeholder="请è¾å
¥é¡¹ç®åç§°" |
| | | maxlength="50" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="é¡¹ç®æè¿°" prop="description"> |
| | | <el-input |
| | | v-model="form.description" |
| | | type="textarea" |
| | | :rows="4" |
| | | placeholder="请è¾å
¥é¡¹ç®æè¿°" |
| | | /> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¥æ" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="form.startDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©å¼å§æ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¥æ" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="form.endDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©ç»ææ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="项ç®è´è´£äºº" prop="managerId"> |
| | | <!-- <el-input |
| | | v-model="form.managerName" |
| | | placeholder="è¯·éæ©é¡¹ç®è´è´£äºº" |
| | | readonly |
| | | @focus="showUserSelect = true" |
| | | /> --> |
| | | <!-- <el-input |
| | | v-model="userSearchKey" |
| | | placeholder="æç´¢ç¨æ·" |
| | | v-if="showUserSelect" |
| | | @keyup.enter="searchUsers" |
| | | style="margin-top: 10px" |
| | | /> --> |
| | | <el-select |
| | | v-model="form.managerId" |
| | | style="width: 100%; margin-top: 10px" |
| | | @change="selectUser" |
| | | filterable |
| | | remote |
| | | :remote-method="searchUsers" |
| | | :loading="userLoading" |
| | | placeholder="鿩项ç®è´è´£äºº" |
| | | > |
| | | <el-option |
| | | v-for="user in userList" |
| | | :key="user.userId" |
| | | :label="user.nickName" |
| | | :value="user.userId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="项ç®å®æåº¦" prop="completionRate"> |
| | | <el-slider v-model="form.completionRate" :max="100" /> |
| | | </el-form-item> |
| | | <el-form-item label="项ç®ç¶æ" prop="status"> |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio label="planning">è§åä¸</el-radio> |
| | | <el-radio label="inProgress">è¿è¡ä¸</el-radio> |
| | | <el-radio label="completed">已宿</el-radio> |
| | | <el-radio label="paused">å·²æå</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-form> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, watch, defineExpose } from 'vue'; |
| | | import { listUser } from '@/api/system/user'; |
| | | import { ElMessage } from 'element-plus'; |
| | | const props = defineProps({ |
| | | form: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | rules: { |
| | | type: Object, |
| | | required: true |
| | | }, |
| | | visible: { |
| | | type: Boolean, |
| | | required: true |
| | | } |
| | | }); |
| | | |
| | | const formRef = ref(); |
| | | const showUserSelect = ref(false); |
| | | const userSearchKey = ref(''); |
| | | const userList = ref([]); |
| | | const userLoading = ref(false); |
| | | // 模æç¨æ·æ°æ® |
| | | const mockUserData = [ |
| | | { userId: '1', nickName: 'å¼ ä¸', userName: 'zhangsan' }, |
| | | { userId: '2', nickName: 'æå', userName: 'lisi' }, |
| | | { userId: '3', nickName: 'çäº', userName: 'wangwu' }, |
| | | { userId: '4', nickName: 'èµµå
', userName: 'zhaoliu' }, |
| | | { userId: '5', nickName: 'é±ä¸', userName: 'qianqi' }, |
| | | { userId: '6', nickName: 'åå
«', userName: 'sunba' }, |
| | | { userId: '7', nickName: 'å¨ä¹', userName: 'zhoujiu' }, |
| | | { userId: '8', nickName: 'å´å', userName: 'wushi' } |
| | | ]; |
| | | // æç´¢ç¨æ· |
| | | const searchUsers = async (query) => { |
| | | userLoading.value = true; |
| | | try { |
| | | // 模æç½ç»å»¶è¿ |
| | | await new Promise(resolve => setTimeout(resolve, 300)); |
| | | |
| | | // æ£ç¡®è®¾ç½®ç¨æ·å表 |
| | | if (!query) { |
| | | userList.value = [...mockUserData]; |
| | | } else { |
| | | userList.value = mockUserData.filter(user => |
| | | user.nickName.includes(query) || user.userName.includes(query) |
| | | ); |
| | | } |
| | | } catch (error) { |
| | | console.error('æç´¢ç¨æ·å¤±è´¥:', error); |
| | | } finally { |
| | | userLoading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // éæ©ç¨æ· |
| | | const selectUser = (userId) => { |
| | | // å
ä»userListæ¥æ¾ï¼å¦ææ¾ä¸å°åä»mockUserData䏿¥æ¾ |
| | | let selectedUser = userList.value.find(user => user.userId === userId); |
| | | if (!selectedUser) { |
| | | selectedUser = mockUserData.find(user => user.userId === userId); |
| | | } |
| | | |
| | | // 使ç¨Vue.setç¡®ä¿ååºå¼æ´æ° |
| | | if (selectedUser) { |
| | | Object.assign(props.form, { managerName: selectedUser.nickName }); |
| | | } else { |
| | | Object.assign(props.form, { managerName: '' }); |
| | | } |
| | | showUserSelect.value = false; |
| | | }; |
| | | |
| | | // é置表å |
| | | const resetFields = () => { |
| | | if (formRef.value) { |
| | | formRef.value.resetFields(); |
| | | } |
| | | showUserSelect.value = false; |
| | | userSearchKey.value = ''; |
| | | userList.value = []; |
| | | }; |
| | | |
| | | // éªè¯è¡¨å |
| | | const validate = () => { |
| | | return new Promise((resolve, reject) => { |
| | | if (formRef.value) { |
| | | formRef.value.validate((valid) => { |
| | | if (valid) { |
| | | // é¢å¤éªè¯ï¼ç»ææ¥æä¸è½æ©äºå¼å§æ¥æ |
| | | if (props.form.startDate && props.form.endDate) { |
| | | const startDate = new Date(props.form.startDate); |
| | | const endDate = new Date(props.form.endDate); |
| | | if (endDate < startDate) { |
| | | ElMessage.error('ç»ææ¥æä¸è½æ©äºå¼å§æ¥æ'); |
| | | reject(new Error('æ¥æéªè¯å¤±è´¥')); |
| | | return; |
| | | } |
| | | } |
| | | resolve(); |
| | | } else { |
| | | reject(new Error('表åéªè¯å¤±è´¥')); |
| | | } |
| | | }); |
| | | } else { |
| | | resolve(); |
| | | } |
| | | }); |
| | | }; |
| | | |
| | | // çå¬å¯¹è¯æ¡æ¾ç¤ºç¶æ |
| | | watch(() => props.visible, (newVisible) => { |
| | | if (!newVisible) { |
| | | resetFields(); |
| | | // å»¶è¿éç½®ï¼ç¡®ä¿æ°æ®å·²ç»æäº¤å°å端 |
| | | // setTimeout(() => { |
| | | // resetFields(); |
| | | // }, 300); |
| | | } |
| | | }); |
| | | |
| | | // æ´é²æ¹æ³ |
| | | defineExpose({ |
| | | resetFields, |
| | | validate |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="task-tree-container"> |
| | | <!-- 任塿 æä½æé® --> |
| | | <div class="tree-actions mb10"> |
| | | <el-button type="primary" size="small" icon="Plus" @click="handleAddTask">æ·»å ä»»å¡</el-button> |
| | | <el-button type="success" size="small" icon="RefreshRight" @click="refreshTree">å·æ°</el-button> |
| | | <el-button type="info" size="small" icon="Filter" @click="toggleFilter"> |
| | | {{ showFilter ? 'éèçé' : 'æ¾ç¤ºçé' }} |
| | | </el-button> |
| | | </div> |
| | | |
| | | <!-- ç鿡件 --> |
| | | <div v-if="showFilter" class="filter-section mb10"> |
| | | <el-form :inline="true" :model="filterParams"> |
| | | <el-form-item label="ä»»å¡ç¶æ"> |
| | | <el-select v-model="filterParams.status" placeholder="å
¨é¨" clearable style="width: 120px"> |
| | | <el-option label="æªå¼å§" value="notStarted" /> |
| | | <el-option label="è¿è¡ä¸" value="inProgress" /> |
| | | <el-option label="已宿" value="completed" /> |
| | | <el-option label="已龿" value="overdue" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è´è´£äºº"> |
| | | <el-input v-model="filterParams.assignee" placeholder="è¾å
¥è´è´£äºº" clearable style="width: 150px" /> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" size="small" @click="filterTree">çé</el-button> |
| | | <el-button size="small" @click="resetFilter">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | </div> |
| | | |
| | | <!-- 任塿 --> |
| | | <div class="tree-content"> |
| | | <el-tree |
| | | v-loading="loading" |
| | | :data="taskTreeData" |
| | | :props="defaultProps" |
| | | :expand-on-click-node="false" |
| | | node-key="nodeId" |
| | | ref="treeRef" |
| | | @node-contextmenu="handleContextMenu" |
| | | @node-click="handleNodeClick" |
| | | > |
| | | <template #default="{ node, data }"> |
| | | <!-- èç¹å
容 --> |
| | | <div class="tree-node-content" :class="{ 'phase-node': data.type === 'phase', 'task-node': data.type === 'task' }"> |
| | | <!-- èç¹å¾æ --> |
| | | <div class="node-icon"> |
| | | <i v-if="data.type === 'phase'" class="el-icon-folder text-primary" /> |
| | | <i v-else-if="data.status === 'completed'" class="el-icon-circle-check text-success" /> |
| | | <i v-else-if="data.status === 'inProgress'" class="el-icon-circle-check text-primary" /> |
| | | <i v-else-if="data.status === 'overdue'" class="el-icon-alarm-clock text-danger" /> |
| | | <i v-else class="el-icon-circle-close text-gray-400" /> |
| | | </div> |
| | | |
| | | <!-- èç¹æ é¢åæè¿° --> |
| | | <div class="node-info"> |
| | | <div class="node-title" :class="{ 'overdue-title': data.type === 'task' && data.status === 'overdue' }"> |
| | | {{ node.label }} |
| | | <span v-if="data.type === 'task' && data.priority === 'high'" class="priority-tag">é«ä¼</span> |
| | | <span v-else-if="data.type === 'task' && data.priority === 'medium'" class="priority-tag medium">ä¸ä¼</span> |
| | | </div> |
| | | <div v-if="data.description" class="node-description">{{ data.description }}</div> |
| | | |
| | | <!-- ä»»å¡å
ä¿¡æ¯ --> |
| | | <div v-if="data.type === 'task'" class="task-meta"> |
| | | <span class="meta-item"> |
| | | <i class="el-icon-user"></i> |
| | | {{ data.assigneeName || 'æªåé
' }} |
| | | </span> |
| | | <span class="meta-item"> |
| | | <i class="el-icon-calendar"></i> |
| | | {{ formatDateRange(data.startDate, data.endDate) }} |
| | | </span> |
| | | </div> |
| | | </div> |
| | | |
| | | <!-- ä»»å¡è¿åº¦æ¡ --> |
| | | <div v-if="data.type === 'task'" class="task-progress"> |
| | | <el-progress :percentage="data.progress || 0" :stroke-width="4" :show-text="false" /> |
| | | </div> |
| | | |
| | | <!-- æä½æé® --> |
| | | <div class="node-actions"> |
| | | <el-button |
| | | v-if="data.type === 'task'" |
| | | type="text" |
| | | size="small" |
| | | icon="Edit" |
| | | @click.stop="handleEditTask(data)" |
| | | v-hasPermi="['oaSystem:task:edit']" |
| | | /> |
| | | <el-button |
| | | v-if="data.type === 'phase'" |
| | | type="text" |
| | | size="small" |
| | | icon="Plus" |
| | | @click.stop="handleAddTaskUnderPhase(data)" |
| | | v-hasPermi="['oaSystem:task:add']" |
| | | /> |
| | | <el-button |
| | | type="text" |
| | | size="small" |
| | | icon="Delete" |
| | | @click.stop="handleDeleteNode(data)" |
| | | v-hasPermi="['oaSystem:task:remove']" |
| | | /> |
| | | </div> |
| | | </div> |
| | | </template> |
| | | </el-tree> |
| | | </div> |
| | | |
| | | <!-- å³é®èå --> |
| | | <div v-if="showContextMenu" :style="contextMenuStyle" class="context-menu"> |
| | | <el-menu @select="handleContextMenuSelect"> |
| | | <el-menu-item v-if="selectedNode.type === 'task'" index="edit">ç¼è¾ä»»å¡</el-menu-item> |
| | | <el-menu-item v-if="selectedNode.type === 'phase'" index="addTask">æ·»å åä»»å¡</el-menu-item> |
| | | <el-menu-item index="delete">å é¤</el-menu-item> |
| | | <el-menu-item index="expandAll">å±å¼å
¨é¨</el-menu-item> |
| | | <el-menu-item index="collapseAll">æ¶èµ·å
¨é¨</el-menu-item> |
| | | </el-menu> |
| | | </div> |
| | | |
| | | <!-- ä»»å¡è¡¨åå¯¹è¯æ¡ --> |
| | | <el-dialog :title="dialogTitle" v-model="dialogOpen" width="600px" append-to-body> |
| | | <el-form ref="taskFormRef" :model="taskForm" :rules="taskRules" label-width="80px"> |
| | | <el-form-item label="ä»»å¡åç§°" prop="taskName"> |
| | | <el-input v-model="taskForm.taskName" placeholder="请è¾å
¥ä»»å¡åç§°" /> |
| | | </el-form-item> |
| | | <el-form-item label="è´è´£äºº" prop="assigneeId"> |
| | | <el-input v-model="taskForm.assigneeId" placeholder="请è¾å
¥è´è´£äººID" /> |
| | | </el-form-item> |
| | | <el-form-item label="å¼å§æ¥æ" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="taskForm.startDate" |
| | | type="date" |
| | | placeholder="éæ©å¼å§æ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç»ææ¥æ" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="taskForm.endDate" |
| | | type="date" |
| | | placeholder="éæ©ç»ææ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ä¼å
级" prop="priority"> |
| | | <el-select v-model="taskForm.priority" placeholder="éæ©ä¼å
级"> |
| | | <el-option label="ä½" value="low" /> |
| | | <el-option label="ä¸" value="medium" /> |
| | | <el-option label="é«" value="high" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="è¿åº¦" prop="progress"> |
| | | <el-input-number v-model="taskForm.progress" :min="0" :max="100" style="width: 100%" /> |
| | | </el-form-item> |
| | | <el-form-item label="æè¿°" prop="description"> |
| | | <el-input v-model="taskForm.description" type="textarea" placeholder="请è¾å
¥ä»»å¡æè¿°" /> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="dialogOpen = false">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitTaskForm">ç¡®å®</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, watch, onMounted } from 'vue'; |
| | | import { ElMessage, ElMessageBox, ElMenu, ElMenuItem } from 'element-plus'; |
| | | // import { getProject, addTask, updateTask, deleteTask, deletePhase } from '@/api/oaSystem/projectManagement'; |
| | | |
| | | const props = defineProps({ |
| | | projectId: { |
| | | type: String, |
| | | required: true |
| | | } |
| | | }); |
| | | |
| | | const emit = defineEmits(['refresh']); |
| | | |
| | | // ç»ä»¶ç¶æ |
| | | const loading = ref(false); |
| | | const treeRef = ref(); |
| | | const showContextMenu = ref(false); |
| | | const contextMenuStyle = ref({}); |
| | | const selectedNode = ref({}); |
| | | const showFilter = ref(false); |
| | | const dialogOpen = ref(false); |
| | | const dialogTitle = ref(''); |
| | | const taskFormRef = ref(); |
| | | |
| | | // çéåæ° |
| | | const filterParams = reactive({ |
| | | status: '', |
| | | assignee: '' |
| | | }); |
| | | |
| | | // ä»»å¡è¡¨åæ°æ® |
| | | const taskForm = reactive({ |
| | | taskId: undefined, |
| | | taskName: '', |
| | | description: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | assigneeId: '', |
| | | assigneeName: '', |
| | | status: 'notStarted', |
| | | progress: 0, |
| | | priority: 'medium', |
| | | phaseId: '', |
| | | projectId: props.projectId |
| | | }); |
| | | |
| | | // 表åéªè¯è§å |
| | | const taskRules = { |
| | | taskName: [ |
| | | { required: true, message: 'ä»»å¡åç§°ä¸è½ä¸ºç©º', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: 'ä»»å¡åç§°é¿åº¦å¨ 2 å° 50 个å符', trigger: 'blur' } |
| | | ], |
| | | startDate: [ |
| | | { required: true, message: 'å¼å§æ¥æä¸è½ä¸ºç©º', trigger: 'change' } |
| | | ], |
| | | endDate: [ |
| | | { required: true, message: 'ç»ææ¥æä¸è½ä¸ºç©º', trigger: 'change' } |
| | | ], |
| | | assigneeId: [ |
| | | { required: true, message: 'è´è´£äººä¸è½ä¸ºç©º', trigger: 'blur' } |
| | | ], |
| | | progress: [ |
| | | { required: true, message: 'è¿åº¦ä¸è½ä¸ºç©º', trigger: 'blur' }, |
| | | { type: 'number', min: 0, max: 100, message: 'è¿åº¦å¿
é¡»å¨ 0 å° 100 ä¹é´', trigger: 'blur' } |
| | | ] |
| | | }; |
| | | |
| | | // 任塿 æ°æ® |
| | | const rawTaskTreeData = ref([]); |
| | | |
| | | // 模æä»»å¡æ°æ® |
| | | const mockTaskData = { |
| | | 'PRJ2023001': [ |
| | | { |
| | | phaseId: 'PHASE001', |
| | | phaseName: 'éæ±åæ', |
| | | startDate: '2023-11-01', |
| | | endDate: '2023-11-15', |
| | | status: 'completed', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK001', |
| | | taskName: 'éæ±è°ç ', |
| | | description: 'è°ç ç¨æ·éæ±åä¸å¡æµç¨', |
| | | startDate: '2023-11-01', |
| | | endDate: '2023-11-05', |
| | | assigneeId: 'USER001', |
| | | assigneeName: 'å¼ ä¸', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'medium' |
| | | }, |
| | | { |
| | | taskId: 'TASK002', |
| | | taskName: 'éæ±ææ¡£ç¼å', |
| | | description: 'ç¼å详ç»çéæ±è§æ ¼è¯´æä¹¦', |
| | | startDate: '2023-11-06', |
| | | endDate: '2023-11-15', |
| | | assigneeId: 'USER002', |
| | | assigneeName: 'æå', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | phaseId: 'PHASE002', |
| | | phaseName: 'ç³»ç»è®¾è®¡', |
| | | startDate: '2023-11-16', |
| | | endDate: '2023-12-10', |
| | | status: 'completed', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK003', |
| | | taskName: 'ç³»ç»æ¶æè®¾è®¡', |
| | | description: 'è®¾è®¡ç³»ç»æ´ä½æ¶æ', |
| | | startDate: '2023-11-16', |
| | | endDate: '2023-11-25', |
| | | assigneeId: 'USER003', |
| | | assigneeName: 'çäº', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'high' |
| | | }, |
| | | { |
| | | taskId: 'TASK004', |
| | | taskName: 'æ°æ®åºè®¾è®¡', |
| | | description: 'è®¾è®¡æ°æ®åºè¡¨ç»æåå
³ç³»', |
| | | startDate: '2023-11-26', |
| | | endDate: '2023-12-10', |
| | | assigneeId: 'USER004', |
| | | assigneeName: 'èµµå
', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'medium' |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | phaseId: 'PHASE003', |
| | | phaseName: 'å¼åå®ç°', |
| | | startDate: '2023-12-11', |
| | | endDate: '2024-01-31', |
| | | status: 'inProgress', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK005', |
| | | taskName: 'å端å¼å', |
| | | description: 'å¼åç¨æ·çé¢å交äºé»è¾', |
| | | startDate: '2023-12-11', |
| | | endDate: '2024-01-15', |
| | | assigneeId: 'USER005', |
| | | assigneeName: 'é±ä¸', |
| | | status: 'inProgress', |
| | | progress: 70, |
| | | priority: 'high' |
| | | }, |
| | | { |
| | | taskId: 'TASK006', |
| | | taskName: 'å端å¼å', |
| | | description: 'å¼åä¸å¡é»è¾åAPIæ¥å£', |
| | | startDate: '2023-12-11', |
| | | endDate: '2024-01-20', |
| | | assigneeId: 'USER006', |
| | | assigneeName: 'åå
«', |
| | | status: 'inProgress', |
| | | progress: 60, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | } |
| | | ], |
| | | // é»è®¤æ°æ® |
| | | default: [ |
| | | { |
| | | phaseId: 'PHASE_DEFAULT1', |
| | | phaseName: 'åå¤é¶æ®µ', |
| | | startDate: '2023-01-01', |
| | | endDate: '2023-03-31', |
| | | status: 'completed', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK_DEFAULT1', |
| | | taskName: '项ç®å¯å¨', |
| | | description: 'å¬å¼é¡¹ç®å¯å¨ä¼è®®', |
| | | startDate: '2023-01-01', |
| | | endDate: '2023-01-05', |
| | | assigneeId: 'USER_DEFAULT1', |
| | | assigneeName: 'è´è´£äººA', |
| | | status: 'completed', |
| | | progress: 100, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | }, |
| | | { |
| | | phaseId: 'PHASE_DEFAULT2', |
| | | phaseName: 'æ§è¡é¶æ®µ', |
| | | startDate: '2023-04-01', |
| | | endDate: '2023-09-30', |
| | | status: 'inProgress', |
| | | tasks: [ |
| | | { |
| | | taskId: 'TASK_DEFAULT2', |
| | | taskName: 'æ ¸å¿åè½å¼å', |
| | | description: 'å¼åç³»ç»æ ¸å¿åè½æ¨¡å', |
| | | startDate: '2023-04-01', |
| | | endDate: '2023-06-30', |
| | | assigneeId: 'USER_DEFAULT2', |
| | | assigneeName: 'è´è´£äººB', |
| | | status: 'inProgress', |
| | | progress: 50, |
| | | priority: 'high' |
| | | } |
| | | ] |
| | | } |
| | | ] |
| | | }; |
| | | |
| | | const taskTreeData = computed(() => { |
| | | // åºç¨ç鿡件 |
| | | if (!showFilter.value || (!filterParams.status && !filterParams.assignee)) { |
| | | return rawTaskTreeData.value; |
| | | } |
| | | |
| | | // æ·±æ·è´åå§æ°æ®ä»¥é¿å
ä¿®æ¹ |
| | | const filteredData = JSON.parse(JSON.stringify(rawTaskTreeData.value)); |
| | | |
| | | // éå½çéèç¹ |
| | | const filterNodes = (nodes) => { |
| | | const result = []; |
| | | |
| | | nodes.forEach(node => { |
| | | // 对äºé¶æ®µèç¹ï¼æ£æ¥å
¶å任塿¯å¦ç¬¦åç鿡件 |
| | | if (node.type === 'phase' && node.children) { |
| | | const filteredChildren = filterNodes(node.children); |
| | | if (filteredChildren.length > 0) { |
| | | // ä¿çè³å°æä¸ä¸ªåä»»å¡ç¬¦åæ¡ä»¶çé¶æ®µ |
| | | node.children = filteredChildren; |
| | | result.push(node); |
| | | } |
| | | } |
| | | // 对äºä»»å¡èç¹ï¼ç´æ¥åºç¨ç鿡件 |
| | | else if (node.type === 'task') { |
| | | const statusMatch = !filterParams.status || node.status === filterParams.status; |
| | | const assigneeMatch = !filterParams.assignee || |
| | | (node.assigneeName && node.assigneeName.includes(filterParams.assignee)); |
| | | |
| | | if (statusMatch && assigneeMatch) { |
| | | result.push(node); |
| | | } |
| | | } |
| | | }); |
| | | |
| | | return result; |
| | | }; |
| | | |
| | | return filterNodes(filteredData); |
| | | }); |
| | | |
| | | // æ èç¹é
ç½® |
| | | const defaultProps = { |
| | | children: 'children', |
| | | label: (data) => { |
| | | if (data.type === 'phase') { |
| | | return `${data.phaseName}`; |
| | | } else { |
| | | return `${data.taskName}`; |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // å è½½ä»»å¡æ æ°æ® |
| | | const loadTaskTree = async () => { |
| | | loading.value = true; |
| | | // try { |
| | | // const { data } = await getProject(props.projectId); |
| | | // rawTaskTreeData.value = buildTaskTree(data.phases || []); |
| | | // } catch (error) { |
| | | // ElMessage.error('å è½½ä»»å¡æ 失败'); |
| | | // console.error('å è½½ä»»å¡æ 失败:', error); |
| | | // } finally { |
| | | // loading.value = false; |
| | | // } |
| | | try { |
| | | // 模æç½ç»å»¶è¿ |
| | | await new Promise(resolve => setTimeout(resolve, 500)); |
| | | |
| | | // ä½¿ç¨æ¨¡ææ°æ®æ¿ä»£APIè¯·æ± |
| | | const phases = mockTaskData[props.projectId] || mockTaskData.default; |
| | | rawTaskTreeData.value = buildTaskTree(phases); |
| | | } catch (error) { |
| | | ElMessage.error('å è½½ä»»å¡æ 失败'); |
| | | console.error('å è½½ä»»å¡æ 失败:', error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // æå»ºä»»å¡æ |
| | | const buildTaskTree = (phases) => { |
| | | return phases.map(phase => ({ |
| | | nodeId: phase.phaseId, |
| | | phaseId: phase.phaseId, |
| | | phaseName: phase.phaseName, |
| | | type: 'phase', |
| | | children: (phase.tasks || []).map(task => ({ |
| | | nodeId: task.taskId, |
| | | taskId: task.taskId, |
| | | taskName: task.taskName, |
| | | description: task.description, |
| | | startDate: task.startDate, |
| | | endDate: task.endDate, |
| | | assigneeId: task.assigneeId, |
| | | assigneeName: task.assigneeName, |
| | | status: task.status, |
| | | progress: task.progress, |
| | | priority: task.priority, |
| | | phaseId: task.phaseId, |
| | | projectId: props.projectId, |
| | | type: 'task' |
| | | })) |
| | | })); |
| | | }; |
| | | |
| | | // æ ¼å¼åæ¥æèå´ |
| | | const formatDateRange = (startDate, endDate) => { |
| | | if (!startDate || !endDate) return ''; |
| | | return `${startDate} - ${endDate}`; |
| | | }; |
| | | |
| | | // å·æ°æ |
| | | const refreshTree = () => { |
| | | loadTaskTree(); |
| | | // éç¥ç¶ç»ä»¶å·æ°æ°æ® |
| | | emit('refresh'); |
| | | }; |
| | | |
| | | // 忢çé颿¿ |
| | | const toggleFilter = () => { |
| | | showFilter.value = !showFilter.value; |
| | | }; |
| | | |
| | | // åºç¨çé |
| | | const filterTree = () => { |
| | | // çéé»è¾å·²ç»å¨computedä¸å®ç° |
| | | }; |
| | | |
| | | // éç½®çé |
| | | const resetFilter = () => { |
| | | filterParams.status = ''; |
| | | filterParams.assignee = ''; |
| | | }; |
| | | |
| | | // å¤çèç¹ç¹å» |
| | | const handleNodeClick = (data, node) => { |
| | | // 忢å±å¼/æ¶èµ·ç¶æ |
| | | if (data.type === 'phase') { |
| | | node.expanded = !node.expanded; |
| | | } |
| | | }; |
| | | |
| | | // å¤çå³é®èå |
| | | const handleContextMenu = (event, data) => { |
| | | event.preventDefault(); |
| | | selectedNode.value = data; |
| | | contextMenuStyle.value = { |
| | | position: 'fixed', |
| | | left: `${event.clientX}px`, |
| | | top: `${event.clientY}px`, |
| | | zIndex: 1000 |
| | | }; |
| | | showContextMenu.value = true; |
| | | }; |
| | | |
| | | // å¤çå³é®èåéæ© |
| | | const handleContextMenuSelect = (index) => { |
| | | showContextMenu.value = false; |
| | | switch (index) { |
| | | case 'edit': |
| | | if (selectedNode.value.type === 'task') { |
| | | handleEditTask(selectedNode.value); |
| | | } |
| | | break; |
| | | case 'addTask': |
| | | if (selectedNode.value.type === 'phase') { |
| | | handleAddTaskUnderPhase(selectedNode.value); |
| | | } |
| | | break; |
| | | case 'delete': |
| | | handleDeleteNode(selectedNode.value); |
| | | break; |
| | | case 'expandAll': |
| | | treeRef.value?.expandAll(); |
| | | break; |
| | | case 'collapseAll': |
| | | treeRef.value?.collapseAll(); |
| | | break; |
| | | } |
| | | }; |
| | | |
| | | // æ·»å ä»»å¡ |
| | | const handleAddTask = () => { |
| | | resetTaskForm(); |
| | | dialogTitle.value = 'æ·»å ä»»å¡'; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // 卿å®é¶æ®µä¸æ·»å ä»»å¡ |
| | | const handleAddTaskUnderPhase = (phase) => { |
| | | resetTaskForm(); |
| | | taskForm.phaseId = phase.phaseId; |
| | | dialogTitle.value = 'æ·»å åä»»å¡'; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // ç¼è¾ä»»å¡ |
| | | const handleEditTask = (task) => { |
| | | resetTaskForm(); |
| | | Object.assign(taskForm, { ...task }); |
| | | dialogTitle.value = 'ç¼è¾ä»»å¡'; |
| | | dialogOpen.value = true; |
| | | }; |
| | | |
| | | // å é¤èç¹ |
| | | const handleDeleteNode = async (node) => { |
| | | const confirmMessage = node.type === 'phase' |
| | | ? `ç¡®å®è¦å é¤é¶æ®µ "${node.phaseName}" åå
¶ææåä»»å¡åï¼` |
| | | : `ç¡®å®è¦å é¤ä»»å¡ "${node.taskName}" åï¼`; |
| | | |
| | | await ElMessageBox.confirm(confirmMessage, '确认æä½', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }).catch(() => { |
| | | throw new Error('åæ¶å é¤'); |
| | | }); |
| | | |
| | | try { |
| | | if (node.type === 'phase') { |
| | | await deletePhase(node.phaseId); |
| | | } else { |
| | | await deleteTask(node.taskId); |
| | | } |
| | | ElMessage.success('å 餿å'); |
| | | refreshTree(); |
| | | } catch (error) { |
| | | if (error.message !== 'åæ¶å é¤') { |
| | | ElMessage.error('å é¤å¤±è´¥'); |
| | | console.error('å é¤å¤±è´¥:', error); |
| | | } |
| | | } |
| | | }; |
| | | |
| | | // é置任å¡è¡¨å |
| | | const resetTaskForm = () => { |
| | | taskForm.taskId = undefined; |
| | | taskForm.taskName = ''; |
| | | taskForm.description = ''; |
| | | taskForm.startDate = ''; |
| | | taskForm.endDate = ''; |
| | | taskForm.assigneeId = ''; |
| | | taskForm.assigneeName = ''; |
| | | taskForm.status = 'notStarted'; |
| | | taskForm.progress = 0; |
| | | taskForm.priority = 'medium'; |
| | | taskForm.phaseId = ''; |
| | | taskForm.projectId = props.projectId; |
| | | |
| | | if (taskFormRef.value) { |
| | | taskFormRef.value.resetFields(); |
| | | } |
| | | }; |
| | | |
| | | // æäº¤ä»»å¡è¡¨å |
| | | const submitTaskForm = async () => { |
| | | try { |
| | | await taskFormRef.value.validate(); |
| | | |
| | | if (taskForm.taskId) { |
| | | await updateTask(taskForm); |
| | | ElMessage.success('ä¿®æ¹ä»»å¡æå'); |
| | | } else { |
| | | await addTask(taskForm); |
| | | ElMessage.success('æ·»å 任塿å'); |
| | | } |
| | | |
| | | dialogOpen.value = false; |
| | | refreshTree(); |
| | | } catch (error) { |
| | | console.error('æäº¤è¡¨å失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // ç¹å»å
¶ä»åºåå
³éå³é®èå |
| | | document.addEventListener('click', () => { |
| | | if (showContextMenu.value) { |
| | | showContextMenu.value = false; |
| | | } |
| | | }); |
| | | |
| | | // çå¬é¡¹ç®IDåå |
| | | watch(() => props.projectId, (newProjectId) => { |
| | | if (newProjectId) { |
| | | loadTaskTree(); |
| | | } |
| | | }); |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | loadTaskTree(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .task-tree-container { |
| | | padding: 10px; |
| | | } |
| | | |
| | | .tree-actions { |
| | | display: flex; |
| | | gap: 10px; |
| | | align-items: center; |
| | | } |
| | | |
| | | .filter-section { |
| | | background: #f5f7fa; |
| | | padding: 10px; |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .tree-content { |
| | | background: #fff; |
| | | border: 1px solid #ebeef5; |
| | | border-radius: 4px; |
| | | padding: 10px; |
| | | max-height: 600px; |
| | | overflow-y: auto; |
| | | } |
| | | |
| | | .tree-node-content { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 8px; |
| | | padding: 5px 0; |
| | | min-height: 40px; |
| | | } |
| | | |
| | | .phase-node { |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .task-node { |
| | | color: #606266; |
| | | } |
| | | |
| | | .node-icon { |
| | | display: flex; |
| | | align-items: center; |
| | | width: 20px; |
| | | } |
| | | |
| | | .node-info { |
| | | flex: 1; |
| | | min-width: 0; |
| | | } |
| | | |
| | | .node-title { |
| | | font-weight: 500; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | margin-bottom: 2px; |
| | | } |
| | | |
| | | .overdue-title { |
| | | color: #f56c6c; |
| | | font-weight: bold; |
| | | } |
| | | |
| | | .priority-tag { |
| | | background: #f56c6c; |
| | | color: white; |
| | | font-size: 10px; |
| | | padding: 1px 4px; |
| | | border-radius: 2px; |
| | | margin-left: 5px; |
| | | } |
| | | |
| | | .priority-tag.medium { |
| | | background: #e6a23c; |
| | | } |
| | | |
| | | .node-description { |
| | | font-size: 12px; |
| | | color: #909399; |
| | | white-space: nowrap; |
| | | overflow: hidden; |
| | | text-overflow: ellipsis; |
| | | } |
| | | |
| | | .task-meta { |
| | | display: flex; |
| | | gap: 15px; |
| | | font-size: 12px; |
| | | color: #909399; |
| | | margin-top: 2px; |
| | | } |
| | | |
| | | .meta-item { |
| | | display: flex; |
| | | align-items: center; |
| | | gap: 3px; |
| | | } |
| | | |
| | | .task-progress { |
| | | width: 120px; |
| | | margin: 0 10px; |
| | | } |
| | | |
| | | .node-actions { |
| | | display: flex; |
| | | gap: 5px; |
| | | opacity: 0; |
| | | transition: opacity 0.3s; |
| | | } |
| | | |
| | | .tree-node-content:hover .node-actions { |
| | | opacity: 1; |
| | | } |
| | | |
| | | .context-menu { |
| | | background: white; |
| | | border: 1px solid #ebeef5; |
| | | box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1); |
| | | border-radius: 4px; |
| | | } |
| | | |
| | | .context-menu .el-menu { |
| | | min-width: 120px; |
| | | border: none; |
| | | } |
| | | |
| | | .context-menu .el-menu-item { |
| | | padding: 0 15px; |
| | | height: 36px; |
| | | line-height: 36px; |
| | | } |
| | | |
| | | .context-menu .el-menu-item:hover { |
| | | background-color: #f5f7fa; |
| | | } |
| | | |
| | | .text-gray-400 { |
| | | color: #c0c4cc; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- é¡¶é¨æç´¢åæä½æ --> |
| | | <el-form :model="queryParams" ref="queryRef" :inline="true" label-width="80px"> |
| | | <el-form-item label="项ç®åç§°" prop="projectName"> |
| | | <el-input |
| | | v-model="queryParams.projectName" |
| | | placeholder="请è¾å
¥é¡¹ç®åç§°" |
| | | clearable |
| | | style="width: 240px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="è´è´£äºº" prop="managerName"> |
| | | <el-input |
| | | v-model="queryParams.managerName" |
| | | placeholder="请è¾å
¥è´è´£äººå§å" |
| | | clearable |
| | | style="width: 240px" |
| | | @keyup.enter="handleQuery" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select |
| | | v-model="queryParams.status" |
| | | placeholder="项ç®ç¶æ" |
| | | clearable |
| | | style="width: 150px" |
| | | > |
| | | <el-option label="è§åä¸" value="planning" /> |
| | | <el-option label="è¿è¡ä¸" value="inProgress" /> |
| | | <el-option label="已宿" value="completed" /> |
| | | <el-option label="å·²æå" value="paused" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item> |
| | | <el-button type="primary" icon="Search" @click="handleQuery">æç´¢</el-button> |
| | | <el-button icon="Refresh" @click="resetQuery">éç½®</el-button> |
| | | </el-form-item> |
| | | </el-form> |
| | | |
| | | <!-- å·¥å
·æ --> |
| | | <el-row :gutter="10" class="mb8"> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="primary" |
| | | plain |
| | | icon="Plus" |
| | | @click="handleAdd" |
| | | v-hasPermi="['oaSystem:project:add']" |
| | | >æ°å¢é¡¹ç®</el-button> |
| | | </el-col> |
| | | <!-- <el-col :span="1.5"> |
| | | <el-button |
| | | type="success" |
| | | plain |
| | | icon="Edit" |
| | | :disabled="single" |
| | | @click="handleUpdate" |
| | | v-hasPermi="['oaSystem:project:edit']" |
| | | >ç¼è¾é¡¹ç®</el-button> |
| | | </el-col> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="danger" |
| | | plain |
| | | icon="Delete" |
| | | :disabled="multiple" |
| | | @click="handleDelete" |
| | | v-hasPermi="['oaSystem:project:remove']" |
| | | >å é¤é¡¹ç®</el-button> |
| | | </el-col> --> |
| | | <el-col :span="1.5"> |
| | | <el-button |
| | | type="warning" |
| | | plain |
| | | icon="Download" |
| | | @click="handleExport" |
| | | v-hasPermi="['oaSystem:project:export']" |
| | | >导åºé¡¹ç®</el-button> |
| | | </el-col> |
| | | </el-row> |
| | | |
| | | <!-- 项ç®åè¡¨è¡¨æ ¼ --> |
| | | <el-table |
| | | v-loading="loading" |
| | | :data="projectList" |
| | | @selection-change="handleSelectionChange" |
| | | > |
| | | <el-table-column type="selection" width="50" align="center" /> |
| | | <el-table-column |
| | | label="项ç®ç¼å·" |
| | | align="center" |
| | | prop="projectId" |
| | | width="100" |
| | | /> |
| | | <el-table-column |
| | | label="项ç®åç§°" |
| | | align="center" |
| | | prop="projectName" |
| | | :show-overflow-tooltip="true" |
| | | /> |
| | | <el-table-column |
| | | label="è´è´£äºº" |
| | | align="center" |
| | | prop="managerName" |
| | | /> |
| | | <el-table-column |
| | | label="å¼å§æ¥æ" |
| | | align="center" |
| | | prop="startDate" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | label="ç»ææ¥æ" |
| | | align="center" |
| | | prop="endDate" |
| | | width="120" |
| | | /> |
| | | <el-table-column |
| | | label="ç¶æ" |
| | | align="center" |
| | | prop="status" |
| | | width="90" |
| | | > |
| | | <template #default="scope"> |
| | | <el-tag :type="getStatusType(scope.row.status)">{{ getStatusText(scope.row.status) }}</el-tag> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="å®æåº¦" |
| | | align="center" |
| | | prop="completionRate" |
| | | width="100" |
| | | > |
| | | <template #default="scope"> |
| | | <el-progress :percentage="scope.row.completionRate" :stroke-width="6" /> |
| | | </template> |
| | | </el-table-column> |
| | | <el-table-column |
| | | label="æä½" |
| | | align="center" |
| | | width="180" |
| | | class-name="small-padding fixed-width" |
| | | > |
| | | <template #default="scope"> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | icon="Search" |
| | | @click="handleView(scope.row)" |
| | | v-hasPermi="['oaSystem:project:query']" |
| | | >详æ
</el-button> |
| | | <el-button |
| | | link |
| | | type="primary" |
| | | icon="Edit" |
| | | @click="handleUpdate(scope.row)" |
| | | v-hasPermi="['oaSystem:project:edit']" |
| | | >ç¼è¾</el-button> |
| | | <el-button |
| | | link |
| | | type="danger" |
| | | icon="Delete" |
| | | @click="handleDelete(scope.row)" |
| | | v-hasPermi="['oaSystem:project:remove']" |
| | | >å é¤</el-button> |
| | | </template> |
| | | </el-table-column> |
| | | </el-table> |
| | | |
| | | <!-- å页ç»ä»¶ --> |
| | | <pagination |
| | | v-show="total > 0" |
| | | :total="total" |
| | | v-model:page="queryParams.pageNum" |
| | | v-model:limit="queryParams.pageSize" |
| | | @pagination="getList" |
| | | /> |
| | | |
| | | <!-- 项ç®è¡¨åå¯¹è¯æ¡ --> |
| | | <el-dialog :title="title" v-model="open" width="600px" append-to-body> |
| | | <project-form |
| | | ref="projectFormRef" |
| | | :form="form" |
| | | :rules="rules" |
| | | :visible="open" |
| | | /> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="cancel">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitForm">ç¡®å®</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, computed, onMounted } from 'vue'; |
| | | import { ElMessage, ElMessageBox } from 'element-plus'; |
| | | import Pagination from '@/components/Pagination'; |
| | | import ProjectForm from './components/projectForm.vue'; |
| | | import { useRouter } from 'vue-router'; |
| | | const { proxy } = getCurrentInstance(); |
| | | // 导å
¥é¡¹ç®ç®¡çAPIæ¥å£ |
| | | import { listProject, addProject, updateProject, delProject, exportProject } from '@/api/oaSystem/projectManagement'; |
| | | // import { listUser } from '@/api/system/user'; // 导å
¥ç¨æ·å表APIæ¥å£ |
| | | |
| | | // å建routerå®ä¾ |
| | | const router = useRouter(); |
| | | |
| | | // è¡¨æ ¼æ°æ® |
| | | const projectList = ref([]); |
| | | const loading = ref(true); |
| | | const total = ref(0); |
| | | const queryParams = reactive({ |
| | | pageNum: 1, |
| | | pageSize: 10, |
| | | projectName: '', |
| | | managerName: '', |
| | | status: '' |
| | | }); |
| | | |
| | | // è¡¨åæ°æ® |
| | | const form = reactive({ |
| | | projectId: undefined, |
| | | projectName: '', |
| | | description: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | managerId: '', |
| | | managerName: '', |
| | | status: 'planning', |
| | | completionRate: 0 |
| | | }); |
| | | |
| | | // è¡¨åæ ¡éªè§å |
| | | const rules = { |
| | | projectName: [ |
| | | { required: true, message: '项ç®åç§°ä¸è½ä¸ºç©º', trigger: 'blur' }, |
| | | { min: 2, max: 50, message: '项ç®åç§°é¿åº¦å¨ 2 å° 50 个å符', trigger: 'blur' } |
| | | ], |
| | | startDate: [ |
| | | { required: true, message: 'å¼å§æ¥æä¸è½ä¸ºç©º', trigger: 'change' } |
| | | ], |
| | | endDate: [ |
| | | { required: true, message: 'ç»ææ¥æä¸è½ä¸ºç©º', trigger: 'change' } |
| | | ], |
| | | managerId: [ |
| | | { required: true, message: 'è´è´£äººä¸è½ä¸ºç©º', trigger: 'blur' } |
| | | ] |
| | | }; |
| | | |
| | | // å¯¹è¯æ¡ç¶æ |
| | | const open = ref(false); |
| | | const title = ref(''); |
| | | const projectFormRef = ref(); |
| | | const queryRef = ref(); |
| | | |
| | | // éä¸ç¶æ |
| | | const multiple = computed(() => { |
| | | return selectedRowKeys.value.length === 0; |
| | | }); |
| | | const single = computed(() => { |
| | | return selectedRowKeys.value.length !== 1; |
| | | }); |
| | | const selectedRowKeys = ref([]); |
| | | |
| | | // è·å项ç®å表 |
| | | const getList = async () => { |
| | | loading.value = true; |
| | | try { |
| | | const { data } = await listProject(queryParams); |
| | | projectList.value = data.records; |
| | | total.value = data.total; |
| | | } catch (error) { |
| | | ElMessage.error('è·å项ç®å表失败'); |
| | | console.error('è·å项ç®å表失败:', error); |
| | | } finally { |
| | | loading.value = false; |
| | | } |
| | | }; |
| | | |
| | | // æç´¢ |
| | | const handleQuery = () => { |
| | | queryParams.pageNum = 1; |
| | | getList(); |
| | | }; |
| | | |
| | | // éç½® |
| | | const resetQuery = () => { |
| | | if (queryRef.value) { |
| | | queryRef.value.resetFields(); |
| | | } |
| | | handleQuery(); |
| | | }; |
| | | |
| | | // éä¸è¡åå |
| | | const handleSelectionChange = (selection) => { |
| | | selectedRowKeys.value = selection.map(item => item.projectId); |
| | | }; |
| | | |
| | | // æ°å¢é¡¹ç® |
| | | const handleAdd = () => { |
| | | resetForm(); |
| | | open.value = true; |
| | | title.value = 'æ°å¢é¡¹ç®'; |
| | | }; |
| | | |
| | | // ç¼è¾é¡¹ç® |
| | | const handleUpdate = async (row) => { |
| | | resetForm(); |
| | | const projectId = row.projectId || selectedRowKeys.value[0]; |
| | | try { |
| | | // const { data } = await getProject(projectId); |
| | | Object.assign(form, row); |
| | | open.value = true; |
| | | title.value = 'ç¼è¾é¡¹ç®'; |
| | | } catch (error) { |
| | | ElMessage.error('è·å项ç®è¯¦æ
失败'); |
| | | console.error('è·å项ç®è¯¦æ
失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // å é¤é¡¹ç® |
| | | const handleDelete = async (row) => { |
| | | // const projectIds = row.projectId ? [row.projectId] : selectedRowKeys.value; |
| | | const projectNames = row.projectName ? [row.projectName] : |
| | | projectList.value.filter(item => projectIds.includes(item.projectId)).map(item => item.projectName); |
| | | |
| | | const confirmMessage = `ç¡®å®è¦å é¤é¡¹ç® "${projectNames.join('ã')}" åï¼`; |
| | | await ElMessageBox.confirm(confirmMessage, '确认æä½', { |
| | | confirmButtonText: 'ç¡®å®', |
| | | cancelButtonText: 'åæ¶', |
| | | type: 'warning' |
| | | }).catch(() => { |
| | | throw new Error('åæ¶å é¤'); |
| | | }); |
| | | |
| | | try { |
| | | // if (projectIds.length === 1) { |
| | | await delProject(row.projectId); |
| | | // } else { |
| | | // await delProjectBatch(projectIds); |
| | | // } |
| | | ElMessage.success('å 餿å'); |
| | | getList(); |
| | | } catch (error) { |
| | | if (error.message !== 'åæ¶å é¤') { |
| | | ElMessage.error('å é¤å¤±è´¥'); |
| | | console.error('å é¤é¡¹ç®å¤±è´¥:', error); |
| | | } |
| | | } |
| | | // try { |
| | | // await ElMessageBox.confirm(confirmMessage, '确认æä½', { |
| | | // confirmButtonText: 'ç¡®å®', |
| | | // cancelButtonText: 'åæ¶', |
| | | // type: 'warning' |
| | | // }); |
| | | |
| | | // // 模æç½ç»å»¶è¿ |
| | | // await new Promise(resolve => setTimeout(resolve, 300)); |
| | | |
| | | |
| | | // ElMessage.success('å 餿å'); |
| | | // getList(); |
| | | // } catch (error) { |
| | | // if (error !== 'cancel') { |
| | | // console.error('å é¤é¡¹ç®å¤±è´¥:', error); |
| | | // } |
| | | // } |
| | | }; |
| | | |
| | | // æ¥ç项ç®è¯¦æ
|
| | | const handleView = (row) => { |
| | | const projectId = row.projectId; |
| | | // 跳转å°é¡¹ç®è¯¦æ
é¡µé¢ |
| | | router.push({ |
| | | path: `/oaSystem/projectManagement/projectDetail/${projectId}`, |
| | | query: { projectName: row.projectName } |
| | | }); |
| | | }; |
| | | |
| | | // 导åºé¡¹ç® |
| | | const handleExport = async () => { |
| | | let ids = []; |
| | | if (selectedRowKeys.value.length > 0) { |
| | | ids = selectedRowKeys.value; // 导åºéä¸çé¡¹ç® |
| | | } else { |
| | | ids = projectList.value.map(item => item.projectId); // å¯¼åºææé¡¹ç® |
| | | } |
| | | ElMessageBox.confirm("éä¸çå
容å°è¢«å¯¼åºï¼æ¯å¦ç¡®è®¤å¯¼åºï¼", "导åº", { |
| | | confirmButtonText: "确认", |
| | | cancelButtonText: "åæ¶", |
| | | type: "warning", |
| | | }) |
| | | .then(() => { |
| | | proxy.download(`/oA/project/export/${ids.join(',')}`, {}, "é¡¹ç®æ°æ®.xlsx"); |
| | | ElMessage.success("å¯¼åºæå"); |
| | | ids = []; |
| | | }) |
| | | .catch(() => { |
| | | proxy.$modal.msg("已忶"); |
| | | }); |
| | | }; |
| | | // æäº¤è¡¨å |
| | | const submitForm = async () => { |
| | | try { |
| | | await projectFormRef.value.validate(); |
| | | |
| | | if (form.projectId) { |
| | | await updateProject(form); |
| | | ElMessage.success('ä¿®æ¹é¡¹ç®æå'); |
| | | } else { |
| | | console.log("form",form); |
| | | await addProject(form); |
| | | ElMessage.success('æ°å¢é¡¹ç®æå'); |
| | | } |
| | | open.value = false; |
| | | getList(); |
| | | } catch (error) { |
| | | console.error('æäº¤è¡¨å失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // åæ¶ |
| | | const cancel = () => { |
| | | open.value = false; |
| | | resetForm(); |
| | | }; |
| | | |
| | | // é置表å |
| | | const resetForm = () => { |
| | | form.projectId = undefined; |
| | | form.projectName = ''; |
| | | form.description = ''; |
| | | form.startDate = ''; |
| | | form.endDate = ''; |
| | | form.managerId = ''; |
| | | form.managerName = ''; |
| | | form.status = 'planning'; |
| | | form.completionRate = 0; |
| | | if (projectFormRef.value) { |
| | | projectFormRef.value.resetFields(); |
| | | } |
| | | }; |
| | | |
| | | // è·åç¶ææ ç¾ç±»å |
| | | const getStatusType = (status) => { |
| | | const statusTypeMap = { |
| | | planning: 'info', |
| | | inProgress: 'primary', |
| | | completed: 'success', |
| | | paused: 'warning' |
| | | }; |
| | | return statusTypeMap[status] || 'default'; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusTextMap = { |
| | | planning: 'è§åä¸', |
| | | inProgress: 'è¿è¡ä¸', |
| | | completed: '已宿', |
| | | paused: 'å·²æå' |
| | | }; |
| | | return statusTextMap[status] || status; |
| | | }; |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | getList(); |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | </style> |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | // ... existing code ... |
| | | <template> |
| | | <div class="app-container"> |
| | | <!-- 项ç®åºæ¬ä¿¡æ¯ --> |
| | | <el-card class="mb20"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>项ç®åºæ¬ä¿¡æ¯</span> |
| | | </div> |
| | | </template> |
| | | <el-descriptions :column="2" border> |
| | | <el-descriptions-item label="项ç®åç§°">{{ projectInfo.projectName }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®è´è´£äºº">{{ projectInfo.managerName }}</el-descriptions-item> |
| | | <el-descriptions-item label="å¼å§æ¥æ">{{ projectInfo.startDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="ç»ææ¥æ">{{ projectInfo.endDate }}</el-descriptions-item> |
| | | <el-descriptions-item label="项ç®ç¶æ"> |
| | | <el-tag :type="getStatusType(projectInfo.status)">{{ getStatusText(projectInfo.status) }}</el-tag> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="å®æåº¦"> |
| | | <el-progress :percentage="projectInfo.completionRate" :stroke-width="6" /> |
| | | </el-descriptions-item> |
| | | <el-descriptions-item label="é¡¹ç®æè¿°" :span="2">{{ projectInfo.description || '-' }}</el-descriptions-item> |
| | | </el-descriptions> |
| | | </el-card> |
| | | |
| | | <!-- 项ç®è¿åº¦æ¦è§ --> |
| | | <el-card class="mb20"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>项ç®è¿åº¦æ¦è§</span> |
| | | </div> |
| | | </template> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="6"> |
| | | <div class="progress-item"> |
| | | <div class="progress-title">æ»ä½è¿åº¦</div> |
| | | <div class="progress-number">{{ projectInfo.completionRate }}%</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="progress-item"> |
| | | <div class="progress-title">é¶æ®µæ»æ°</div> |
| | | <div class="progress-number">{{ statistics.totalPhases }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="progress-item"> |
| | | <div class="progress-title">任塿»æ°</div> |
| | | <div class="progress-number">{{ statistics.totalTasks }}</div> |
| | | </div> |
| | | </el-col> |
| | | <el-col :span="6"> |
| | | <div class="progress-item"> |
| | | <div class="progress-title">已宿任å¡</div> |
| | | <div class="progress-number">{{ statistics.completedTasks }}</div> |
| | | </div> |
| | | </el-col> |
| | | </el-row> |
| | | </el-card> |
| | | |
| | | <!-- é¶æ®µåä»»å¡ç®¡ç --> |
| | | <!-- <el-card class="mb20"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>项ç®ä»»å¡ç»æ</span> |
| | | <el-button type="primary" size="small" @click="handleAddPhase">æ·»å é¶æ®µ</el-button> |
| | | </div> |
| | | </template> |
| | | <task-tree :project-id="projectId" @refresh="getProjectDetail" /> |
| | | </el-card> --> |
| | | |
| | | <!-- éç¨ç¢ç®¡ç --> |
| | | <el-card class="mb20"> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>项ç®é¶æ®µéç¨ç¢</span> |
| | | <el-button type="primary" size="small" @click="handleAddMilestone">æ·»å éç¨ç¢</el-button> |
| | | </div> |
| | | </template> |
| | | <milestone-list :project-id="projectId" @refresh="getProjectDetail" :key="`milestone-${refreshProjectId}`"/> |
| | | </el-card> |
| | | |
| | | <!-- é¶æ®µç®æ 管ç --> |
| | | <el-card> |
| | | <template #header> |
| | | <div class="card-header"> |
| | | <span>é¶æ®µä»»å¡</span> |
| | | <el-button type="primary" size="small" @click="handleAddPhaseGoal">æ·»å é¶æ®µç®æ </el-button> |
| | | </div> |
| | | </template> |
| | | <phase-goal-list :project-id="projectId" @refresh="getProjectDetail" @editGoal="handleEditPhaseGoal" :key="`phaseGoal-${refreshProjectId}`"/> |
| | | </el-card> |
| | | |
| | | <!-- éç¨ç¢ç®¡çå¼¹æ¡ --> |
| | | <el-dialog :title="title" v-model="open" width="600px" append-to-body> |
| | | <el-form :model="form" ref="formRef" label-width="100px"> |
| | | <el-form-item label="项ç®é¶æ®µåç§°" prop="phaseName"> |
| | | <el-input |
| | | v-model="form.phaseName" |
| | | placeholder="请è¾å
¥é¡¹ç®é¶æ®µåç§°" |
| | | maxlength="50" |
| | | /> |
| | | </el-form-item> |
| | | <el-row :gutter="20"> |
| | | <el-col :span="12"> |
| | | <el-form-item label="å¼å§æ¥æ" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="form.startDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©å¼å§æ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | <el-col :span="12"> |
| | | <el-form-item label="ç»ææ¥æ" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="form.endDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©ç»ææ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | </el-col> |
| | | </el-row> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-radio-group v-model="form.status"> |
| | | <el-radio label="notStarted">æªå¼å§</el-radio> |
| | | <el-radio label="completed">已宿</el-radio> |
| | | <el-radio label="delayed">已延è¿</el-radio> |
| | | </el-radio-group> |
| | | </el-form-item> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="cancel">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitForm">ç¡®å®</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | |
| | | <!-- é¶æ®µä»»å¡ç®¡çå¼¹æ¡ --> |
| | | <el-dialog :title="goalTitle" v-model="goalOpen" width="600px" append-to-body> |
| | | <el-form :model="goalForm" ref="goalFormRef" label-width="100px"> |
| | | <el-form-item label="æå±é¶æ®µ" prop="phaseId"> |
| | | <el-select v-model="goalForm.phaseId" placeholder="è¯·éæ©æå±é¶æ®µ"> |
| | | <el-option |
| | | v-for="phase in phaseList" |
| | | :key="phase.phaseId" |
| | | :label="phase.phaseName" |
| | | :value="phase.phaseId" |
| | | /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <el-form-item label="ç®æ åç§°" prop="taskName"> |
| | | <el-input |
| | | v-model="goalForm.taskName" |
| | | placeholder="请è¾å
¥ç®æ åç§°" |
| | | maxlength="50" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç®æ å¼" prop="targetValue"> |
| | | <el-input-number |
| | | v-model="goalForm.targetValue" |
| | | :min="0" |
| | | :precision="2" |
| | | placeholder="请è¾å
¥ç®æ å¼" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="å½åå¼" prop="currentValue"> |
| | | <el-input-number |
| | | v-model="goalForm.currentValue" |
| | | :min="0" |
| | | :precision="2" |
| | | placeholder="请è¾å
¥å½åå¼" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="åä½" prop="unit"> |
| | | <el-input |
| | | v-model="goalForm.unit" |
| | | placeholder="请è¾å
¥åä½" |
| | | maxlength="10" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ä»»å¡å®ææ¥æ" prop="targetDate"> |
| | | <el-date-picker |
| | | v-model="goalForm.targetDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©ç®æ æ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="å¼å§æ¥æ" prop="startDate"> |
| | | <el-date-picker |
| | | v-model="goalForm.startDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©ç®æ æ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç»ææ¥æ" prop="endDate"> |
| | | <el-date-picker |
| | | v-model="goalForm.endDate" |
| | | type="date" |
| | | format="YYYY-MM-DD" |
| | | value-format="YYYY-MM-DD" |
| | | placeholder="éæ©ç®æ æ¥æ" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> |
| | | <el-form-item label="ç¶æ" prop="status"> |
| | | <el-select v-model="goalForm.status" placeholder="è¯·éæ©ç¶æ"> |
| | | <el-option label="æªå¼å§" value="notStarted" /> |
| | | <el-option label="è¿è¡ä¸" value="inProgress" /> |
| | | <el-option label="已宿" value="completed" /> |
| | | <el-option label="已延è¿" value="delayed" /> |
| | | </el-select> |
| | | </el-form-item> |
| | | <!-- <el-form-item label="å®æåº¦" prop="completionRate"> |
| | | <el-input-number |
| | | v-model="goalForm.completionRate" |
| | | :min="0" |
| | | :max="100" |
| | | :precision="2" |
| | | placeholder="请è¾å
¥å®æåº¦" |
| | | style="width: 100%" |
| | | /> |
| | | </el-form-item> --> |
| | | </el-form> |
| | | <template #footer> |
| | | <div class="dialog-footer"> |
| | | <el-button @click="cancelGoal">åæ¶</el-button> |
| | | <el-button type="primary" @click="submitGoalForm">ç¡®å®</el-button> |
| | | </div> |
| | | </template> |
| | | </el-dialog> |
| | | </div> |
| | | </template> |
| | | |
| | | <script setup> |
| | | import { ref, reactive, onMounted, watch } from 'vue'; |
| | | import { useRoute, useRouter } from 'vue-router'; |
| | | import { ElMessage } from 'element-plus'; |
| | | import TaskTree from './components/taskTree.vue'; |
| | | import MilestoneList from './components/milestoneList.vue'; |
| | | import ProjectForm from './components/projectForm.vue'; |
| | | import PhaseGoalList from './components/phaseGoalList.vue'; |
| | | import { getProject, addProjectPhase, listProjectPhase, addProjectTask, updateProjectTask } from '@/api/oaSystem/projectManagement'; |
| | | |
| | | const route = useRoute(); |
| | | const router = useRouter(); |
| | | const open = ref(false); |
| | | const title = ref(''); |
| | | const projectFormRef = ref(); |
| | | const formRef = ref(); |
| | | // 项ç®ID |
| | | // å¨å
¶ä»refå®ä¹éè¿æ·»å |
| | | const refreshProjectId = ref(0); |
| | | |
| | | const projectId = ref(route.params.projectId); |
| | | |
| | | // 项ç®ä¿¡æ¯ |
| | | const projectInfo = reactive({ |
| | | projectId: '', |
| | | projectName: '', |
| | | description: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | managerId: '', |
| | | managerName: '', |
| | | status: 'planning', |
| | | completionRate: 0 |
| | | }); |
| | | |
| | | // ç»è®¡ä¿¡æ¯ |
| | | const statistics = reactive({ |
| | | totalPhases: 0, |
| | | totalTasks: 0, |
| | | completedTasks: 0 |
| | | }); |
| | | const form = reactive({ |
| | | phaseId: '', |
| | | phaseName: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | status: 'planning', |
| | | oaProjectId: projectId.value, |
| | | }) |
| | | |
| | | // é¶æ®µç®æ ç¸å
³ |
| | | const goalOpen = ref(false); |
| | | const goalTitle = ref(''); |
| | | const goalFormRef = ref(); |
| | | const phaseList = ref([]); |
| | | const goalForm = reactive({ |
| | | taskId: '', |
| | | phaseId: '', |
| | | taskName: '', |
| | | targetValue: 100, |
| | | currentValue: 0, |
| | | unit: '%', |
| | | targetDate: '', |
| | | startDate: '', |
| | | endDate: '', |
| | | status: 'notStarted', |
| | | completionRate: 0 |
| | | }); |
| | | |
| | | // è·å项ç®è¯¦æ
|
| | | const getProjectDetail = async () => { |
| | | try { |
| | | getProject().then((res)=>{ |
| | | console.log("项ç®è¯¦æ
",res) |
| | | const projectData = res.data[projectId.value]; |
| | | // æ´æ°é¡¹ç®ä¿¡æ¯ |
| | | Object.assign(projectInfo, projectData); |
| | | |
| | | // æ´æ°ç»è®¡ä¿¡æ¯ |
| | | updateStatistics(projectData); |
| | | |
| | | // å¼ºå¶æ´æ°DOM以确ä¿åç»ä»¶è½æ£ç¡®å·æ° |
| | | // è¿ééè¿è§¦årefreshProjectIdäºä»¶æ¥å¼ºå¶å·æ°åç»ä»¶ |
| | | refreshProjectId.value++; |
| | | }) |
| | | } catch (error) { |
| | | ElMessage.error('è·å项ç®è¯¦æ
失败'); |
| | | console.error('è·å项ç®è¯¦æ
失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // æ´æ°ç»è®¡ä¿¡æ¯ |
| | | const updateStatistics = (projectData) => { |
| | | // è¿éå设projectDataä¸å
å«äºç»è®¡ä¿¡æ¯ |
| | | // å¦ææ²¡æï¼éè¦åç¬è¯·æ±ç»è®¡æ°æ® |
| | | statistics.totalPhases = projectData.phases ? projectData.phases.length : 0; |
| | | statistics.totalTasks = projectData.tasks ? projectData.tasks.length : 0; |
| | | statistics.completedTasks = projectData.tasks ? |
| | | projectData.tasks.filter(task => task.status === 'completed').length : 0; |
| | | }; |
| | | |
| | | // è·å项ç®é¶æ®µå表 |
| | | const getPhaseList = async () => { |
| | | try { |
| | | const { data } = await listProjectPhase(projectId.value); |
| | | phaseList.value = data.rows || data; |
| | | } catch (error) { |
| | | ElMessage.error('è·å项ç®é¶æ®µå表失败'); |
| | | console.error('è·å项ç®é¶æ®µå表失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // 计ç®å®æåº¦ |
| | | const calculateCompletionRate = () => { |
| | | if (goalForm.targetValue > 0) { |
| | | goalForm.completionRate = Math.min(Math.round((goalForm.currentValue / goalForm.targetValue) * 100), 100); |
| | | } else { |
| | | goalForm.completionRate = 0; |
| | | } |
| | | }; |
| | | |
| | | // æ·»å é¶æ®µ |
| | | const handleAddPhase = () => { |
| | | // resetForm(); |
| | | ElMessage.info('æ·»å é¶æ®µåè½å¾
å®ç°'); |
| | | }; |
| | | |
| | | // æ·»å éç¨ç¢ |
| | | const handleAddMilestone = () => { |
| | | resetForm(); |
| | | open.value = true; |
| | | title.value = 'æ°å¢é¡¹ç®é¶æ®µ'; |
| | | }; |
| | | |
| | | // æ·»å é¶æ®µä»»å¡ |
| | | const handleAddPhaseGoal = () => { |
| | | goalForm.taskId = ''; |
| | | goalForm.phaseId = ''; |
| | | goalForm.taskName = ''; |
| | | goalForm.targetValue = 0; |
| | | goalForm.currentValue = 0; |
| | | goalForm.unit = '%'; |
| | | goalForm.targetDate = ''; |
| | | goalForm.startDate = ''; |
| | | goalForm.endDate = ''; |
| | | goalForm.status = 'notStarted'; |
| | | goalForm.completionRate = 0; |
| | | if (goalFormRef.value) { |
| | | goalFormRef.value.resetFields(); |
| | | } |
| | | getPhaseList(); |
| | | goalTitle.value = 'æ°å¢é¶æ®µç®æ '; |
| | | goalOpen.value = true; |
| | | }; |
| | | |
| | | // æäº¤è¡¨å |
| | | const submitForm = async () => { |
| | | try { |
| | | await formRef.value.validate(); |
| | | |
| | | if (form.phaseId) { |
| | | // await updateProject(form); |
| | | // ElMessage.success('ä¿®æ¹é¡¹ç®é¶æ®µæå'); |
| | | } else { |
| | | console.log("form",form); |
| | | await addProjectPhase(form); |
| | | ElMessage.success('æ°å¢é¡¹ç®é¶æ®µæå'); |
| | | getProjectDetail(); |
| | | } |
| | | open.value = false; |
| | | } catch (error) { |
| | | console.error('æäº¤è¡¨å失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // æäº¤é¶æ®µä»»å¡è¡¨å |
| | | const submitGoalForm = async () => { |
| | | try { |
| | | await goalFormRef.value.validate(); |
| | | calculateCompletionRate(); |
| | | |
| | | const goalData = { |
| | | ...goalForm, |
| | | oaProjectId: projectId.value |
| | | }; |
| | | |
| | | if (goalForm.taskId) { |
| | | await updateProjectTask(goalData); |
| | | ElMessage.success('ä¿®æ¹é¶æ®µç®æ æå'); |
| | | |
| | | } else { |
| | | await addProjectTask(goalData); |
| | | ElMessage.success('æ°å¢é¶æ®µç®æ æå'); |
| | | |
| | | } |
| | | // è°ç¨getProjectDetailå·æ°ææç¸å
³æ°æ® |
| | | getProjectDetail(); |
| | | goalOpen.value = false; |
| | | |
| | | } catch (error) { |
| | | console.error('æäº¤é¶æ®µç®æ 表å失败:', error); |
| | | } |
| | | }; |
| | | |
| | | // éç½®éç¨ç¢è¡¨å |
| | | const resetForm = () => { |
| | | form.phaseId = ''; |
| | | form.phaseName = ''; |
| | | form.startDate = ''; |
| | | form.endDate = ''; |
| | | form.status = 'planning'; |
| | | form.oaProjectId = projectId.value; |
| | | if (formRef.value) { |
| | | formRef.value.resetFields(); |
| | | } |
| | | }; |
| | | |
| | | // åæ¶é¶æ®µä»»å¡æä½ |
| | | const cancelGoal = () => { |
| | | goalOpen.value = false; |
| | | }; |
| | | |
| | | // åæ¶æä½ |
| | | const cancel = () => { |
| | | open.value = false; |
| | | }; |
| | | // ç¼è¾é¶æ®µä»»å¡ |
| | | const handleEditPhaseGoal = async (goal) => { |
| | | // å¤å¶ç®æ æ°æ®å°è¡¨å |
| | | Object.assign(goalForm, goal); |
| | | |
| | | // è·å项ç®é¶æ®µå表 |
| | | await getPhaseList(); |
| | | |
| | | // æå¼ç¼è¾å¼¹çª |
| | | goalTitle.value = 'ç¼è¾é¶æ®µç®æ '; |
| | | goalOpen.value = true; |
| | | }; |
| | | // è·åç¶ææ ç¾ç±»å |
| | | const getStatusType = (status) => { |
| | | const statusTypeMap = { |
| | | planning: 'info', |
| | | inProgress: 'primary', |
| | | completed: 'success', |
| | | paused: 'warning' |
| | | }; |
| | | return statusTypeMap[status] || 'default'; |
| | | }; |
| | | |
| | | // è·åç¶æææ¬ |
| | | const getStatusText = (status) => { |
| | | const statusTextMap = { |
| | | planning: 'è§åä¸', |
| | | inProgress: 'è¿è¡ä¸', |
| | | completed: '已宿', |
| | | paused: 'å·²æå' |
| | | }; |
| | | return statusTextMap[status] || status; |
| | | }; |
| | | |
| | | // çå¬è·¯ç±åæ°åå |
| | | watch(() => route.params.projectId, (newProjectId) => { |
| | | // console.log('è·¯ç±åæ°åå:', projectId); |
| | | if (newProjectId) { |
| | | projectId.value = newProjectId; |
| | | getProjectDetail(); |
| | | } |
| | | }); |
| | | |
| | | // çå¬å½åå¼åç®æ å¼ååï¼éæ°è®¡ç®å®æåº¦ |
| | | watch(() => [goalForm.currentValue, goalForm.targetValue], () => { |
| | | calculateCompletionRate(); |
| | | }); |
| | | |
| | | // åå§å |
| | | onMounted(() => { |
| | | if (projectId.value) { |
| | | getProjectDetail(); |
| | | } |
| | | }); |
| | | </script> |
| | | |
| | | <style scoped> |
| | | .app-container { |
| | | padding: 20px; |
| | | } |
| | | |
| | | .card-header { |
| | | display: flex; |
| | | justify-content: space-between; |
| | | align-items: center; |
| | | } |
| | | |
| | | .progress-item { |
| | | text-align: center; |
| | | padding: 20px; |
| | | background-color: #f5f7fa; |
| | | border-radius: 8px; |
| | | } |
| | | |
| | | .progress-title { |
| | | font-size: 14px; |
| | | color: #606266; |
| | | margin-bottom: 10px; |
| | | } |
| | | |
| | | .progress-number { |
| | | font-size: 24px; |
| | | font-weight: bold; |
| | | color: #409eff; |
| | | } |
| | | |
| | | .mb20 { |
| | | margin-bottom: 20px; |
| | | } |
| | | </style> |