shiyanyu 2 napja
szülő
commit
c975bcebea

+ 9 - 0
src/api/catalogManage.js

@@ -269,3 +269,12 @@ export function getTemplateDetails(data) {
     method: 'get',
   })
 }
+
+// 新成本调查表列表
+export function getCostSurveyPage(params) {
+  return request({
+    url: url + '/api/costCatalogSurvey/v1/page',
+    method: 'post',
+    data: params,
+  })
+}

+ 9 - 0
src/api/uc.js

@@ -134,3 +134,12 @@ export function getUserList(params = {}) {
     },
   })
 }
+
+// 监审任务负责人
+export function getTaskUserList(params) {
+  return request({
+    url: `${uc}/api/costProjectApproval/v1/getUserList`,
+    method: 'get',
+    params,
+  })
+}

+ 35 - 4
src/components/costAudit/EstablishmentDialog.vue

@@ -293,7 +293,7 @@
             style="width: 100%"
           >
             <el-option
-              v-for="(item, index) in areaUserList"
+              v-for="(item, index) in areaLeaderUserList"
               :key="index"
               :label="item.fullname"
               :value="item.userId"
@@ -372,7 +372,7 @@
     addProjectApproval,
     editProjectApproval,
   } from '@/api/auditInitiation'
-  import { getAllUserList } from '@/api/uc'
+  import { getAllUserList, getTaskUserList } from '@/api/uc'
   import { dictMixin } from '@/mixins/useDict'
   import { getDefaultDem, getOrgListByDemId } from '@/api/annualReviewPlan'
   export default {
@@ -505,6 +505,9 @@
           ],
         },
         areaUserList: [],
+        // 负责人列表:通过 getTaskUserList 获取
+        leaderUserList: [],
+        areaLeaderUserList: [],
       }
     },
     computed: {
@@ -641,6 +644,7 @@
       this.getAllUnitList()
       this.getDefaultDem()
       this.getUser() // 获取用户信息
+      this.getLeaderUser() // 获取负责人列表
     },
     methods: {
       getAllUnitList() {
@@ -670,7 +674,7 @@
           }
         })
       },
-      // 获取用户信息
+      // 获取用户信息(用于任务组成员)
       getUser() {
         getAllUserList()
           .then((res) => {
@@ -679,6 +683,16 @@
           .catch(() => {})
       },
 
+      // 获取负责人列表(用于监审任务负责人)
+      getLeaderUser() {
+        getTaskUserList()
+          .then((res) => {
+            this.leaderUserList = res.value || []
+            this.areaLeaderUserList = this.leaderUserList
+          })
+          .catch(() => {})
+      },
+
       // 立项依据文件处理
       saveAccordingFiles(data) {
         this.accordingFileList = data
@@ -735,8 +749,10 @@
       handleRegionChange(region) {
         if (region && region.code) {
           this.formData.areaCode = region.code
-          // 筛选一下该地区的用户
+          // 筛选一下该地区的用户(任务组成员)
           this.filterUser(region)
+          // 筛选一下该地区的负责人
+          this.filterLeaderUser(region)
           // 筛选一下该地区的主体
           this.filterOrg(region)
         }
@@ -756,6 +772,21 @@
           )
         }
       },
+      filterLeaderUser(region) {
+        if (region.level == 0) {
+          this.areaLeaderUserList = this.leaderUserList.filter(
+            (item) => item.provinceCode === region.code
+          )
+        } else if (region.level == 1) {
+          this.areaLeaderUserList = this.leaderUserList.filter(
+            (item) => item.cityCode == region.code
+          )
+        } else if (region.level == 2) {
+          this.areaLeaderUserList = this.leaderUserList.filter(
+            (item) => item.countyCode == region.code
+          )
+        }
+      },
       filterOrg(region) {
         if (region.level == 0) {
           this.areaOrgList = this.OrgList.filter(

+ 322 - 113
src/views/EntDeclaration/auditTaskManagement/components/FixedTableDialog.vue

@@ -20,7 +20,7 @@
         <template slot-scope="scope">
           <span>{{ scope.row.seq }}</span>
         </template>
-      </el-table-column> -->
+</el-table-column> -->
 
       <!-- 项目列(只读) -->
       <!-- <el-table-column
@@ -126,25 +126,32 @@
         </template>
       </el-table-column>
 
-      <!-- 动态年份列 -->
+      <!-- 监审期间动态列:surveyData.fixedHeaders 中 isAuditPeriod=true 的字段,按 年份 + fieldName 展开(单层表头:${year}${fieldName}) -->
       <el-table-column
-        v-for="year in yearColumns"
-        :key="year"
-        :label="`${year}年`"
-        :prop="`year_${year}`"
+        v-for="col in auditPeriodColumns()"
+        :key="col.key"
+        :label="col.label"
+        :prop="col.prop"
         width="150"
         align="center"
       >
         <template slot-scope="scope">
           <el-input
             v-if="!scope.row.isCategory"
-            v-model="scope.row[`year_${year}`]"
+            v-model="scope.row[col.prop]"
             inputmode="decimal"
-            :placeholder="`请输入${year}年数据`"
+            :placeholder="`请输入${col.label}`"
             :disabled="isViewMode"
             style="width: 100%"
-            @input="handleNumericInput(scope.row, year, $event)"
-            @blur="handleCellBlur(scope.row, year)"
+            @input="
+              handleAuditPeriodNumericInput(
+                scope.row,
+                col.year,
+                col.meta,
+                $event
+              )
+            "
+            @blur="handleAuditPeriodCellBlur(scope.row, col.year, col.meta)"
           />
         </template>
       </el-table-column>
@@ -512,17 +519,28 @@
           }
         }
       },
-      // 由父组件传入的 fixedFields 生成表头标签数组
+      // 由父组件传入的 fixedFields 生成表头标签数组(仅“非监审期间字段”)
       dynamicColumns() {
-        if (!this.fixedFields) return []
+        if (!this.fixedFields) {
+          // 即使 fixedFields 为空,也允许从元数据中补充 showVisible=1 的非期间字段
+          const metas = this.metaList()
+          return metas
+            .filter((m) => !this.isAuditPeriodMeta(m))
+            .filter((m) => m && String(m.showVisible || '').trim() === '1')
+            .map((m) => String(m.label || m.fieldName || m.name || '').trim())
+            .filter(Boolean)
+        }
+
         const labels = String(this.fixedFields)
           .split(',')
           .map((s) => s.trim())
           .filter(Boolean)
-        // union: 追加任何 meta 中 (showVisible==='1' 或 有 dictCode) 的列
+
+        // union: 追加任何 meta 中 (showVisible==='1' 或 有 dictCode) 的“非期间字段”
         const metas = this.metaList()
         const extras = []
         metas.forEach((m) => {
+          if (!m || this.isAuditPeriodMeta(m)) return
           const label = String(m?.label || m?.fieldName || m?.name || '').trim()
           if (!label) return
           const hasDict = !!(
@@ -542,26 +560,86 @@
             extras.push(label)
           }
         })
+
         let base = labels.concat(extras)
-        // 基于元数据控制显示:showVisible==='1' 强制显示;否则当 isAuditPeriod==='false' 隐藏
+
+        // 基于元数据控制显示:固定表头只保留“非期间字段”(isAuditPeriod=true 的字段一律不显示,即使 showVisible=1 也不例外)
         let filtered = base.filter((label) => {
           const m = this.getFieldMeta(label)
           if (!m) return true
-          // 强制显示优先
+          // 期间字段一律从固定表头剔除,避免与“${year}${fieldName}”动态列重复
+          if (this.isAuditPeriodMeta(m)) return false
+
+          // 非期间字段:showVisible=1 强制显示
           const sv = m.showVisible
           if (sv != null && String(sv).trim() === '1') return true
-          const v = m.isAuditPeriod
-          if (v === undefined || v === null || v === '') return true
-          const str = String(v).trim().toLowerCase()
-          return !(str === 'false' || str === '0' || str === 'no')
+          return true
         })
-        // 移除纯年份表头(如“2023”或“2023年”),年份列统一由 yearColumns 渲染
+
+        // 移除纯年份表头(如“2023”或“2023年”),期间列统一由 yearColumns 渲染
         filtered = filtered.filter((label) => !/^\d{4}(年)?$/.test(label))
-        // 若全部被过滤导致空表头,则回退为全部显示,避免页面空白
+
         return filtered.length > 0
           ? filtered
           : base.filter((label) => !/^\d{4}(年)?$/.test(label))
       },
+
+      // 期间字段:来自 fixedHeaders 中 isAuditPeriod=true
+      auditPeriodFields() {
+        const metas = this.metaList()
+        const list = (Array.isArray(metas) ? metas : []).filter((m) =>
+          this.isAuditPeriodMeta(m)
+        )
+        return list.map((m, idx) => ({
+          ...m,
+          __key: String(m.fieldName || m.label || m.name || idx),
+        }))
+      },
+
+      isAuditPeriodMeta(meta) {
+        if (!meta) return false
+        const v = meta.isAuditPeriod
+        if (v === true) return true
+        if (v === false) return false
+        const s = String(v == null ? '' : v)
+          .trim()
+          .toLowerCase()
+        return ['true', '1', 'y', 'yes', '是'].includes(s)
+      },
+
+      // 动态期间字段在行对象上的 prop:ap_${year}__${fieldName}
+      getAuditPeriodProp(year, meta) {
+        const y = String(year)
+        const f = String(
+          meta?.fieldName || meta?.label || meta?.name || ''
+        ).trim()
+        return `ap_${y}__${f}`
+      },
+
+      // 单层表头列:[{ key, label, prop, year, meta }]
+      auditPeriodColumns() {
+        const years = Array.isArray(this.yearColumns) ? this.yearColumns : []
+        const fields = this.auditPeriodFields()
+        const cols = []
+        years.forEach((year) => {
+          fields.forEach((m) => {
+            const fieldName = String(
+              m.fieldName || m.label || m.name || ''
+            ).trim()
+            if (!fieldName) return
+            const prop = this.getAuditPeriodProp(year, m)
+            cols.push({
+              key: `${year}__${m.__key}`,
+              label: `${year}${fieldName}`,
+              prop,
+              year,
+              meta: m,
+            })
+          })
+        })
+        return cols
+      },
+
       // ====== fixedFields 渲染辅助(基于 columnsMeta)======
       getFieldMeta(label) {
         const metas = this.metaList()
@@ -987,8 +1065,8 @@
         // 构建公式索引并计算一次
         this.buildFormulaEngine()
         this.recomputeAllFormulas()
-        // 初始化后按年份对父项进行汇总
-        this.recomputeAllParentYears()
+        // 初始化后按监审期间字段对父项进行汇总
+        this.recomputeAllParentAuditFields()
       },
       // 创建行数据
       createRowData(item, isChild, rowid) {
@@ -1004,10 +1082,18 @@
           isCategory: item.isCategory || false,
         }
 
-        // 初始化年份数据
-        this.yearColumns.forEach((year) => {
-          rowData[`year_${year}`] = item[`year_${year}`] || ''
-        })
+        // 初始化监审期间动态字段(isAuditPeriod=true):year_${year}__${fieldName}
+        const auditFields = this.auditPeriodFields()
+        if (Array.isArray(auditFields) && auditFields.length > 0) {
+          this.yearColumns.forEach((year) => {
+            auditFields.forEach((m) => {
+              const prop = this.getAuditPeriodProp(year, m)
+              if (rowData[prop] === undefined) {
+                this.$set(rowData, prop, item[prop] || '')
+              }
+            })
+          })
+        }
 
         // 初始化固定表头(fixedFields)对应的列,确保 v-model 有响应式属性
         const labels = this.dynamicColumns()
@@ -1062,14 +1148,48 @@
                 : res.value.items || res.value.records || []
 
               // 按 rowid 分组数据
+              // 兼容:监审期间动态列的行标识为 "${rowid}_${year}",其 rkey 可能为:
+              // - 旧:fieldName
+              // - 新:fieldName_year(如 A1_2025)
+              // 页面侧统一转换成:dataByRowid[baseRowid]["${year}${fieldName}"]
               const dataByRowid = {}
               detailData.forEach((item) => {
-                if (item.rowid) {
-                  if (!dataByRowid[item.rowid]) {
-                    dataByRowid[item.rowid] = {}
+                if (!item.rowid) return
+
+                const ridRaw = String(item.rowid)
+                let baseRowid = ridRaw
+                let year = ''
+
+                // 如果 rowid 形如 "xxx_2025",拆出 year
+                ;(Array.isArray(this.yearColumns) ? this.yearColumns : []).some(
+                  (y) => {
+                    const ys = String(y)
+                    const suffix = `_${ys}`
+                    if (ridRaw.endsWith(suffix)) {
+                      baseRowid = ridRaw.slice(0, -suffix.length)
+                      year = ys
+                      return true
+                    }
+                    return false
+                  }
+                )
+
+                if (!dataByRowid[baseRowid]) dataByRowid[baseRowid] = {}
+
+                const rkeyRaw = item.rkey
+                // 监审期间动态列:后端返回 rowid_年份
+                // - 旧:rkey=fieldName
+                // - 新:rkey=fieldName_year
+                // 页面侧用 "${year}${fieldName}" 作为回显键
+                let fieldName = rkeyRaw
+                if (year) {
+                  const suffix = `_${year}`
+                  if (typeof rkeyRaw === 'string' && rkeyRaw.endsWith(suffix)) {
+                    fieldName = rkeyRaw.slice(0, -suffix.length)
                   }
-                  dataByRowid[item.rowid][item.rkey] = item.rvalue
                 }
+                const storeKey = year ? `${year}${fieldName}` : rkeyRaw
+                dataByRowid[baseRowid][storeKey] = item.rvalue
               })
 
               // 回显数据到表格
@@ -1092,12 +1212,23 @@
                     row.remark = rowData['备注']
                   }
 
-                  // 回显年份数据
-                  this.yearColumns.forEach((year) => {
-                    if (rowData[year] !== undefined) {
-                      row[`year_${year}`] = rowData[year]
-                    }
-                  })
+                  // 回显监审期间动态字段(rkey 使用“${year}${fieldName}”)
+                  const auditFields = this.auditPeriodFields()
+                  if (Array.isArray(auditFields) && auditFields.length > 0) {
+                    this.yearColumns.forEach((year) => {
+                      auditFields.forEach((m) => {
+                        const fieldName = String(
+                          m.fieldName || m.label || m.name || ''
+                        ).trim()
+                        if (!fieldName) return
+                        const rkey = `${year}${fieldName}`
+                        if (rowData[rkey] !== undefined) {
+                          const prop = this.getAuditPeriodProp(year, m)
+                          this.$set(row, prop, rowData[rkey])
+                        }
+                      })
+                    })
+                  }
 
                   // 回显固定表头字段(与保存时一致,rkey 使用中文表头 label)
                   const labels = this.dynamicColumns()
@@ -1116,8 +1247,8 @@
 
               // 回显后根据公式重算
               this.recomputeAllFormulas()
-              // 回显后根据子项汇总父项年份
-              this.recomputeAllParentYears()
+              // 回显后根据子项汇总父项(按监审期间字段汇总)
+              this.recomputeAllParentAuditFields()
               // 回显后再做一次字典值规范化,确保显示选中项
               this.normalizeAllDictValues()
             }
@@ -1126,12 +1257,14 @@
           }
         }
       },
-      // =========== 年份父项汇总 ==========
-      recomputeAllParentYears() {
+      // =========== 父项汇总:按监审期间动态字段汇总 ==========
+      recomputeAllParentAuditFields() {
         if (!Array.isArray(this.tableData) || this.tableData.length === 0)
           return
         const parents = this.tableData.filter((r) => r && r.isCategory)
         const years = Array.isArray(this.yearColumns) ? this.yearColumns : []
+        const auditFields = this.auditPeriodFields()
+
         parents.forEach((parent) => {
           const pid = parent.rowid || parent.id
           const children = this.tableData.filter(
@@ -1140,16 +1273,20 @@
               !r.isCategory &&
               (r.parentid === pid || String(r.parentid) === String(pid))
           )
+
           years.forEach((y) => {
-            const field = `year_${y}`
-            const sum = children.reduce(
-              (acc, c) => acc + (Number(c[field]) || 0),
-              0
-            )
-            this.$set(parent, field, sum === 0 ? '' : String(sum))
+            auditFields.forEach((m) => {
+              const prop = this.getAuditPeriodProp(y, m)
+              const sum = children.reduce(
+                (acc, c) => acc + (Number(c[prop]) || 0),
+                0
+              )
+              this.$set(parent, prop, sum === 0 ? '' : String(sum))
+            })
           })
         })
       },
+
       // 获取假数据(用于测试)
       getMockTableData() {
         // return [
@@ -1231,66 +1368,59 @@
         }
         return ''
       },
-      // 单元格输入事件
-      handleCellInput(row, year) {
-        // 实时验证勾稽关系
-        this.validateLinkage(row, year)
-        // 实时联动计算
-        this.recomputeDependentsForRow(row, year)
-        // 失焦也触发父项汇总
-        this.recomputeAllParentYears()
-      },
-      // 仅数字输入:按行规则限制小数位;整数不允许小数
-      handleNumericInput(row, year, val) {
-        if (!row) return
-        const rules = (row && row.validateRules) || {}
-        const field = `year_${year}`
+      // 监审期间单元格输入事件(用于公式联动/父项汇总)
+      handleAuditPeriodCellInput(row, year, meta) {
+        this.recomputeDependentsForAuditField(row, year, meta)
+        this.recomputeAllParentAuditFields()
+      },
+
+      // 监审期间数字输入:参考原动态年份列,但根据 fixedHeaders 字段类型控制小数位
+      handleAuditPeriodNumericInput(row, year, meta, val) {
+        if (!row || !meta) return
+        const prop = this.getAuditPeriodProp(year, meta)
+
+        const t = String(meta.fieldType || meta.type || '').toLowerCase()
+        const isInteger = t.includes('int')
+        const dlenRaw =
+          meta.fieldTypenointlen ||
+          meta.fieldTypeNointLen ||
+          meta.decimalLength ||
+          meta.scale
+        const dlen = isInteger
+          ? 0
+          : typeof dlenRaw === 'number'
+          ? dlenRaw
+          : parseInt(dlenRaw)
+
         let s = String(val == null ? '' : val)
-        // 允许负号和小数点,其余去除
         s = s.replace(/[^0-9+\-\.]/g, '')
-        // 只保留第一个负号在最前
         s = s.replace(/(?!^)-/g, '')
         if (s.startsWith('+')) s = s.slice(1)
-        // 多个点只保留第一个
+
         const firstDot = s.indexOf('.')
         if (firstDot >= 0) {
           s =
             s.slice(0, firstDot + 1) + s.slice(firstDot + 1).replace(/\./g, '')
         }
-        const t = String(rules.type || '').toLowerCase()
-        const isInteger =
-          t === 'integer' ||
-          (t === 'number' &&
-            (rules.fieldTypenointlen === 0 || rules.decimalLength === 0))
-        if (isInteger && s.includes('.')) {
+
+        if (dlen === 0 && s.includes('.')) {
           s = s.split('.')[0]
+        } else if (!isNaN(dlen) && dlen >= 0 && firstDot >= 0) {
+          const parts = s.split('.')
+          parts[1] = (parts[1] || '').slice(0, dlen)
+          s = parts.join('.')
         }
-        if (!isInteger) {
-          const dlen =
-            rules.fieldTypenointlen ||
-            rules.fieldTypeNointLen ||
-            rules.decimalLength
-          if (typeof dlen === 'number' && dlen >= 0 && firstDot >= 0) {
-            const parts = s.split('.')
-            parts[1] = (parts[1] || '').slice(0, dlen)
-            s = parts.join('.')
-          }
-        }
-        // 写回且触发联动
-        this.$set(row, field, s)
-        this.handleCellInput(row, year)
-        // 年份联动:子项变更后,汇总到父项
-        this.recomputeAllParentYears()
-      },
-      // 单元格失焦事件
-      handleCellBlur(row, year) {
-        // 验证格式和非空
-        this.validateCell(row, year)
-        // 验证勾稽关系
-        this.validateLinkage(row, year)
-        // 失焦后再次联动计算,确保取整/格式等影响后结果一致
-        this.recomputeDependentsForRow(row, year)
+
+        this.$set(row, prop, s)
+        this.handleAuditPeriodCellInput(row, year, meta)
       },
+
+      handleAuditPeriodCellBlur(row, year, meta) {
+        // 失焦时也触发一次联动计算,保持一致
+        this.recomputeDependentsForAuditField(row, year, meta)
+        this.recomputeAllParentAuditFields()
+      },
+
       // 验证单元格(非空和格式验证)
       validateCell(row, year) {
         const fieldName = `year_${year}`
@@ -1878,21 +2008,30 @@
               })
             }
 
-            // 保存年份数据(所有行都可以有年份数据)
-            this.yearColumns.forEach((year) => {
-              const yearValue = row[`year_${year}`]
-              if (
-                yearValue !== undefined &&
-                yearValue !== null &&
-                yearValue !== ''
-              ) {
-                saveData.push({
-                  rowid: rowid,
-                  rkey: String(year),
-                  rvalue: String(yearValue),
+            // 保存监审期间动态字段的值:rowid 不带年份,rkey 使用 "${fieldName}_${year}"(注意:auditPeriodFields 有多个字段时也不会冲突)
+            const auditFields = this.auditPeriodFields()
+            if (Array.isArray(auditFields) && auditFields.length > 0) {
+              this.yearColumns.forEach((year) => {
+                const yearStr = String(year)
+                auditFields.forEach((m) => {
+                  const fieldName = String(
+                    m.fieldName || m.label || m.name || ''
+                  ).trim()
+                  if (!fieldName) return
+                  const prop = this.getAuditPeriodProp(yearStr, m)
+                  const v = row[prop]
+                  if (v !== undefined && v !== null && v !== '') {
+                    saveData.push({
+                      rowid: String(rowid),
+                      rkey: `${fieldName}_${yearStr}`,
+                      rvalue: String(v),
+                    })
+                  }
                 })
-              }
-            })
+              })
+            }
+
+            // (旧)year_YYYY 年份列不再保存:已由“监审期间+fieldName”替代
 
             // 保存备注(所有行都可以有备注)
             if (
@@ -2032,17 +2171,85 @@
           this.recomputeDependentsForRow(depRow, year)
         )
       },
-      // 对所有含公式的行、所有年重算
+      // 对所有含公式的行、所有年、所有期间字段重算
       recomputeAllFormulas() {
-        if (!this.yearColumns || this.yearColumns.length === 0) return
+        const years = Array.isArray(this.yearColumns) ? this.yearColumns : []
+        const auditFields = this.auditPeriodFields()
+        if (years.length === 0 || auditFields.length === 0) return
+
         const formulaRows = this.tableData.filter(
           (r) => r && r.calculationFormula
         )
-        this.yearColumns.forEach((year) => {
-          formulaRows.forEach((r) => this.recomputeRowForYear(r, year))
+        years.forEach((year) => {
+          auditFields.forEach((m) => {
+            formulaRows.forEach((r) =>
+              this.recomputeRowForAuditField(r, year, m)
+            )
+          })
         })
         this.$forceUpdate()
       },
+
+      // 重新计算一个公式行在某年、某个期间字段下的值
+      recomputeRowForAuditField(row, year, meta) {
+        if (!row || !row.calculationFormula || !meta) return
+        const expr = row.calculationFormula.toString()
+
+        // 1) 先替换跨表引用:形如 “表名.A1”(token 中包含一个点)
+        const replacedCross = expr.replace(
+          /([^+\-*/()\s]+\.[A-Za-z0-9_]+)/g,
+          (token) => {
+            if (
+              this.crossTableInfo &&
+              typeof this.crossTableInfo.getValue === 'function'
+            ) {
+              const raw = this.crossTableInfo.getValue(year, token)
+              const num = Number(raw)
+              return !isNaN(num) ? String(num) : '0'
+            }
+            const dataByYear = this.crossTableInfo && this.crossTableInfo.data
+            const y = year === undefined || year === null ? '' : String(year)
+            const raw =
+              dataByYear && y && dataByYear[y]
+                ? dataByYear[y][token]
+                : undefined
+            const num = Number(raw)
+            return !isNaN(num) ? String(num) : '0'
+          }
+        )
+
+        // 2) 再替换本表引用:A1/B2... -> 取“同年同字段”的值
+        const replaced = replacedCross.replace(/[A-Z]+\d+/g, (code) => {
+          const refRow = this.cellCodeIndex[code]
+          const prop = this.getAuditPeriodProp(year, meta)
+          const v = refRow ? Number(refRow[prop]) : 0
+          return isNaN(v) ? '0' : String(v)
+        })
+
+        const safe = this.safeEvalExpression(replaced)
+        if (safe !== null && !isNaN(safe)) {
+          const prop = this.getAuditPeriodProp(year, meta)
+          row[prop] = String(safe)
+        }
+      },
+
+      // 在某年、某字段针对一个输入行,联动所有依赖它的行
+      recomputeDependentsForAuditField(row, year, meta) {
+        if (!row || !meta) return
+        const code = row.cellCode
+        if (!code) return
+        const dependents = this.dependentsMap[code]
+        if (!dependents || dependents.size === 0) return
+
+        dependents.forEach((depRow) =>
+          this.recomputeRowForAuditField(depRow, year, meta)
+        )
+        // 可能存在链式依赖,递归触发
+        dependents.forEach((depRow) =>
+          this.recomputeDependentsForAuditField(depRow, year, meta)
+        )
+      },
+
       // 简单安全表达式求值:仅支持数字、+ - * / 和括号
       safeEvalExpression(expr) {
         const cleaned = expr.replace(/\s+/g, '')
@@ -2067,6 +2274,7 @@
   .dialog-footer {
     text-align: right;
     margin-top: 20px;
+
     .el-button {
       margin: 0 10px;
     }
@@ -2074,6 +2282,7 @@
 
   ::v-deep .el-dialog__header {
     padding: 20px 20px 10px;
+
     .el-dialog__title {
       font-size: 18px;
       font-weight: 600;

+ 69 - 2
src/views/costAudit/auditInfo/auditManage/mainDetails.vue

@@ -108,6 +108,28 @@
       custom-class="process-dialog"
     >
       <div class="dialog-content">
+        <div
+          v-if="currentProcessType === 'next'"
+          class="form-item process-form-item"
+        >
+          <label class="form-label">下一步办理人:</label>
+          <div class="form-content">
+            <el-select
+              v-model="processParams.userIds"
+              placeholder="请选择下一步环节办理人员"
+              style="width: 100%"
+              filterable
+              clearable
+            >
+              <el-option
+                v-for="(item, index) in nextUserList"
+                :key="index"
+                :label="item.fullname"
+                :value="item.userId"
+              />
+            </el-select>
+          </div>
+        </div>
         <div class="form-item process-form-item">
           <label class="form-label">意见:</label>
           <div class="form-content">
@@ -189,6 +211,7 @@
     doProcessBtn,
   } from '@/api/dataPreliminaryReview'
   import { getReviewTask } from '@/api/audit/reviewTask'
+  import { getTaskUserList } from '@/api/uc'
   export default {
     name: 'Details',
     components: {
@@ -241,9 +264,12 @@
         additionalParams: {},
         // 当前操作按钮信息
         currentButton: null,
+        // 流转下一步可选办理人列表
+        nextUserList: [],
         // 流转/退回操作参数
         processParams: {
           content: '', // 意见
+          userIds: '', // 下一步环节办理人员
           // sendType: [], // 发送方式
         },
         // 当前操作类型:'next' 流转下一步, 'prev' 退回上一步
@@ -377,6 +403,8 @@
         this.currentProcessType = ''
         this.processParams = {
           content: '',
+          userIds: '',
+          userIdNames: '',
         }
       },
       // 根据 currentNode 和 currentStatus 设置活动标签页
@@ -403,6 +431,23 @@
           this.buttonData = []
         }
       },
+      // 获取下一步环节办理人员列表
+      async loadNextUserList() {
+        try {
+          const res = await getTaskUserList()
+          this.nextUserList = res.value || []
+        } catch (e) {
+          this.nextUserList = []
+        }
+      },
+      // 根据选择的 userId 反查 fullname(用于提交 userIdNames)
+      getSelectedNextUserFullname(userId) {
+        if (!userId) return ''
+        const matched = (this.nextUserList || []).find(
+          (u) => String(u.userId) === String(userId)
+        )
+        return matched?.fullname || ''
+      },
       handleClose() {
         // 关闭弹窗时更新本地状态并触发事件
         this.drawerVisible = false
@@ -437,12 +482,15 @@
       handleAuditPass(item) {},
 
       // 流转下一步
-      handleNextStep() {
+      async handleNextStep() {
         this.currentProcessType = 'next'
         this.processParams = {
           content: '',
+          userIds: '',
+          userIdNames: '',
           // sendType: [],
         }
+        await this.loadNextUserList()
         this.showProcessDialog = true
       },
 
@@ -451,7 +499,9 @@
         this.currentProcessType = 'prev'
         this.processParams = {
           content: '',
-          // sendType: [],
+          userIds: '',
+          userIdNames: '',
+          // userIds: [],
         }
         this.showProcessDialog = true
       },
@@ -461,6 +511,8 @@
         this.currentProcessType = 'complete'
         this.processParams = {
           content: '',
+          userIds: '',
+          userIdNames: '',
           // sendType: [],
         }
         this.showProcessDialog = true
@@ -473,6 +525,11 @@
           return
         }
 
+        // if (this.currentProcessType === 'next' && !this.processParams.userIds) {
+        //   this.$message.warning('请选择下一步环节办理人员')
+        //   return
+        // }
+
         // 验证发送方式
         // if (
         //   !this.processParams.sendType ||
@@ -499,6 +556,14 @@
             key: keyValue,
             // sendType: this.processParams.sendType.join(','), // 发送方式用","分割
             content: this.processParams.content || '', // 意见
+            userIds: this.processParams.userIds || '',
+          }
+
+          // 流转下一步:带上下一步办理人姓名(后端需要 userIdNames)
+          if (this.currentProcessType === 'next') {
+            params.userIdNames = this.getSelectedNextUserFullname(
+              this.processParams.userIds
+            )
           }
 
           const response = await getReviewTask(params)
@@ -519,6 +584,8 @@
             // 重置参数
             this.processParams = {
               content: '',
+              userIds: '',
+              userIdNames: '',
               // sendType: [],
             }
             this.currentProcessType = ''

+ 58 - 4
src/views/costAudit/auditInfo/auditManage/workDraft.vue

@@ -246,6 +246,12 @@
         },
         // 工作底稿数据
         workingPaperContent: '',
+        // 自动保存相关
+        autoSaveDelay: 1200,
+        autoSaveTimer: null,
+        isSettingWorkingPaperContent: false, // 回显/程序赋值时,避免触发自动保存
+        isAutoSaving: false,
+        lastSavedWorkingPaperContent: '',
         // 工作底稿记录列表
         workingPaperRecords: [],
         // 工作底稿弹窗
@@ -295,6 +301,11 @@
         },
         immediate: true,
       },
+      // 在线编辑内容变更:防抖自动保存
+      workingPaperContent() {
+        if (this.isSettingWorkingPaperContent) return
+        this.scheduleAutoSaveWorkingPaper()
+      },
     },
     mounted() {
       if (this.id) {
@@ -302,6 +313,12 @@
         this.getWorkingPaperContent()
       }
     },
+    beforeDestroy() {
+      if (this.autoSaveTimer) {
+        clearTimeout(this.autoSaveTimer)
+        this.autoSaveTimer = null
+      }
+    },
     methods: {
       // 获取结束时间可选择的范围
       getEndTimeSelectableRange() {
@@ -426,6 +443,7 @@
         }
         try {
           const res = await getTaskDraftOnlineEdit({ taskId: this.id })
+          this.isSettingWorkingPaperContent = true
           if (res && res.code === 200 && res.value) {
             // 回显内容到编辑器
             this.workingPaperContent = res.value.content || ''
@@ -433,18 +451,51 @@
             // 如果没有数据,初始化为空
             this.workingPaperContent = ''
           }
+          // 以当前回显为“已保存”基准,避免刚回显就触发自动保存
+          this.lastSavedWorkingPaperContent = this.workingPaperContent || ''
         } catch (error) {
           console.error('获取工作底稿在线编辑内容失败:', error)
           this.workingPaperContent = ''
+          this.lastSavedWorkingPaperContent = ''
+        } finally {
+          // nextTick 后再放开监听,避免 editor 内部二次赋值导致误触发
+          this.$nextTick(() => {
+            setTimeout(() => {
+              this.isSettingWorkingPaperContent = false
+            }, 0)
+          })
+        }
+      },
+      // 防抖自动保存调度
+      scheduleAutoSaveWorkingPaper() {
+        if (!this.id) return
+        if (
+          (this.workingPaperContent || '') ===
+          (this.lastSavedWorkingPaperContent || '')
+        ) {
+          return
+        }
+        if (this.autoSaveTimer) {
+          clearTimeout(this.autoSaveTimer)
+          this.autoSaveTimer = null
         }
+        this.autoSaveTimer = setTimeout(() => {
+          this.handleOnlineEdit({ silent: true, fromAutoSave: true })
+        }, this.autoSaveDelay)
       },
       // 保存工作底稿在线编辑内容
-      async handleOnlineEdit() {
+      async handleOnlineEdit(options = {}) {
+        const { silent = false } = options
         if (!this.id) {
-          this.$message.warning('缺少任务ID')
+          if (!silent) this.$message.warning('缺少任务ID')
           return
         }
+        // 防止并发重复保存(例如快速多次触发)
+        if (this.isAutoSaving) return
+
         try {
+          this.isAutoSaving = true
+
           // 获取当前时间,格式化为 yyyy-MM-dd HH:mm:ss
           const now = new Date()
           const year = now.getFullYear()
@@ -463,13 +514,16 @@
 
           const res = await saveTaskDraftOnlineEdit(params)
           if (res && res.code === 200) {
-            this.$message.success(res.message || '保存成功')
+            this.lastSavedWorkingPaperContent = this.workingPaperContent || ''
+            if (!silent) this.$message.success(res.message || '保存成功')
           } else {
-            this.$message.error(res.message || '保存失败')
+            if (!silent) this.$message.error(res.message || '保存失败')
           }
         } catch (error) {
           console.error('保存工作底稿在线编辑内容失败:', error)
           // this.$message.error('保存失败')
+        } finally {
+          this.isAutoSaving = false
         }
       },
       // 工作底稿操作

+ 2 - 47
src/views/costAudit/auditInfo/completedSupervisionQuery/index.vue

@@ -154,14 +154,6 @@
       :current-status="cbjsInfoData && cbjsInfoData.status"
     />
     <taskDetail ref="taskDetail" />
-    <!-- 成本监审任务制定弹窗(用于“任务详情”只读查看) -->
-    <task-customized-release-dialog
-      :visible.sync="taskReleaseDialogVisible"
-      :project="project"
-      :is-view="true"
-      @backToList="taskReleaseDialogVisible = false"
-      @close="taskReleaseDialogVisible = false"
-    />
   </div>
 </template>
 
@@ -171,15 +163,12 @@
   import taskInfo from '@/components/task/taskInfo.vue'
   import cbjsInfo from '@/components/task/cbjsInfo.vue'
   import taskDetail from '@/components/task/taskDetail.vue'
-  import TaskCustomizedReleaseDialog from '@/components/task/TaskCustomizedReleaseDialog.vue'
-  import { getCostProjectDetail } from '@/api/taskCustomizedRelease.js'
   export default {
     name: 'CompletedSupervisionQuery',
     components: {
       taskDetail,
       taskInfo,
       cbjsInfo,
-      TaskCustomizedReleaseDialog,
     },
     data() {
       return {
@@ -207,8 +196,6 @@
         // cbjsInfo弹窗相关
         cbjsInfoVisible: false,
         cbjsInfoData: null,
-        // 任务详情(项目)弹窗
-        taskReleaseDialogVisible: false,
       }
     },
     created() {
@@ -344,41 +331,9 @@
         }
       },
 
-      // // 查看任务详情
-      // handleViewTaskDetail(row) {
-      //   // this.selectedTask = row
-      //   // this.activeTab = 'detail'
-      //   // this.showTaskDetail = true
-      //   this.$refs.taskDetail.open(row, 'chengben')
-      // },
-
-      // 查看任务详情
+      // 查看任务详情(弹出 taskDetail.vue 弹窗)
       handleViewTaskDetail(row) {
-        // 使用成本监审任务制定弹窗(只读)
-        this.openTaskReleaseDialog(row)
-      },
-      // 打开成本监审任务制定弹窗(只读查看)
-      openTaskReleaseDialog(row) {
-        if (!row) return
-        const projectId =
-          row.projectId || row.projectID || row.id || row.taskId || ''
-        if (!projectId) {
-          this.$message &&
-            this.$message.warning &&
-            this.$message.warning('缺少项目ID,无法查看详情')
-          return
-        }
-        this.isView = true
-        getCostProjectDetail({ id: projectId })
-          .then((res) => {
-            this.project = (res && res.value) || {}
-            this.taskReleaseDialogVisible = true
-          })
-          .catch(() => {
-            // 回退:接口失败时至少展示当前行数据
-            this.project = row || {}
-            this.taskReleaseDialogVisible = true
-          })
+        this.$refs.taskDetail.open(row, 'chengben')
       },
 
       // 切换视图

+ 47 - 93
src/views/costAudit/baseInfo/catalogManage/index.vue

@@ -198,11 +198,7 @@
           <template slot-scope="scope">
             <div>
               <el-button
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  scope.row.status != 0 &&
-                  scope.row.nodeType !== 'nr'
-                "
+                v-if="$permission.getUserInfo().dataScope === 0"
                 v-region-permission="{
                   category: 'catalogManage',
                   action: 'edit',
@@ -214,11 +210,7 @@
                 修改类别
               </el-button>
               <el-button
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  scope.row.status != 0 &&
-                  scope.row.nodeType !== 'nr'
-                "
+                v-if="$permission.getUserInfo().dataScope === 0"
                 v-region-permission="{
                   category: 'catalogManage',
                   action: 'add',
@@ -230,12 +222,7 @@
                 添加子类
               </el-button>
               <el-button
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  !scope.row.auditType &&
-                  scope.row.status != 0 &&
-                  scope.row.nodeType !== 'nr'
-                "
+                v-if="$permission.getUserInfo().dataScope === 0"
                 v-region-permission="{
                   category: 'catalogManage',
                   action: 'add',
@@ -247,11 +234,7 @@
                 添加内容
               </el-button>
               <el-button
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  scope.row.nodeType == 'nr' &&
-                  scope.row.status != 0
-                "
+                v-if="$permission.getUserInfo().dataScope === 0"
                 v-region-permission="{
                   category: 'catalogManage',
                   action: 'edit',
@@ -263,11 +246,6 @@
                 修改内容
               </el-button>
               <el-button
-                v-if="scope.row.nodeType == 'nr' && scope.row.status != 0"
-                v-region-permission="{
-                  category: 'catalogManage',
-                  action: 'edit',
-                }"
                 type="text"
                 size="mini"
                 @click="handleDropdownCommand('infoMaintain', scope.row)"
@@ -275,26 +253,7 @@
                 信息维护
               </el-button>
               <el-button
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  scope.row.nodeType == 'nr' &&
-                  scope.row.status == 0
-                "
-                v-region-permission="{
-                  category: 'catalogManage',
-                  action: 'edit',
-                }"
-                type="text"
-                @click.native="handleDropdownCommand('status', scope.row)"
-              >
-                {{ scope.row.status === 1 ? '停用' : '启用' }}
-              </el-button>
-              <el-button
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  scope.row.nodeType == 'lx' &&
-                  scope.row.status == 0
-                "
+                v-if="$permission.getUserInfo().dataScope === 0"
                 v-region-permission="{
                   category: 'catalogManage',
                   action: 'edit',
@@ -305,10 +264,7 @@
                 {{ scope.row.status === 1 ? '停用' : '启用' }}
               </el-button>
               <el-button
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  scope.row.nodeType == 'nr'
-                "
+                v-if="$permission.getUserInfo().dataScope === 0"
                 v-region-permission="{
                   category: 'catalogManage',
                   action: 'delete',
@@ -320,57 +276,39 @@
                 删除
               </el-button>
               <!-- 第四个以后的按钮放入更多下拉菜单 -->
-              <el-dropdown
-                v-if="
-                  $permission.getUserInfo().dataScope === 0 &&
-                  scope.row.status != 0 &&
-                  scope.row.nodeType !== 'nr'
-                "
-                v-region-permission="{
+              <!-- <el-dropdown v-if="
+                $permission.getUserInfo().dataScope === 0 &&
+                scope.row.status != 0 &&
+                scope.row.nodeType !== 'nr'
+              " v-region-permission="{
                   category: 'catalogManage',
                   action: 'edit',
-                }"
-                trigger="click"
-                class="ml10"
-              >
+                }" trigger="click" class="ml10">
                 <el-button type="text" size="mini">
                   更多
                   <i class="el-icon-arrow-down el-icon--right"></i>
                 </el-button>
                 <el-dropdown-menu slot="dropdown">
-                  <el-dropdown-item
-                    v-if="scope.row.nodeType == 'nr'"
-                    v-region-permission="{
-                      category: 'catalogManage',
-                      action: 'edit',
-                    }"
-                    @click.native="
-                      handleDropdownCommand('infoMaintain', scope.row)
-                    "
-                  >
+                  <el-dropdown-item v-region-permission="{
+                    category: 'catalogManage',
+                    action: 'edit',
+                  }" @click.native="handleDropdownCommand('infoMaintain', scope.row)">
                     信息维护
                   </el-dropdown-item>
-                  <el-dropdown-item
-                    v-region-permission="{
-                      category: 'catalogManage',
-                      action: 'edit',
-                    }"
-                    divided
-                    @click.native="handleDropdownCommand('status', scope.row)"
-                  >
+                  <el-dropdown-item v-region-permission="{
+                    category: 'catalogManage',
+                    action: 'edit',
+                  }" divided @click.native="handleDropdownCommand('status', scope.row)">
                     {{ scope.row.status === 1 ? '停用' : '启用' }}
                   </el-dropdown-item>
-                  <el-dropdown-item
-                    v-region-permission="{
-                      category: 'catalogManage',
-                      action: 'delete',
-                    }"
-                    @click.native="handleDropdownCommand('delete', scope.row)"
-                  >
+                  <el-dropdown-item v-region-permission="{
+                    category: 'catalogManage',
+                    action: 'delete',
+                  }" @click.native="handleDropdownCommand('delete', scope.row)">
                     删除
                   </el-dropdown-item>
                 </el-dropdown-menu>
-              </el-dropdown>
+              </el-dropdown> -->
             </div>
           </template>
         </el-table-column>
@@ -966,6 +904,7 @@
     deleteDocumentCatalog,
     // 成本调查与模板相关API
     getCostSurveyForms,
+    getCostSurveyPage,
     getallCurrentCostSurveyList,
     addCostSurvey,
     batchDeleteSurvey,
@@ -1234,10 +1173,17 @@
             // slotName: 'surveyTemplateName',
           },
           {
-            prop: 'year',
+            prop: 'createTime',
             label: '年度',
             width: 100,
             align: 'center',
+            formatter: (row) => {
+              const ct = row && row.createTime
+              if (!ct) return '-'
+              // 常见格式:"2025-01-02 12:34:56" / "2025-01-02" / ISO
+              const m = String(ct).match(/\b(\d{4})\b/)
+              return m ? m[1] : '-'
+            },
           },
           {
             prop: 'status',
@@ -1260,6 +1206,7 @@
             slotName: 'action',
           },
         ],
+
         selectCostSurvey: [],
         costSurveyDialogVisible: false,
         selectCostSurveyData: [],
@@ -1401,10 +1348,13 @@
         )
       },
     },
-
     mounted() {
       this.getCategoryOpt()
       this.fetchData()
+      console.log(
+        this.$permission.getUserInfo().dataScope,
+        '$permission.getUserInfo().dataScope'
+      )
     },
     methods: {
       distinct(arr1, arr2, key) {
@@ -2191,13 +2141,13 @@
       // 成本调查表相关方法
       // 加载成本调查表数据
       loadCostSurveyData() {
-        getCostSurveyForms({
+        getCostSurveyPage({
           catalogId: this.currentCatalogId,
-          pageNum: this.costSurveyPagination.currentPage,
+          page: this.costSurveyPagination.currentPage,
           pageSize: this.costSurveyPagination.pageSize,
         }).then((res) => {
-          this.costSurveyData = res.value.records
-          this.costSurveyPagination.total = res.value.total
+          this.costSurveyData = res.rows
+          this.costSurveyPagination.total = res.total
         })
       },
       // 获取成本调查表选项
@@ -2327,11 +2277,14 @@
 
 <style lang="scss" scoped>
   @import '@/styles/costAudit.scss';
+
   .catalog-manage-container {
     padding: 20px;
+
     .operation-bar {
       margin-bottom: 20px;
     }
+
     .divider-space {
       display: inline-block;
       margin: 0 4px;
@@ -2345,6 +2298,7 @@
   /* 信息维护视图样式 */
   .info-maintain-container {
     padding: 20px;
+
     .page-header {
       margin-bottom: 10px;
       display: flex;

+ 3 - 1
src/views/costAudit/baseInfo/catalogManage/surveyDialog.vue

@@ -566,7 +566,9 @@
       },
       getData() {
         listByCurrentTemplateId({
-          surveyTemplateId: this.contentEditForm.data.surveyId,
+          surveyTemplateId:
+            this.contentEditForm.data.surveyId ||
+            this.contentEditForm.data.surveyTemplateId,
         }).then((res) => {
           // 根据模板类型解析并显示数据
           // if (this.contentEditForm.templateType === '1') {