#20 专家库修改、任务分解

Scalone
yanghao scala 13 commity/ów z shuzhihua/sso do shuzhihua/master 1 tydzień temu

+ 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 })

+ 6 - 1
src/components/ContentWrap/src/ContentWrap.vue

@@ -16,7 +16,12 @@ defineProps({
 </script>
 
 <template>
-  <ElCard :body-style="bodyStyle" :class="[prefixCls, 'mb-15px']" shadow="never">
+  <ElCard
+    :body-style="bodyStyle"
+    :class="[prefixCls, 'mb-15px']"
+    shadow="never"
+    style="border: none"
+  >
     <template v-if="title" #header>
       <div class="flex items-center">
         <span class="text-16px font-700">{{ title }}</span>

+ 7 - 4
src/components/DeptTreeSelect/index.vue

@@ -120,8 +120,9 @@ onMounted(loadTree)
 
 <template>
   <div
-    class="dept-aside-container relative bg-white dark:bg-[#1d1e1f] shadow rounded-lg transition-all duration-300 ease-in-out overflow-visible"
-    :class="[isCollapsed ? 'is-collapsed' : 'p-4']">
+    class="dept-aside-container relative bg-white dark:bg-[#1d1e1f] rounded-md transition-all duration-300 ease-in-out overflow-visible"
+    :class="[isCollapsed ? 'is-collapsed' : 'p-4']"
+  >
     <div v-show="!isCollapsed" class="h-full flex flex-col gap-4 overflow-hidden w-full">
       <h1 v-if="showTitle" class="text-lg font-medium truncate shrink-0">{{ props.title }}</h1>
 
@@ -131,7 +132,8 @@ onMounted(loadTree)
           placeholder="请输入部门名称"
           clearable
           size="default"
-          :prefix-icon="Search" />
+          :prefix-icon="Search"
+        />
       </div>
 
       <div class="flex-1 relative overflow-hidden">
@@ -148,7 +150,8 @@ onMounted(loadTree)
                 highlight-current
                 :current-node-key="modelValue"
                 :default-expanded-keys="expandedKeys"
-                @node-click="handleNodeClick" />
+                @node-click="handleNodeClick"
+              />
             </el-scrollbar>
           </template>
         </el-auto-resizer>

+ 6 - 5
src/components/ZmTable/index.vue

@@ -518,7 +518,8 @@ defineExpose({
       'is-hover-highlight-disabled': hoverHighlight === false
     }"
     v-bind="bindProps"
-    :data="data">
+    :data="data"
+  >
     <template v-for="(_, name) in forwardedSlots" #[name]="slotData">
       <slot :name="name" v-bind="slotData || {}"></slot>
     </template>
@@ -534,10 +535,10 @@ defineExpose({
   --zm-table-strong-text-color: #24364d;
   --zm-table-row-font-weight: 500;
   --zm-table-bg: var(--el-bg-color);
-  --zm-table-border-color: #e7edf4;
+  --zm-table-border-color: none;
   --zm-table-radius: 10px;
-  --zm-table-header-bg: var(--el-fill-color-extra-light, #f7f9fc);
-  --zm-table-header-text-color: var(--el-text-color-secondary, #6b7f99);
+  --zm-table-header-bg: #fafcff;
+  --zm-table-header-text-color: #909399;
   --zm-table-header-border-color: var(--zm-table-border-color);
   --zm-table-header-cell-height: 36px;
   --zm-table-header-group-cell-height: 42px;
@@ -552,7 +553,7 @@ defineExpose({
   --zm-table-header-icon-btn-active-color: var(--zm-table-header-icon-btn-hover-color);
   --zm-table-header-icon-btn-active-bg: var(--zm-table-header-icon-btn-hover-bg);
   --zm-table-header-icon-size: 16px;
-  --zm-table-row-border-color: #edf2f7;
+  --zm-table-row-border-color: none;
   --zm-table-stripe-bg: #fcfdff;
   --zm-table-hover-bg: #f5f9ff;
   --zm-table-current-bg: #eef6ff;

+ 50 - 0
src/styles/var.css

@@ -54,6 +54,56 @@
 
   --transition-time-02: 0.2s;
 }
+.el-button--default:not(.el-button--primary):not(.el-button--success):not(.el-button--warning):not(
+    .el-button--danger
+  ):not(.el-button--info) {
+  background-color: #f3f4f6;
+  border-color: transparent;
+}
+:root {
+  --el-color-primary: #1060ff !important;
+  --el-color-primary-light-3: #4a8aff;
+  --el-color-primary-light-5: #7aaaff;
+  --el-color-primary-light-7: #aacaff;
+  --el-color-primary-light-8: #c2daff;
+  --el-color-primary-light-9: #e3edff;
+  --el-color-primary-dark-2: #0d4fd6;
+  --el-border-radius-base: 2px !important; /* 把默认的基础圆角改成 8px */
+
+  --el-input-bg-color: #f5f5f5 !important; /* 背景色 */
+  --el-input-border-color: transparent !important; /* 边框透明 */
+  --el-input-hover-border-color: transparent !important;
+  --el-input-focus-border-color: transparent !important;
+  --el-input-border-radius: 2px !important; /* 小圆角 */
+  --el-input-box-shadow: none !important; /* 去掉阴影 */
+}
+
+.el-input__wrapper,
+.el-select__wrapper {
+  background-color: #f5f5f5 !important;
+  box-shadow: none !important;
+  border: none !important;
+  font-weight: 400 !important;
+}
+
+.el-input__wrapper.is-focus,
+.el-select__wrapper.is-focused {
+  background-color: #f0f0f0 !important;
+  box-shadow: none !important;
+}
+
+.el-textarea__inner {
+  background-color: #f3f4f6 !important;
+  border: none !important;
+  border-radius: 4px !important;
+  box-shadow: none !important;
+}
+
+.el-textarea__inner:focus {
+  /* background-color: #e5e7eb;
+  box-shadow: none; */
+  border: none !important;
+}
 
 .dark {
   --app-content-bg-color: var(--el-bg-color);

+ 3 - 1
src/utils/dict.ts

@@ -277,5 +277,7 @@ export enum DICT_TYPE {
   // 科研项目
   PROJECT_TYPE = 'rd_project_type', // 项目类型
   // 项目级别
-  PROJECT_LEVEL = 'rd_project_level'
+  PROJECT_LEVEL = 'rd_project_level',
+  // 执行单位
+  EXECUTION_UNIT = 'execution_unit'
 }

+ 92 - 67
src/views/expert/list/index.vue

@@ -4,19 +4,12 @@
       <section class="filter-panel">
         <div class="filter-fields">
           <div class="filter-field filter-field-name">
-            <span class="filter-label">按姓名搜索</span>
-            <el-input
-              v-model="queryParams.expertName"
-              class="expert-name-input"
-              placeholder="输入专家姓名..."
-              clearable
-            >
+            <el-input v-model="queryParams.expertName" placeholder="输入专家姓名..." clearable>
               <template #prefix><Icon icon="ep:search" /></template>
             </el-input>
           </div>
 
           <div class="filter-field">
-            <span class="filter-label">按学历搜索</span>
             <el-select v-model="queryParams.education" placeholder="请选择专家学历" clearable>
               <el-option
                 v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_EDUCATION)"
@@ -28,7 +21,6 @@
           </div>
 
           <div class="filter-field">
-            <span class="filter-label">按梯队搜索</span>
             <el-select v-model="queryParams.echelon" placeholder="请选择专家梯队" clearable>
               <el-option
                 v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_ECHELON)"
@@ -40,7 +32,6 @@
           </div>
 
           <div class="filter-field">
-            <span class="filter-label">按专业搜索</span>
             <el-select
               v-model="queryParams.specialDirection"
               placeholder="请选择专业方向"
@@ -56,7 +47,7 @@
           </div>
 
           <div class="filter-actions">
-            <el-button color="#1e3a8a" type="primary" @click="handleAdd">
+            <el-button type="primary" @click="handleAdd" v-hasPermi="['rd:expert-pool:create']">
               <Icon icon="ep:plus" class="mr-5px" />新增专家
             </el-button>
             <el-button @click="getList"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
@@ -229,11 +220,16 @@
                 </div>
                 <div class="summary-item">
                   <span class="summary-label"><Icon icon="ep:office-building" />目前所在单位</span>
-                  <strong>{{ activeExpert.company || '-' }}</strong>
+                  <strong>{{ activeExpert.deptName || '-' }}</strong>
                 </div>
                 <div class="summary-item">
                   <span class="summary-label"><Icon icon="ep:phone" />联系方式</span>
-                  <strong>{{ activeExpert.contact || '-' }}</strong>
+                  <strong>{{ activeExpert.phone || '-' }}</strong>
+                </div>
+
+                <div class="summary-item">
+                  <span class="summary-label"><Icon icon="ep:eleme" />邮箱</span>
+                  <strong>{{ activeExpert.email || '-' }}</strong>
                 </div>
               </div>
             </div>
@@ -279,10 +275,18 @@
 
               <div class="profile-actions">
                 <el-button type="primary" size="large" @click="handleJoinTeam">加入团队</el-button>
-                <el-button size="large" type="primary" plain @click="handleEdit"
+                <el-button
+                  size="large"
+                  type="primary"
+                  @click="handleEdit"
+                  v-hasPermi="['rd:expert-pool:update']"
                   >维护档案</el-button
                 >
-                <el-button size="large" plain type="danger" @click="handleDeleteExpert"
+                <el-button
+                  size="large"
+                  type="danger"
+                  @click="handleDeleteExpert"
+                  v-hasPermi="['rd:expert-pool:delete']"
                   >删除专家</el-button
                 >
               </div>
@@ -372,6 +376,19 @@
         </el-row>
 
         <el-row :gutter="20">
+          <el-col :span="12">
+            <el-form-item label="专家梯队" prop="echelon">
+              <el-select v-model="formData.echelon" placeholder="请选择专家梯队" clearable>
+                <el-option
+                  v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_ECHELON)"
+                  :key="dict.value"
+                  :label="dict.label"
+                  :value="dict.value"
+                />
+              </el-select>
+            </el-form-item>
+          </el-col>
+
           <el-col :span="12">
             <el-form-item label="专家序列" prop="expertRank">
               <el-select
@@ -389,18 +406,6 @@
               </el-select>
             </el-form-item>
           </el-col>
-          <el-col :span="12">
-            <el-form-item label="专家梯队" prop="echelon">
-              <el-select v-model="formData.echelon" placeholder="请选择专家梯队" clearable>
-                <el-option
-                  v-for="dict in getStrDictOptions(DICT_TYPE.RD_EXPERT_ECHELON)"
-                  :key="dict.value"
-                  :label="dict.label"
-                  :value="dict.value"
-                />
-              </el-select>
-            </el-form-item>
-          </el-col>
         </el-row>
 
         <el-row :gutter="20">
@@ -499,6 +504,22 @@
           </el-col>
         </el-row>
 
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="联系电话" prop="phone">
+              <el-input v-model="formData.phone" placeholder="请输入联系电话" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
+        <el-row :gutter="20">
+          <el-col :span="24">
+            <el-form-item label="邮箱" prop="email">
+              <el-input v-model="formData.email" placeholder="请输入邮箱" />
+            </el-form-item>
+          </el-col>
+        </el-row>
+
         <el-row :gutter="20">
           <el-col :span="24">
             <el-form-item label="工作经历" prop="formerPost">
@@ -637,7 +658,9 @@ const defaultFormData = () => ({
   formerPost: '',
   jobTitle: '',
   professionalTitle: '',
-  expertRank: []
+  expertRank: [],
+  phone: '',
+  email: ''
 })
 
 const deptList2 = ref<Tree[]>([]) // 树形结构
@@ -715,7 +738,9 @@ const buildFormDataFromExpertDetail = (detail: any) => {
     formerPost: detail?.formerPost || '',
     jobTitle: detail?.jobTitle || '',
     professionalTitle: detail?.professionalTitle || '',
-    expertRank: detail?.expertRank || []
+    expertRank: detail?.expertRank || [],
+    phone: detail?.phone || '',
+    email: detail?.email || ''
   }
 }
 
@@ -773,6 +798,9 @@ type ExpertProfile = {
   tags: string[]
   achievement: string
   records: ExpertRecord[]
+  phone: string
+  email: string
+  deptName: string
 }
 
 type ExpertItem = {
@@ -799,6 +827,8 @@ type ExpertItem = {
   softwareSkills: number[]
   teams?: Array<{ id: number; teamName?: string }>
   sort?: number // 同梯队同序列内的排序值
+  email: string
+  deptName: string
 }
 
 type ExpertGroup = {
@@ -957,7 +987,10 @@ const buildExpertProfile = (expert: ExpertItem, groupLabel?: string): ExpertProf
     honors: expert.remark || '',
     tags: [expert.professionalTitle].filter(Boolean),
     achievement: expert.achievement || '暂无',
-    records: []
+    records: [],
+    phone: expert.phone || '',
+    email: expert.email || '',
+    deptName: expert.deptName || ''
   }
 }
 
@@ -1416,8 +1449,8 @@ onMounted(async () => {
 .expert-carousel {
   margin: 0 0 18px;
   padding: 18px 22px 20px;
-  border: 1px solid #dfe6ef;
-  border-radius: 10px;
+
+  border-radius: 5px;
   background: #fff;
   box-shadow: 0 2px 4px rgba(34, 57, 86, 0.06);
 }
@@ -1529,8 +1562,8 @@ onMounted(async () => {
 .filter-panel {
   min-width: 0;
   padding: 28px 29px 26px;
-  border: 1px solid #dfe6ef;
-  border-radius: 10px;
+
+  border-radius: 5px;
   box-sizing: border-box;
   background: #fff;
   box-shadow: 0 2px 4px rgba(34, 57, 86, 0.06);
@@ -1574,15 +1607,6 @@ onMounted(async () => {
   width: 100%;
 }
 
-.filter-fields :deep(.el-input__wrapper),
-.filter-fields :deep(.el-select__wrapper) {
-  min-height: 32px;
-  border-radius: 8px;
-  background: #fff;
-  box-shadow: 0 0 0 1px #dce4ee inset;
-  transition: box-shadow 180ms ease;
-}
-
 .filter-fields :deep(.el-input__wrapper:hover),
 .filter-fields :deep(.el-select__wrapper:hover),
 .filter-fields :deep(.el-input__wrapper.is-focus),
@@ -2194,7 +2218,7 @@ onMounted(async () => {
   margin: 0;
   color: #102234;
   font-size: 18px;
-  font-weight: 800;
+  font-weight: normal;
 }
 
 .drawer-header p {
@@ -2211,9 +2235,9 @@ onMounted(async () => {
   margin: 18px;
   padding: 16px;
   border: 1px solid rgba(28, 44, 62, 0.08);
-  border-radius: 8px;
+  border-radius: 5px;
   background: #fff;
-  box-shadow: 0 8px 30px rgba(28, 44, 62, 0.06);
+  // box-shadow: 0 8px 30px rgba(28, 44, 62, 0.06);
 }
 
 .profile-aside {
@@ -2228,7 +2252,7 @@ onMounted(async () => {
   justify-content: center;
   min-height: 150px;
   max-height: 150px;
-  border-radius: 14px;
+  border-radius: 5px;
   background: linear-gradient(145deg, #b7d3cd, #e8f0ee);
   color: #0f5b58;
   font-size: 46px;
@@ -2255,8 +2279,8 @@ onMounted(async () => {
   flex-direction: column;
   gap: 3px;
   padding: 9px 10px;
-  border: 1px solid #dbe3df;
-  border-radius: 8px;
+  border-bottom: 1px dashed rgba(28, 44, 62, 0.08);
+  border-radius: 5px;
   background: #fdfefe;
   overflow-wrap: anywhere;
 }
@@ -2313,8 +2337,9 @@ onMounted(async () => {
 
 .info-block {
   padding: 12px 14px;
-  border: 1px solid #dbe3df;
-  border-radius: 12px;
+  border: none;
+  border-bottom: 1px dashed rgba(28, 44, 62, 0.08);
+
   background: #fdfefe;
 }
 
@@ -2327,9 +2352,9 @@ onMounted(async () => {
 }
 
 .info-block strong {
-  color: #203130;
-  font-size: 13px;
-  font-weight: 800;
+  // color: #203130;
+  // font-size: 13px;
+  font-weight: normal;
   line-height: 1.55;
 }
 
@@ -2339,22 +2364,22 @@ onMounted(async () => {
   gap: 10px;
 }
 
-.profile-actions :deep(.el-button) {
-  height: 42px;
-  border-radius: 10px;
-  font-size: 14px;
-  font-weight: 700;
-}
+// .profile-actions :deep(.el-button) {
+//   height: 42px;
+//   border-radius: 10px;
+//   font-size: 14px;
+//   font-weight: 700;
+// }
 
-.profile-actions :deep(.el-button--success) {
-  background: #237b63;
-  border-color: #237b63;
-}
+// .profile-actions :deep(.el-button--success) {
+//   background: #237b63;
+//   border-color: #237b63;
+// }
 
 .record-panel {
   margin: 0 20px 26px;
-  border: 1px solid rgba(28, 44, 62, 0.08);
-  border-radius: 8px;
+
+  border-radius: 5px;
   overflow: hidden;
   background: #fff;
   // box-shadow: 0 8px 30px rgba(28, 44, 62, 0.05);
@@ -2369,7 +2394,7 @@ onMounted(async () => {
   margin: 0;
   color: #1e2d3a;
   font-size: 18px;
-  font-weight: 800;
+  font-weight: normal;
 }
 
 .record-header p {

+ 15 - 3
src/views/expert/team/index.vue

@@ -38,7 +38,7 @@
           <Icon icon="ep:refresh" class="mr-5px" />
           重置
         </el-button>
-        <el-button type="primary" plain @click="openForm('create')">
+        <el-button type="primary" @click="openForm('create')" v-hasPermi="['rd:team:create']">
           <Icon icon="ep:plus" class="mr-5px" />
           新增
         </el-button>
@@ -82,8 +82,20 @@
       </zm-table-column>
       <zm-table-column label="操作" align="center" width="160" fixed="right" action>
         <template #default="{ row }">
-          <el-button link type="primary" @click.stop="openForm('update', row.id)">编辑</el-button>
-          <el-button link type="danger" @click.stop="handleDelete(row.id)">删除</el-button>
+          <el-button
+            v-hasPermi="['rd:team:update']"
+            link
+            type="primary"
+            @click.stop="openForm('update', row.id)"
+            >编辑</el-button
+          >
+          <el-button
+            v-hasPermi="['rd:team:delete']"
+            link
+            type="danger"
+            @click.stop="handleDelete(row.id)"
+            >删除</el-button
+          >
         </template>
       </zm-table-column>
     </zm-table>

+ 1 - 1
src/views/findings/copyright/index.vue

@@ -47,7 +47,7 @@
         <el-form-item>
           <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
           <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
-          <el-button type="primary" plain @click="openForm('create')">
+          <el-button type="primary" @click="openForm('create')">
             <Icon icon="ep:plus" class="mr-5px" />新增
           </el-button>
         </el-form-item>

+ 1 - 1
src/views/news/list/index.vue

@@ -34,7 +34,7 @@
       <el-form-item>
         <el-button @click="handleQuery"><Icon icon="ep:search" class="mr-5px" />搜索</el-button>
         <el-button @click="resetQuery"><Icon icon="ep:refresh" class="mr-5px" />重置</el-button>
-        <el-button type="primary" plain @click="openForm('create')">
+        <el-button type="primary" @click="openForm('create')">
           <Icon icon="ep:plus" class="mr-5px" />新增
         </el-button>
       </el-form-item>

+ 371 - 326
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,397 +26,435 @@
     </el-form>
   </ContentWrap>
 
-  <ContentWrap>
-    <zm-table
-      v-loading="loading"
-      :data="filteredProjects"
-      row-key="id"
-      highlight-current-row
-      class="project-table"
-      @current-change="selectProject"
-    >
-      <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>
-    <Pagination
-      v-model:page="queryParams.pageNo"
-      v-model:limit="queryParams.pageSize"
-      :total="filteredProjects.length"
-    />
-  </ContentWrap>
-
-  <ContentWrap 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>
-      <el-button type="primary" @click="openTaskForm()"
-        ><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
-    >
-    <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
-        >
+  <ContentWrap style="border: none">
+    <zm-table v-loading="loading" :data="projectList" row-key="id">
+      <zm-table-column label="项目名称" prop="projectName" min-width="240" show-overflow-tooltip />
+      <zm-table-column label="项目负责人" prop="projectLeader" width="120" />
+      <zm-table-column label="项目类型" prop="projectType" min-width="160">
+        <template #default="{ row }">
+          <dict-tag :type="DICT_TYPE.PROJECT_TYPE" :value="row.projectType" />
+        </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="projectDate" width="120">
+        <template #default="scope">
+          <span v-if="scope.row.projectDate">
+            {{ formatToDate(scope.row.projectDate) }}
+          </span>
+        </template>
+      </zm-table-column>
+      <zm-table-column label="任务数" prop="taskTotal" width="90" align="center" />
+      <zm-table-column label="操作" width="200" fixed="right" align="center">
         <template #default="{ row }">
           <el-button
-            v-if="row.level === 1"
             link
             type="primary"
-            :disabled="row.children.length >= MAX_CHILDREN"
+            v-hasPermi="['rd:project-task:create']"
             @click="openTaskForm(row)"
-            >新增子任务</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>
+            <Icon icon="ep:plus" /> 新增任务
+          </el-button>
+          <el-button link type="primary" @click="openTaskDrawer(row)">查看任务</el-button>
         </template>
       </zm-table-column>
     </zm-table>
+    <Pagination
+      v-model:page="queryParams.pageNo"
+      v-model:limit="queryParams.pageSize"
+      :total="total"
+      @pagination="getProjectList"
+    />
   </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 }}
+    </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>
+
+  <!-- 查看任务抽屉 -->
+  <el-drawer
+    v-model="drawerVisible"
+    :title="`任务列表:${currentProject?.projectName || ''}`"
+    size="75%"
+    destroy-on-close
+  >
+    <div class="drawer-header">
+      <span>共 {{ taskTree.length }} 个任务</span>
+      <el-button
+        type="primary"
+        style="background-color: #1560fc"
+        @click="openTaskForm()"
+        v-hasPermi="['rd:project-task:create']"
+      >
+        <Icon icon="ep:plus" /> 新增一级任务
+      </el-button>
+    </div>
+
+    <el-empty v-if="!taskTree.length" description="暂未分解任务">
+      <!-- <el-button type="primary" @click="openTaskForm()">
+        <Icon icon="ep:plus" /> 新增一级任务
+      </el-button> -->
+    </el-empty>
+
+    <el-table
+      v-else
+      :data="taskTree"
+      row-key="id"
+      default-expand-all
+      :tree-props="{ children: 'children' }"
+      :header-cell-style="{ background: '#f5f7fa', color: '#606266', fontWeight: 'none' }"
+    >
+      <el-table-column label="任务名称" min-width="220" class-name="task-name-col">
+        <template #default="{ row }">
+          <!-- 将 div 换成 span,并设为 flex 布局 -->
+          <span class="task-name">
+            <span>{{ row.taskName }}</span>
+            <!-- <el-tag v-if="row.level === 3" type="info" effect="plain" size="small">末级</el-tag> -->
+          </span>
+        </template>
+      </el-table-column>
+      <el-table-column label="开始时间" prop="planStartTime" width="120" />
+      <el-table-column label="结束时间" prop="planEndTime" width="120" />
+      <el-table-column label="责任人" prop="ownerName" width="110" />
+      <el-table-column label="优先级" width="90" align="center">
+        <template #default="{ row }">
+          <el-tag :type="priorityTagType(row.priority)">
+            {{ priorityLabel(row.priority) }}
+          </el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="进度" width="180" align="center">
+        <template #default="{ row }">
+          <el-progress
+            :percentage="row.progress || 0"
+            :status="row.status === 3 ? 'exception' : row.status === 2 ? 'success' : ''"
+            :stroke-width="8"
+          />
+        </template>
+      </el-table-column>
+      <el-table-column label="任务描述" prop="taskDesc" min-width="180" show-overflow-tooltip />
+      <el-table-column label="操作" width="220" fixed="right" align="center">
+        <template #default="{ row }">
+          <el-button
+            v-if="row.level < 3"
+            link
+            style="color: #4b7bb1"
+            type="primary"
+            v-hasPermi="['rd:project-task:create']"
+            @click="openTaskForm(row)"
+          >
+            新增子任务
+          </el-button>
+          <!-- <el-button v-else link type="info" disabled>不可再分解</el-button> -->
+          <el-button
+            link
+            type="primary"
+            v-hasPermi="['rd:project-task:update']"
+            @click="openTaskForm(row, true)"
+            style="color: #4b7bb1"
+          >
+            编辑
+          </el-button>
+          <el-button
+            link
+            type="danger"
+            v-hasPermi="['rd:project-task:delete']"
+            @click="removeTask(row)"
+          >
+            删除
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+  </el-drawer>
 </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'
+import { formatToDate } from '@/utils/dateUtil'
 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 drawerVisible = ref(false)
+const currentProject = ref<any>(null)
+const taskTree = ref<any[]>([])
+
+const openTaskDrawer = async (project: any) => {
+  currentProject.value = project
+  drawerVisible.value = true
+  await loadTaskTree()
+}
+
+const loadTaskTree = async () => {
+  if (!currentProject.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(currentProject.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) => {
-  if (!selectedProject.value) return
-  if (!editing && task && (task.level !== 1 || task.children.length >= MAX_CHILDREN)) {
-    ElMessage.warning(
-      task.level !== 1 ? '孙子任务不能继续分解' : `每个子任务最多添加 ${MAX_CHILDREN} 个孙子任务`
-    )
+/**
+ * 打开任务表单
+ * - project: 从项目列表点击时传入(新增一级任务)
+ * - task: 从任务树点击"新增子任务"或"编辑"时传入
+ * - editing: 是否编辑模式
+ */
+const openTaskForm = (projectOrTask?: any, editing = false) => {
+  // 场景 1:从项目列表点"新增任务"(projectOrTask 是项目对象)
+  if (projectOrTask && projectOrTask.projectName && !projectOrTask.taskName) {
+    editingTask.value = undefined
+    parentTask.value = undefined
+    taskDialogTitle.value = '新增一级任务'
+    Object.assign(taskForm, createEmptyTask(), {
+      projectId: projectOrTask.id
+    })
+    taskDialogVisible.value = true
     return
   }
-  editingTask.value = editing ? task : undefined
-  parentTask.value = !editing && task ? task : undefined
-  taskDialogTitle.value = editing ? '编辑任务' : task ? '新增子任务' : '新增一级任务'
-  resetTaskForm(editing && task ? task : createTask(task ? 2 : 1))
+
+  // 场景 2:从任务树里点"新增子任务"或"编辑"(projectOrTask 是任务对象)
+  const task = projectOrTask
+  if (editing && task) {
+    // 编辑任务
+    editingTask.value = task
+    parentTask.value = undefined
+    taskDialogTitle.value = '编辑任务'
+    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 {
+    // 新增子任务
+    editingTask.value = undefined
+    parentTask.value = task || undefined
+    taskDialogTitle.value = task ? '新增子任务' : '新增一级任务'
+    Object.assign(taskForm, createEmptyTask(), {
+      projectId: currentProject.value?.id || null,
+      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)
+    }
+    ElMessage.success(editingTask.value ? '任务已更新' : '任务已添加')
+    taskDialogVisible.value = false
+    // 如果抽屉是打开的,刷新任务树
+    if (drawerVisible.value) {
+      await loadTaskTree()
     }
-    const task = createTask(taskForm.level, { ...taskForm, id: Date.now() })
-    if (parentTask.value) parentTask.value.children.push(task)
-    else selectedProject.value.tasks.push(task)
+    // 刷新项目列表(任务数变了)
+    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()
 }
-</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;
+// ========== 辅助 ==========
+const priorityLabel = (p: number) => {
+  const map: Record<number, string> = { 1: '高', 2: '中', 3: '低' }
+  return map[p] || '中'
 }
-.page-heading {
-  margin-bottom: 20px;
+const priorityTagType = (p: number) => {
+  const map: Record<number, string> = { 1: 'danger', 2: 'warning', 3: 'info' }
+  return map[p] || 'info'
 }
-.page-heading h2,
-.task-heading h3 {
-  margin: 0;
-  color: var(--el-text-color-primary);
-}
-.page-heading p,
-.task-heading span {
-  margin: 6px 0 0;
-  color: var(--el-text-color-secondary);
-  font-size: 13px;
-}
-.page-heading :deep(.el-tag .iconify) {
-  margin-right: 4px;
+
+const formatDate = (dt: string) => {
+  if (!dt) return ''
+  if (typeof dt === 'string') {
+    return dt.replace('T', ' ').substring(0, 10)
+  }
+  return ''
 }
+
+// ========== 初始化 ==========
+onMounted(() => {
+  getProjectList()
+})
+</script>
+
+<style scoped lang="scss">
 .project-filter {
   margin-bottom: -15px;
 }
-.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;
@@ -433,12 +463,27 @@ const removeTask = async (task: Task) => {
 .parent-alert {
   margin-bottom: 18px;
 }
-@media (max-width: 900px) {
-  .page-heading,
-  .task-heading {
-    align-items: flex-start;
-    flex-direction: column;
-    gap: 14px;
-  }
+.drawer-header {
+  display: flex;
+  align-items: center;
+  justify-content: space-between;
+  margin-bottom: 16px;
+}
+.drawer-header span {
+  color: var(--el-text-color-secondary);
+  font-size: 13px;
+}
+
+/* 让该列的单元格内容变成 flex 布局,使图标和文字在同一行 */
+:deep(.task-name-col .cell) {
+  display: flex;
+  align-items: center;
+}
+
+/* 任务名称的容器样式 */
+.task-name {
+  display: inline-flex;
+  align-items: center;
+  gap: 8px; /* 给文字和标签之间加点间距 */
 }
 </style>

+ 282 - 77
src/views/researchProject/list/index.vue

@@ -30,7 +30,7 @@
           <Icon icon="ep:search" class="mr-5px" />搜索
         </el-button>
         <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
-        <el-button type="primary" plain @click="openForm()" v-hasPermi="['rd:project:create']">
+        <el-button type="primary" @click="openForm()" v-hasPermi="['rd:project:create']">
           <Icon icon="ep:plus" class="mr-5px" />新增项目
         </el-button>
       </el-form-item>
@@ -40,20 +40,37 @@
   <ContentWrap style="border: none">
     <zm-table v-loading="loading" :data="list" row-key="id">
       <zm-table-column label="项目名称" prop="projectName" min-width="220" show-overflow-tooltip />
-      <zm-table-column label="立项时间" prop="projectDate" min-width="220" show-overflow-tooltip />
+      <zm-table-column label="立项时间" prop="projectDate" min-width="220" show-overflow-tooltip>
+        <template #default="scope">
+          <span v-if="scope.row.projectDate">
+            {{ formatToDate(scope.row.projectDate) }}
+          </span>
+        </template>
+      </zm-table-column>
       <zm-table-column
         label="计划结题时间"
-        prop="planEndDaate"
+        prop="planEndDate"
         min-width="220"
         show-overflow-tooltip
-      />
+      >
+        <template #default="scope">
+          <span v-if="scope.row.planEndDate">
+            {{ scope.row.planEndDate }}
+          </span>
+        </template>
+      </zm-table-column>
 
-      <zm-table-column
-        label="执行单位"
-        prop="executingUnitName"
-        min-width="180"
-        show-overflow-tooltip
-      />
+      <zm-table-column label="执行单位" min-width="180" show-overflow-tooltip>
+        <template #default="scope">
+          <dict-tag
+            class="mr-5px"
+            v-for="(item, index) in scope.row.executingUnit"
+            :key="index"
+            :type="DICT_TYPE.EXECUTION_UNIT"
+            :value="item"
+          />
+        </template>
+      </zm-table-column>
       <zm-table-column label="项目级别" prop="projectLevel" min-width="150">
         <template #default="scope">
           <dict-tag :type="DICT_TYPE.PROJECT_LEVEL" :value="scope.row.projectLevel" />
@@ -126,7 +143,7 @@
     />
   </ContentWrap>
 
-  <!-- 新增/编辑/查看 弹窗 -->
+  <!-- 新增/编辑 弹窗 -->
   <ResearchProjectDialog
     v-model="dialogVisible"
     :title="dialogTitle"
@@ -139,7 +156,6 @@
       v-loading="formLoading"
       :model="formData"
       :rules="formRules"
-      :disabled="isViewMode"
       label-width="112px"
       class="project-form"
     >
@@ -159,17 +175,19 @@
         </el-col>
         <el-col :span="12">
           <el-form-item label="执行单位" prop="executingUnit">
-            <el-tree-select
-              clearable
+            <el-select
               v-model="formData.executingUnit"
-              :data="deptList"
-              :props="defaultProps"
-              :check-strictly="true"
-              node-key="id"
-              filterable
+              multiple
               placeholder="请选择执行单位"
-              class="w-full"
-            />
+              clearable
+            >
+              <el-option
+                v-for="dict in getIntDictOptions(DICT_TYPE.EXECUTION_UNIT)"
+                :key="dict.value"
+                :label="dict.label"
+                :value="dict.value"
+              />
+            </el-select>
           </el-form-item>
         </el-col>
         <el-col :span="12">
@@ -202,6 +220,34 @@
             </el-select>
           </el-form-item>
         </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="项目所属单位" prop="deptId">
+            <el-tree-select
+              clearable
+              v-model="formData.deptId"
+              :data="deptList"
+              :props="defaultProps"
+              :check-strictly="true"
+              node-key="id"
+              filterable
+              placeholder="请选择项目所属单位"
+              class="w-full"
+            />
+          </el-form-item>
+        </el-col>
+
+        <el-col :span="12">
+          <el-form-item label="计划结题时间" prop="planEndDate">
+            <el-date-picker
+              v-model="formData.planEndDate"
+              placeholder="请选择计划结题时间"
+              type="date"
+              value-format="YYYY-MM-DD"
+              class="w-full!"
+            />
+          </el-form-item>
+        </el-col>
       </el-row>
 
       <el-divider content-position="left">项目内容</el-divider>
@@ -240,7 +286,7 @@
       <div class="schedule-editor">
         <div class="schedule-toolbar">
           <span>按阶段填写工作计划与截止时间</span>
-          <el-button v-if="!isViewMode" type="primary" plain size="small" @click="addSchedule">
+          <el-button type="primary" plain size="small" @click="addSchedule">
             <Icon icon="ep:plus" class="mr-5px" />增加工作
           </el-button>
         </div>
@@ -262,7 +308,7 @@
               />
             </template>
           </zm-table-column>
-          <zm-table-column v-if="!isViewMode" label="操作" width="90" align="center">
+          <zm-table-column label="操作" width="90" align="center">
             <template #default="{ $index }">
               <el-button link type="danger" @click="removeSchedule($index)">
                 <Icon icon="ep:delete" />
@@ -276,7 +322,7 @@
       <div class="budget-editor">
         <div class="budget-toolbar">
           <span>请按财务科目录入项目预算,金额需填写到元。</span>
-          <el-button v-if="!isViewMode" type="primary" plain size="small" @click="addBudgetDetail">
+          <el-button type="primary" plain size="small" @click="addBudgetDetail">
             <Icon icon="ep:plus" class="mr-5px" />新增预算科目
           </el-button>
         </div>
@@ -321,7 +367,7 @@
               />
             </template>
           </zm-table-column>
-          <zm-table-column v-if="!isViewMode" label="操作" width="90" align="center">
+          <zm-table-column label="操作" width="90" align="center">
             <template #default="{ $index }">
               <el-button
                 link
@@ -346,7 +392,6 @@
           v-model="applicationFile"
           :return-object="true"
           :limit="10"
-          :disabled="isViewMode"
           :file-type="['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg']"
         />
       </el-form-item>
@@ -355,7 +400,6 @@
           v-model="openingReportFile"
           :return-object="true"
           :limit="10"
-          :disabled="isViewMode"
           :file-type="['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg']"
         />
       </el-form-item>
@@ -364,7 +408,6 @@
           v-model="otherFile"
           :return-object="true"
           :limit="10"
-          :disabled="isViewMode"
           :file-type="['doc', 'docx', 'pdf', 'xls', 'xlsx', 'jpg', 'png']"
         />
       </el-form-item>
@@ -382,16 +425,166 @@
           box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
         "
       >
-        <el-button @click="dialogVisible = false">
-          {{ isViewMode ? '关闭' : '取消' }}
-        </el-button>
-        <el-button v-if="!isViewMode" type="primary" :loading="submitLoading" @click="submitForm">
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">
           保存项目
         </el-button>
       </div>
     </template>
   </ResearchProjectDialog>
 
+  <!-- 查看详情抽屉 -->
+  <el-drawer
+    v-model="detailVisible"
+    title="查看科研项目"
+    size="50%"
+    direction="rtl"
+    destroy-on-close
+  >
+    <div v-loading="detailLoading" class="detail-container">
+      <el-descriptions :column="2" border>
+        <el-descriptions-item label="项目名称">
+          {{ detailData.projectName }}
+        </el-descriptions-item>
+        <el-descriptions-item label="项目级别">
+          <dict-tag
+            v-if="detailData.projectLevel"
+            :type="DICT_TYPE.PROJECT_LEVEL"
+            :value="detailData.projectLevel"
+          />
+        </el-descriptions-item>
+        <el-descriptions-item label="项目类型">
+          <dict-tag
+            v-if="detailData.projectType"
+            :type="DICT_TYPE.PROJECT_TYPE"
+            :value="detailData.projectType"
+          />
+        </el-descriptions-item>
+        <el-descriptions-item label="执行单位">
+          <dict-tag
+            class="mr-5px"
+            v-for="(item, index) in detailData.executingUnit"
+            :key="index"
+            :type="DICT_TYPE.EXECUTION_UNIT"
+            :value="item"
+          />
+        </el-descriptions-item>
+        <el-descriptions-item label="项目负责人">
+          {{ detailData.projectLeader }}
+        </el-descriptions-item>
+        <el-descriptions-item label="项目组成员">
+          {{ detailData.teamMembers || '-' }}
+        </el-descriptions-item>
+        <el-descriptions-item label="预审状态">
+          <el-tag :type="getPreAuditTagType(detailData.preAuditStatus)">
+            {{ getPreAuditLabel(detailData.preAuditStatus) }}
+          </el-tag>
+        </el-descriptions-item>
+        <el-descriptions-item label="正式立项状态">
+          <el-tag :type="getFormalAuditTagType(detailData.formalAuditStatus)">
+            {{ getFormalAuditLabel(detailData.formalAuditStatus) }}
+          </el-tag>
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <el-divider content-position="left">项目内容</el-divider>
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="目标和意义">
+          {{ detailData.goalAndSignificance }}
+        </el-descriptions-item>
+        <el-descriptions-item label="主要研究内容">
+          {{ detailData.mainResearchContent }}
+        </el-descriptions-item>
+        <el-descriptions-item label="创新点">
+          {{ detailData.innovationPoint }}
+        </el-descriptions-item>
+      </el-descriptions>
+
+      <el-divider content-position="left">进度安排</el-divider>
+      <zm-table :data="detailData.scheduleList" class="detail-table">
+        <zm-table-column type="index" label="#" width="56" align="center" />
+        <zm-table-column label="工作计划" prop="workPlan" min-width="200" show-overflow-tooltip />
+        <zm-table-column label="截止时间" prop="deadline" width="150">
+          <template #default="{ row }">
+            <span>{{ row.deadline }}</span>
+          </template>
+        </zm-table-column>
+      </zm-table>
+
+      <el-divider content-position="left">费用预算明细(元)</el-divider>
+      <zm-table :data="detailData.budgetList" class="detail-table">
+        <zm-table-column type="index" label="#" width="56" align="center" />
+        <zm-table-column
+          label="财务科目"
+          prop="financialSubject"
+          min-width="150"
+          show-overflow-tooltip
+        />
+        <zm-table-column label="预算金额(元)" prop="budgetAmount" width="150" align="right" />
+        <zm-table-column
+          label="预算说明"
+          prop="budgetDescription"
+          min-width="200"
+          show-overflow-tooltip
+        />
+      </zm-table>
+      <div class="budget-total">
+        <span>预算合计</span>
+        <strong>{{ computeBudgetTotal(detailData.budgetList).toFixed(2) }}</strong>
+        <span>元</span>
+      </div>
+
+      <el-divider content-position="left">附件信息</el-divider>
+      <el-descriptions :column="1" border>
+        <el-descriptions-item label="立项申请表">
+          <div v-if="detailApplicationFile.length">
+            <el-link
+              v-for="(file, index) in detailApplicationFile"
+              :key="index"
+              :href="file.filePath"
+              type="primary"
+              target="_blank"
+              class="mr-10px"
+            >
+              {{ file.fileName || `附件${index + 1}` }}
+            </el-link>
+          </div>
+          <span v-else>暂无</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="开题报告">
+          <div v-if="detailOpeningReportFile.length">
+            <el-link
+              v-for="(file, index) in detailOpeningReportFile"
+              :key="index"
+              :href="file.filePath"
+              type="primary"
+              target="_blank"
+              class="mr-10px"
+            >
+              {{ file.fileName || `附件${index + 1}` }}
+            </el-link>
+          </div>
+          <span v-else>暂无</span>
+        </el-descriptions-item>
+        <el-descriptions-item label="其他文件">
+          <div v-if="detailOtherFile.length">
+            <el-link
+              v-for="(file, index) in detailOtherFile"
+              :key="index"
+              :href="file.filePath"
+              type="primary"
+              target="_blank"
+              class="mr-10px"
+            >
+              {{ file.name || `附件${index + 1}` }}
+            </el-link>
+          </div>
+          <span v-else>暂无</span>
+        </el-descriptions-item>
+      </el-descriptions>
+    </div>
+  </el-drawer>
+
   <!-- 审批弹窗 -->
   <el-dialog v-model="approveVisible" title="项目审批" width="600px">
     <el-form :model="approveForm" label-width="80px">
@@ -422,9 +615,8 @@
 <script setup lang="ts">
 import { computed, reactive, ref, onMounted } from 'vue'
 import { ElMessage, ElMessageBox, type FormRules } from 'element-plus'
-import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import { DICT_TYPE, getStrDictOptions, getIntDictOptions } from '@/utils/dict'
 import { getSimpleDeptList } from '@/api/system/dept'
-import { getUserProfile } from '@/api/system/user/profile'
 import {
   getProjectPage,
   getProject,
@@ -436,6 +628,7 @@ import {
   approvePre,
   approveFormal
 } from '@/api/project'
+import { formatToDate } from '@/utils/dateUtil'
 
 defineOptions({ name: 'ResearchProjectList' })
 
@@ -464,7 +657,6 @@ const defaultProps = {
   value: 'id'
 }
 const deptList = ref<any[]>([])
-const deptId = ref('')
 
 // ========== 类型 ==========
 type ScheduleItem = { workPlan: string; deadline: string; sort?: number }
@@ -475,7 +667,7 @@ type BudgetDetail = {
   sort?: number
 }
 
-// ========== 附件:三个独立 ref,完全不进 formData ==========
+// ========== 附件:表单使用 ==========
 const applicationFile = ref<any[]>([])
 const openingReportFile = ref<any[]>([])
 const otherFile = ref<any[]>([])
@@ -581,15 +773,13 @@ const canSubmitFormal = (row: any) =>
 
 const canDelete = (row: any) => row.preAuditStatus === 0 || row.preAuditStatus === 3
 
-// ========== 表单 ==========
+// ========== 表单(新增/编辑) ==========
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增科研项目')
 const formLoading = ref(false)
 const submitLoading = ref(false)
 const formRef = ref()
 const editingId = ref<number>()
-// ⭐ 新增:只读模式标记
-const isViewMode = ref(false)
 
 const createSchedule = (): ScheduleItem => ({ workPlan: '', deadline: '' })
 const createBudgetDetail = (): BudgetDetail => ({
@@ -599,7 +789,8 @@ const createBudgetDetail = (): BudgetDetail => ({
 })
 
 const createForm = () => ({
-  deptId: deptId.value,
+  deptId: '',
+  planEndDate: '',
   projectLevel: '',
   executingUnit: null as number | null,
   projectName: '',
@@ -626,41 +817,30 @@ const formRules = reactive<FormRules>({
   projectType: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
   goalAndSignificance: [{ required: true, message: '请输入目标和意义', trigger: 'blur' }],
   mainResearchContent: [{ required: true, message: '请输入主要研究内容', trigger: 'blur' }],
-  innovationPoint: [{ required: true, message: '请输入创新点', trigger: 'blur' }]
+  innovationPoint: [{ required: true, message: '请输入创新点', trigger: 'blur' }],
+  deptId: [{ required: true, message: '请选择项目所属部门', trigger: 'change' }],
+  planEndDate: [{ required: true, message: '请选择计划结束时间', trigger: 'change' }]
 })
 
 const budgetTotal = computed(() =>
   formData.value.budgetList.reduce((sum, item) => sum + Number(item.budgetAmount || 0), 0)
 )
 
-// ========== 打开表单(含只读模式) ==========
-/**
- * @param project 项目数据(有 id 表示编辑/查看)
- * @param mode    'view' 只读 | 'edit' 可编辑
- */
-const openForm = async (project?: any, mode: 'view' | 'edit' = 'edit') => {
+const computeBudgetTotal = (budgetList: BudgetDetail[]) => {
+  if (!budgetList) return 0
+  return budgetList.reduce((sum, item) => sum + Number(item.budgetAmount || 0), 0)
+}
+
+const openForm = async (project?: any) => {
   dialogVisible.value = true
-  isViewMode.value = mode === 'view'
-  editingId.value = mode === 'edit' ? project?.id : undefined
+  editingId.value = project?.id
 
-  if (isViewMode.value) {
-    dialogTitle.value = '查看科研项目'
-  } else {
-    dialogTitle.value = project ? '编辑科研项目' : '新增科研项目'
-  }
+  dialogTitle.value = project ? '编辑科研项目' : '新增科研项目'
 
   formLoading.value = true
   try {
     if (project?.id) {
       const data = await getProject(project.id)
-
-      // ⭐ 从 attachments 里按 type 拆出 3 个数组
-      const attachments = data.attachments || []
-      const applicationFileData = attachments.filter((a: any) => a.type === 'APPLICATION')
-      const openingReportFileData = attachments.filter((a: any) => a.type === 'OPENING_REPORT')
-      const otherFileData = attachments.filter((a: any) => a.type === 'OTHER')
-
-      // ⭐ 显式挑字段,避免 ...data 把 id、createTime、审批状态等脏数据带进 formData
       formData.value = {
         ...createForm(),
         projectLevel: data.projectLevel,
@@ -673,15 +853,17 @@ const openForm = async (project?: any, mode: 'view' | 'edit' = 'edit') => {
         mainResearchContent: data.mainResearchContent,
         innovationPoint: data.innovationPoint,
         involveBudget: data.involveBudget,
-        deptId: deptId.value,
+        deptId: data.deptId,
         scheduleList: data.scheduleList?.length ? data.scheduleList : [createSchedule()],
-        budgetList: data.budgetList?.length ? data.budgetList : [createBudgetDetail()]
+        budgetList: data.budgetList?.length ? data.budgetList : [createBudgetDetail()],
+        planEndDate: data.planEndDate
       }
 
-      // ⭐ 附件回显到独立 ref
-      applicationFile.value = applicationFileData
-      openingReportFile.value = openingReportFileData
-      otherFile.value = otherFileData
+      // 附件回显
+      const attachments = data.attachments || []
+      applicationFile.value = attachments.filter((a: any) => a.type === 'APPLICATION')
+      openingReportFile.value = attachments.filter((a: any) => a.type === 'OPENING_REPORT')
+      otherFile.value = attachments.filter((a: any) => a.type === 'OTHER')
     } else {
       formData.value = createForm()
       resetAttachments()
@@ -691,9 +873,28 @@ const openForm = async (project?: any, mode: 'view' | 'edit' = 'edit') => {
   }
 }
 
-// ⭐ 查看:走只读模式
+// ========== 抽屉详情 ==========
+const detailVisible = ref(false)
+const detailLoading = ref(false)
+const detailData = ref<any>({})
+const detailApplicationFile = ref<any[]>([])
+const detailOpeningReportFile = ref<any[]>([])
+const detailOtherFile = ref<any[]>([])
+
 const openDetail = async (row: any) => {
-  await openForm(row, 'view')
+  detailVisible.value = true
+  detailLoading.value = true
+  try {
+    const data = await getProject(row.id)
+    detailData.value = data
+
+    const attachments = data.attachments || []
+    detailApplicationFile.value = attachments.filter((a: any) => a.type === 'APPLICATION')
+    detailOpeningReportFile.value = attachments.filter((a: any) => a.type === 'OPENING_REPORT')
+    detailOtherFile.value = attachments.filter((a: any) => a.type === 'OTHER')
+  } finally {
+    detailLoading.value = false
+  }
 }
 
 // ========== 子表增删 ==========
@@ -704,9 +905,6 @@ const removeBudgetDetail = (index: number) => formData.value.budgetList.splice(i
 
 // ========== 保存 ==========
 const submitForm = async () => {
-  // ⭐ 只读模式不允许提交
-  if (isViewMode.value) return
-
   const valid = await formRef.value?.validate().catch(() => false)
   if (!valid) return
 
@@ -836,9 +1034,6 @@ const removeProject = async (id: number) => {
 
 // ========== 生命周期 ==========
 onMounted(async () => {
-  // 先获取用户信息拿 deptId,再加载列表(createForm 依赖 deptId)
-  const data = await getUserProfile()
-  deptId.value = data.dept.id
   await getList()
   deptList.value = await getSimpleDeptList()
 })
@@ -874,7 +1069,8 @@ onMounted(async () => {
 }
 
 .budget-table,
-.schedule-table {
+.schedule-table,
+.detail-table {
   width: 100%;
 }
 
@@ -892,6 +1088,15 @@ onMounted(async () => {
   font-size: 22px;
 }
 
+.detail-container {
+  padding: 0 20px 20px 0;
+}
+
+.detail-container :deep(.el-divider__text) {
+  color: var(--el-color-primary);
+  font-weight: 600;
+}
+
 @media (max-width: 900px) {
   .budget-table {
     min-width: 760px;

+ 4 - 4
src/views/technology‌/list/index2.vue

@@ -19,7 +19,7 @@
         :model="queryParams"
         :inline="true"
         @submit.prevent
-        class="bg-white dark:bg-[#1d1e1f] rounded-lg shadow px-8 pt-4 flex items-center flex-wrap min-w-0"
+        class="bg-white dark:bg-[#1d1e1f] rounded-md px-8 pt-4 flex items-center flex-wrap min-w-0"
       >
         <el-form-item label="技术类型" prop="technologyType">
           <el-select
@@ -78,10 +78,10 @@
         <el-form-item>
           <el-button @click="handleQuery"> <Icon icon="ep:search" class="mr-5px" />搜索 </el-button>
           <el-button @click="resetQuery"> <Icon icon="ep:refresh" class="mr-5px" />重置 </el-button>
-          <el-button type="primary" plain @click="openForm('create')">
+          <el-button type="primary" @click="openForm('create')">
             <Icon icon="ep:plus" class="mr-5px" />新增
           </el-button>
-          <el-button type="success" plain :loading="exportLoading" @click="handleExport">
+          <el-button :loading="exportLoading" @click="handleExport">
             <Icon icon="ep:download" class="mr-5px" />导出
           </el-button>
         </el-form-item>
@@ -90,7 +90,7 @@
 
     <div class="min-w-0"></div>
 
-    <div class="bg-white dark:bg-[#1d1e1f] shadow rounded-lg flex flex-col p-2 pt-4 min-w-0">
+    <div class="bg-white dark:bg-[#1d1e1f] rounded-md flex flex-col p-2 pt-4 min-w-0">
       <div class="flex-1 relative min-h-0">
         <el-auto-resizer class="absolute">
           <template #default="{ width, height }">