yaowanxin
2025-09-24 bf04601b6a1cb0d52eebe7b6830e8221c22c6ca0
OA系统-项目任务协同。项目,项目阶段,项目阶段任务
已添加7个文件
已修改1个文件
2723 ■■■■■ 文件已修改
src/api/oaSystem/projectManagement.js 154 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/router/index.js 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/milestoneList.vue 289 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/phaseGoalList.vue 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/projectForm.vue 221 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/components/taskTree.vue 834 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/index.vue 481 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/oaSystem/projectManagement/projectDetail.vue 565 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/api/oaSystem/projectManagement.js
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,154 @@
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"
//   });
// }
src/router/index.js
@@ -92,6 +92,20 @@
    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" },
      },
    ],
  },
];
// åŠ¨æ€è·¯ç”±ï¼ŒåŸºäºŽç”¨æˆ·æƒé™åŠ¨æ€åŽ»åŠ è½½
src/views/oaSystem/projectManagement/components/milestoneList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,289 @@
// ... 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>
src/views/oaSystem/projectManagement/components/phaseGoalList.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
<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>
src/views/oaSystem/projectManagement/components/projectForm.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,221 @@
<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>
src/views/oaSystem/projectManagement/components/taskTree.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,834 @@
<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>
src/views/oaSystem/projectManagement/index.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,481 @@
<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>
src/views/oaSystem/projectManagement/projectDetail.vue
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,565 @@
// ... 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>