Bläddra i källkod

refactor:将任务详情封成组件并引入运用

shiyanyu 1 månad sedan
förälder
incheckning
a09444a0d7

+ 870 - 0
src/components/task/taskComponents/auditNoticeTab.vue

@@ -0,0 +1,870 @@
+<template>
+  <div class="catalog-manage">
+    <div class="documents-layout">
+      <!-- 左侧文书类型列表 -->
+      <div class="documents-type-list">
+        <h3>监审文书类型:</h3>
+        <div
+          v-for="type in documentData.documentTypes"
+          :key="type.id"
+          class="type-item"
+          :class="{ active: activeDocumentTypeId === type.id }"
+          @click="handleDocumentTypeClick(type)"
+        >
+          {{ type.documentName }}
+        </div>
+      </div>
+
+      <!-- 右侧文书列表表格 -->
+      <div class="documents-content">
+        <!-- <div class="operation-bar">
+          <el-button
+            plain
+            type="success"
+            icon="el-icon-circle-plus"
+            :disabled="isView"
+            @click="handleGenerateDocument"
+          >
+            生成文书
+          </el-button>
+        </div> -->
+        <CostAuditTable
+          :table-data="documentData.list"
+          :columns="documentData.documentColumns"
+          :show-index="true"
+          :show-pagination="true"
+          :show-action-column="true"
+          :pagination="documentData.pagination"
+          @pagination-change="handlePaginationChange"
+        >
+          <template #documentId="{ row }">
+            {{ getDocumenType(row) }}
+          </template>
+          <template #enterpriseId="{ row }">
+            {{ getEnterpriseName(row) }}
+          </template>
+          <template #scanDocumentUrl="scope">
+            <el-button
+              type="text"
+              size="mini"
+              :disabled="isView"
+              @click="handleUploadScan(scope.row, 'scanDocumentUrl')"
+            >
+              上传附件
+            </el-button>
+            <el-button
+              type="text"
+              size="mini"
+              @click="
+                handleViewScan(scope.row.scanDocumentUrl, 'scanDocumentUrl')
+              "
+            >
+              查看附件
+            </el-button>
+          </template>
+          <template #feedbackDocumentUrl="scope">
+            <!-- <el-button
+              type="text"
+              size="mini"
+              :disabled="isView"
+              @click="handleUploadScan(scope.row, 'feedbackDocumentUrl')"
+            >
+              上传附件
+            </el-button> -->
+            <el-button
+              type="text"
+              size="mini"
+              @click="
+                handleViewScan(
+                  scope.row.feedbackDocumentUrl,
+                  'feedbackDocumentUrl'
+                )
+              "
+            >
+              查看附件
+            </el-button>
+          </template>
+          <template #electronicDocumentUrl="scope">
+            <el-button
+              type="text"
+              size="mini"
+              :disabled="isView"
+              @click="handleEditDocument(scope.row)"
+            >
+              修改
+            </el-button>
+            <el-button
+              type="text"
+              size="mini"
+              :disabled="isView"
+              @click="handleSignDocument(scope.row)"
+            >
+              签章
+            </el-button>
+            <el-button
+              type="text"
+              size="mini"
+              :disabled="isView"
+              @click="handleDeleteDocument(scope.row)"
+            >
+              删除
+            </el-button>
+            <el-button
+              type="text"
+              size="mini"
+              @click="handleDownloadDocument(scope.row)"
+            >
+              下载
+            </el-button>
+          </template>
+        </CostAuditTable>
+      </div>
+    </div>
+    <div style="margin-top: 20px; font-size: 14px" class="table-description">
+      说明:此处只能生成各被监审单位的《成本监审通知书》和《送达回证》,同时接收或上传被监审单位的反馈的《送达回证》。
+    </div>
+
+    <!-- 编辑监审通知书 -->
+    <CostAuditDialog
+      :title="documentDialogTitle"
+      :visible="documentDialogVisible"
+      :width="dialogWidth"
+      :close-on-click-modal="false"
+      @cancel="handleCancel"
+      @confirm="handleConfirm"
+    >
+      <div class="document-edit-container">
+        <!-- 左侧:文书参数设置 -->
+        <div class="document-params">
+          <h4>文书参数设置:</h4>
+          <el-form
+            v-loading="loading.saveDocument"
+            :model="document"
+            label-width="170px"
+            size="small"
+            :rules="documentRules"
+          >
+            <el-form-item label="选择模板:" prop="documentId">
+              <el-select
+                v-model="document.documentId"
+                placeholder="请选择模板"
+                style="width: 100%"
+                @change="handleTemplateChange"
+              >
+                <el-option
+                  v-for="item in documentData.documentTypes"
+                  :key="item.id"
+                  :label="item.documentName"
+                  :value="item.id"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="通知书文号:" prop="documentNumber">
+              <el-input
+                v-model="document.documentNumber"
+                placeholder="请选择通知书文号"
+                style="width: 74%"
+              ></el-input>
+              <!-- disabled -->
+              <el-button
+                type="primary"
+                size="small"
+                class="ml10"
+                @click="selectClick"
+              >
+                选择文号
+              </el-button>
+            </el-form-item>
+            <el-form-item label="被监审单位:" prop="enterpriseId">
+              <el-select
+                v-model="document.enterpriseId"
+                placeholder="请选择被监审单位"
+                style="width: 100%"
+                clearable
+                multiple
+              >
+                <el-option
+                  v-for="item in allUnits"
+                  :key="item.unitId"
+                  :label="item.unitName"
+                  :value="item.unitId"
+                ></el-option>
+              </el-select>
+            </el-form-item>
+            <el-form-item label="是否推送被监审单位:" prop="isPushed">
+              <!-- 是否推送被监审单位 -->
+              <el-radio-group v-model="document.isPushed">
+                <el-radio label="1">是</el-radio>
+                <el-radio label="0">否</el-radio>
+              </el-radio-group>
+            </el-form-item>
+            <!-- <el-form-item label="被监审单位:">
+              <el-select
+                v-model="document.enterpriseId"
+                placeholder="请选择被监审单位"
+                style="width: 100%"
+                clearable
+              >
+                <el-option
+                  v-for="item in allUnits"
+                  :key="item.unitId"
+                  :label="item.unitName"
+                  :value="item.unitId"
+                ></el-option>
+              </el-select>
+            </el-form-item> -->
+
+            <!-- 数据内容区域 -->
+            <div style="margin-top: 20px">
+              <h4 style="margin-bottom: 10px">数据内容:</h4>
+              <el-table
+                :data="costDocumentTemplateFiles"
+                style="
+                  width: 100%;
+                  border: 1px solid #dcdfe6;
+                  border-radius: 4px;
+                "
+              >
+                <el-table-column
+                  prop="originalText"
+                  label="数据项"
+                  width="120"
+                  align="center"
+                  show-overflow-tooltip
+                ></el-table-column>
+                <el-table-column
+                  prop="labelValue"
+                  label="标签"
+                  width="100"
+                  align="center"
+                  show-overflow-tooltip
+                ></el-table-column>
+                <el-table-column
+                  prop="originalText"
+                  label="描述"
+                  min-width="120"
+                  align="center"
+                  show-overflow-tooltip
+                ></el-table-column>
+                <el-table-column
+                  prop="dataValue"
+                  label="数据值"
+                  min-width="150"
+                  align="center"
+                  show-overflow-tooltip
+                ></el-table-column>
+              </el-table>
+              <div style="margin-top: 10px; font-size: 12px; color: #909399">
+                说明:数据内容不可修改,已在监审文书管理中配置完成,数据值为本次监审项目的相关数据。
+              </div>
+            </div>
+          </el-form>
+        </div>
+
+        <!-- 右侧:模板预览和编辑区 -->
+        <div class="document-preview">
+          <!-- 预览/修改标签页 -->
+          <TemplatePreviewEdit :active-tab="activeTab" :file-url="fileUrl" />
+        </div>
+      </div>
+    </CostAuditDialog>
+    <CostAuditDialog
+      :title="dialogTitle"
+      :visible="dialogVisible"
+      :width="dialogWidth"
+      :close-on-click-modal="false"
+      @cancel="handleCancel"
+      @confirm="handleConfirm"
+    >
+      <cost-audit-table
+        :table-data="selectDocumentWhData"
+        :columns="selectDocumentWhColumns"
+        :show-selection="true"
+        :show-pagination="true"
+        :pagination="selectDocumentWhPagination"
+        @pagination-change="selectDocumentWhPaginationChange"
+        @selection-change="selectDocumentWhSelectionChange"
+      >
+        <!-- 创建时间自定义单元格 -->
+        <template #createTime="{ row }">
+          <div>{{ row.createTime ? row.createTime.split(' ')[0] : '-' }}</div>
+          <div>{{ row.createTime ? row.createTime.split(' ')[1] : '-' }}</div>
+        </template>
+      </cost-audit-table>
+    </CostAuditDialog>
+  </div>
+</template>
+<script>
+  // import { taskMixin } from './index.js'
+  import CostAuditTable from '@/components/costAudit/CostAuditTable.vue'
+  import CostAuditDialog from '@/components/costAudit/CostAuditDialog.vue'
+  import TemplatePreviewEdit from '@/components/costAudit/TemplatePreviewEdit.vue'
+  import { getAllUnitList } from '@/api/auditEntityManage'
+  import {
+    getWhCateList,
+    queryByDocumentId,
+  } from '@/api/auditReviewDocManage.js'
+  import { getData } from '@/api/auditDocNoManage.js'
+  import {
+    addCostProjectDocument,
+    updateCostProjectDocument,
+    deleteCostProjectDocument,
+  } from '@/api/taskCustomizedRelease.js'
+  import { dictMixin, regionMixin } from '@/mixins/useDict'
+  import { uploadFile } from '@/api/file'
+  export default {
+    components: { CostAuditTable, CostAuditDialog, TemplatePreviewEdit },
+    mixins: [dictMixin, regionMixin],
+    props: {
+      // 父组件传递的参数
+      project: {
+        type: Object,
+        default: () => {},
+      },
+      isView: {
+        type: Boolean,
+        default: false,
+      },
+      documentData: {
+        type: Object,
+        default: () => {},
+      },
+    },
+    data() {
+      return {
+        dictData: {
+          whGenerateType: [],
+        },
+        activeDocumentTypeId: '',
+        document: {
+          documentId: '',
+          documentWhId: '',
+          documentNumber: '',
+          enterpriseId: [],
+          dataList: [],
+          isPushed: '1',
+        },
+        loading: {
+          saveDocument: false,
+        },
+        activeView: 'list', // list edit
+        activeTab: 'preview', // 当前标签页,preview:预览,edit:修改
+        // 所有单位列表
+        allUnits: [],
+        dialogVisible: false,
+        dialogTitle: '选择文号',
+        documentDialogVisible: false,
+        documentDialogTitle: '编辑监审通知书',
+        dialogWidth: '70%',
+        fileUrl: '',
+        selectDocumentWhData: [],
+        selectDocumentWhPagination: {
+          currentPage: 1,
+          pageSize: 10,
+          total: 0,
+        },
+        selectDocumentWhSelection: [],
+        costDocumentTemplateFiles: [],
+        documentRules: {
+          documentNumber: [
+            {
+              required: true,
+              message: '请选择通知书文号',
+              trigger: 'change',
+            },
+          ],
+          enterpriseId: [
+            {
+              required: true,
+              message: '请选择被监审单位',
+              trigger: 'change',
+            },
+          ],
+          documentId: [
+            {
+              required: true,
+              message: '请选择模板',
+              trigger: 'change',
+            },
+          ],
+          isPushed: [
+            {
+              required: true,
+              message: '请选择否推送被监审单位',
+              trigger: 'change',
+            },
+          ],
+        },
+      }
+    },
+    computed: {
+      selectDocumentWhColumns() {
+        return [
+          {
+            prop: 'whType',
+            label: '文号分类',
+            showOverflowTooltip: true,
+            align: 'center',
+            formatter: (row) => {
+              let documentName =
+                this.documentData.documentTypes.find(
+                  (item) => item.id == row.whType
+                )?.documentName || '-'
+              return documentName
+            },
+          },
+          {
+            prop: 'whName',
+            label: '文号名称',
+            showOverflowTooltip: true,
+            align: 'center',
+          },
+          {
+            prop: 'areaCode',
+            label: '适用区域',
+            showOverflowTooltip: true,
+            align: 'center',
+            formatter: (row) => this.regionNameMap[row.areaCode] || '-',
+          },
+          {
+            prop: 'generateType',
+            label: '生成类型',
+            showOverflowTooltip: true,
+            align: 'center',
+            width: 120,
+            formatter: (row) =>
+              this.getDictName('whGenerateType', row.generateType),
+          },
+        ]
+      },
+    },
+    watch: {},
+    mounted() {
+      this.loadOpts()
+    },
+    methods: {
+      handleDocumentTypeClick(data) {
+        this.activeDocumentTypeId = data.id
+        this.$emit('refresh', data)
+      },
+      getEnterpriseName(row) {
+        // 处理enterpriseId,无论是数组还是逗号分隔的字符串
+        let enterpriseIds = []
+        if (Array.isArray(row.enterpriseId)) {
+          enterpriseIds = row.enterpriseId
+        } else if (typeof row.enterpriseId === 'string') {
+          // 处理逗号分隔的字符串
+          enterpriseIds = row.enterpriseId
+            .split(',')
+            .map((id) => id.trim())
+            .filter((id) => id)
+        } else if (row.enterpriseId) {
+          // 处理其他可能的非空值
+          enterpriseIds = [row.enterpriseId]
+        }
+
+        if (enterpriseIds.length > 0) {
+          // 返回多个企业名称,用逗号分隔
+          return enterpriseIds
+            .map(
+              (id) => this.allUnits.find((item) => item.unitId == id)?.unitName
+            )
+            .filter((name) => name)
+            .join(', ')
+        }
+        return '-'
+      },
+      getDocumenType(row) {
+        return this.documentData.documentTypes.find(
+          (item) => item.id == row.documentId
+        )?.documentName
+      },
+      handlePaginationChange({ currentPage, pageSize }) {
+        this.$emit('paginationChange', { currentPage, pageSize })
+      },
+
+      // 加载选项数据
+      loadOpts() {
+        // 加载所有单位列表
+        getAllUnitList().then((res) => {
+          this.allUnits = res.value || []
+          // 过滤掉状态为停用的数据
+          this.allUnits = this.allUnits.filter((item) => item.status == 1)
+        })
+      },
+
+      // 生成文书
+      handleGenerateDocument() {
+        this.documentDialogVisible = true
+        this.activeView = 'form'
+        this.document = {
+          documentId: '',
+          documentWhId: '',
+          documentNumber: '',
+          enterpriseId: [],
+          isPushed: '1', // 默认设置为'1'(是)
+        }
+        if (this.activeDocumentTypeId) {
+          this.document.documentId = this.activeDocumentTypeId
+        }
+        this.costProjectDocumentFiles = []
+      },
+      selectClick() {
+        this.dialogVisible = true
+        this.activeView = 'table'
+        this.getWhListData()
+      },
+      getWhListData() {
+        getData({
+          pageNum: this.selectDocumentWhPagination.currentPage,
+          pageSize: this.selectDocumentWhPagination.pageSize,
+          whType: this.document.documentId,
+        }).then((res) => {
+          this.selectDocumentWhData = res.value.records || []
+          this.selectDocumentWhPagination.total = res.value.total || 0
+        })
+      },
+      selectDocumentWhPaginationChange({ currentPage, pageSize }) {
+        this.selectDocumentWhPagination.currentPage = currentPage
+        this.selectDocumentWhPagination.pageSize = pageSize
+      },
+      selectDocumentWhSelectionChange(selection) {
+        if (selection.length > 1) {
+          this.$message.error('只能选择一个文号!')
+          return
+        } else {
+          this.selectDocumentWhSelection = selection
+        }
+      },
+      handleTemplateChange() {
+        this.fileUrl = this.documentData.documentTypes.find(
+          (item) => item.id === this.document.documentId
+        ).fileUrl
+        this.getDocumentData(this.document.documentId)
+      },
+      getDocumentData(documentId) {
+        if (documentId) {
+          queryByDocumentId({ documentId }).then((res) => {
+            this.costDocumentTemplateFiles = res.value || []
+          })
+        }
+      },
+      handleConfirm() {
+        switch (this.activeView) {
+          case 'table':
+            this.handleConfirmSelect()
+            break
+          case 'form':
+            this.handleSaveDocument()
+            break
+          default:
+            break
+        }
+      },
+      handleConfirmSelect() {
+        if (this.selectDocumentWhSelection.length !== 1) {
+          this.$message.error('请选择一个文号!')
+          return
+        }
+
+        this.document.documentNumber = this.selectDocumentWhSelection[0].whNo
+        this.document.documentWhId = this.selectDocumentWhSelection[0].id // 假设这是正确的字段名
+        this.dialogVisible = false
+        this.activeView = 'form'
+      },
+      // 保存文档
+      handleSaveDocument() {
+        // 验证是否选择了企业
+        if (
+          !this.document.enterpriseId ||
+          this.document.enterpriseId.length === 0
+        ) {
+          this.$message.error('请至少选择一个被监审单位!')
+          return
+        }
+
+        this.loading.saveDocument = true
+        if (this.document.id) {
+          updateCostProjectDocument({
+            id: this.document.id,
+            // documentAlias: this.document.documentAlias,
+            documentId: this.document.documentId,
+            documentNumber: this.document.documentNumber,
+            documentWhId: this.document.documentWhId,
+            costProjectDocumentFiles: this.costDocumentTemplateFiles,
+            isPushed: this.document.isPushed, // 添加isPushed字段
+            projectId: this.project.projectId,
+            // electronicDocumentUrl: '',
+            enterpriseId: this.document.enterpriseId.join(','), // 保存时转换为逗号分隔的字符串
+            // feedbackDocumentUrl: '',
+            // feedbackTime: '',
+            // generateTime: '',
+          })
+            .then((res) => {
+              this.loading.saveDocument = false
+              this.$message.success('保存成功!')
+              this.documentDialogVisible = false
+              this.activeView = ''
+              this.$emit('refresh', this.project.projectId)
+            })
+            .catch((err) => {
+              this.loading.saveDocument = false
+            })
+        } else {
+          // 处理多选逻辑,如果选择了多个单位,为每个单位创建一个文档记录
+          const promises = this.document.enterpriseId.map((enterpriseId) => {
+            return addCostProjectDocument({
+              // documentAlias: this.document.documentAlias,
+              projectId: this.project.projectId,
+              documentId: this.document.documentId,
+              documentNumber: this.document.documentNumber,
+              documentWhId: this.document.documentWhId,
+              costProjectDocumentFiles: this.costDocumentTemplateFiles || [],
+              enterpriseId: enterpriseId,
+              // electronicDocumentUrl: '',
+              // feedbackDocumentUrl: '',
+              // feedbackTime: '',
+              // generateTime: '',
+              isPushed: this.document.isPushed, // 使用isPushed,如果不存在则使用isPushed
+              // orderNum: 0,
+              // pushTime: '',
+              // scanDocumentUrl: '',
+            })
+          })
+
+          Promise.all(promises)
+            .then(() => {
+              this.loading.saveDocument = false
+              this.$message.success('保存成功!')
+              this.documentDialogVisible = false
+              this.activeView = ''
+              this.$emit('refresh', this.project.projectId)
+            })
+            .catch((err) => {
+              this.loading.saveDocument = false
+            })
+        }
+      },
+      // 处理取消
+      handleCancel() {
+        if (this.activeView === 'form') {
+          this.documentDialogVisible = false
+        } else {
+          this.activeView = 'form'
+          this.dialogVisible = false
+        }
+      },
+
+      // 上传扫描件
+      handleUploadScan(row, type) {
+        let loading = null
+        // 第一步:创建文件选择器
+        const input = document.createElement('input')
+        input.type = 'file'
+        input.accept = '.pdf,.doc,.docx,.xls,.xlsx,.csv' // 允许的文件类型
+
+        input.onchange = async (event) => {
+          const file = event.target.files[0]
+          if (!file) return
+
+          try {
+            // 校验文件大小(50MB)
+            const maxSize = 50 * 1024 * 1024 // 50MB
+            if (file.size > maxSize) {
+              this.$message.error('文件大小不能超过50MB!')
+              return
+            }
+
+            // 校验文件格式
+            const allowedFormats = [
+              '.pdf',
+              '.doc',
+              '.docx',
+              '.xls',
+              '.xlsx',
+              'csv',
+            ]
+            const fileName = file.name.toLowerCase()
+            const isValidFormat = allowedFormats.some((format) =>
+              fileName.endsWith(format)
+            )
+
+            if (!isValidFormat) {
+              this.$message.error(
+                '只允许上传.pdf,.doc,.docx,.xls,.xlsx,.csv格式的文件!'
+              )
+              return
+            }
+
+            // 显示遮罩层
+            loading = this.$baseLoading(1, '文件上传中...')
+
+            // 第三步:创建FormData并上传文件
+            const formData = new FormData()
+            formData.append('file', file)
+
+            // 先调用上传API
+            const uploadRes = await uploadFile('/api/file/v1/upload', formData)
+
+            // 第四步:检查上传结果
+            if (!uploadRes || !uploadRes.value) {
+              // this.$message.error('文件上传失败!');
+              return
+            }
+
+            // 第五步:文件上传成功后,再更新数据
+            const fileInfo = uploadRes.value
+            // 创建更新数据对象
+            const updateData = {
+              ...row,
+              scanDocumentUrl: fileInfo?.savePath, // 更新扫描件URL
+            }
+
+            // 第六步:调用更新API
+            await updateCostProjectDocument(updateData)
+
+            // 第七步:更新成功,显示提示并刷新
+            this.$message.success('文件上传成功并更新数据!')
+            this.$emit('refresh', this.project.projectId) // 通知父组件刷新
+          } catch (error) {
+            // 错误处理
+            this.$message.error('操作失败:' + (error.message || '未知错误'))
+          } finally {
+            // 关闭遮罩层
+            loading.close()
+          }
+        }
+        // 触发文件选择
+        input.click()
+      },
+      // 查看扫描件
+      handleViewScan(fileUrl, type) {
+        if (!fileUrl) {
+          this.$message.error('暂无文件!')
+          return
+        }
+        // 对文件URL进行Base64编码
+        const encodedUrl = encodeURIComponent(
+          Base64.encode(window.context.form + fileUrl)
+        )
+
+        // 构建 kkFileView 预览URL
+        // onlinePreview - 在线预览
+        // onlinePreview?type=pdf - 强制使用PDF模式预览
+        window.open(`${host}:8012/onlinePreview?url=${encodedUrl}`)
+      },
+      // 编辑文档
+      handleEditDocument(row) {
+        this.documentDialogVisible = true
+        this.activeView = 'form'
+        this.loadOpts()
+        // 确保enterpriseId是数组格式,处理可能的逗号分隔字符串
+        const enterpriseId = row.enterpriseId
+          ? Array.isArray(row.enterpriseId)
+            ? row.enterpriseId
+            : typeof row.enterpriseId === 'string'
+            ? row.enterpriseId
+                .split(',')
+                .map((id) => id.trim())
+                .filter((id) => id) // 将逗号分隔字符串转换为数组
+            : [row.enterpriseId]
+          : []
+        this.document = {
+          ...row,
+          documentId: Number(row.documentId),
+          enterpriseId,
+          // 确保isPushed有值,如果row中没有,设置默认值'1'
+          isPushed: row.isPushed !== undefined ? row.isPushed : '1',
+        }
+        this.fileUrl = this.documentData.documentTypes.find(
+          (item) => item.id === this.document.documentId
+        ).fileUrl
+        this.getDocumentData(this.document.documentId)
+      },
+
+      // 签章
+      handleSignDocument(row) {},
+
+      // 删除文档
+      handleDeleteDocument(row) {
+        this.$confirm('确定要删除该数据吗?', '提示', {
+          confirmButtonText: '确定',
+          cancelButtonText: '取消',
+          type: 'warning',
+        }).then(() => {
+          deleteCostProjectDocument(row.id).then((res) => {
+            this.$message.success('删除成功!')
+            this.$emit('refresh', this.project.projectId)
+          })
+        })
+      },
+
+      // 下载文档
+      handleDownloadDocument(row) {},
+    },
+  }
+</script>
+<style lang="scss" scoped>
+  @import '@/styles/costAudit.scss';
+  .documents-layout {
+    display: flex;
+    margin-bottom: 20px;
+  }
+
+  .documents-type-list {
+    width: 200px;
+    border: 1px solid #ebeef5;
+    border-radius: 5px;
+    padding: 10px;
+    margin-right: 20px;
+  }
+
+  .documents-type-list h3 {
+    margin-bottom: 10px;
+    font-size: 14px;
+    font-weight: bold;
+  }
+
+  .type-item {
+    padding: 5px 0;
+    cursor: pointer;
+    font-size: 12px;
+  }
+
+  .type-item:hover {
+    color: $base-color-default;
+  }
+
+  .type-item.active {
+    color: $base-color-default;
+  }
+
+  .documents-content {
+    flex: 1;
+  }
+
+  .generate-btn {
+    margin-bottom: 10px;
+  }
+
+  .cursor-pointer {
+    cursor: pointer;
+  }
+
+  .mt10 {
+    margin-top: 10px;
+  }
+
+  .mt20 {
+    margin-top: 20px;
+  }
+  .document-edit-container {
+    display: flex;
+    .document-params {
+      width: 50%;
+    }
+    .document-preview {
+      width: 50%;
+    }
+  }
+</style>

+ 433 - 0
src/components/task/taskComponents/basicInfoTab.vue

@@ -0,0 +1,433 @@
+<template>
+  <div
+    v-loading="loading"
+    class="basic-info-tab"
+    element-loading-text="加载中..."
+  >
+    <el-form
+      ref="initiationForm"
+      :model="formData.basicInfo"
+      label-width="150px"
+      class="initiation-form two-column-form"
+      disabled
+    >
+      <!-- 成本监审项目名称 -->
+      <el-form-item label="成本监审项目名称:" prop="projectName">
+        <el-input
+          v-model="formData.basicInfo.projectName"
+          placeholder="请输入成本监审项目名称"
+          style="width: 100%"
+        ></el-input>
+      </el-form-item>
+
+      <!-- 关联成本监审目录 -->
+      <el-form-item label="关联成本监审目录:" prop="catalogId">
+        <CatalogCascader
+          ref="catalogCascader"
+          :form-item="{ placeholder: '请选择监审目录' }"
+          style="width: 100%"
+          :value="formData.basicInfo.catalogId"
+        />
+      </el-form-item>
+
+      <!-- 监审地区 -->
+      <el-form-item label="监审地区:">
+        <RegionSelector
+          :initial-area-code="formData.basicInfo.areaCode"
+          :disabled="false"
+          style="width: 100%"
+        ></RegionSelector>
+      </el-form-item>
+
+      <!-- 被监审单位 -->
+      <el-form-item label="被监审单位" prop="auditedUnitName">
+        <el-select
+          v-model="formData.basicInfo.auditedUnitId"
+          placeholder="请选择单位"
+          clearable
+          multiple
+          style="width: 100%"
+          readonly
+        >
+          <el-option
+            v-for="unit in unitList"
+            :key="unit.unitId"
+            :label="unit.unitName"
+            :value="unit.unitId"
+          />
+        </el-select>
+      </el-form-item>
+
+      <!-- 监审主体 -->
+      <el-form-item label="监审主体:" prop="orgId">
+        <el-select
+          v-model="formData.basicInfo.orgId"
+          placeholder="请选择监审主体"
+          style="width: 100%"
+          clearable
+        >
+          <el-option
+            v-for="Org in OrgList"
+            :key="Org.id"
+            :label="Org.name"
+            :value="Org.id"
+          />
+        </el-select>
+      </el-form-item>
+
+      <!-- 归属年度 -->
+      <el-form-item label="归属年度:" prop="projectYear">
+        <el-date-picker
+          v-model="formData.basicInfo.projectYear"
+          style="width: 100%"
+          type="year"
+          placeholder="请选择归属年度"
+          format="yyyy"
+          value-format="yyyy"
+          clearable
+        ></el-date-picker>
+      </el-form-item>
+
+      <!-- 立项来源 -->
+      <el-form-item label="立项来源:" prop="sourceType">
+        <el-select
+          v-model="formData.basicInfo.sourceType"
+          placeholder="请选择立项类型"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in dictData['projectProposal']"
+            :key="item.key"
+            :label="item.name"
+            :value="item.key"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+
+      <!-- 监审形式 -->
+      <el-form-item label="监审形式:" prop="auditType">
+        <el-select
+          v-model="formData.basicInfo.auditType"
+          placeholder="请选择监审形式"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="item in dictData['auditType']"
+            :key="item.key"
+            :label="item.name"
+            :value="item.key"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+
+      <!-- 监审期间 -->
+      <el-form-item label="监审期间:" prop="auditPeriodArray">
+        <div class="cost-period-container">
+          <div class="cost-years-wrapper">
+            <div
+              v-for="(year, index) in formData.basicInfo.auditPeriodArray"
+              :key="index"
+              class="cost-year-item"
+            >
+              <el-date-picker
+                v-model="year.value"
+                style="width: 100%"
+                type="year"
+                placeholder="请选择年份"
+                format="yyyy"
+                value-format="yyyy"
+                clearable
+              ></el-date-picker>
+            </div>
+          </div>
+        </div>
+      </el-form-item>
+
+      <!-- 是否参加听证 -->
+      <el-form-item label="是否参加听证:" prop="needHearing">
+        <el-radio-group v-model="formData.basicInfo.needHearing">
+          <el-radio label="0">是</el-radio>
+          <el-radio label="1">否</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <!-- 是否应急项目 -->
+      <el-form-item label="是否应急项目:" prop="isEmergency">
+        <el-radio-group v-model="formData.basicInfo.isEmergency">
+          <el-radio label="0">是</el-radio>
+          <el-radio label="1">否</el-radio>
+        </el-radio-group>
+      </el-form-item>
+
+      <!-- 立项理由 -->
+      <el-form-item label="立项理由:" prop="establishmentReason">
+        <el-input
+          v-model="formData.basicInfo.establishmentReason"
+          type="textarea"
+          :rows="4"
+          placeholder="请输入立项理由"
+          style="width: 100%"
+        ></el-input>
+      </el-form-item>
+
+      <!-- 立项依据 -->
+      <el-form-item label="立项依据:">
+        <UploadComponent
+          :upload-mode="'single'"
+          :max-size="50 * 1024 * 1024"
+          :allowed-types="['xlsx', 'xls', 'doc', 'docx', 'pdf']"
+          :files-list="accordingFileList"
+          :is-disabled="true"
+        />
+      </el-form-item>
+
+      <!-- 其他材料 -->
+      <el-form-item label="其他材料:">
+        <UploadComponent
+          :upload-mode="'single'"
+          :max-size="50 * 1024 * 1024"
+          :allowed-types="['xlsx', 'xls', 'doc', 'docx', 'pdf']"
+          :files-list="otherFileList"
+          :is-disabled="true"
+        />
+      </el-form-item>
+
+      <!-- 监审任务负责人 -->
+      <el-form-item label="监审任务负责人:" prop="leaderId">
+        <el-select
+          v-model="formData.basicInfo.leaderId"
+          placeholder="请选择负责人"
+          style="width: 100%"
+        >
+          <el-option
+            v-for="(item, index) in userList"
+            :key="index"
+            :label="item.fullname"
+            :value="item.userId"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+
+      <!-- 监审任务组成员 -->
+      <el-form-item label="监审任务组成员:" prop="auditTeamMembers">
+        <el-select
+          v-model="formData.basicInfo.auditTeamMembers"
+          placeholder="请选择成员"
+          style="width: 100%"
+          multiple
+        >
+          <el-option
+            v-for="(item, index) in userList"
+            :key="index"
+            :label="item.fullname"
+            :value="item.userId"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+
+      <!-- 其他要求 -->
+      <el-form-item label="其他专家:" prop="expertStr">
+        <el-input
+          v-model="formData.basicInfo.expertStr"
+          placeholder="请输入其他专家"
+          style="width: 100%"
+        ></el-input>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+  import { taskMixin } from '@/views/costAudit/projectInfo/auditTaskManage/taskCustomizedRelease/index.js'
+  import RegionSelector from '@/views/costAudit/projectInfo/auditProjectManage/annualReviewPlan/RegionSelector.vue'
+  import UploadComponent from '@/components/costAudit/UploadComponent.vue'
+  import CatalogCascader from '@/views/costAudit/projectInfo/auditProjectManage/annualReviewPlan/CatalogCascader.vue'
+  import { getDefaultDem, getOrgListByDemId } from '@/api/annualReviewPlan'
+  import { getAllUnitList } from '@/api/auditEntityManage'
+  import { getProjectInformationInfo } from '@/api/auditTaskProcessing'
+  export default {
+    name: 'BasicInfoTab',
+    components: {
+      RegionSelector,
+      UploadComponent,
+      CatalogCascader,
+    },
+    mixins: [taskMixin],
+    props: {
+      project: {
+        type: Object,
+        default: () => {},
+      },
+    },
+    data() {
+      return {
+        userList: [],
+        OrgList: [],
+        accordingFileList: [],
+        otherFileList: [],
+        unitList: [],
+        loading: false,
+        projectIdCache: null, // 缓存已加载的 projectId,避免重复加载
+      }
+    },
+    watch: {
+      project: {
+        handler(newVal) {
+          if (newVal && newVal.projectId) {
+            // 如果 projectId 没有变化,不重复加载
+            if (this.projectIdCache === newVal.projectId) {
+              return
+            }
+            this.projectIdCache = newVal.projectId
+            // 调用接口加载立项信息
+            this.loadProjectInfo()
+          }
+        },
+        deep: true,
+        immediate: true,
+      },
+    },
+    mounted() {
+      this.getAllUnitList()
+      this.getUser()
+      this.getDefaultDem()
+    },
+    methods: {
+      // 加载立项信息
+      async loadProjectInfo() {
+        if (!this.project || !this.project.projectId) {
+          return
+        }
+
+        try {
+          this.loading = true
+          const res = await getProjectInformationInfo(this.project.projectId)
+
+          if (res && res.code === 200 && res.value) {
+            const data = res.value
+
+            // 处理监审期间数组
+            let auditPeriodArray = [{ value: '' }]
+            if (data.auditPeriod && typeof data.auditPeriod === 'string') {
+              const periods = data.auditPeriod.split(',').filter(Boolean)
+              auditPeriodArray = periods.map((p) => ({ value: p }))
+            }
+
+            // 处理被监审单位ID(可能是字符串或数组)
+            let auditedUnitId = []
+            if (data.auditUnitId) {
+              if (typeof data.auditUnitId === 'string') {
+                auditedUnitId = data.auditUnitId.split(',').filter(Boolean)
+              } else if (Array.isArray(data.auditUnitId)) {
+                auditedUnitId = data.auditUnitId
+              }
+            }
+
+            // 处理监审任务组成员(可能是字符串或数组)
+            let auditTeamMembers = []
+            if (data.projectMembers) {
+              if (typeof data.projectMembers === 'string') {
+                auditTeamMembers = data.projectMembers
+                  .split(',')
+                  .filter(Boolean)
+              } else if (Array.isArray(data.projectMembers)) {
+                auditTeamMembers = data.projectMembers
+              }
+            }
+
+            // 处理监审任务负责人
+            let leaderId = ''
+            if (data.leaderId) {
+              if (typeof data.leaderId === 'string') {
+                leaderId = data.leaderId
+              } else if (Array.isArray(data.leaderId)) {
+                leaderId = data.leaderId[0] || ''
+              }
+            }
+
+            // 处理立项依据文件
+            this.accordingFileList = data.accordingFileUrl
+              ? typeof data.accordingFileUrl === 'string'
+                ? data.accordingFileUrl.split(',')
+                : data.accordingFileUrl
+              : []
+
+            // 处理其他材料文件
+            this.otherFileList = data.otherFileUrl
+              ? typeof data.otherFileUrl === 'string'
+                ? data.otherFileUrl.split(',')
+                : data.otherFileUrl
+              : []
+
+            // 更新表单数据
+            this.formData.basicInfo = {
+              projectName: data.projectName || '',
+              catalogId: data.catalogId || '',
+              areaCode: data.areaCode || '',
+              auditedUnitId: auditedUnitId,
+              auditedUnitName: data.auditedUnitName || '',
+              orgId: data.orgId || '',
+              orgName: data.orgName || '',
+              projectYear: data.projectYear || '',
+              sourceType: data.sourceType || '',
+              auditType: data.auditType || '',
+              auditPeriod: data.auditPeriod || '',
+              auditPeriodArray: auditPeriodArray,
+              needHearing:
+                data.needHearing !== undefined ? String(data.needHearing) : '1',
+              isEmergency:
+                data.isEmergency !== undefined ? String(data.isEmergency) : '1',
+              establishmentReason: data.establishmentReason || '',
+              projectMembers: data.projectMembers || '',
+              auditTeamMembers: auditTeamMembers,
+              leaderId: leaderId,
+              expertStr: data.expertStr || '',
+              plannedAuditStartDate: data.plannedAuditStartDate || '',
+              plannedAuditEndDate: data.plannedAuditEndDate || '',
+            }
+
+            console.log('立项信息加载完成:', {
+              areaCode: this.formData.basicInfo.areaCode,
+              auditedUnitId: this.formData.basicInfo.auditedUnitId,
+              auditPeriodArray: this.formData.basicInfo.auditPeriodArray,
+            })
+          }
+        } catch (error) {
+          console.error('加载立项信息失败:', error)
+          this.$message.error('加载立项信息失败')
+        } finally {
+          this.loading = false
+        }
+      },
+      getAllUnitList() {
+        getAllUnitList().then((res) => {
+          this.unitList = res.value || []
+        })
+      },
+      getDefaultDem() {
+        getDefaultDem().then((res) => {
+          if (res && res.code === 200) {
+            const demId = res.value ? res.value.id : null
+            if (demId) {
+              this.getOrgListByDemId(demId)
+            }
+          }
+        })
+      },
+      getOrgListByDemId(demId) {
+        getOrgListByDemId({ demId }).then((res) => {
+          if (res && res.code === 200) {
+            this.OrgList = res.value || []
+          }
+        })
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  .two-column-form {
+    display: grid;
+    grid-template-columns: repeat(2, 1fr);
+  }
+</style>

+ 315 - 0
src/components/task/taskComponents/materialTab.vue

@@ -0,0 +1,315 @@
+<template>
+  <div class="material-tab">
+    <!-- <span class="link-text">被监审单位需报送材料:</span> -->
+    <!-- <el-button
+      plain
+      type="success"
+      icon="el-icon-circle-plus"
+      style="margin-bottom: 20px"
+      :disabled="isView"
+      @click="handleAddMaterial"
+    >
+      添加材料
+    </el-button> -->
+    <CostAuditTable
+      v-loading="loading"
+      :table-data="actualMaterialData.list"
+      :columns="actualMaterialData.materialColumns"
+      :show-index="true"
+      :show-pagination="true"
+      :show-action-column="true"
+      :pagination="actualMaterialData.pagination"
+      @pagination-change="handlePaginationChange"
+    >
+      <template #orderNum="scope">
+        <el-input
+          v-model="scope.row.orderNum"
+          size="mini"
+          style="width: 60px"
+          @blur="handleSortChange(scope.row)"
+        ></el-input>
+      </template>
+    </CostAuditTable>
+    <legal-dialog
+      ref="legalDialog"
+      :dialog-visible="materialData.materialDialogVisible"
+      :dialog-title="materialData.materialDialogTitle"
+      :template-data="templateData"
+      :template-columns="templateColumns"
+      :form-data="formData.material"
+      @submit="handleMaterialSubmit"
+      @cancel="handleMaterialCancel"
+    />
+  </div>
+</template>
+
+<script>
+  import {
+    addCostProjectMaterial,
+    updateCostProjectMaterial,
+    deleteCostProjectMaterial,
+    getCostProjectMaterialDetail,
+  } from '@/api/taskCustomizedRelease.js'
+  import CostAuditTable from '@/components/costAudit/CostAuditTable.vue'
+  import LegalDialog from '@/views/costAudit/baseInfo/catalogManage/legalDialog.vue'
+  export default {
+    components: {
+      CostAuditTable,
+      LegalDialog,
+    },
+    props: {
+      project: {
+        type: Object,
+        default: () => {},
+      },
+      isView: {
+        type: Boolean,
+        default: false,
+      },
+      materialData: {
+        type: Object,
+        default: () => ({
+          list: [],
+          pagination: {
+            currentPage: 1,
+            pageSize: 50,
+            total: 0,
+          },
+          materialColumns: [],
+          materialDialogVisible: false,
+          materialDialogTitle: '添加',
+        }),
+      },
+    },
+    data() {
+      return {
+        loading: false,
+        projectIdCache: null, // 缓存已加载的 projectId,避免重复加载
+        templateData: [],
+        templateColumns: [
+          {
+            prop: 'surveyTemplateName',
+            label: '模板名称',
+            align: 'center',
+          },
+          {
+            prop: 'surveyTemplateCode',
+            label: '模板编码',
+            align: 'center',
+          },
+          {
+            prop: 'createTime',
+            label: '创建时间',
+            align: 'center',
+          },
+        ],
+        formData: {
+          material: {},
+        },
+        // 内部管理的数据
+        internalMaterialData: {
+          list: [],
+          pagination: {
+            currentPage: 1,
+            pageSize: 50,
+            total: 0,
+          },
+        },
+      }
+    },
+    computed: {
+      // 合并外部 materialData 和内部数据
+      actualMaterialData() {
+        return {
+          ...this.materialData,
+          list: this.internalMaterialData.list,
+          pagination: this.internalMaterialData.pagination,
+        }
+      },
+    },
+    watch: {
+      project: {
+        handler(newVal) {
+          if (newVal && newVal.projectId) {
+            // 如果 projectId 没有变化,不重复加载
+            if (this.projectIdCache === newVal.projectId) {
+              return
+            }
+            this.projectIdCache = newVal.projectId
+            // 重置分页
+            this.internalMaterialData.pagination.currentPage = 1
+            // 调用接口加载数据
+            this.loadMaterialData()
+          }
+        },
+        deep: true,
+        immediate: true,
+      },
+      materialData: {
+        handler(val) {
+          if (val) {
+            this.formData.material = val.material || {}
+          }
+        },
+        deep: true,
+      },
+    },
+    mounted() {
+      // 如果已有 projectId,立即加载
+      if (this.project && this.project.projectId) {
+        this.loadMaterialData()
+      }
+    },
+    methods: {
+      handleSortChange(row) {
+        let data = {
+          ...row,
+          projectId: this.project.projectId,
+        }
+        updateCostProjectMaterial(data)
+          .then((res) => {
+            this.$message.success('修改成功')
+            // 重新加载数据
+            this.loadMaterialData()
+            this.$emit('refresh')
+          })
+          .catch(() => {})
+      },
+      handleAddMaterial() {
+        this.formData.material = {
+          informationType: '',
+          informationName: '',
+          informationRequire: '',
+          formatRequired: '',
+          templateId: '',
+          surveyTemplateName: '',
+          orderNum: 1,
+          isRequired: '0',
+        }
+        this.$emit('update:materialData', {
+          ...this.materialData,
+          addMaterial: true,
+          materialDialogVisible: true,
+          materialDialogTitle: '添加',
+        })
+      },
+      handleEditMaterial(row) {
+        this.formData.material = { ...row }
+        this.$emit('update:materialData', {
+          ...this.materialData,
+          materialDialogTitle: '修改',
+          materialDialogVisible: true,
+        })
+      },
+      handleDeleteMaterial(row) {
+        this.$confirm(
+          `确定要删除资料名称为${row.informationName}的数据吗?`,
+          '提示',
+          {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+          }
+        )
+          .then(() => {
+            try {
+              deleteCostProjectMaterial(row.id).then((res) => {
+                this.$message.success('删除成功')
+                // 重新加载数据
+                this.loadMaterialData()
+                this.$emit('refresh')
+              })
+            } catch (error) {
+              console.error('删除失败:', error)
+            }
+          })
+          .catch(() => {
+            this.$message.info('已取消删除')
+          })
+      },
+      handleMaterialSubmit(formData) {
+        let data = {
+          ...formData,
+          projectId: this.project.projectId,
+        }
+        if (!formData.id) {
+          addCostProjectMaterial(data).then((res) => {
+            this.$message.success('添加成功')
+            this.$refs.legalDialog.setSubmitting(false)
+            this.$emit('update:materialData', {
+              ...this.materialData,
+              materialDialogVisible: false,
+            })
+            // 重新加载数据
+            this.loadMaterialData()
+            this.$emit('refresh')
+          })
+        } else {
+          updateCostProjectMaterial(data).then((res) => {
+            this.$message.success('修改成功')
+            this.$refs.legalDialog.setSubmitting(false)
+            this.$emit('update:materialData', {
+              ...this.materialData,
+              materialDialogVisible: false,
+            })
+            // 重新加载数据
+            this.loadMaterialData()
+            this.$emit('refresh')
+          })
+        }
+      },
+      handleLegalSubmit() {},
+      handleMaterialCancel() {
+        this.$emit('update:materialData', {
+          ...this.materialData,
+          materialDialogVisible: false,
+        })
+      },
+      getTemplateOptions() {},
+      templatePaginationChange() {},
+      // 加载报送资料数据
+      async loadMaterialData() {
+        if (!this.project || !this.project.projectId) {
+          return
+        }
+
+        try {
+          this.loading = true
+          const res = await getCostProjectMaterialDetail({
+            pageNum: this.internalMaterialData.pagination.currentPage,
+            pageSize: this.internalMaterialData.pagination.pageSize,
+            id: this.project.projectId,
+          })
+
+          if (res && res.value && res.value.code == 200) {
+            this.internalMaterialData.list = res.value.value.records || []
+            this.internalMaterialData.pagination.total =
+              res.value.value.total || 0
+          } else {
+            this.internalMaterialData.list = []
+            this.internalMaterialData.pagination.total = 0
+          }
+        } catch (error) {
+          console.error('加载报送资料数据失败:', error)
+          this.$message.error('加载报送资料数据失败')
+          this.internalMaterialData.list = []
+          this.internalMaterialData.pagination.total = 0
+        } finally {
+          this.loading = false
+        }
+      },
+      handlePaginationChange({ currentPage, pageSize }) {
+        this.internalMaterialData.pagination.currentPage = currentPage
+        this.internalMaterialData.pagination.pageSize = pageSize
+        // 重新加载数据
+        this.loadMaterialData()
+        // 同时通知父组件(如果需要)
+        this.$emit('paginationChange', { currentPage, pageSize })
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped>
+  @import '@/styles/costAudit.scss';
+</style>

+ 164 - 0
src/components/task/taskComponents/surveyTab.vue

@@ -0,0 +1,164 @@
+<template>
+  <div v-loading="loading" element-loading-text="加载中...">
+    <CostAuditTable
+      :table-data="internalSurveyData.list"
+      :columns="getSurveyColumns()"
+      :show-index="true"
+      :show-action-column="true"
+    ></CostAuditTable>
+    <!-- 成本调查表查看弹窗 -->
+    <SurveyDialog
+      :dialog-visible="contentEditDialogVisible"
+      :dialog-title="contentEditDialogTitle"
+      :form-data="contentEditForm"
+      :disabled="contentEditDisabled"
+      @cancel="handleContentEditCancel"
+    />
+  </div>
+</template>
+
+<script>
+  import CostAuditTable from '@/components/costAudit/CostAuditTable.vue'
+  import SurveyDialog from '@/views/costAudit/baseInfo/catalogManage/surveyDialog.vue'
+  import { getCostProjectSurveyDetail } from '@/api/taskCustomizedRelease'
+  export default {
+    components: {
+      CostAuditTable,
+      SurveyDialog,
+    },
+    props: {
+      project: {
+        type: Object,
+        default: () => {},
+      },
+      isView: {
+        type: Boolean,
+        default: false,
+      },
+      surveyData: {
+        type: Object,
+        default: () => ({
+          list: [],
+          surveyColumns: [],
+        }),
+      },
+    },
+    data() {
+      return {
+        loading: false,
+        projectIdCache: null, // 缓存已加载的 projectId,避免重复加载
+        // 内部管理的数据
+        internalSurveyData: {
+          list: [],
+        },
+        contentEditDialogVisible: false,
+        contentEditDialogTitle: '内容维护',
+        contentEditDisabled: true,
+        contentEditForm: {
+          surveyTemplateName: '',
+          templateType: '1',
+          tableHeaders: [],
+          fixedTable: [],
+          dynamicTable: [],
+        },
+      }
+    },
+    watch: {
+      project: {
+        handler(newVal) {
+          if (newVal && newVal.projectId) {
+            // 如果 projectId 没有变化,不重复加载
+            if (this.projectIdCache === newVal.projectId) {
+              return
+            }
+            this.projectIdCache = newVal.projectId
+            // 调用接口加载数据
+            this.loadSurveyData()
+          }
+        },
+        deep: true,
+        immediate: true,
+      },
+    },
+    mounted() {
+      // 如果已有 projectId,立即加载
+      if (this.project && this.project.projectId) {
+        this.loadSurveyData()
+      }
+    },
+    methods: {
+      // 加载成本调查表数据
+      async loadSurveyData() {
+        if (!this.project || !this.project.projectId) {
+          return
+        }
+
+        try {
+          this.loading = true
+          const res = await getCostProjectSurveyDetail(this.project.projectId)
+
+          if (res && res.value) {
+            // 接口返回的数据格式可能是数组或对象
+            if (Array.isArray(res.value)) {
+              this.internalSurveyData.list = res.value
+            } else if (res.value.records) {
+              this.internalSurveyData.list = res.value.records || []
+            } else {
+              this.internalSurveyData.list = []
+            }
+          } else {
+            this.internalSurveyData.list = []
+          }
+        } catch (error) {
+          console.error('加载成本调查表数据失败:', error)
+          this.$message.error('加载成本调查表数据失败')
+          this.internalSurveyData.list = []
+        } finally {
+          this.loading = false
+        }
+      },
+      getSurveyColumns() {
+        const columns = JSON.parse(
+          JSON.stringify(this.surveyData.surveyColumns || [])
+        )
+        const actionColumn = columns.find((col) => col.prop === 'action')
+        if (actionColumn) {
+          actionColumn.actions = [
+            {
+              name: 'view',
+              label: '查看模板',
+              type: 'text',
+              onClick: this.handleViewTemplate,
+            },
+          ]
+        }
+        return columns
+      },
+      handleViewTemplate(data) {
+        this.contentEditForm = {
+          surveyTemplateName: data.surveyTemplateName || '',
+          templateType: data.templateType || '',
+          data:
+            {
+              ...data,
+              surveyId: data.surveyTemplateId,
+            } || {},
+        }
+        this.contentEditDialogTitle = '查看'
+        this.contentEditDialogVisible = true
+      },
+      handleContentEditCancel() {
+        this.contentEditDialogVisible = false
+        this.contentEditForm = {
+          surveyTemplateName: '',
+          templateType: '1',
+          tableHeaders: [],
+          fixedTable: [],
+          dynamicTable: [],
+        }
+      },
+    },
+  }
+</script>
+
+<style lang="scss" scoped></style>

+ 299 - 0
src/components/task/taskComponents/workPlanTab.vue

@@ -0,0 +1,299 @@
+<template>
+  <div class="work-plan-tab">
+    <el-form
+      v-loading="actualLoading"
+      :model="localFormData"
+      label-width="150px"
+      label-position="top"
+      style="width: 50%"
+      :disabled="true"
+    >
+      <el-form-item label="监审项目基本情况" class="form-section">
+        <el-input
+          v-model="localFormData.basicInfo"
+          type="textarea"
+          :rows="5"
+          placeholder="(非必填写)"
+          class="form-textarea"
+          :maxlength="500"
+          show-word-limit
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item
+        label="监审的范围、内容、重点及其他重要事项"
+        class="form-section"
+      >
+        <el-input
+          v-model="localFormData.scopeContent"
+          type="textarea"
+          :rows="5"
+          placeholder="(非必填写)"
+          class="form-textarea"
+          :maxlength="500"
+          show-word-limit
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="监审的步骤和方法" class="form-section">
+        <el-input
+          v-model="localFormData.stepsMethods"
+          type="textarea"
+          :rows="5"
+          placeholder="(非必填写)"
+          class="form-textarea"
+          :maxlength="500"
+          show-word-limit
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="其他有关内容" class="form-section">
+        <el-input
+          v-model="localFormData.otherContent"
+          type="textarea"
+          :rows="5"
+          placeholder="(非必填写)"
+          class="form-textarea"
+          :maxlength="500"
+          show-word-limit
+        ></el-input>
+      </el-form-item>
+
+      <el-form-item label="相关资料" class="form-section">
+        <div class="file-upload-wrapper">
+          <UploadComponent
+            :upload-mode="'multiple'"
+            :max-size="50 * 1024 * 1024"
+            :allowed-types="['xlsx', 'xls', 'doc', 'docx', 'pdf']"
+            :files-list="localFormData.attachmentIds"
+            button-text="上传附件"
+            :is-disabled="isView"
+            @removeFile="handleRemoveFile"
+            @saveFiles="handleSaveFiles"
+          />
+        </div>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script>
+  import UploadComponent from '@/components/costAudit/UploadComponent.vue'
+  import { getCostProjectScenarioDetail } from '@/api/taskCustomizedRelease'
+  export default {
+    name: 'WorkPlanTab',
+    components: {
+      UploadComponent,
+    },
+    props: {
+      formData: {
+        type: Object,
+        default: () => ({
+          basicInfo: '',
+          scopeContent: '',
+          stepsMethods: '',
+          otherContent: '',
+          attachmentIds: [],
+        }),
+      },
+      loading: {
+        type: Boolean,
+        default: false,
+      },
+      isView: {
+        type: Boolean,
+        default: false,
+      },
+      project: {
+        type: Object,
+        default: () => {},
+      },
+    },
+    data() {
+      return {
+        internalLoading: false,
+        projectIdCache: null, // 缓存已加载的 projectId,避免重复加载
+        // 内部表单数据,避免直接修改 prop
+        localFormData: {
+          basicInfo: '',
+          scopeContent: '',
+          stepsMethods: '',
+          otherContent: '',
+          attachmentIds: [],
+        },
+      }
+    },
+    computed: {
+      // 合并外部 loading 和内部 loading
+      actualLoading() {
+        return this.loading || this.internalLoading
+      },
+    },
+    watch: {
+      project: {
+        handler(newVal) {
+          if (newVal && newVal.projectId) {
+            // 如果 projectId 没有变化,不重复加载
+            if (this.projectIdCache === newVal.projectId) {
+              return
+            }
+            this.projectIdCache = newVal.projectId
+            // 调用接口加载工作方案数据
+            this.loadWorkPlan()
+          }
+        },
+        deep: true,
+        immediate: true,
+      },
+      // 监听 formData prop 变化,同步到内部数据
+      formData: {
+        handler(newVal) {
+          if (newVal) {
+            this.localFormData = {
+              basicInfo: newVal.basicInfo || '',
+              scopeContent: newVal.scopeContent || '',
+              stepsMethods: newVal.stepsMethods || '',
+              otherContent: newVal.otherContent || '',
+              attachmentIds: Array.isArray(newVal.attachmentIds)
+                ? newVal.attachmentIds
+                : [],
+            }
+          }
+        },
+        deep: true,
+        immediate: true,
+      },
+    },
+    methods: {
+      // 加载工作方案数据
+      async loadWorkPlan() {
+        if (!this.project || !this.project.projectId) {
+          return
+        }
+
+        try {
+          this.internalLoading = true
+          const res = await getCostProjectScenarioDetail({
+            projectId: this.project.projectId,
+          })
+
+          if (res && res.value) {
+            const data = res.value
+
+            // 处理附件ID(可能是字符串或数组)
+            let attachmentIds = []
+            if (data.attachmentIds) {
+              if (typeof data.attachmentIds === 'string') {
+                attachmentIds = data.attachmentIds.split(',').filter(Boolean)
+              } else if (Array.isArray(data.attachmentIds)) {
+                attachmentIds = data.attachmentIds
+              }
+            }
+
+            // 更新内部表单数据
+            this.localFormData = {
+              basicInfo: data.basicInfo || '',
+              scopeContent: data.scopeContent || '',
+              stepsMethods: data.stepsMethods || '',
+              otherContent: data.otherContent || '',
+              attachmentIds: attachmentIds,
+            }
+
+            // 通知父组件更新
+            this.$emit('update:formData', { ...this.localFormData })
+
+            console.log('工作方案数据加载完成:', {
+              basicInfo: this.localFormData.basicInfo,
+              attachmentIds: this.localFormData.attachmentIds,
+            })
+          } else {
+            // 如果没有数据,清空表单
+            this.localFormData = {
+              basicInfo: '',
+              scopeContent: '',
+              stepsMethods: '',
+              otherContent: '',
+              attachmentIds: [],
+            }
+            // 通知父组件更新
+            this.$emit('update:formData', { ...this.localFormData })
+          }
+        } catch (error) {
+          console.error('加载工作方案数据失败:', error)
+          this.$message.error('加载工作方案数据失败')
+        } finally {
+          this.internalLoading = false
+        }
+      },
+      handleSaveFiles(data) {
+        try {
+          if (!Array.isArray(this.localFormData.attachmentIds)) {
+            this.localFormData.attachmentIds = []
+          }
+          if (!Array.isArray(data)) {
+            console.warn('saveFiles方法期望接收数组类型参数')
+            return
+          }
+          const newPaths = data
+            .map((element) => {
+              if (typeof element === 'string') {
+                return element
+              }
+              return element?.savePath || element?.filePath || null
+            })
+            .filter((path) => path && typeof path === 'string')
+          const uniquePaths = [
+            ...new Set([...this.localFormData.attachmentIds, ...newPaths]),
+          ]
+          this.localFormData.attachmentIds = uniquePaths
+          // 通知父组件更新
+          this.$emit('update:formData', { ...this.localFormData })
+        } catch (error) {
+          console.error('处理文件路径时发生错误:', error)
+        }
+      },
+      handleRemoveFile(index, removedFile, currentFiles) {
+        try {
+          const currentPaths = this.localFormData.attachmentIds || []
+          if (currentPaths.length > 0 && typeof currentPaths[0] === 'string') {
+            currentPaths.splice(index, 1)
+            this.localFormData.attachmentIds = [...currentPaths]
+          } else {
+            const filteredPaths = currentPaths.filter((item) => {
+              if (typeof item === 'string') {
+                const fileName = item.substring(item.lastIndexOf('/') + 1)
+                return fileName !== removedFile.fileName
+              }
+              return (
+                item !== removedFile && item.fileName !== removedFile.fileName
+              )
+            })
+            this.localFormData.attachmentIds = filteredPaths
+          }
+          // 通知父组件更新
+          this.$emit('update:formData', { ...this.localFormData })
+        } catch (error) {
+          console.error('删除文件时发生错误:', error)
+        }
+      },
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+  .work-plan-tab {
+    padding: 20px;
+  }
+
+  .form-section {
+    margin-bottom: 20px;
+  }
+
+  .form-textarea {
+    width: 100%;
+  }
+
+  .file-upload-wrapper {
+    width: 100%;
+  }
+</style>

+ 629 - 0
src/components/task/taskComponents/workflowTab.vue

@@ -0,0 +1,629 @@
+<template>
+  <div class="task-formulate">
+    <div class="operation-bar">
+      <span>预定的监审工作起止时间:</span>
+      <el-date-picker
+        v-model="formData.workflow.plannedAuditStartDate"
+        type="date"
+        placeholder="开始日期"
+        style="width: 150px; margin: 0 10px"
+        format="yyyy-MM-dd"
+        value-format="yyyy-MM-dd"
+        disabled
+      ></el-date-picker>
+      <span>至</span>
+      <el-date-picker
+        v-model="formData.workflow.plannedAuditEndDate"
+        type="date"
+        placeholder="结束日期"
+        style="width: 150px; margin: 0 10px"
+        format="yyyy-MM-dd"
+        value-format="yyyy-MM-dd"
+        disabled
+      ></el-date-picker>
+    </div>
+    <!-- <div class="operation-bar">
+      <el-button
+        type="success"
+        plain
+        icon="el-icon-circle-plus"
+        :disabled="isView"
+        @click="handleSetWorkflow"
+      >
+        工作环节设置
+      </el-button>
+    </div> -->
+    <CostAuditTable
+      :table-data="actualWorkflowData.list"
+      :columns="actualWorkflowData.listColumns"
+      :show-index="true"
+      :show-action-column="false"
+    ></CostAuditTable>
+    <div class="table-description">
+      说明:为所有内部环节预置办理人员、流转操作按钮、环节截止时间;环节时限不能超过预定的工作流程起止时间。
+    </div>
+    <el-dialog
+      title="工作环节设置"
+      :visible.sync="dialogs.setStepDialogVisible"
+      width="70%"
+    >
+      <div class="operation-bar">
+        <span>请选择预定的监审工作起止时间:</span>
+        <el-date-picker
+          v-model="formData.workflow.plannedAuditStartDate"
+          type="date"
+          placeholder="开始日期"
+          style="width: 150px; margin: 0 10px"
+          format="yyyy-MM-dd"
+          value-format="yyyy-MM-dd"
+        ></el-date-picker>
+        <span>至</span>
+        <el-date-picker
+          v-model="formData.workflow.plannedAuditEndDate"
+          type="date"
+          placeholder="结束日期"
+          style="width: 150px; margin: 0 10px"
+          format="yyyy-MM-dd"
+          value-format="yyyy-MM-dd"
+        ></el-date-picker>
+      </div>
+      <CostAuditTable
+        :table-data="actualWorkflowData.stepList"
+        :columns="actualWorkflowData.workflowColumns"
+        :show-index="true"
+        :show-pagination="true"
+        :show-action-column="true"
+        :table-props="{
+          maxHeight: '500',
+        }"
+        :pagination="actualWorkflowData.pagination"
+        @pagination-change="handlePaginationChange"
+      >
+        <template #mainUserId="{ row }">
+          <el-select
+            v-if="row.nodeType == 1"
+            v-model="row.mainUserId"
+            filterable
+            allow-create
+            default-first-option
+            placeholder="请选择主办人员"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in formatterMainUserList()"
+              :key="item.userId"
+              :label="item.fullname"
+              :value="item.userId"
+            />
+          </el-select>
+        </template>
+        <template #userId="{ row }">
+          <el-select
+            v-if="row.nodeType == 1"
+            v-model="row.userId"
+            multiple
+            filterable
+            allow-create
+            default-first-option
+            placeholder="请选择从办人员"
+            style="width: 100%"
+            :disabled="!row.mainUserId"
+          >
+            <el-option
+              v-for="item in formatterUserList(row.mainUserId)"
+              :key="item.userId"
+              :label="item.fullname"
+              :value="item.userId"
+            />
+          </el-select>
+        </template>
+        <template #status="{ row }">
+          <el-select
+            v-if="row.nodeType == 1"
+            v-model="row.status"
+            multiple
+            filterable
+            allow-create
+            default-first-option
+            placeholder="请选择预置流转操作"
+            style="width: 100%"
+          >
+            <el-option
+              v-for="item in dictData['processStatus']"
+              :key="item.key"
+              :label="item.name"
+              :value="item.key"
+            />
+          </el-select>
+        </template>
+        <template #endTime="{ row }">
+          <el-date-picker
+            v-model="row.endTime"
+            type="date"
+            placeholder="请选择截止时间"
+            format="yyyy-MM-dd"
+            value-format="yyyy-MM-dd"
+            style="width: 100%"
+            :picker-options="getPickerOptions(row)"
+          ></el-date-picker>
+        </template>
+      </CostAuditTable>
+      <div slot="footer" class="dialog-footer">
+        <el-button
+          type="primary"
+          :loading="loading.saveStep"
+          @click="handleSaveSetStep"
+        >
+          确认
+        </el-button>
+        <el-button @click="dialogs.setStepDialogVisible = false">
+          取消
+        </el-button>
+      </div>
+    </el-dialog>
+    <!-- 设置流程环节弹窗 -->
+    <el-dialog
+      v-loading="loading.save"
+      title="设置流程环节"
+      :visible.sync="dialogs.stepDialogVisible"
+      width="50%"
+      :close-on-click-modal="false"
+    >
+      <el-form
+        ref="currentStepForm"
+        :model="formData.currentStep"
+        label-width="120px"
+        :rules="formData.currentStepRules"
+      >
+        <el-form-item label="流程环节:" prop="processNodeValue">
+          <el-input
+            v-model="formData.currentStep.processNodeValue"
+            disabled
+          ></el-input>
+        </el-form-item>
+        <el-form-item label="主办人员:" prop="mainUserId">
+          <el-select
+            v-model="formData.currentStep.mainUserId"
+            filterable
+            allow-create
+            default-first-option
+            placeholder="请选择主办人员"
+            style="width: 100%"
+            :disabled="formData.currentStep.nodeType == 0"
+          >
+            <el-option
+              v-for="item in formatterMainUserList()"
+              :key="item.userId"
+              :label="item.fullname"
+              :value="item.userId"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="从办人员:" prop="userId">
+          <el-select
+            v-model="formData.currentStep.userId"
+            multiple
+            filterable
+            allow-create
+            default-first-option
+            placeholder="请选择从办人员"
+            style="width: 100%"
+            :disabled="
+              !formData.currentStep.mainUserId ||
+              formData.currentStep.nodeType == 0
+            "
+          >
+            <el-option
+              v-for="item in formatterUserList(formData.currentStep.mainUserId)"
+              :key="item.userId"
+              :label="item.fullname"
+              :value="item.userId"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="预置流转操作:" prop="status">
+          <el-select
+            v-model="formData.currentStep.status"
+            filterable
+            multiple
+            allow-create
+            default-first-option
+            placeholder="请选择预置流转操作"
+            style="width: 100%"
+            :disabled="formData.currentStep.nodeType == 0"
+          >
+            <el-option
+              v-for="item in dictData['processStatus']"
+              :key="item.key"
+              :label="item.name"
+              :value="item.key"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="截止时间:" prop="endTime">
+          <el-date-picker
+            v-model="formData.currentStep.endTime"
+            type="date"
+            placeholder="请选择截止时间"
+            format="yyyy-MM-dd"
+            value-format="yyyy-MM-dd"
+            style="width: 100%"
+            :disabled="isTimePickerDisabled"
+            :picker-options="getPickerOptions(formData.currentStep)"
+          ></el-date-picker>
+        </el-form-item>
+      </el-form>
+
+      <div slot="footer" class="dialog-footer">
+        <el-button
+          type="primary"
+          :loading="loading.save"
+          @click="handleSaveStep('currentStepForm', 'list')"
+        >
+          确认
+        </el-button>
+        <el-button @click="dialogs.stepDialogVisible = false">取消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+  import { taskMixin } from '@/views/costAudit/projectInfo/auditTaskManage/taskCustomizedRelease/index.js'
+  import {
+    getCostProjectNodeTmpletePageList,
+    saveProcess,
+  } from '@/api/taskCustomizedRelease.js'
+  import CostAuditTable from '@/components/costAudit/CostAuditTable.vue'
+  export default {
+    components: { CostAuditTable },
+    mixins: [taskMixin],
+    props: {
+      project: {
+        type: Object,
+        default: () => {},
+      },
+      isView: {
+        type: Boolean,
+        default: false,
+      },
+      workflowData: {
+        type: Object,
+        default: () => {},
+      },
+    },
+    data() {
+      return {
+        formData: {
+          currentStepRules: {
+            endTime: [
+              { required: true, message: '请选择截止时间', trigger: 'change' },
+              {
+                validator: (rule, value, callback) => {
+                  if (
+                    !this.formData.workflow.plannedAuditStartDate ||
+                    !this.formData.workflow.plannedAuditEndDate
+                  ) {
+                    return callback()
+                  }
+                  if (value < this.formData.workflow.plannedAuditStartDate) {
+                    return callback(new Error('截止时间不能早于工作开始时间'))
+                  }
+                  if (value > this.formData.workflow.plannedAuditEndDate) {
+                    return callback(new Error('截止时间不能晚于工作结束时间'))
+                  }
+                  callback()
+                },
+                trigger: 'change',
+              },
+            ],
+          },
+        },
+        // 内部工作流数据,避免直接修改 prop
+        localWorkflowData: {
+          list: [],
+          stepList: [],
+          pagination: {
+            currentPage: 1,
+            pageSize: 10,
+            total: 0,
+          },
+        },
+      }
+    },
+    computed: {
+      // 合并外部 workflowData 和内部数据
+      actualWorkflowData() {
+        return {
+          ...this.workflowData,
+          list:
+            this.localWorkflowData.list.length > 0
+              ? this.localWorkflowData.list
+              : this.workflowData.list || [],
+          stepList:
+            this.localWorkflowData.stepList.length > 0
+              ? this.localWorkflowData.stepList
+              : this.workflowData.stepList || [],
+          pagination: {
+            ...(this.workflowData.pagination || {}),
+            ...this.localWorkflowData.pagination,
+          },
+        }
+      },
+      isTimePickerDisabled() {
+        if (
+          !this.formData.currentStep ||
+          !this.formData.currentStep.processNodeKey
+        ) {
+          return false
+        }
+        const stepList =
+          this.actualWorkflowData.list.length > 0
+            ? this.actualWorkflowData.list
+            : this.actualWorkflowData.stepList
+        if (!stepList || stepList.length <= 1) {
+          return false
+        }
+        const currentIndex = stepList.findIndex(
+          (item) =>
+            item.processNodeKey === this.formData.currentStep.processNodeKey
+        )
+        if (currentIndex <= 0) {
+          return false
+        }
+        const prevStep = stepList[currentIndex - 1]
+        return !prevStep.endTime
+      },
+    },
+    watch: {
+      // 监听 workflowData prop 变化,同步到内部数据
+      workflowData: {
+        handler(newVal) {
+          if (newVal) {
+            this.localWorkflowData.list = newVal.list || []
+            this.localWorkflowData.stepList = newVal.stepList || []
+            if (newVal.pagination) {
+              this.localWorkflowData.pagination = {
+                ...this.localWorkflowData.pagination,
+                ...newVal.pagination,
+              }
+            }
+          }
+        },
+        deep: true,
+        immediate: true,
+      },
+      'workflowData.stepList': {
+        handler(newList) {
+          if (newList && newList.length > 0) {
+          }
+        },
+        deep: true,
+      },
+    },
+    mounted() {
+      this.getWorkflow()
+    },
+    methods: {
+      formatterUserList(userId) {
+        return this.formatterMainUserList().filter(
+          (item) => item.userId !== userId
+        )
+      },
+      formatterMainUserList() {
+        if (!this.userList || !Array.isArray(this.userList)) {
+          return []
+        }
+        if (
+          !this.project ||
+          !this.project.projectMembers ||
+          typeof this.project.projectMembers !== 'string'
+        ) {
+          return this.userList
+        }
+        try {
+          const projectMemberIds = this.project.projectMembers.split(',')
+          const filteredUsers = this.userList.filter((item) =>
+            projectMemberIds.includes(item.userId)
+          )
+          return filteredUsers
+        } catch (error) {
+          return this.userList
+        }
+      },
+      getPickerOptions(row) {
+        const stepList = this.actualWorkflowData.stepList
+        const rowIndex = stepList.findIndex(
+          (item) => item.processNodeKey === row.processNodeKey
+        )
+        let pickerOptions = {
+          disabledDate: (time) => {
+            const startLimit = this.formData.workflow.plannedAuditStartDate
+              ? new Date(this.formData.workflow.plannedAuditStartDate).getTime()
+              : null
+            const endLimit = this.formData.workflow.plannedAuditEndDate
+              ? new Date(this.formData.workflow.plannedAuditEndDate).getTime()
+              : null
+            if (startLimit && time.getTime() < startLimit - 8.64e7) {
+              return true
+            }
+            if (endLimit && time.getTime() > endLimit) {
+              return true
+            }
+            if (rowIndex > 0) {
+              const prevStep = stepList[rowIndex - 1]
+              if (prevStep && prevStep.endTime) {
+                const prevEndTime = new Date(prevStep.endTime).getTime()
+                if (time.getTime() <= prevEndTime - 8.64e7) {
+                  return true
+                }
+              }
+            }
+            return false
+          },
+        }
+        return pickerOptions
+      },
+      handleSetWorkflow() {
+        this.getUser()
+        this.dialogs.setStepDialogVisible = true
+        if (this.actualWorkflowData.list.length > 0) {
+          this.localWorkflowData.stepList = this.actualWorkflowData.list.map(
+            (item) => ({
+              ...item,
+              userId: item.userId ? item.userId.split(',') : [],
+              status: item.status ? item.status.split(',') : [],
+            })
+          )
+          this.notifyWorkflowDataUpdate()
+        } else {
+          getCostProjectNodeTmpletePageList({
+            pageNum: this.actualWorkflowData.pagination.currentPage,
+            pageSize: this.actualWorkflowData.pagination.pageSize,
+            processId: '1',
+          }).then((res) => {
+            this.localWorkflowData.stepList = res.value.records.map((item) => ({
+              ...item,
+              userId: item.userId ? item.userId.split(',') : [],
+              status: item.status ? item.status.split(',') : [],
+            }))
+            this.localWorkflowData.pagination.total = res.value.total
+            this.notifyWorkflowDataUpdate()
+          })
+        }
+      },
+      // 通知父组件更新 workflowData
+      notifyWorkflowDataUpdate() {
+        this.$emit('update:workflowData', { ...this.actualWorkflowData })
+      },
+      handleSetStep(row) {
+        this.getUser()
+        this.formData.currentStep = {
+          ...row,
+          userId: row.userId ? row.userId.split(',') : [],
+          status: row.status ? row.status.split(',') : [],
+        }
+        this.dialogs.stepDialogVisible = true
+        this.loading.save = false
+        this.$nextTick(() => {
+          if (this.$refs.currentStepForm) {
+            this.$refs.currentStepForm.clearValidate()
+          }
+        })
+      },
+      handleSaveStep(formName, type) {
+        this.$refs[formName].validate((valid) => {
+          if (valid) {
+            if (type == 'list') {
+              this.localWorkflowData.list = this.actualWorkflowData.list.map(
+                (item) => {
+                  if (
+                    item.processNodeKey ==
+                    this.formData.currentStep.processNodeKey
+                  ) {
+                    return {
+                      ...item,
+                      ...this.formData.currentStep,
+                      userId: this.formData.currentStep.userId.join(','),
+                      status: this.formData.currentStep.status.join(','),
+                    }
+                  }
+                  return item
+                }
+              )
+              this.notifyWorkflowDataUpdate()
+              this.handleSaveSetStep('list')
+            }
+          } else {
+            return false
+          }
+        })
+      },
+      handleSaveSetStep(type) {
+        if (!this.formData.workflow.plannedAuditStartDate) {
+          this.$message.error('请选择预定的监审工作开始日期')
+          return
+        }
+        if (!this.formData.workflow.plannedAuditEndDate) {
+          this.$message.error('请选择预定的监审工作结束日期')
+          return
+        }
+        let stepList =
+          type === 'list'
+            ? this.actualWorkflowData.list
+            : this.actualWorkflowData.stepList
+        if (stepList && stepList.length > 1) {
+          const sortedSteps = [...stepList].sort((a, b) => {
+            return stepList.indexOf(a) - stepList.indexOf(b)
+          })
+          for (let i = 1; i < sortedSteps.length; i++) {
+            const currentStep = sortedSteps[i]
+            const prevStep = sortedSteps[i - 1]
+            if (currentStep.endTime && prevStep.endTime) {
+              if (new Date(currentStep.endTime) <= new Date(prevStep.endTime)) {
+                this.$message.error(
+                  `第${i + 1}个环节的截止时间必须大于第${i}个环节的截止时间`
+                )
+                return
+              }
+            } else if (currentStep.endTime && !prevStep.endTime) {
+              this.$message.error(`请先设置第${i}个环节的截止时间`)
+              return
+            } else if (!currentStep.endTime && prevStep.endTime) {
+              this.$message.error(`请设置第${i + 1}个环节的截止时间`)
+              return
+            }
+          }
+        }
+        this.loading.save = true
+        let data = {
+          nodeReqList: [
+            {
+              endTime: '',
+              id: '',
+              mainUserId: '',
+              processNodeKey: '',
+              status: '',
+              userId: '',
+            },
+          ],
+          plannedAuditEndDate: this.formData.workflow.plannedAuditEndDate,
+          plannedAuditStartDate: this.formData.workflow.plannedAuditStartDate,
+          processId: this.actualWorkflowData.detailInfo?.processId || '1',
+          projectId: this.project.projectId,
+        }
+        if (type == 'list') {
+          data.nodeReqList = this.actualWorkflowData.list
+        } else {
+          data.nodeReqList = this.actualWorkflowData.stepList.map((item) => {
+            return {
+              ...item,
+              userId: item.userId.join(','),
+              status: item.status.join(','),
+            }
+          })
+        }
+        saveProcess(data)
+          .then((res) => {
+            if (res.code === 200) {
+              this.$message.success('保存成功')
+              this.loading.save = false
+              if (type == 'list') {
+                this.dialogs.stepDialogVisible = false
+              } else {
+                this.dialogs.setStepDialogVisible = false
+              }
+              this.getWorkflow()
+            }
+          })
+          .catch(() => {
+            this.loading.save = false
+          })
+      },
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+  @import '@/styles/costAudit.scss';
+</style>

+ 337 - 0
src/components/task/taskDetail.vue

@@ -0,0 +1,337 @@
+<template>
+  <el-dialog
+    :visible.sync="dialogVisible"
+    :title="title"
+    width="90%"
+    :close-on-click-modal="false"
+    :close-on-press-escape="false"
+    @close="handleClose"
+  >
+    <!-- 操作按钮 -->
+    <div slot="title" class="operation-bar">
+      <span>{{ title }}</span>
+    </div>
+
+    <!-- 标签页容器 -->
+    <div class="tabs-container">
+      <el-tabs
+        v-model="activeTab"
+        type="border-card"
+        @tab-click="handleTabClick"
+      >
+        <!-- 监审立项信息 -->
+        <el-tab-pane label="监审立项信息" name="basicInfo">
+          <basicInfoTab :project="currentProjectData" :is-view="isView" />
+        </el-tab-pane>
+
+        <!-- 监审工作方案 -->
+        <el-tab-pane label="监审工作方案" name="scenario">
+          <workPlanTab
+            :form-data.sync="formData.workPlan"
+            :loading="scenarioData.saveScenario"
+            :is-view="isView"
+            :project="actualProject"
+            @saveFiles="handleSaveFiles"
+            @removeFile="handleRemoveFile"
+          />
+        </el-tab-pane>
+
+        <!-- 报送资料要求 -->
+        <el-tab-pane label="报送资料要求" name="material">
+          <materialTab
+            :project="actualProject"
+            :is-view="isView"
+            :material-data="materialData"
+            @refresh="getMaterialData"
+            @paginationChange="handlePaginationChange"
+            @update:materialData="(val) => (materialData = val)"
+          />
+        </el-tab-pane>
+
+        <!-- 成本调查表 -->
+        <el-tab-pane label="成本调查表" name="survey">
+          <surveyTab
+            :project="actualProject"
+            :is-view="isView"
+            :survey-data="surveyData"
+          />
+        </el-tab-pane>
+
+        <!-- 监审工作流程 -->
+        <el-tab-pane label="监审工作流程" name="workflow">
+          <workflowTab
+            :project="actualProject"
+            :is-view="isView"
+            :workflow-data.sync="workflowData"
+          />
+        </el-tab-pane>
+
+        <!-- 监审通知 -->
+        <el-tab-pane label="监审通知" name="auditNotice">
+          <auditNoticeTab
+            ref="auditNoticeTab"
+            :project="actualProject"
+            :is-view="isView"
+            :document-data="documentData"
+            @refresh="getDocumentData"
+            @paginationChange="handlePaginationChange"
+          />
+        </el-tab-pane>
+      </el-tabs>
+    </div>
+  </el-dialog>
+</template>
+
+<script>
+  import {
+    addCostProjectScenario,
+    updateCostProjectScenario,
+  } from '@/api/taskCustomizedRelease.js'
+  import basicInfoTab from './taskComponents/basicInfoTab.vue'
+  import workPlanTab from './taskComponents/workPlanTab.vue'
+  import materialTab from './taskComponents/materialTab.vue'
+  import surveyTab from './taskComponents/surveyTab.vue'
+  import workflowTab from './taskComponents/workflowTab.vue'
+  import auditNoticeTab from './taskComponents/auditNoticeTab.vue'
+  import { taskMixin } from '@/views/costAudit/projectInfo/auditTaskManage/taskCustomizedRelease/index.js'
+  export default {
+    name: 'TaskDetail',
+    components: {
+      basicInfoTab,
+      workPlanTab,
+      materialTab,
+      surveyTab,
+      workflowTab,
+      auditNoticeTab,
+    },
+    mixins: [taskMixin],
+    props: {
+      visible: {
+        type: Boolean,
+        default: false,
+      },
+      title: {
+        type: String,
+        default: '任务详情',
+      },
+      project: {
+        type: Object,
+        default: () => {},
+      },
+      isView: {
+        type: Boolean,
+        default: false,
+      },
+    },
+    data() {
+      return {
+        dialogVisible: false,
+        currentProjectData: null,
+      }
+    },
+    computed: {
+      // 优先使用 currentProjectData,其次使用 project prop
+      actualProject() {
+        return this.currentProjectData || this.project
+      },
+    },
+    watch: {
+      visible(newVal) {
+        this.dialogVisible = newVal
+        if (newVal && this.actualProject && this.actualProject.projectId) {
+          this.initData()
+        }
+      },
+      dialogVisible(newVal) {
+        this.$emit('update:visible', newVal)
+      },
+      // 监听 actualProject 变化,确保数据更新时重新加载
+      'actualProject.projectId': {
+        handler(newProjectId) {
+          if (newProjectId && this.dialogVisible) {
+            this.initData()
+          }
+        },
+        immediate: false,
+      },
+    },
+    mounted() {
+      this.dialogVisible = this.visible
+      if (this.visible && this.actualProject && this.actualProject.projectId) {
+        this.initData()
+      }
+    },
+    methods: {
+      initData() {
+        this.handleTabClick()
+      },
+      // 保存
+      handleSave() {
+        switch (this.activeTab) {
+          case 'scenario':
+            this.saveWorkPlan()
+            break
+          case 'material':
+            this.saveMaterial()
+            break
+          case 'auditNotice':
+            this.$refs.auditNoticeTab.handleSaveDocument()
+            break
+          default:
+            break
+        }
+      },
+      // saveFiles 方法
+      handleSaveFiles(data) {
+        this.saveFiles(data)
+      },
+      // removeFile 方法
+      handleRemoveFile(index, removedFile, currentFiles) {
+        this.removeFile(index, removedFile, currentFiles)
+      },
+      // saveFiles 方法(来自 mixin 的方法,但需要在组件中实现)
+      saveFiles(data) {
+        try {
+          if (!Array.isArray(this.formData.workPlan.attachmentIds)) {
+            this.formData.workPlan.attachmentIds = []
+          }
+          if (!Array.isArray(data)) {
+            console.warn('saveFiles方法期望接收数组类型参数')
+            return
+          }
+          const newPaths = data
+            .map((element) => {
+              if (typeof element === 'string') {
+                return element
+              }
+              return element?.savePath || element?.filePath || null
+            })
+            .filter((path) => path && typeof path === 'string')
+          const uniquePaths = [
+            ...new Set([...this.formData.workPlan.attachmentIds, ...newPaths]),
+          ]
+          this.formData.workPlan.attachmentIds = uniquePaths
+        } catch (error) {
+          console.error('处理文件路径时发生错误:', error)
+        }
+      },
+      // removeFile 方法
+      removeFile(index, removedFile, currentFiles) {
+        try {
+          const currentPaths = this.formData.workPlan.attachmentIds || []
+          if (currentPaths.length > 0 && typeof currentPaths[0] === 'string') {
+            currentPaths.splice(index, 1)
+            this.formData.workPlan.attachmentIds = [...currentPaths]
+          } else {
+            const filteredPaths = currentPaths.filter((item) => {
+              if (typeof item === 'string') {
+                const fileName = item.substring(item.lastIndexOf('/') + 1)
+                return fileName !== removedFile.fileName
+              }
+              return (
+                item !== removedFile && item.fileName !== removedFile.fileName
+              )
+            })
+            this.formData.workPlan.attachmentIds = filteredPaths
+          }
+        } catch (error) {
+          console.error('删除文件时发生错误:', error)
+        }
+      },
+      // 打开弹窗方法
+      open(data, type) {
+        console.log('taskDetail open - data:', data)
+        console.log('taskDetail open - type:', type)
+
+        // 设置项目数据
+        if (data) {
+          // 根据类型提取 projectId
+          const projectId =
+            type === 'chengben'
+              ? data.projectId || data.id
+              : data.id || data.projectId
+
+          // 构造项目对象
+          this.currentProjectData = {
+            projectId: projectId,
+            taskId:
+              type === 'chengben'
+                ? data.taskId || data.id
+                : data.userTask?.id || data.taskId || data.id,
+            ...data,
+          }
+        }
+
+        // 打开弹窗
+        this.dialogVisible = true
+        this.activeTab = 'basicInfo'
+
+        // 初始化数据
+        if (this.currentProjectData && this.currentProjectData.projectId) {
+          this.$nextTick(() => {
+            this.initData()
+          })
+        }
+      },
+      // 保存工作方案
+      saveWorkPlan() {
+        this.scenarioData.saveScenario = true
+        let data = {
+          ...this.formData.workPlan,
+          attachmentIds: this.formData.workPlan.attachmentIds.join(','),
+          projectId: this.actualProject.projectId,
+        }
+        if (this.scenarioData.addScenario) {
+          addCostProjectScenario(data)
+            .then((res) => {
+              this.scenarioData.saveScenario = false
+              this.$message.success('保存成功')
+            })
+            .catch(() => {
+              this.scenarioData.saveScenario = false
+            })
+        } else {
+          updateCostProjectScenario(data)
+            .then((res) => {
+              this.scenarioData.saveScenario = false
+              this.$message.success('保存成功')
+            })
+            .catch(() => {
+              this.scenarioData.saveScenario = false
+            })
+        }
+      },
+      // 关闭
+      handleClose() {
+        this.dialogVisible = false
+        this.$emit('close')
+      },
+    },
+  }
+</script>
+
+<style scoped lang="scss">
+  .operation-bar {
+    display: flex;
+    justify-content: space-between;
+    align-items: center;
+    width: 100%;
+
+    .header-buttons {
+      display: flex;
+      gap: 10px;
+    }
+  }
+
+  .tabs-container {
+    margin-top: 10px;
+  }
+
+  ::v-deep .el-dialog__header {
+    padding: 20px 20px 10px;
+  }
+
+  ::v-deep .el-dialog__body {
+    padding: 10px 20px 20px;
+  }
+</style>

+ 14 - 3
src/views/costAudit/auditInfo/auditManage/index.vue

@@ -87,7 +87,7 @@
         <el-table-column label="操作" align="center" width="260">
           <template slot-scope="scope">
             <span v-if="!scope.row.isSubTask" class="action-buttons">
-              <el-button type="text" @click="handleOpenDetails(scope.row)">
+              <el-button type="text" @click="handleViewTaskDetail(scope.row)">
                 任务详情
               </el-button>
               <el-button type="text" @click="handleOpenMainDetails(scope.row)">
@@ -186,8 +186,6 @@
       :visible.sync="mainDetailsVisible"
       :current-node="selectedProject && selectedProject.currentNode"
       :current-status="selectedProject && selectedProject.status"
-      :project-name="selectedProject && selectedProject.projectName"
-      :audit-object="selectedProject && selectedProject.auditObject"
       @close="handleMainDetailsClose"
       @refresh="handleMainRefresh"
     />
@@ -199,6 +197,7 @@
       :current-node="cbjsInfoData && cbjsInfoData.currentNode"
       :current-status="cbjsInfoData && cbjsInfoData.status"
     />
+    <taskDetail ref="taskDetail" />
   </div>
 </template>
 <script>
@@ -209,6 +208,7 @@
   import { getReviewTaskList } from '@/api/audit/auditIndex'
   import taskInfo from '@/components/task/taskInfo.vue'
   import cbjsInfo from '@/components/task/cbjsInfo.vue'
+  import taskDetail from '@/components/task/taskDetail.vue'
   export default {
     name: 'CostAuditManagement',
     components: {
@@ -216,6 +216,7 @@
       taskInfo,
       cbjsInfo,
       mainDetailsDialog,
+      taskDetail,
     },
     data() {
       return {
@@ -284,6 +285,8 @@
                 isSubTask: record.pid !== '0',
                 currentNodeName: record.currentNodeName,
                 currentNode: record.currentNode,
+                projectId: record.projectId,
+                auditedUnitId: record.auditedUnitId,
                 children: record.childTasks
                   ? record.childTasks.map((child) => ({
                       id: child.id,
@@ -354,6 +357,14 @@
         this.loadAuditProjectList()
       },
 
+      // 查看任务详情
+      handleViewTaskDetail(row) {
+        // this.selectedTask = row
+        // this.activeTab = 'detail'
+        // this.showTaskDetail = true
+        this.$refs.taskDetail.open(row, 'chengben')
+      },
+
       // 打开详情弹窗
       handleOpenDetails(project) {
         console.log('project', project)

+ 38 - 13
src/views/costAudit/auditInfo/completedSupervisionQuery/index.vue

@@ -5,7 +5,7 @@
       <el-form :model="searchForm" inline>
         <el-form-item label="年度:">
           <el-date-picker
-            v-model="searchForm.year"
+            v-model="searchForm.projectYear"
             type="year"
             placeholder="请选择年度"
             format="yyyy"
@@ -100,7 +100,11 @@
               </el-button>
             </span>
             <span v-if="scope.row.isSubTask" class="action-buttons">
-              <el-button type="text" size="small" @click="handleView(row)">
+              <el-button
+                type="text"
+                size="small"
+                @click="handleView(scope.row, 'chengben')"
+              >
                 查看
               </el-button>
             </span>
@@ -130,28 +134,38 @@
         </el-breadcrumb-item>
         <el-breadcrumb-item>任务详情</el-breadcrumb-item>
       </el-breadcrumb>
-      <taskDetail />
+      <!-- <taskDetail /> -->
     </div>
     <!-- 任务信息弹窗 -->
     <taskInfo ref="taskInfo" />
+    <cbjs-info
+      :id="cbjsInfoData && cbjsInfoData.id"
+      :visible.sync="cbjsInfoVisible"
+      :current-node="cbjsInfoData && cbjsInfoData.currentNode"
+      :current-status="cbjsInfoData && cbjsInfoData.status"
+    />
+    <taskDetail ref="taskDetail" />
   </div>
 </template>
 
 <script>
   import { getCompletedSupervisionList } from '@/api/audit/reviewTask'
-  import taskDetail from '@/views/costAudit/auditInfo/auditManage/taskDetail.vue'
+  // import taskDetail from '@/views/costAudit/auditInfo/auditManage/taskDetail.vue'
   import taskInfo from '@/components/task/taskInfo.vue'
+  import cbjsInfo from '@/components/task/cbjsInfo.vue'
+  import taskDetail from '@/components/task/taskDetail.vue'
   export default {
     name: 'CompletedSupervisionQuery',
     components: {
       taskDetail,
       taskInfo,
+      cbjsInfo,
     },
     data() {
       return {
         // 搜索条件
         searchForm: {
-          year: '2025',
+          projectYear: '2025',
           projectName: '',
         },
         // 表格数据
@@ -170,6 +184,9 @@
         selectedTask: null,
         // 是否显示任务详情
         showTaskDetail: false,
+        // cbjsInfo弹窗相关
+        cbjsInfoVisible: false,
+        cbjsInfoData: null,
       }
     },
     created() {
@@ -194,7 +211,7 @@
           const params = {
             currentPage: this.pagination.currentPage,
             pageSize: this.pagination.pageSize,
-            year: this.searchForm.year,
+            projectYear: this.searchForm.projectYear,
             projectName: this.searchForm.projectName,
           }
           const response = await getCompletedSupervisionList(params)
@@ -213,6 +230,9 @@
                 source: this.getSourceTypeText(record.sourceType),
                 form: this.getAuditTypeText(record.auditType),
                 isSubTask: record.pid !== '0',
+                projectId: record.projectId,
+                auditedUnitId: record.auditedUnitId,
+                taskId: record.id,
                 children: record.childTasks
                   ? record.childTasks.map((child) => ({
                       id: child.id,
@@ -274,7 +294,7 @@
       // 处理重置
       handleReset() {
         this.searchForm = {
-          year: '2025',
+          projectYear: '2025',
           projectName: '',
         }
         this.pagination.currentPage = 1
@@ -293,16 +313,21 @@
       },
 
       // 查看任务(打开taskInfo弹窗)
-      handleView(row) {
-        // 打开taskInfo弹窗,传入任务数据和类型
-        this.$refs.taskInfo.open(row, 'chengben')
+      handleView(row, type) {
+        if (type === 'chengben') {
+          this.cbjsInfoData = row
+          this.cbjsInfoVisible = true
+        } else {
+          this.$refs.taskInfo.open(row, type)
+        }
       },
 
       // 查看任务详情
       handleViewTaskDetail(row) {
-        this.selectedTask = row
-        this.activeTab = 'detail'
-        this.showTaskDetail = true
+        // this.selectedTask = row
+        // this.activeTab = 'detail'
+        // this.showTaskDetail = true
+        this.$refs.taskDetail.open(row, 'chengben')
       },
 
       // 切换视图