yanghao před 1 týdnem
rodič
revize
6dc127015c
3 změnil soubory, kde provedl 254 přidání a 198 odebrání
  1. 26 0
      src/api/project/index.ts
  2. 6 1
      src/utils/dict.ts
  3. 222 197
      src/views/researchProject/list/index.vue

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

@@ -0,0 +1,26 @@
+import request from '@/config/axios'
+
+// 获取专家列表
+export const getProjectPage = (params) => {
+  return request.get({ url: '/rd/research-project/page', params })
+}
+
+// 新增项目
+export const createProject = (data: any) => {
+  return request.post({ url: '/rd/research-project/create', data })
+}
+
+// 删除项目
+export const deleteProject = (id) => {
+  return request.delete({ url: '/rd/research-project/delete?id=' + id })
+}
+
+// 修改项目
+export const updateProject = (data: any) => {
+  return request.put({ url: '/rd/research-project/update', data })
+}
+
+// 获取项目详情
+export const getProject = (id: number) => {
+  return request.get({ url: '/rd/research-project/get?id=' + id })
+}

+ 6 - 1
src/utils/dict.ts

@@ -272,5 +272,10 @@ export enum DICT_TYPE {
   HONOR_SUB_TYPE = 'rd_honor_sub_type', // 荣誉级别子类型
 
   // 资讯类别   数据字典
-  NEWS_CATEGORY = 'rd_news_category' // 资讯类别
+  NEWS_CATEGORY = 'rd_news_category', // 资讯类别
+
+  // 科研项目
+  PROJECT_TYPE = 'rd_project_type', // 项目类型
+  // 项目级别
+  PROJECT_LEVEL = 'rd_project_level'
 }

+ 222 - 197
src/views/researchProject/list/index.vue

@@ -21,33 +21,36 @@
         </el-select>
       </el-form-item>
       <el-form-item>
-        <el-button type="primary" @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()"
-          ><Icon icon="ep:plus" class="mr-5px" />新增项目</el-button
-        >
+        <el-button type="primary" @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()" v-hasPermi="['rd:project:create']">
+          <Icon icon="ep:plus" class="mr-5px" />新增项目
+        </el-button>
       </el-form-item>
     </el-form>
   </ContentWrap>
 
   <ContentWrap style="border: none">
-    <div class="project-table-heading">
-      <div><h2>科研项目</h2><span>维护项目申报信息、预算及阶段性计划</span></div>
-      <el-tag type="info" effect="plain">共 {{ filteredList.length }} 个项目</el-tag>
-    </div>
-    <zm-table v-loading="loading" :data="filteredList" row-key="id">
+    <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="projectLevel" min-width="180" />
+      <zm-table-column label="立项时间" prop="projectDate" min-width="250" show-overflow-tooltip />
+      <zm-table-column
+        label="计划结题时间"
+        prop="projectEndDate"
+        min-width="250"
+        show-overflow-tooltip
+      />
       <zm-table-column
         label="执行单位"
         prop="executingUnit"
         min-width="210"
         show-overflow-tooltip
       />
-      <zm-table-column label="项目负责人" prop="projectLeader" width="130" />
-      <zm-table-column label="项目类型" prop="projectType" min-width="180" />
+      <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'">{{
@@ -58,7 +61,7 @@
       <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="primary" @click="openForm(row)">编辑</el-button
           ><el-button link type="danger" @click="removeProject(row.id)">删除</el-button></template
         ></zm-table-column
@@ -67,7 +70,8 @@
     <Pagination
       v-model:page="queryParams.pageNo"
       v-model:limit="queryParams.pageSize"
-      :total="filteredList.length"
+      :total="total"
+      @pagination="getList"
     />
   </ContentWrap>
 
@@ -89,19 +93,29 @@
       <el-divider content-position="left">项目基本信息</el-divider>
       <el-row :gutter="24">
         <el-col :span="12"
-          ><el-form-item label="项目级别" prop="projectLevel"
-            ><el-select v-model="formData.projectLevel" class="w-full" placeholder="请选择项目级别"
-              ><el-option
-                v-for="item in projectLevels"
-                :key="item"
-                :label="item"
-                :value="item" /></el-select></el-form-item
-        ></el-col>
+          ><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)"
+                :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="executingUnit"
-            ><el-input
-              v-model="formData.executingUnit"
-              placeholder="请输入执行单位" /></el-form-item
+          ><el-form-item label="执行单位" prop="executingUnit">
+            <el-tree-select
+              clearable
+              v-model="formData.deptId"
+              :data="deptList2"
+              :props="defaultProps"
+              :check-strictly="false"
+              node-key="id"
+              filterable
+              placeholder="请选择所在部门"
+            /> </el-form-item
         ></el-col>
         <el-col :span="12"
           ><el-form-item label="项目名称" prop="projectName"
@@ -121,12 +135,13 @@
         ></el-col>
         <el-col :span="12"
           ><el-form-item label="项目类型" prop="projectType"
-            ><el-select v-model="formData.projectType" class="w-full" placeholder="请选择项目类型"
-              ><el-option
-                v-for="item in projectTypes"
-                :key="item"
-                :label="item"
-                :value="item" /></el-select></el-form-item
+            ><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-row>
       <el-divider content-position="left">项目内容</el-divider>
@@ -161,9 +176,9 @@
         <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-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" />
@@ -182,8 +197,8 @@
             ></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-button link type="danger" @click="removeSchedule($index)">
+                  <Icon icon="ep:delete" /> </el-button></template
             ></el-table-column>
           </el-table>
         </div>
@@ -252,30 +267,33 @@
         </div>
       </div>
       <el-divider content-position="left">附件上传</el-divider>
-      <el-form-item label="立项申请表" required
-        ><UploadFile
-          v-model="formData.attachments.application"
+      <el-form-item label="立项申请表" required>
+        <UploadFile
+          v-model="formData.applicationFile"
           :return-object="true"
           :auto-upload="false"
           :limit="1"
           :file-type="['doc', 'docx', 'pdf']"
-      /></el-form-item>
-      <el-form-item label="开题报告" required
-        ><UploadFile
-          v-model="formData.attachments.report"
+        />
+      </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']"
-      /></el-form-item>
-      <el-form-item label="其他文件"
-        ><UploadFile
-          v-model="formData.attachments.others"
+        />
+      </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']"
-      /></el-form-item>
+        />
+      </el-form-item>
     </el-form>
     <template #footer>
       <div
@@ -299,30 +317,19 @@
 </template>
 
 <script setup lang="ts">
-import { computed, reactive, ref } from 'vue'
+import { computed, reactive, ref, onMounted } from 'vue'
 import { ElMessage, ElMessageBox, type FormRules } from 'element-plus'
+import { DICT_TYPE, getStrDictOptions } from '@/utils/dict'
+import {
+  getProjectPage,
+  getProject,
+  createProject,
+  updateProject,
+  deleteProject
+} from '@/api/project'
 
 defineOptions({ name: 'ResearchProjectList' })
-type ScheduleItem = { workPlan: string; deadline: string }
-type BudgetDetail = { subject: string; amount: number; remark: string }
-type Project = {
-  id: number
-  projectLevel: string
-  executingUnit: string
-  projectName: string
-  projectLeader: string
-  projectMembers: string
-  projectType: string
-  purpose: string
-  researchContent: string
-  innovation: string
-  schedule: ScheduleItem[]
-  budgetDetails: BudgetDetail[]
-  budget: Record<string, number>
-  attachments: { application: any[]; report: any[]; others: any[] }
-  status: string
-}
-const projectLevels = ['专业公司级/石油技术公司级', '集团公司级', '国家级']
+
 const projectTypes = [
   '基础研究类',
   '原理方法类',
@@ -346,163 +353,165 @@ const budgetItems = [
   { key: 'technical', label: '技术服务' },
   { key: 'other', label: '其他' }
 ]
+
+// ========== 类型 ==========
+type ScheduleItem = { workPlan: string; deadline: string }
+type BudgetDetail = { financialSubject: string; budgetAmount: number; budgetDescription: string }
+
+// ========== 列表与查询 ==========
+const list = ref<any[]>([])
+const total = ref(0)
+const loading = ref(false)
+const queryFormRef = ref()
+const queryParams = reactive({
+  pageNo: 1,
+  pageSize: 10,
+  projectName: '',
+  projectType: ''
+})
+
+const getList = async () => {
+  loading.value = true
+  try {
+    const data = await getProjectPage(queryParams)
+    list.value = data.list
+    total.value = data.total
+  } finally {
+    loading.value = false
+  }
+}
+onMounted(getList)
+
+const handleQuery = () => {
+  queryParams.pageNo = 1
+  getList()
+}
+const resetQuery = () => {
+  queryFormRef.value?.resetFields()
+  handleQuery()
+}
+
+// ========== 表单 ==========
+const dialogVisible = ref(false)
+const dialogTitle = ref('新增科研项目')
+const formLoading = ref(false)
+const submitLoading = ref(false)
+const formRef = ref()
+const editingId = ref<number>()
+
 const createSchedule = (): ScheduleItem => ({ workPlan: '', deadline: '' })
-const createBudgetDetail = (data: Partial<BudgetDetail> = {}): BudgetDetail => ({
-  subject: '',
-  amount: 0,
-  remark: '',
-  ...data
+const createBudgetDetail = (): BudgetDetail => ({
+  financialSubject: '',
+  budgetAmount: 0,
+  budgetDescription: ''
 })
-const createBudget = () =>
-  Object.fromEntries(budgetItems.map(({ key }) => [key, 0])) as Record<string, number>
+
 const createForm = () => ({
+  deptId: '',
   projectLevel: '',
   executingUnit: '',
   projectName: '',
   projectLeader: '',
   projectMembers: '',
   projectType: '',
+  goalAndSignificance: '',
+  mainResearchContent: '',
+  innovationPoint: '',
+  applicationFile: '',
+  openingReportFile: '',
+  otherFile: '',
+  status: 0,
+  remark: '',
+  scheduleList: [createSchedule()],
+  budgetList: [createBudgetDetail()],
   purpose: '',
   researchContent: '',
   innovation: '',
-  schedule: [createSchedule(), createSchedule(), createSchedule()],
-  budgetDetails: [createBudgetDetail()],
-  budget: createBudget(),
-  attachments: { application: [], report: [], others: [] }
+  schedule: ''
 })
-const list = ref<Project[]>([
-  {
-    id: 1,
-    projectLevel: '专业公司级/石油技术公司级',
-    executingUnit: '瑞恒兴仪 / 四川瑞都',
-    projectName: '高温高压井下工具可靠性研究',
-    projectLeader: '张明远',
-    projectMembers: '李强,王璐',
-    projectType: '产品、工具和设备研发类',
-    purpose: '提升复杂工况下工具运行可靠性。',
-    researchContent: '开展材料、结构及工艺适应性研究。',
-    innovation: '建立高温高压工况下的评价方法。',
-    schedule: [
-      { workPlan: '完成方案设计', deadline: '2026-06-30' },
-      { workPlan: '完成样机试制', deadline: '2026-10-31' }
-    ],
-    budgetDetails: [
-      createBudgetDetail({ subject: '直接材料投入', amount: 180000, remark: '样机材料及加工费用' }),
-      createBudgetDetail({ subject: '检验费', amount: 65000, remark: '高温高压环境测试' }),
-      createBudgetDetail({ subject: '差旅费', amount: 30000, remark: '现场调研与试验支持' })
-    ],
-    budget: createBudget(),
-    attachments: { application: [], report: [], others: [] },
-    status: '进行中'
-  }
-])
-const loading = ref(false)
-const queryFormRef = ref()
-const queryParams = reactive({ pageNo: 1, pageSize: 10, projectName: '', projectType: '' })
-const filteredList = computed(() =>
-  list.value.filter(
-    (item) =>
-      (!queryParams.projectName || item.projectName.includes(queryParams.projectName)) &&
-      (!queryParams.projectType || item.projectType === queryParams.projectType)
-  )
-)
-const dialogVisible = ref(false)
-const dialogTitle = ref('新增科研项目')
-const formLoading = ref(false)
-const submitLoading = ref(false)
-const formRef = ref()
-const editingId = ref<number>()
+
 const formData = ref(createForm())
+
 const formRules = reactive<FormRules>({
   projectLevel: [{ required: true, message: '请选择项目级别', trigger: 'change' }],
   executingUnit: [{ required: true, message: '请输入执行单位', trigger: 'blur' }],
   projectName: [{ required: true, message: '请输入项目名称', trigger: 'blur' }],
   projectLeader: [{ required: true, message: '请输入项目负责人', trigger: 'blur' }],
   projectType: [{ required: true, message: '请选择项目类型', trigger: 'change' }],
-  purpose: [{ required: true, message: '请输入目标和意义', trigger: 'blur' }],
-  researchContent: [{ required: true, message: '请输入主要研究内容', trigger: 'blur' }],
-  innovation: [{ required: true, message: '请输入创新点', trigger: 'blur' }]
+  goalAndSignificance: [{ required: true, message: '请输入目标和意义', trigger: 'blur' }],
+  mainResearchContent: [{ required: true, message: '请输入主要研究内容', trigger: 'blur' }],
+  innovationPoint: [{ required: true, message: '请输入创新点', trigger: 'blur' }]
 })
+
 const budgetTotal = computed(() =>
-  formData.value.budgetDetails.reduce((sum, item) => sum + Number(item.amount || 0), 0)
+  formData.value.budgetList.reduce((sum, item) => sum + Number(item.budgetAmount || 0), 0)
 )
-const handleQuery = () => {
-  queryParams.pageNo = 1
-}
-const resetQuery = () => {
-  queryFormRef.value?.resetFields()
-  handleQuery()
-}
-const cloneAttachments = (attachments: Project['attachments']) => ({
-  application:
-    attachments?.application?.map((file) =>
-      file && typeof file === 'object' ? { ...file } : file
-    ) || [],
-  report:
-    attachments?.report?.map((file) => (file && typeof file === 'object' ? { ...file } : file)) ||
-    [],
-  others:
-    attachments?.others?.map((file) => (file && typeof file === 'object' ? { ...file } : file)) ||
-    []
-})
-const cloneProjectForm = (project: Project) => ({
-  projectLevel: project.projectLevel,
-  executingUnit: project.executingUnit,
-  projectName: project.projectName,
-  projectLeader: project.projectLeader,
-  projectMembers: project.projectMembers,
-  projectType: project.projectType,
-  purpose: project.purpose,
-  researchContent: project.researchContent,
-  innovation: project.innovation,
-  schedule: project.schedule.map((item) => ({ ...item })),
-  budgetDetails:
-    project.budgetDetails?.length > 0
-      ? project.budgetDetails.map((item) => ({ ...item }))
-      : budgetItems
-          .filter(({ key }) => Number(project.budget?.[key] || 0) > 0)
-          .map(({ label, key }) =>
-            createBudgetDetail({ subject: label, amount: project.budget[key] })
-          ),
-  budget: { ...project.budget },
-  attachments: cloneAttachments(project.attachments)
-})
-const openForm = (project?: Project) => {
+
+// ========== 打开表单 ==========
+const openForm = async (project?: any) => {
+  dialogVisible.value = true
   editingId.value = project?.id
   dialogTitle.value = project ? '编辑科研项目' : '新增科研项目'
-  formData.value = project ? cloneProjectForm(project) : createForm()
-  dialogVisible.value = true
+  formLoading.value = true
+  try {
+    if (project?.id) {
+      const data = await getProject(project.id)
+      formData.value = {
+        ...createForm(),
+        ...data,
+        scheduleList: data.scheduleList?.length ? data.scheduleList : [createSchedule()],
+        budgetList: data.budgetList?.length ? data.budgetList : [createBudgetDetail()]
+      }
+    } else {
+      formData.value = createForm()
+    }
+  } finally {
+    formLoading.value = false
+  }
 }
-const addSchedule = () => formData.value.schedule.push(createSchedule())
-const removeSchedule = (index: number) => formData.value.schedule.splice(index, 1)
-const addBudgetDetail = () => formData.value.budgetDetails.push(createBudgetDetail())
-const removeBudgetDetail = (index: number) => formData.value.budgetDetails.splice(index, 1)
+
+// ========== 子表增删 ==========
+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
-  if (formData.value.budgetDetails.some((item) => !item.subject.trim())) {
+
+  if (formData.value.budgetList.some((item) => !item.financialSubject?.trim())) {
     ElMessage.warning('请完善预算科目后再保存')
     return
   }
+  if (formData.value.scheduleList.some((item) => !item.workPlan?.trim() || !item.deadline)) {
+    ElMessage.warning('请完善进度安排后再保存')
+    return
+  }
+
   submitLoading.value = true
-  const project = {
-    ...formData.value,
-    budget: Object.fromEntries(
-      formData.value.budgetDetails.map((item, index) => [
-        item.subject || `subject_${index}`,
-        item.amount
-      ])
-    ),
-    id: editingId.value || Date.now(),
-    status: '进行中'
-  } as Project
-  const index = list.value.findIndex((item) => item.id === project.id)
-  if (index === -1) list.value.unshift(project)
-  else list.value[index] = project
-  submitLoading.value = false
-  dialogVisible.value = false
-  ElMessage.success('项目已保存')
+  try {
+    // 设置 sort(按数组下标)
+    formData.value.scheduleList.forEach((item, index) => (item.sort = index))
+    formData.value.budgetList.forEach((item, index) => (item.sort = index))
+
+    const payload = { ...formData.value }
+    if (editingId.value) {
+      payload.id = editingId.value
+      await updateProject(payload)
+    } else {
+      await createProject(payload)
+    }
+    ElMessage.success('保存成功')
+    dialogVisible.value = false
+    await getList()
+  } finally {
+    submitLoading.value = false
+  }
 }
+
+// ========== 删除 ==========
 const removeProject = async (id: number) => {
   const confirmed = await ElMessageBox.confirm('确定删除这个科研项目吗?', '提示', {
     type: 'warning',
@@ -512,8 +521,9 @@ const removeProject = async (id: number) => {
     .then(() => true)
     .catch(() => false)
   if (confirmed) {
-    list.value = list.value.filter((item) => item.id !== id)
+    await deleteProject(id)
     ElMessage.success('删除成功')
+    await getList()
   }
 }
 </script>
@@ -522,31 +532,38 @@ const removeProject = async (id: number) => {
 .project-filter {
   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;
 }
+
 .project-form :deep(.el-divider__text) {
   color: var(--el-color-primary);
   font-weight: 600;
 }
+
 .schedule-editor {
   width: 100%;
 }
+
 .schedule-toolbar {
   display: flex;
   align-items: center;
@@ -555,9 +572,11 @@ const removeProject = async (id: number) => {
   color: var(--el-text-color-secondary);
   font-size: 13px;
 }
+
 .budget-editor {
   width: 100%;
 }
+
 .budget-toolbar {
   display: flex;
   align-items: center;
@@ -566,9 +585,11 @@ const removeProject = async (id: number) => {
   color: var(--el-text-color-secondary);
   font-size: 13px;
 }
+
 .budget-table {
   width: 100%;
 }
+
 .budget-total {
   display: flex;
   justify-content: flex-end;
@@ -577,18 +598,22 @@ const removeProject = async (id: number) => {
   padding: 14px 8px 0;
   color: var(--el-text-color-secondary);
 }
+
 .budget-total strong {
   color: var(--el-color-primary);
   font-size: 22px;
 }
+
 @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;
   }