From bf04601b6a1cb0d52eebe7b6830e8221c22c6ca0 Mon Sep 17 00:00:00 2001
From: yaowanxin <3588231647@qq.com>
Date: 星期三, 24 九月 2025 17:46:57 +0800
Subject: [PATCH] OA系统-项目任务协同。项目,项目阶段,项目阶段任务
---
src/views/oaSystem/projectManagement/components/taskTree.vue | 834 ++++++++++++++++++
src/views/oaSystem/projectManagement/components/phaseGoalList.vue | 165 +++
src/views/oaSystem/projectManagement/components/milestoneList.vue | 289 ++++++
src/views/oaSystem/projectManagement/index.vue | 481 ++++++++++
src/api/oaSystem/projectManagement.js | 154 +++
src/views/oaSystem/projectManagement/components/projectForm.vue | 221 ++++
src/router/index.js | 14
src/views/oaSystem/projectManagement/projectDetail.vue | 565 ++++++++++++
8 files changed, 2,723 insertions(+), 0 deletions(-)
diff --git a/src/api/oaSystem/projectManagement.js b/src/api/oaSystem/projectManagement.js
new file mode 100644
index 0000000..56c9287
--- /dev/null
+++ b/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"
+// });
+// }
\ No newline at end of file
diff --git a/src/router/index.js b/src/router/index.js
index bc7fd70..e5dc580 100644
--- a/src/router/index.js
+++ b/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" },
+ },
+ ],
+ },
];
// 鍔ㄦ�佽矾鐢憋紝鍩轰簬鐢ㄦ埛鏉冮檺鍔ㄦ�佸幓鍔犺浇
diff --git a/src/views/oaSystem/projectManagement/components/milestoneList.vue b/src/views/oaSystem/projectManagement/components/milestoneList.vue
new file mode 100644
index 0000000..47b0027
--- /dev/null
+++ b/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') {
+ // 琛ㄥ崟楠岃瘉澶辫触锛孍lement 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>
diff --git a/src/views/oaSystem/projectManagement/components/phaseGoalList.vue b/src/views/oaSystem/projectManagement/components/phaseGoalList.vue
new file mode 100644
index 0000000..5054299
--- /dev/null
+++ b/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>
diff --git a/src/views/oaSystem/projectManagement/components/projectForm.vue b/src/views/oaSystem/projectManagement/components/projectForm.vue
new file mode 100644
index 0000000..50b671c
--- /dev/null
+++ b/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));
+
+ // 姝g‘璁剧疆鐢ㄦ埛鍒楄〃
+ 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>
\ No newline at end of file
diff --git a/src/views/oaSystem/projectManagement/components/taskTree.vue b/src/views/oaSystem/projectManagement/components/taskTree.vue
new file mode 100644
index 0000000..11e3ae8
--- /dev/null
+++ b/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="璇疯緭鍏ヨ礋璐d汉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: '闇�姹傛枃妗g紪鍐�',
+ 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: '寮�鍙戜笟鍔¢�昏緫鍜孉PI鎺ュ彛',
+ 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: '璐熻矗浜篈',
+ 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: '璐熻矗浜築',
+ 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 = () => {
+ // 绛涢�夐�昏緫宸茬粡鍦╟omputed涓疄鐜�
+};
+
+// 閲嶇疆绛涢��
+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>
\ No newline at end of file
diff --git a/src/views/oaSystem/projectManagement/index.vue b/src/views/oaSystem/projectManagement/index.vue
new file mode 100644
index 0000000..2a0ec3a
--- /dev/null
+++ b/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="璇疯緭鍏ヨ礋璐d汉濮撳悕"
+ 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>
\ No newline at end of file
diff --git a/src/views/oaSystem/projectManagement/projectDetail.vue b/src/views/oaSystem/projectManagement/projectDetail.vue
new file mode 100644
index 0000000..c3b0779
--- /dev/null
+++ b/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
+// 鍦ㄥ叾浠杛ef瀹氫箟闄勮繎娣诲姞
+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>
--
Gitblit v1.9.3