|
|
@@ -1,13 +1,5 @@
|
|
|
<template>
|
|
|
- <ContentWrap>
|
|
|
- <!-- <div class="page-heading">
|
|
|
- <div>
|
|
|
- <h2>项目分解</h2>
|
|
|
- <p>项目生效后,项目责任人可将项目拆解为多个子任务,每个子任务最多分解三个孙子任务。</p>
|
|
|
- </div>
|
|
|
- <el-tag type="success" effect="light"><Icon icon="ep:circle-check" /> 项目已生效</el-tag>
|
|
|
- </div> -->
|
|
|
-
|
|
|
+ <ContentWrap style="border: none">
|
|
|
<el-form ref="queryFormRef" :model="queryParams" inline class="project-filter">
|
|
|
<el-form-item label="项目名称" prop="projectName">
|
|
|
<el-input
|
|
|
@@ -34,10 +26,10 @@
|
|
|
</el-form>
|
|
|
</ContentWrap>
|
|
|
|
|
|
- <ContentWrap>
|
|
|
+ <ContentWrap style="border: none">
|
|
|
<zm-table
|
|
|
v-loading="loading"
|
|
|
- :data="filteredProjects"
|
|
|
+ :data="projectList"
|
|
|
row-key="id"
|
|
|
highlight-current-row
|
|
|
class="project-table"
|
|
|
@@ -45,370 +37,407 @@
|
|
|
>
|
|
|
<zm-table-column label="项目名称" prop="projectName" min-width="280" show-overflow-tooltip />
|
|
|
<zm-table-column label="项目负责人" prop="projectLeader" width="140" />
|
|
|
- <zm-table-column label="项目类型" prop="projectType" min-width="180" />
|
|
|
- <zm-table-column label="生效日期" prop="effectiveDate" width="130" />
|
|
|
- <zm-table-column label="任务数" width="90" align="center"
|
|
|
- ><template #default="{ row }">{{ countTasks(row.tasks) }}</template></zm-table-column
|
|
|
- >
|
|
|
- <zm-table-column label="操作" width="120" fixed="right" align="center"
|
|
|
- ><template #default="{ row }"
|
|
|
- ><zm-button link type="primary" @click="selectProject(row)">分解任务</zm-button></template
|
|
|
- ></zm-table-column
|
|
|
- >
|
|
|
+ <zm-table-column label="项目类型" prop="projectType" min-width="180">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <dict-tag :type="DICT_TYPE.PROJECT_TYPE" :value="row.projectType" />
|
|
|
+ </template>
|
|
|
+ </zm-table-column>
|
|
|
+ <zm-table-column label="生效日期" prop="projectDate" width="160">
|
|
|
+ <template #default="{ row }">{{ formatDate(row.projectDate) }}</template>
|
|
|
+ </zm-table-column>
|
|
|
+ <zm-table-column label="任务数" prop="taskTotal" width="90" align="center" />
|
|
|
+ <zm-table-column label="操作" width="120" fixed="right" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-button link type="primary" @click="selectProject(row)">分解任务</el-button>
|
|
|
+ </template>
|
|
|
+ </zm-table-column>
|
|
|
</zm-table>
|
|
|
<Pagination
|
|
|
v-model:page="queryParams.pageNo"
|
|
|
v-model:limit="queryParams.pageSize"
|
|
|
- :total="filteredProjects.length"
|
|
|
+ :total="total"
|
|
|
+ @pagination="getProjectList"
|
|
|
/>
|
|
|
</ContentWrap>
|
|
|
|
|
|
- <ContentWrap v-if="selectedProject" class="task-panel">
|
|
|
+ <ContentWrap style="border: none" v-if="selectedProject" class="task-panel">
|
|
|
<div class="task-heading">
|
|
|
<div>
|
|
|
- <div class="task-title-row"
|
|
|
- ><h3>{{ selectedProject.projectName }}</h3
|
|
|
- ><el-tag type="success" size="small">已生效</el-tag></div
|
|
|
- >
|
|
|
- <span
|
|
|
- >项目负责人:{{ selectedProject.projectLeader }} · 已分解
|
|
|
- {{ countTasks(selectedProject.tasks) }} 个任务</span
|
|
|
- >
|
|
|
+ <div class="task-title-row">
|
|
|
+ <h3>{{ selectedProject.projectName }}</h3>
|
|
|
+ <el-tag type="success" size="small">已生效</el-tag>
|
|
|
+ </div>
|
|
|
+ <span>
|
|
|
+ 项目负责人:{{ selectedProject.projectLeader }} · 已分解
|
|
|
+ {{ selectedProject.taskTotal || 0 }} 个任务
|
|
|
+ </span>
|
|
|
</div>
|
|
|
- <el-button type="primary" @click="openTaskForm()"
|
|
|
- ><Icon icon="ep:plus" /> 新增一级任务</el-button
|
|
|
- >
|
|
|
+ <el-button type="primary" @click="openTaskForm()" v-hasPermi="['rd:project-task:create']">
|
|
|
+ <Icon icon="ep:plus" /> 新增一级任务
|
|
|
+ </el-button>
|
|
|
</div>
|
|
|
|
|
|
- <el-empty v-if="!selectedProject.tasks.length" description="暂未分解任务"
|
|
|
- ><el-button type="primary" @click="openTaskForm()"
|
|
|
- ><Icon icon="ep:plus" /> 新增一级任务</el-button
|
|
|
- ></el-empty
|
|
|
+ <el-empty v-if="!taskTree.length" description="暂未分解任务">
|
|
|
+ <el-button type="primary" @click="openTaskForm()">
|
|
|
+ <Icon icon="ep:plus" /> 新增一级任务
|
|
|
+ </el-button>
|
|
|
+ </el-empty>
|
|
|
+ <zm-table
|
|
|
+ v-else
|
|
|
+ :data="taskTree"
|
|
|
+ row-key="id"
|
|
|
+ default-expand-all
|
|
|
+ border
|
|
|
+ :tree-props="{ children: 'children' }"
|
|
|
>
|
|
|
- <zm-table v-else :data="selectedProject.tasks" row-key="id" default-expand-all border>
|
|
|
- <zm-table-column label="任务名称" min-width="250" tree-node>
|
|
|
- <template #default="{ row }"
|
|
|
- ><div class="task-name"
|
|
|
- ><span>{{ row.name }}</span
|
|
|
- ><el-tag v-if="row.level === 3" type="info" effect="plain" size="small"
|
|
|
- >末级</el-tag
|
|
|
- ></div
|
|
|
- ></template
|
|
|
- >
|
|
|
+ <zm-table-column label="任务名称" min-width="250">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <div class="task-name">
|
|
|
+ <span>{{ row.taskName }}</span>
|
|
|
+ <el-tag v-if="row.level === 3" type="info" effect="plain" size="small">末级</el-tag>
|
|
|
+ </div>
|
|
|
+ </template>
|
|
|
+ </zm-table-column>
|
|
|
+ <zm-table-column label="开始时间" prop="planStartTime" width="130" />
|
|
|
+ <zm-table-column label="结束时间" prop="planEndTime" width="130" />
|
|
|
+ <zm-table-column label="责任人" prop="ownerName" width="120" />
|
|
|
+ <zm-table-column label="优先级" width="100" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-tag :type="priorityTagType(row.priority)">
|
|
|
+ {{ priorityLabel(row.priority) }}
|
|
|
+ </el-tag>
|
|
|
+ </template>
|
|
|
+ </zm-table-column>
|
|
|
+ <zm-table-column label="进度" width="120" align="center">
|
|
|
+ <template #default="{ row }">
|
|
|
+ <el-progress
|
|
|
+ :percentage="row.progress || 0"
|
|
|
+ :status="row.status === 3 ? 'exception' : row.status === 2 ? 'success' : ''"
|
|
|
+ :stroke-width="8"
|
|
|
+ />
|
|
|
+ </template>
|
|
|
</zm-table-column>
|
|
|
- <zm-table-column label="开始时间" prop="startDate" width="130" />
|
|
|
- <zm-table-column label="结束时间" prop="endDate" width="130" />
|
|
|
- <zm-table-column label="责任人" prop="owner" width="120" />
|
|
|
- <zm-table-column label="优先级" width="100" align="center"
|
|
|
- ><template #default="{ row }"
|
|
|
- ><el-tag :type="priorityTagType(row.priority)">{{ row.priority }}</el-tag></template
|
|
|
- ></zm-table-column
|
|
|
- >
|
|
|
- <zm-table-column label="任务描述" prop="description" min-width="220" show-overflow-tooltip />
|
|
|
- <zm-table-column label="操作" width="220" fixed="right" align="center">
|
|
|
+ <zm-table-column label="任务描述" prop="taskDesc" min-width="220" show-overflow-tooltip />
|
|
|
+ <zm-table-column label="操作" width="240" fixed="right" align="center">
|
|
|
<template #default="{ row }">
|
|
|
<el-button
|
|
|
- v-if="row.level === 1"
|
|
|
+ v-if="row.level < 3"
|
|
|
link
|
|
|
type="primary"
|
|
|
- :disabled="row.children.length >= MAX_CHILDREN"
|
|
|
+ v-hasPermi="['rd:project-task:create']"
|
|
|
@click="openTaskForm(row)"
|
|
|
- >新增子任务</el-button
|
|
|
>
|
|
|
+ 新增子任务
|
|
|
+ </el-button>
|
|
|
<el-button v-else link type="info" disabled>不可再分解</el-button>
|
|
|
- <el-button link type="primary" @click="openTaskForm(row, true)">编辑</el-button>
|
|
|
- <el-button link type="danger" @click="removeTask(row)">删除</el-button>
|
|
|
+ <el-button
|
|
|
+ link
|
|
|
+ type="primary"
|
|
|
+ v-hasPermi="['rd:project-task:update']"
|
|
|
+ @click="openTaskForm(row, true)"
|
|
|
+ >
|
|
|
+ 编辑
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ link
|
|
|
+ type="danger"
|
|
|
+ v-hasPermi="['rd:project-task:delete']"
|
|
|
+ @click="removeTask(row)"
|
|
|
+ >
|
|
|
+ 删除
|
|
|
+ </el-button>
|
|
|
</template>
|
|
|
</zm-table-column>
|
|
|
</zm-table>
|
|
|
</ContentWrap>
|
|
|
|
|
|
<el-dialog v-model="taskDialogVisible" :title="taskDialogTitle" width="720px" destroy-on-close>
|
|
|
- <el-alert v-if="parentTask" type="info" :closable="false" class="parent-alert"
|
|
|
- >当前父任务:{{ parentTask.name }},最多可添加 {{ MAX_CHILDREN }} 个孙子任务(已添加
|
|
|
- {{ parentTask.children.length }} 个)</el-alert
|
|
|
- >
|
|
|
+ <el-alert v-if="parentTask" type="info" :closable="false" class="parent-alert">
|
|
|
+ 当前父任务:{{ parentTask.taskName }}(已有子任务 {{ parentTask.children?.length || 0 }} 个)
|
|
|
+ </el-alert>
|
|
|
<el-form ref="taskFormRef" :model="taskForm" :rules="taskRules" label-width="auto">
|
|
|
- <el-form-item label="任务名称" prop="name"
|
|
|
- ><el-input
|
|
|
- v-model="taskForm.name"
|
|
|
+ <el-form-item label="任务名称" prop="taskName">
|
|
|
+ <el-input
|
|
|
+ v-model="taskForm.taskName"
|
|
|
maxlength="100"
|
|
|
show-word-limit
|
|
|
placeholder="请输入任务名称"
|
|
|
- /></el-form-item>
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
<el-row :gutter="18">
|
|
|
- <el-col :span="12"
|
|
|
- ><el-form-item label="开始时间" prop="startDate"
|
|
|
- ><el-date-picker
|
|
|
- v-model="taskForm.startDate"
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="开始时间" prop="planStartTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="taskForm.planStartTime"
|
|
|
type="date"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
placeholder="请选择开始时间"
|
|
|
- class="w-full" /></el-form-item
|
|
|
- ></el-col>
|
|
|
- <el-col :span="12"
|
|
|
- ><el-form-item label="结束时间" prop="endDate"
|
|
|
- ><el-date-picker
|
|
|
- v-model="taskForm.endDate"
|
|
|
+ class="w-full"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="结束时间" prop="planEndTime">
|
|
|
+ <el-date-picker
|
|
|
+ v-model="taskForm.planEndTime"
|
|
|
type="date"
|
|
|
value-format="YYYY-MM-DD"
|
|
|
placeholder="请选择结束时间"
|
|
|
- class="w-full" /></el-form-item
|
|
|
- ></el-col>
|
|
|
+ class="w-full"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
</el-row>
|
|
|
<el-row :gutter="18">
|
|
|
- <el-col :span="12"
|
|
|
- ><el-form-item label="责任人" prop="owner"
|
|
|
- ><el-input v-model="taskForm.owner" placeholder="请输入责任人" /></el-form-item
|
|
|
- ></el-col>
|
|
|
- <el-col :span="12"
|
|
|
- ><el-form-item label="任务优先级" prop="priority"
|
|
|
- ><el-radio-group v-model="taskForm.priority"
|
|
|
- ><el-radio-button v-for="item in priorities" :key="item" :label="item">{{
|
|
|
- item
|
|
|
- }}</el-radio-button></el-radio-group
|
|
|
- ></el-form-item
|
|
|
- ></el-col
|
|
|
- >
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="责任人" prop="ownerName">
|
|
|
+ <el-input v-model="taskForm.ownerName" placeholder="请输入责任人" />
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
+ <el-col :span="12">
|
|
|
+ <el-form-item label="任务优先级" prop="priority">
|
|
|
+ <el-radio-group v-model="taskForm.priority">
|
|
|
+ <el-radio-button :label="1">高</el-radio-button>
|
|
|
+ <el-radio-button :label="2">中</el-radio-button>
|
|
|
+ <el-radio-button :label="3">低</el-radio-button>
|
|
|
+ </el-radio-group>
|
|
|
+ </el-form-item>
|
|
|
+ </el-col>
|
|
|
</el-row>
|
|
|
- <el-form-item label="任务描述" prop="description"
|
|
|
- ><el-input
|
|
|
- v-model="taskForm.description"
|
|
|
+ <el-form-item label="任务描述" prop="taskDesc">
|
|
|
+ <el-input
|
|
|
+ v-model="taskForm.taskDesc"
|
|
|
type="textarea"
|
|
|
:rows="4"
|
|
|
maxlength="500"
|
|
|
show-word-limit
|
|
|
placeholder="请输入任务描述"
|
|
|
- /></el-form-item>
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
</el-form>
|
|
|
- <template #footer
|
|
|
- ><el-button @click="taskDialogVisible = false">取消</el-button
|
|
|
- ><el-button type="primary" :loading="submitLoading" @click="submitTask"
|
|
|
- >保存任务</el-button
|
|
|
- ></template
|
|
|
- >
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="taskDialogVisible = false">取消</el-button>
|
|
|
+ <el-button type="primary" :loading="submitLoading" @click="submitTask">保存任务</el-button>
|
|
|
+ </template>
|
|
|
</el-dialog>
|
|
|
</template>
|
|
|
|
|
|
<script setup lang="ts">
|
|
|
-import { computed, reactive, ref } from 'vue'
|
|
|
+import { reactive, ref, onMounted } from 'vue'
|
|
|
import { ElMessage, ElMessageBox, type FormInstance, type FormRules } from 'element-plus'
|
|
|
+import { DICT_TYPE } from '@/utils/dict'
|
|
|
+import { getProjectPage } from '@/api/project'
|
|
|
+import {
|
|
|
+ getTaskTreeByProjectId,
|
|
|
+ createTask as createTaskApi,
|
|
|
+ updateTask as updateTaskApi,
|
|
|
+ deleteTask as deleteTaskApi
|
|
|
+} from '@/api/project-task'
|
|
|
|
|
|
defineOptions({ name: 'ResearchProjectBreakdown' })
|
|
|
-const MAX_CHILDREN = 3
|
|
|
-const priorities = ['高', '中', '低'] as const
|
|
|
-type Priority = (typeof priorities)[number]
|
|
|
-type Task = {
|
|
|
- id: number
|
|
|
- name: string
|
|
|
- startDate: string
|
|
|
- endDate: string
|
|
|
- owner: string
|
|
|
- description: string
|
|
|
- priority: Priority
|
|
|
- level: number
|
|
|
- children: Task[]
|
|
|
+
|
|
|
+// ========== 项目列表 ==========
|
|
|
+const loading = ref(false)
|
|
|
+const queryFormRef = ref<FormInstance>()
|
|
|
+const queryParams = reactive({
|
|
|
+ pageNo: 1,
|
|
|
+ pageSize: 10,
|
|
|
+ projectName: '',
|
|
|
+ projectLeader: ''
|
|
|
+})
|
|
|
+const projectList = ref<any[]>([])
|
|
|
+const total = ref(0)
|
|
|
+
|
|
|
+const getProjectList = async () => {
|
|
|
+ loading.value = true
|
|
|
+ try {
|
|
|
+ const data = await getProjectPage({
|
|
|
+ ...queryParams,
|
|
|
+ formalAuditStatus: 2 // ⭐ 只查正式审批通过的
|
|
|
+ })
|
|
|
+ projectList.value = data.list || []
|
|
|
+ total.value = data.total || 0
|
|
|
+ } finally {
|
|
|
+ loading.value = false
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+const handleQuery = () => {
|
|
|
+ queryParams.pageNo = 1
|
|
|
+ getProjectList()
|
|
|
}
|
|
|
-type Project = {
|
|
|
- id: number
|
|
|
- projectName: string
|
|
|
- projectLeader: string
|
|
|
- projectType: string
|
|
|
- effectiveDate: string
|
|
|
- tasks: Task[]
|
|
|
+const resetQuery = () => {
|
|
|
+ queryFormRef.value?.resetFields()
|
|
|
+ handleQuery()
|
|
|
}
|
|
|
|
|
|
-const createTask = (level = 1, data: Partial<Task> = {}): Task => ({
|
|
|
- id: Date.now() + Math.floor(Math.random() * 1000),
|
|
|
- name: '',
|
|
|
- startDate: '',
|
|
|
- endDate: '',
|
|
|
- owner: '',
|
|
|
- description: '',
|
|
|
- priority: '中',
|
|
|
- level,
|
|
|
- children: [],
|
|
|
- ...data
|
|
|
-})
|
|
|
-const projects = ref<Project[]>([
|
|
|
- {
|
|
|
- id: 1,
|
|
|
- projectName: '高温高压井下工具可靠性研究',
|
|
|
- projectLeader: '张明远',
|
|
|
- projectType: '产品、工具和设备研发类',
|
|
|
- effectiveDate: '2026-03-01',
|
|
|
- tasks: [
|
|
|
- createTask(1, {
|
|
|
- id: 101,
|
|
|
- name: '完成高温高压工况分析',
|
|
|
- startDate: '2026-03-05',
|
|
|
- endDate: '2026-04-10',
|
|
|
- owner: '李强',
|
|
|
- description: '梳理典型工况并输出分析报告。',
|
|
|
- priority: '高',
|
|
|
- children: [
|
|
|
- createTask(2, {
|
|
|
- id: 102,
|
|
|
- name: '收集现场运行数据',
|
|
|
- startDate: '2026-03-05',
|
|
|
- endDate: '2026-03-20',
|
|
|
- owner: '王璐',
|
|
|
- description: '整理近三年现场运行数据。',
|
|
|
- priority: '中',
|
|
|
- children: []
|
|
|
- })
|
|
|
- ]
|
|
|
- })
|
|
|
- ]
|
|
|
- },
|
|
|
- {
|
|
|
- id: 2,
|
|
|
- projectName: '智能钻井参数优化方法研究',
|
|
|
- projectLeader: '赵海宁',
|
|
|
- projectType: '基础研究类',
|
|
|
- effectiveDate: '2026-04-15',
|
|
|
- tasks: []
|
|
|
+// ========== 选中项目 + 任务树 ==========
|
|
|
+const selectedProject = ref<any>(null)
|
|
|
+const taskTree = ref<any[]>([])
|
|
|
+
|
|
|
+const selectProject = async (project?: any) => {
|
|
|
+ if (!project) return
|
|
|
+ selectedProject.value = project
|
|
|
+ await loadTaskTree()
|
|
|
+}
|
|
|
+
|
|
|
+const loadTaskTree = async () => {
|
|
|
+ if (!selectedProject.value?.id) {
|
|
|
+ taskTree.value = []
|
|
|
+ return
|
|
|
}
|
|
|
-])
|
|
|
-const loading = ref(false)
|
|
|
-const queryFormRef = ref<FormInstance>()
|
|
|
-const queryParams = reactive({ pageNo: 1, pageSize: 10, projectName: '', projectLeader: '' })
|
|
|
-const filteredProjects = computed(() =>
|
|
|
- projects.value.filter(
|
|
|
- (project) =>
|
|
|
- (!queryParams.projectName || project.projectName.includes(queryParams.projectName)) &&
|
|
|
- (!queryParams.projectLeader || project.projectLeader.includes(queryParams.projectLeader))
|
|
|
- )
|
|
|
-)
|
|
|
-const selectedProject = ref<Project | undefined>(projects.value[0])
|
|
|
+ taskTree.value = await getTaskTreeByProjectId(selectedProject.value.id)
|
|
|
+}
|
|
|
+
|
|
|
+// ========== 任务表单 ==========
|
|
|
const taskDialogVisible = ref(false)
|
|
|
const taskDialogTitle = ref('新增一级任务')
|
|
|
const taskFormRef = ref<FormInstance>()
|
|
|
const submitLoading = ref(false)
|
|
|
-const editingTask = ref<Task>()
|
|
|
-const parentTask = ref<Task>()
|
|
|
-const taskForm = reactive(createTask())
|
|
|
+const editingTask = ref<any>()
|
|
|
+const parentTask = ref<any>()
|
|
|
+
|
|
|
+const createEmptyTask = () => ({
|
|
|
+ id: null as number | null,
|
|
|
+ projectId: null as number | null,
|
|
|
+ parentId: 0,
|
|
|
+ taskName: '',
|
|
|
+ taskDesc: '',
|
|
|
+ priority: 2,
|
|
|
+ ownerId: null as number | null,
|
|
|
+ ownerName: '',
|
|
|
+ planStartTime: '',
|
|
|
+ planEndTime: ''
|
|
|
+})
|
|
|
+
|
|
|
+const taskForm = reactive(createEmptyTask())
|
|
|
+
|
|
|
const taskRules: FormRules = {
|
|
|
- name: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
|
|
- startDate: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
|
|
- endDate: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
|
|
|
- owner: [{ required: true, message: '请输入责任人', trigger: 'blur' }],
|
|
|
+ taskName: [{ required: true, message: '请输入任务名称', trigger: 'blur' }],
|
|
|
+ planStartTime: [{ required: true, message: '请选择开始时间', trigger: 'change' }],
|
|
|
+ planEndTime: [{ required: true, message: '请选择结束时间', trigger: 'change' }],
|
|
|
priority: [{ required: true, message: '请选择任务优先级', trigger: 'change' }],
|
|
|
- description: [{ required: true, message: '请输入任务描述', trigger: 'blur' }]
|
|
|
+ taskDesc: [{ required: true, message: '请输入任务描述', trigger: 'blur' }]
|
|
|
}
|
|
|
|
|
|
-const countTasks = (tasks: Task[]): number =>
|
|
|
- tasks.reduce((total, task) => total + 1 + countTasks(task.children), 0)
|
|
|
-const handleQuery = () => (queryParams.pageNo = 1)
|
|
|
-const resetQuery = () => {
|
|
|
- queryFormRef.value?.resetFields()
|
|
|
- handleQuery()
|
|
|
-}
|
|
|
-const selectProject = (project?: Project) => {
|
|
|
- if (project) selectedProject.value = project
|
|
|
-}
|
|
|
-const priorityTagType = (priority: Priority): 'danger' | 'warning' | 'info' =>
|
|
|
- (({ 高: 'danger', 中: 'warning', 低: 'info' }) as const)[priority]
|
|
|
-const resetTaskForm = (task: Task) => {
|
|
|
- Object.assign(taskForm, task, { children: [] })
|
|
|
-}
|
|
|
-const openTaskForm = (task?: Task, editing = false) => {
|
|
|
+const openTaskForm = (task?: any, editing = false) => {
|
|
|
if (!selectedProject.value) return
|
|
|
- if (!editing && task && (task.level !== 1 || task.children.length >= MAX_CHILDREN)) {
|
|
|
- ElMessage.warning(
|
|
|
- task.level !== 1 ? '孙子任务不能继续分解' : `每个子任务最多添加 ${MAX_CHILDREN} 个孙子任务`
|
|
|
- )
|
|
|
- return
|
|
|
- }
|
|
|
editingTask.value = editing ? task : undefined
|
|
|
parentTask.value = !editing && task ? task : undefined
|
|
|
taskDialogTitle.value = editing ? '编辑任务' : task ? '新增子任务' : '新增一级任务'
|
|
|
- resetTaskForm(editing && task ? task : createTask(task ? 2 : 1))
|
|
|
+
|
|
|
+ if (editing && task) {
|
|
|
+ Object.assign(taskForm, {
|
|
|
+ id: task.id,
|
|
|
+ projectId: task.projectId,
|
|
|
+ parentId: task.parentId,
|
|
|
+ taskName: task.taskName,
|
|
|
+ taskDesc: task.taskDesc,
|
|
|
+ priority: task.priority,
|
|
|
+ ownerId: task.ownerId,
|
|
|
+ ownerName: task.ownerName,
|
|
|
+ planStartTime: task.planStartTime,
|
|
|
+ planEndTime: task.planEndTime
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ Object.assign(taskForm, createEmptyTask(), {
|
|
|
+ projectId: selectedProject.value.id,
|
|
|
+ parentId: task ? task.id : 0
|
|
|
+ })
|
|
|
+ }
|
|
|
taskDialogVisible.value = true
|
|
|
}
|
|
|
+
|
|
|
const submitTask = async () => {
|
|
|
const valid = await taskFormRef.value?.validate().catch(() => false)
|
|
|
if (!valid) return
|
|
|
- if (!taskForm.startDate || !taskForm.endDate || taskForm.startDate > taskForm.endDate) {
|
|
|
+ if (taskForm.planStartTime > taskForm.planEndTime) {
|
|
|
ElMessage.warning('结束时间不能早于开始时间')
|
|
|
return
|
|
|
}
|
|
|
- if (!selectedProject.value) return
|
|
|
submitLoading.value = true
|
|
|
- if (editingTask.value) Object.assign(editingTask.value, taskForm)
|
|
|
- else {
|
|
|
- if (
|
|
|
- parentTask.value &&
|
|
|
- (parentTask.value.level !== 1 || parentTask.value.children.length >= MAX_CHILDREN)
|
|
|
- ) {
|
|
|
- submitLoading.value = false
|
|
|
- ElMessage.warning(
|
|
|
- parentTask.value.level !== 1
|
|
|
- ? '孙子任务不能继续分解'
|
|
|
- : `每个子任务最多添加 ${MAX_CHILDREN} 个孙子任务`
|
|
|
- )
|
|
|
- return
|
|
|
+ try {
|
|
|
+ if (editingTask.value) {
|
|
|
+ await updateTaskApi(taskForm)
|
|
|
+ } else {
|
|
|
+ await createTaskApi(taskForm)
|
|
|
}
|
|
|
- const task = createTask(taskForm.level, { ...taskForm, id: Date.now() })
|
|
|
- if (parentTask.value) parentTask.value.children.push(task)
|
|
|
- else selectedProject.value.tasks.push(task)
|
|
|
+ ElMessage.success(editingTask.value ? '任务已更新' : '任务已添加')
|
|
|
+ taskDialogVisible.value = false
|
|
|
+ await loadTaskTree()
|
|
|
+ // 刷新项目列表(任务总数变了)
|
|
|
+ await getProjectList()
|
|
|
+ } finally {
|
|
|
+ submitLoading.value = false
|
|
|
}
|
|
|
- submitLoading.value = false
|
|
|
- taskDialogVisible.value = false
|
|
|
- ElMessage.success(editingTask.value ? '任务已更新' : '任务已添加')
|
|
|
}
|
|
|
-const removeTask = async (task: Task) => {
|
|
|
- if (!selectedProject.value) return
|
|
|
+
|
|
|
+const removeTask = async (task: any) => {
|
|
|
+ const hasChild = task.children && task.children.length > 0
|
|
|
const confirmed = await ElMessageBox.confirm(
|
|
|
- task.children.length
|
|
|
- ? '删除任务将同时删除其全部下级任务,确定继续吗?'
|
|
|
- : '确定删除这个任务吗?',
|
|
|
+ hasChild ? '删除任务将同时删除其全部下级任务,确定继续吗?' : '确定删除这个任务吗?',
|
|
|
'删除确认',
|
|
|
{ type: 'warning', confirmButtonText: '确定删除', cancelButtonText: '取消' }
|
|
|
)
|
|
|
.then(() => true)
|
|
|
.catch(() => false)
|
|
|
if (!confirmed) return
|
|
|
- const remove = (tasks: Task[]): boolean => {
|
|
|
- const index = tasks.findIndex((item) => item.id === task.id)
|
|
|
- if (index >= 0) {
|
|
|
- tasks.splice(index, 1)
|
|
|
- return true
|
|
|
- }
|
|
|
- return tasks.some((item) => remove(item.children))
|
|
|
- }
|
|
|
- remove(selectedProject.value.tasks)
|
|
|
+ await deleteTaskApi(task.id)
|
|
|
ElMessage.success('任务已删除')
|
|
|
+ await loadTaskTree()
|
|
|
+ await getProjectList()
|
|
|
+}
|
|
|
+
|
|
|
+// ========== 辅助 ==========
|
|
|
+const priorityLabel = (p: number) => {
|
|
|
+ const map: Record<number, string> = { 1: '高', 2: '中', 3: '低' }
|
|
|
+ return map[p] || '中'
|
|
|
+}
|
|
|
+const priorityTagType = (p: number) => {
|
|
|
+ const map: Record<number, string> = { 1: 'danger', 2: 'warning', 3: 'info' }
|
|
|
+ return map[p] || 'info'
|
|
|
+}
|
|
|
+
|
|
|
+const formatDate = (dt: string) => {
|
|
|
+ if (!dt) return ''
|
|
|
+ if (typeof dt === 'string') {
|
|
|
+ return dt.replace('T', ' ').substring(0, 10)
|
|
|
+ }
|
|
|
+ return ''
|
|
|
}
|
|
|
+
|
|
|
+// ========== 初始化 ==========
|
|
|
+onMounted(async () => {
|
|
|
+ await getProjectList()
|
|
|
+ // 默认选中第一条
|
|
|
+ if (projectList.value.length > 0) {
|
|
|
+ selectProject(projectList.value[0])
|
|
|
+ }
|
|
|
+})
|
|
|
</script>
|
|
|
|
|
|
<style scoped lang="scss">
|
|
|
-.page-heading,
|
|
|
.task-heading,
|
|
|
.task-title-row {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
}
|
|
|
-.page-heading,
|
|
|
.task-heading {
|
|
|
justify-content: space-between;
|
|
|
+ margin-bottom: 18px;
|
|
|
}
|
|
|
-.page-heading {
|
|
|
- margin-bottom: 20px;
|
|
|
-}
|
|
|
-.page-heading h2,
|
|
|
.task-heading h3 {
|
|
|
margin: 0;
|
|
|
+ font-size: 18px;
|
|
|
color: var(--el-text-color-primary);
|
|
|
}
|
|
|
-.page-heading p,
|
|
|
.task-heading span {
|
|
|
- margin: 6px 0 0;
|
|
|
+ margin-top: 6px;
|
|
|
color: var(--el-text-color-secondary);
|
|
|
font-size: 13px;
|
|
|
}
|
|
|
-.page-heading :deep(.el-tag .iconify) {
|
|
|
- margin-right: 4px;
|
|
|
+.task-title-row {
|
|
|
+ gap: 10px;
|
|
|
}
|
|
|
.project-filter {
|
|
|
margin-bottom: -15px;
|
|
|
@@ -416,15 +445,6 @@ const removeTask = async (task: Task) => {
|
|
|
.task-panel {
|
|
|
margin-top: 16px;
|
|
|
}
|
|
|
-.task-heading {
|
|
|
- margin-bottom: 18px;
|
|
|
-}
|
|
|
-.task-title-row {
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-.task-title-row h3 {
|
|
|
- font-size: 18px;
|
|
|
-}
|
|
|
.task-name {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
@@ -434,7 +454,6 @@ const removeTask = async (task: Task) => {
|
|
|
margin-bottom: 18px;
|
|
|
}
|
|
|
@media (max-width: 900px) {
|
|
|
- .page-heading,
|
|
|
.task-heading {
|
|
|
align-items: flex-start;
|
|
|
flex-direction: column;
|