yanghao před 1 týdnem
rodič
revize
d48c343b4c
2 změnil soubory, kde provedl 365 přidání a 173 odebrání
  1. 12 0
      src/api/project/index.ts
  2. 353 173
      src/views/researchProject/list/index.vue

+ 12 - 0
src/api/project/index.ts

@@ -24,3 +24,15 @@ export const updateProject = (data: any) => {
 export const getProject = (id: number) => {
   return request.get({ url: '/rd/research-project/get?id=' + id })
 }
+
+export const submitPre = (id: number) =>
+  request.post({ url: '/rd/research-project/submit-pre', params: { id } })
+
+export const submitFormal = (id: number) =>
+  request.post({ url: '/rd/research-project/submit-formal', params: { id } })
+
+export const approvePre = (data: any) =>
+  request.post({ url: '/rd/research-project/approve-pre', data })
+
+export const approveFormal = (data: any) =>
+  request.post({ url: '/rd/research-project/approve-formal', data })

+ 353 - 173
src/views/researchProject/list/index.vue

@@ -17,7 +17,12 @@
           placeholder="请选择项目类型"
           class="!w-220px"
         >
-          <el-option v-for="item in projectTypes" :key="item" :label="item" :value="item" />
+          <el-option
+            v-for="dict in getStrDictOptions(DICT_TYPE.PROJECT_TYPE)"
+            :key="dict.value"
+            :label="dict.label"
+            :value="dict.value"
+          />
         </el-select>
       </el-form-item>
       <el-form-item>
@@ -34,38 +39,91 @@
 
   <ContentWrap style="border: none">
     <zm-table v-loading="loading" :data="list" row-key="id">
-      <zm-table-column label="项目名称" prop="projectName" min-width="250" show-overflow-tooltip />
-      <zm-table-column label="立项时间" prop="projectDate" min-width="250" show-overflow-tooltip />
+      <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="projectEndDate"
-        min-width="250"
+        prop="planEndDaate"
+        min-width="220"
         show-overflow-tooltip
       />
+
       <zm-table-column
         label="执行单位"
-        prop="executingUnit"
-        min-width="210"
+        prop="executingUnitName"
+        min-width="180"
         show-overflow-tooltip
       />
-      <zm-table-column label="项目级别" prop="projectLevel" min-width="180" />
-
-      <zm-table-column label="项目进度" prop="progress" width="130" />
-      <zm-table-column label="状态" width="110" align="center"
-        ><template #default="{ row }"
-          ><el-tag :type="row.status === '进行中' ? 'success' : 'info'">{{
-            row.status
-          }}</el-tag></template
-        ></zm-table-column
-      >
-      <zm-table-column label="操作" width="240" fixed="right" align="center"
-        ><template #default="{ row }">
-          <el-button link type="primary" @click="openForm(row)">提交</el-button>
-
-          <el-button link type="primary" @click="openForm(row)">编辑</el-button
-          ><el-button link type="danger" @click="removeProject(row.id)">删除</el-button></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" />
+        </template>
+      </zm-table-column>
+      <zm-table-column label="项目负责人" prop="projectLeader" min-width="120" />
+      <zm-table-column label="项目类型" prop="projectType" min-width="180" show-overflow-tooltip>
+        <template #default="scope">
+          <dict-tag :type="DICT_TYPE.PROJECT_TYPE" :value="scope.row.projectType" />
+        </template>
+      </zm-table-column>
+      <zm-table-column label="预审状态" width="110" align="center">
+        <template #default="{ row }">
+          <el-tag :type="getPreAuditTagType(row.preAuditStatus)">
+            {{ getPreAuditLabel(row.preAuditStatus) }}
+          </el-tag>
+        </template>
+      </zm-table-column>
+      <zm-table-column label="正式立项状态" width="130" align="center">
+        <template #default="{ row }">
+          <el-tag :type="getFormalAuditTagType(row.formalAuditStatus)">
+            {{ getFormalAuditLabel(row.formalAuditStatus) }}
+          </el-tag>
+        </template>
+      </zm-table-column>
+      <zm-table-column label="操作" width="260" fixed="right" align="center">
+        <template #default="{ row }">
+          <el-button link type="primary" @click="openDetail(row)">查看</el-button>
+          <el-button
+            v-if="canEdit(row)"
+            link
+            type="primary"
+            v-hasPermi="['rd:project:update']"
+            @click="openForm(row)"
+            >编辑</el-button
+          >
+          <el-button
+            v-if="canSubmitPre(row)"
+            link
+            type="primary"
+            v-hasPermi="['rd:project:submit']"
+            @click="handleSubmitPre(row)"
+            >提交预审</el-button
+          >
+          <el-button
+            v-if="canSubmitFormal(row)"
+            link
+            type="primary"
+            v-hasPermi="['rd:project:submit']"
+            @click="handleSubmitFormal(row)"
+            >提交立项</el-button
+          >
+          <el-button
+            v-if="canApprove(row)"
+            link
+            type="success"
+            v-hasPermi="['rd:project:approve']"
+            @click="openApprove(row)"
+            >审批</el-button
+          >
+          <el-button
+            v-if="canEdit(row)"
+            link
+            type="danger"
+            v-hasPermi="['rd:project:delete']"
+            @click="removeProject(row.id)"
+            >删除</el-button
+          >
+        </template>
+      </zm-table-column>
     </zm-table>
     <Pagination
       v-model:page="queryParams.pageNo"
@@ -75,6 +133,7 @@
     />
   </ContentWrap>
 
+  <!-- 新增/编辑弹窗 -->
   <ResearchProjectDialog
     v-model="dialogVisible"
     :title="dialogTitle"
@@ -92,8 +151,8 @@
     >
       <el-divider content-position="left">项目基本信息</el-divider>
       <el-row :gutter="24">
-        <el-col :span="12"
-          ><el-form-item label="项目级别" prop="projectLevel">
+        <el-col :span="12">
+          <el-form-item label="项目级别" prop="projectLevel">
             <el-select v-model="formData.projectLevel" placeholder="请选择项目级别" clearable>
               <el-option
                 v-for="dict in getStrDictOptions(DICT_TYPE.PROJECT_LEVEL)"
@@ -104,105 +163,121 @@
             </el-select>
           </el-form-item>
         </el-col>
-        <el-col :span="12"
-          ><el-form-item label="执行单位" prop="executingUnit">
+        <el-col :span="12">
+          <el-form-item label="执行单位" prop="executingUnit">
             <el-tree-select
               clearable
-              v-model="formData.deptId"
-              :data="deptList2"
+              v-model="formData.executingUnit"
+              :data="deptList"
               :props="defaultProps"
-              :check-strictly="false"
+              :check-strictly="true"
               node-key="id"
               filterable
-              placeholder="请选择所在部门"
-            /> </el-form-item
-        ></el-col>
-        <el-col :span="12"
-          ><el-form-item label="项目名称" prop="projectName"
-            ><el-input v-model="formData.projectName" placeholder="请输入项目名称" /></el-form-item
-        ></el-col>
-        <el-col :span="12"
-          ><el-form-item label="项目负责人" prop="projectLeader"
-            ><el-input
-              v-model="formData.projectLeader"
-              placeholder="请输入项目负责人" /></el-form-item
-        ></el-col>
-        <el-col :span="12"
-          ><el-form-item label="项目组成员"
-            ><el-input
-              v-model="formData.projectMembers"
-              placeholder="请输入成员姓名,多个成员用逗号分隔" /></el-form-item
-        ></el-col>
-        <el-col :span="12"
-          ><el-form-item label="项目类型" prop="projectType"
-            ><el-select v-model="formData.projectType" placeholder="请选择项目类型" clearable>
+              placeholder="请选择执行单位"
+              class="w-full"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="项目名称" prop="projectName">
+            <el-input v-model="formData.projectName" placeholder="请输入项目名称" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="项目负责人" prop="projectLeader">
+            <el-input v-model="formData.projectLeader" placeholder="请输入项目负责人" />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="项目组成员">
+            <el-input
+              v-model="formData.teamMembers"
+              placeholder="请输入成员姓名,多个成员用逗号分隔"
+            />
+          </el-form-item>
+        </el-col>
+        <el-col :span="12">
+          <el-form-item label="项目类型" prop="projectType">
+            <el-select v-model="formData.projectType" placeholder="请选择项目类型" clearable>
               <el-option
                 v-for="dict in getStrDictOptions(DICT_TYPE.PROJECT_TYPE)"
                 :key="dict.value"
                 :label="dict.label"
                 :value="dict.value"
-              /> </el-select></el-form-item
-        ></el-col>
+              />
+            </el-select>
+          </el-form-item>
+        </el-col>
       </el-row>
+
       <el-divider content-position="left">项目内容</el-divider>
-      <el-form-item label="目标和意义" prop="purpose"
-        ><el-input
-          v-model="formData.purpose"
+      <el-form-item label="目标和意义" prop="goalAndSignificance">
+        <el-input
+          v-model="formData.goalAndSignificance"
           type="textarea"
           :rows="3"
           maxlength="500"
           show-word-limit
           placeholder="请输入项目目标和意义"
-      /></el-form-item>
-      <el-form-item label="主要研究内容" prop="researchContent"
-        ><el-input
-          v-model="formData.researchContent"
+        />
+      </el-form-item>
+      <el-form-item label="主要研究内容" prop="mainResearchContent">
+        <el-input
+          v-model="formData.mainResearchContent"
           type="textarea"
           :rows="4"
           maxlength="2000"
           show-word-limit
           placeholder="请输入主要研究内容"
-      /></el-form-item>
-      <el-form-item label="创新点" prop="innovation"
-        ><el-input
-          v-model="formData.innovation"
+        />
+      </el-form-item>
+      <el-form-item label="创新点" prop="innovationPoint">
+        <el-input
+          v-model="formData.innovationPoint"
           type="textarea"
           :rows="3"
           maxlength="500"
           show-word-limit
           placeholder="请输入项目创新点"
-      /></el-form-item>
-      <el-form-item label="进度安排" prop="schedule">
-        <div class="schedule-editor">
-          <div class="schedule-toolbar"
-            ><span>按阶段填写工作计划与截止时间</span
-            ><el-button type="primary" plain size="small" @click="addSchedule">
-              <Icon icon="ep:plus" class="mr-5px" />增加工作
-            </el-button></div
-          >
-          <el-table :data="formData.schedule" border class="schedule-table">
-            <el-table-column type="index" label="#" width="56" align="center" />
-            <el-table-column label="工作计划" min-width="300"
-              ><template #default="{ row }"
-                ><el-input v-model="row.workPlan" placeholder="请输入工作计划" /></template
-            ></el-table-column>
-            <el-table-column label="截止时间" width="190"
-              ><template #default="{ row }"
-                ><el-date-picker
-                  v-model="row.deadline"
-                  type="date"
-                  value-format="YYYY-MM-DD"
-                  placeholder="选择日期"
-                  class="w-full" /></template
-            ></el-table-column>
-            <el-table-column label="操作" width="90" align="center"
-              ><template #default="{ $index }"
-                ><el-button link type="danger" @click="removeSchedule($index)">
-                  <Icon icon="ep:delete" /> </el-button></template
-            ></el-table-column>
-          </el-table>
-        </div>
+        />
       </el-form-item>
+
+      <el-divider content-position="left">进度安排</el-divider>
+      <div class="schedule-editor">
+        <div class="schedule-toolbar">
+          <span>按阶段填写工作计划与截止时间</span>
+          <el-button type="primary" plain size="small" @click="addSchedule">
+            <Icon icon="ep:plus" class="mr-5px" />增加工作
+          </el-button>
+        </div>
+        <el-table :data="formData.scheduleList" border class="schedule-table">
+          <el-table-column type="index" label="#" width="56" align="center" />
+          <el-table-column label="工作计划" min-width="300">
+            <template #default="{ row }">
+              <el-input v-model="row.workPlan" placeholder="请输入工作计划" />
+            </template>
+          </el-table-column>
+          <el-table-column label="截止时间" width="190">
+            <template #default="{ row }">
+              <el-date-picker
+                v-model="row.deadline"
+                type="date"
+                value-format="YYYY-MM-DD"
+                placeholder="选择日期"
+                class="w-full"
+              />
+            </template>
+          </el-table-column>
+          <el-table-column label="操作" width="90" align="center">
+            <template #default="{ $index }">
+              <el-button link type="danger" @click="removeSchedule($index)">
+                <Icon icon="ep:delete" />
+              </el-button>
+            </template>
+          </el-table-column>
+        </el-table>
+      </div>
+
       <el-divider content-position="left">费用预算明细(元)</el-divider>
       <div class="budget-editor">
         <div class="budget-toolbar">
@@ -211,12 +286,12 @@
             <Icon icon="ep:plus" class="mr-5px" />新增预算科目
           </el-button>
         </div>
-        <el-table :data="formData.budgetDetails" border class="budget-table">
+        <el-table :data="formData.budgetList" border class="budget-table">
           <el-table-column type="index" label="#" width="56" align="center" />
           <el-table-column label="财务科目" min-width="220">
             <template #default="{ row }">
               <el-select
-                v-model="row.subject"
+                v-model="row.financialSubject"
                 filterable
                 allow-create
                 default-first-option
@@ -235,7 +310,7 @@
           <el-table-column label="预算金额(元)" width="220" align="right">
             <template #default="{ row }">
               <el-input-number
-                v-model="row.amount"
+                v-model="row.budgetAmount"
                 :min="0"
                 :precision="2"
                 controls-position="right"
@@ -245,7 +320,11 @@
           </el-table-column>
           <el-table-column label="预算说明" min-width="260">
             <template #default="{ row }">
-              <el-input v-model="row.remark" maxlength="100" placeholder="请输入该科目的预算用途" />
+              <el-input
+                v-model="row.budgetDescription"
+                maxlength="100"
+                placeholder="请输入该科目的预算用途"
+              />
             </template>
           </el-table-column>
           <el-table-column label="操作" width="90" align="center">
@@ -253,7 +332,7 @@
               <el-button
                 link
                 type="danger"
-                :disabled="formData.budgetDetails.length === 1"
+                :disabled="formData.budgetList.length === 1"
                 @click="removeBudgetDetail($index)"
               >
                 <Icon icon="ep:delete" />
@@ -266,30 +345,28 @@
           ><span>元</span>
         </div>
       </div>
+
       <el-divider content-position="left">附件上传</el-divider>
       <el-form-item label="立项申请表" required>
         <UploadFile
           v-model="formData.applicationFile"
           :return-object="true"
-          :auto-upload="false"
           :limit="1"
-          :file-type="['doc', 'docx', 'pdf']"
+          :file-type="['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg']"
         />
       </el-form-item>
       <el-form-item label="开题报告" required>
         <UploadFile
           v-model="formData.openingReportFile"
           :return-object="true"
-          :auto-upload="false"
           :limit="1"
-          :file-type="['doc', 'docx', 'pdf']"
+          :file-type="['doc', 'docx', 'pdf', 'png', 'jpg', 'jpeg']"
         />
       </el-form-item>
       <el-form-item label="其他文件">
         <UploadFile
           v-model="formData.otherFile"
           :return-object="true"
-          :auto-upload="false"
           :limit="3"
           :file-type="['doc', 'docx', 'pdf', 'xls', 'xlsx', 'jpg', 'png']"
         />
@@ -305,38 +382,62 @@
           background: #fff;
           padding: 10px;
           z-index: 100;
-          /* border-top: 1px solid #ccc; */
           box-shadow: 0 -2px 8px rgba(0, 0, 0, 0.1);
         "
       >
-        <el-button @click="dialogVisible = false">取消</el-button
-        ><el-button type="primary" :loading="submitLoading" @click="submitForm">保存项目</el-button>
+        <el-button @click="dialogVisible = false">取消</el-button>
+        <el-button type="primary" :loading="submitLoading" @click="submitForm">保存项目</el-button>
       </div>
     </template>
   </ResearchProjectDialog>
+
+  <!-- 审批弹窗 -->
+  <el-dialog v-model="approveVisible" title="项目审批" width="600px">
+    <el-form :model="approveForm" label-width="80px">
+      <el-form-item label="审批结果">
+        <el-radio-group v-model="approveForm.result">
+          <el-radio :label="1">通过</el-radio>
+          <el-radio :label="2">驳回</el-radio>
+        </el-radio-group>
+      </el-form-item>
+      <el-form-item label="审批意见">
+        <el-input
+          v-model="approveForm.opinion"
+          type="textarea"
+          :rows="4"
+          maxlength="500"
+          show-word-limit
+          placeholder="请输入审批意见"
+        />
+      </el-form-item>
+    </el-form>
+    <template #footer>
+      <el-button @click="approveVisible = false">取消</el-button>
+      <el-button type="primary" :loading="approveLoading" @click="submitApprove">确定</el-button>
+    </template>
+  </el-dialog>
 </template>
 
 <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 { getSimpleDeptList } from '@/api/system/dept'
 import {
   getProjectPage,
   getProject,
   createProject,
   updateProject,
-  deleteProject
+  deleteProject,
+  submitPre,
+  submitFormal,
+  approvePre,
+  approveFormal
 } from '@/api/project'
 
 defineOptions({ name: 'ResearchProjectList' })
 
-const projectTypes = [
-  '基础研究类',
-  '原理方法类',
-  '产品、工具和设备研发类',
-  '成熟工艺技术配套及推广类',
-  '情报调研类'
-]
+// ========== 常量 ==========
 const budgetItems = [
   { key: 'labor', label: '人工' },
   { key: 'materials', label: '直接材料投入' },
@@ -354,9 +455,22 @@ const budgetItems = [
   { key: 'other', label: '其他' }
 ]
 
+// 部门树配置
+const defaultProps = {
+  children: 'children',
+  label: 'name',
+  value: 'id'
+}
+const deptList = ref<any[]>([])
+
 // ========== 类型 ==========
-type ScheduleItem = { workPlan: string; deadline: string }
-type BudgetDetail = { financialSubject: string; budgetAmount: number; budgetDescription: string }
+type ScheduleItem = { workPlan: string; deadline: string; sort?: number }
+type BudgetDetail = {
+  financialSubject: string
+  budgetAmount: number
+  budgetDescription: string
+  sort?: number
+}
 
 // ========== 列表与查询 ==========
 const list = ref<any[]>([])
@@ -380,7 +494,12 @@ const getList = async () => {
     loading.value = false
   }
 }
-onMounted(getList)
+
+onMounted(async () => {
+  await getList()
+  // 加载部门树
+  deptList.value = await getSimpleDeptList()
+})
 
 const handleQuery = () => {
   queryParams.pageNo = 1
@@ -391,6 +510,34 @@ const resetQuery = () => {
   handleQuery()
 }
 
+// ========== 状态辅助方法 ==========
+const getPreAuditLabel = (status: number) => {
+  const map = { 0: '未提交', 1: '审批中', 2: '已通过', 3: '已驳回' }
+  return map[status] || '未知'
+}
+const getPreAuditTagType = (status: number) => {
+  const map = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' }
+  return map[status] || 'info'
+}
+const getFormalAuditLabel = (status: number) => {
+  const map = { 0: '未提交', 1: '审批中', 2: '已通过', 3: '已驳回' }
+  return map[status] || '未知'
+}
+const getFormalAuditTagType = (status: number) => {
+  const map = { 0: 'info', 1: 'warning', 2: 'success', 3: 'danger' }
+  return map[status] || 'info'
+}
+
+// 判断按钮显示
+const canEdit = (row: any) => row.preAuditStatus === 0 || row.preAuditStatus === 3
+const canSubmitPre = (row: any) => row.preAuditStatus === 0 || row.preAuditStatus === 3
+const canSubmitFormal = (row: any) => row.preAuditStatus === 2 && row.formalAuditStatus === 0
+const canApprove = (row: any) => {
+  // 这里简化处理,具体以后端返回的当前审批人是不是自己判断
+  // 实际项目中应该拿 row.preCurrentApproverId / row.formalCurrentApproverId 与当前用户比对
+  return row.preAuditStatus === 1 || row.formalAuditStatus === 1
+}
+
 // ========== 表单 ==========
 const dialogVisible = ref(false)
 const dialogTitle = ref('新增科研项目')
@@ -407,12 +554,12 @@ const createBudgetDetail = (): BudgetDetail => ({
 })
 
 const createForm = () => ({
-  deptId: '',
+  deptId: null as number | null,
   projectLevel: '',
-  executingUnit: '',
+  executingUnit: null as number | null,
   projectName: '',
   projectLeader: '',
-  projectMembers: '',
+  teamMembers: '',
   projectType: '',
   goalAndSignificance: '',
   mainResearchContent: '',
@@ -422,19 +569,16 @@ const createForm = () => ({
   otherFile: '',
   status: 0,
   remark: '',
-  scheduleList: [createSchedule()],
-  budgetList: [createBudgetDetail()],
-  purpose: '',
-  researchContent: '',
-  innovation: '',
-  schedule: ''
+  involveBudget: 0,
+  scheduleList: [createSchedule()] as ScheduleItem[],
+  budgetList: [createBudgetDetail()] as BudgetDetail[]
 })
 
 const formData = ref(createForm())
 
 const formRules = reactive<FormRules>({
   projectLevel: [{ required: true, message: '请选择项目级别', trigger: 'change' }],
-  executingUnit: [{ required: true, message: '请输入执行单位', trigger: 'blur' }],
+  executingUnit: [{ required: true, message: '请选择执行单位', trigger: 'change' }],
   projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
   projectLeader: [{ required: true, message: '请输入项目负责人', trigger: 'blur' }],
   projectType: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
@@ -470,13 +614,18 @@ const openForm = async (project?: any) => {
   }
 }
 
+const openDetail = async (row: any) => {
+  // 详情可以直接复用 openForm,或者单独开只读弹窗
+  openForm(row)
+}
+
 // ========== 子表增删 ==========
 const addSchedule = () => formData.value.scheduleList.push(createSchedule())
 const removeSchedule = (index: number) => formData.value.scheduleList.splice(index, 1)
 const addBudgetDetail = () => formData.value.budgetList.push(createBudgetDetail())
 const removeBudgetDetail = (index: number) => formData.value.budgetList.splice(index, 1)
 
-// ========== 提交 ==========
+// ========== 保存 ==========
 const submitForm = async () => {
   const valid = await formRef.value?.validate().catch(() => false)
   if (!valid) return
@@ -492,11 +641,10 @@ const submitForm = async () => {
 
   submitLoading.value = true
   try {
-    // 设置 sort(按数组下标)
     formData.value.scheduleList.forEach((item, index) => (item.sort = index))
     formData.value.budgetList.forEach((item, index) => (item.sort = index))
 
-    const payload = { ...formData.value }
+    const payload: any = { ...formData.value }
     if (editingId.value) {
       payload.id = editingId.value
       await updateProject(payload)
@@ -511,6 +659,72 @@ const submitForm = async () => {
   }
 }
 
+// ========== 提交审批 ==========
+const handleSubmitPre = async (row: any) => {
+  const confirmed = await ElMessageBox.confirm('确定提交预审吗?提交后不可修改。', '提示', {
+    type: 'warning'
+  })
+    .then(() => true)
+    .catch(() => false)
+  if (!confirmed) return
+  await submitPre(row.id)
+  ElMessage.success('提交成功')
+  await getList()
+}
+
+const handleSubmitFormal = async (row: any) => {
+  const confirmed = await ElMessageBox.confirm('确定提交正式立项吗?', '提示', { type: 'warning' })
+    .then(() => true)
+    .catch(() => false)
+  if (!confirmed) return
+  await submitFormal(row.id)
+  ElMessage.success('提交成功')
+  await getList()
+}
+
+// ========== 审批弹窗 ==========
+const approveVisible = ref(false)
+const approveLoading = ref(false)
+const approveForm = reactive({
+  projectId: 0,
+  stage: 0, // 1预审 2正式
+  result: 1,
+  opinion: ''
+})
+
+const openApprove = (row: any) => {
+  approveForm.projectId = row.id
+  approveForm.stage = row.preAuditStatus === 1 ? 1 : 2
+  approveForm.result = 1
+  approveForm.opinion = ''
+  approveVisible.value = true
+}
+
+const submitApprove = async () => {
+  if (approveForm.result === 2 && !approveForm.opinion.trim()) {
+    ElMessage.warning('驳回时请填写审批意见')
+    return
+  }
+  approveLoading.value = true
+  try {
+    const payload = {
+      projectId: approveForm.projectId,
+      result: approveForm.result,
+      opinion: approveForm.opinion
+    }
+    if (approveForm.stage === 1) {
+      await approvePre(payload)
+    } else {
+      await approveFormal(payload)
+    }
+    ElMessage.success('审批成功')
+    approveVisible.value = false
+    await getList()
+  } finally {
+    approveLoading.value = false
+  }
+}
+
 // ========== 删除 ==========
 const removeProject = async (id: number) => {
   const confirmed = await ElMessageBox.confirm('确定删除这个科研项目吗?', '提示', {
@@ -533,24 +747,6 @@ const removeProject = async (id: number) => {
   margin-bottom: -15px;
 }
 
-.project-table-heading {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 16px;
-}
-
-.project-table-heading h2 {
-  margin: 0 0 4px;
-  color: var(--el-text-color-primary);
-  font-size: 18px;
-}
-
-.project-table-heading span {
-  color: var(--el-text-color-secondary);
-  font-size: 13px;
-}
-
 .project-form {
   padding: 0 8px;
 }
@@ -560,23 +756,12 @@ const removeProject = async (id: number) => {
   font-weight: 600;
 }
 
-.schedule-editor {
-  width: 100%;
-}
-
-.schedule-toolbar {
-  display: flex;
-  align-items: center;
-  justify-content: space-between;
-  margin-bottom: 10px;
-  color: var(--el-text-color-secondary);
-  font-size: 13px;
-}
-
+.schedule-editor,
 .budget-editor {
   width: 100%;
 }
 
+.schedule-toolbar,
 .budget-toolbar {
   display: flex;
   align-items: center;
@@ -586,7 +771,8 @@ const removeProject = async (id: number) => {
   font-size: 13px;
 }
 
-.budget-table {
+.budget-table,
+.schedule-table {
   width: 100%;
 }
 
@@ -605,15 +791,9 @@ const removeProject = async (id: number) => {
 }
 
 @media (max-width: 900px) {
-  .budget-grid :deep(.el-col) {
-    width: 50%;
-    flex: 0 0 50%;
-  }
-
   .budget-table {
     min-width: 760px;
   }
-
   .budget-editor {
     overflow-x: auto;
   }