yanghao преди 1 седмица
родител
ревизия
56f8260812
променени са 3 файла, в които са добавени 322 реда и са изтрити 275 реда
  1. 16 0
      src/api/project-task/index.ts
  2. 12 0
      src/api/task-progress/index.ts
  3. 294 275
      src/views/researchProject/breakdown/index.vue

+ 16 - 0
src/api/project-task/index.ts

@@ -0,0 +1,16 @@
+import request from '@/config/axios'
+
+export const getTaskTreeByProjectId = (projectId: number) =>
+  request.get({ url: '/rd/project-task/tree-by-project', params: { projectId } })
+
+export const getTaskListByProjectId = (projectId: number) =>
+  request.get({ url: '/rd/project-task/list-by-project', params: { projectId } })
+
+export const getTask = (id: number) => request.get({ url: '/rd/project-task/get', params: { id } })
+
+export const createTask = (data: any) => request.post({ url: '/rd/project-task/create', data })
+
+export const updateTask = (data: any) => request.put({ url: '/rd/project-task/update', data })
+
+export const deleteTask = (id: number) =>
+  request.delete({ url: '/rd/project-task/delete', params: { id } })

+ 12 - 0
src/api/task-progress/index.ts

@@ -0,0 +1,12 @@
+import request from '@/config/axios'
+
+export const createProgress = (data: any) => request.post({ url: '/rd/task-progress/create', data })
+
+export const batchCreateProgress = (data: any) =>
+  request.post({ url: '/rd/task-progress/batch-create', data })
+
+export const getProgressListByTaskId = (taskId: number) =>
+  request.get({ url: '/rd/task-progress/list-by-task', params: { taskId } })
+
+export const getProgressPage = (params: any) =>
+  request.get({ url: '/rd/task-progress/page', params })

+ 294 - 275
src/views/researchProject/breakdown/index.vue

@@ -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;