CostSurveyTab.vue 46 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410
  1. <template>
  2. <div>
  3. <!-- 调查表填报弹窗(单记录类型) -->
  4. <survey-form-dialog
  5. :visible.sync="surveyFormDialogVisible"
  6. :survey-data="{ ...currentSurveyRow, ...surveyDetailData }"
  7. :form-fields="formFields"
  8. :is-view-mode="isViewMode"
  9. :audited-unit-id="auditedUnitId"
  10. :upload-id="
  11. currentSurveyRow && currentSurveyRow.id ? currentSurveyRow.id : uploadId
  12. "
  13. :survey-template-id="
  14. currentSurveyRow && currentSurveyRow.surveyTemplateId
  15. ? currentSurveyRow.surveyTemplateId
  16. : surveyTemplateId
  17. "
  18. :catalog-id="catalogId"
  19. @save="handleSurveyFormSave"
  20. @refresh="handleRefresh"
  21. />
  22. <!-- 固定表填报弹窗 -->
  23. <fixed-table-dialog
  24. :visible.sync="fixedTableDialogVisible"
  25. :survey-data="{ ...currentSurveyRow, fixedHeaders }"
  26. :table-items="tableItems"
  27. :audit-periods="auditPeriods"
  28. :is-view-mode="isViewMode"
  29. :audited-unit-id="auditedUnitId"
  30. :upload-id="
  31. currentSurveyRow && currentSurveyRow.id ? currentSurveyRow.id : uploadId
  32. "
  33. :survey-template-id="
  34. currentSurveyRow && currentSurveyRow.surveyTemplateId
  35. ? currentSurveyRow.surveyTemplateId
  36. : surveyTemplateId
  37. "
  38. :catalog-id="catalogId"
  39. @save="handleFixedTableSave"
  40. @refresh="handleRefresh"
  41. />
  42. <!-- 动态表填报弹窗 -->
  43. <dynamic-table-dialog
  44. :key="dynamicDialogKey"
  45. :visible.sync="dynamicTableDialogVisible"
  46. :survey-data="currentSurveyRow"
  47. :table-data="dynamicTableData"
  48. :table-items="tableItems"
  49. :is-view-mode="isViewMode"
  50. :audited-unit-id="auditedUnitId"
  51. :upload-id="
  52. currentSurveyRow && (currentSurveyRow.uploadId || currentSurveyRow.id)
  53. ? currentSurveyRow.uploadId || currentSurveyRow.id
  54. : uploadId
  55. "
  56. :catalog-id="catalogId"
  57. :survey-template-id="
  58. currentSurveyRow && currentSurveyRow.surveyTemplateId
  59. ? currentSurveyRow.surveyTemplateId
  60. : surveyTemplateId
  61. "
  62. @save="handleDynamicTableSave"
  63. @refresh="handleRefresh"
  64. />
  65. <el-table
  66. style="width: 100%; margin-top: 20px"
  67. :data="paginatedData"
  68. border
  69. size="medium"
  70. >
  71. <!-- 序号列 -->
  72. <el-table-column prop="index" label="序号" width="60" align="center">
  73. <template slot-scope="scope">
  74. {{ scope.$index + 1 }}
  75. </template>
  76. </el-table-column>
  77. <el-table-column label="成本调查表" min-width="220">
  78. <template slot-scope="scope">
  79. <span
  80. :style="{
  81. color: scope.row.isDisabled ? '#909399' : '#409EFF',
  82. cursor: scope.row.isDisabled ? 'default' : 'pointer',
  83. }"
  84. @click="
  85. !scope.row.isDisabled && $emit('handle-survey-click', scope.row)
  86. "
  87. >
  88. {{ scope.row.name }}
  89. </span>
  90. </template>
  91. </el-table-column>
  92. <!-- 资料类型列 -->
  93. <el-table-column
  94. prop="dataType"
  95. label="资料类型"
  96. width="120"
  97. align="center"
  98. ></el-table-column>
  99. <!-- 表格类型列 -->
  100. <el-table-column
  101. prop="tableType"
  102. label="表格类型"
  103. width="120"
  104. align="center"
  105. ></el-table-column>
  106. <!-- 是否必填列 -->
  107. <el-table-column
  108. prop="isRequired"
  109. label="是否必填"
  110. width="100"
  111. align="center"
  112. >
  113. <template slot-scope="scope">
  114. <span>
  115. {{
  116. scope.row.isRequired === '是' ||
  117. scope.row.isRequired === '1' ||
  118. scope.row.isRequired === 1
  119. ? '是'
  120. : '否'
  121. }}
  122. </span>
  123. </template>
  124. </el-table-column>
  125. <!-- 是否上传列(红色“未上传”、绿色“已上传”) -->
  126. <el-table-column label="是否上传" width="100" align="center">
  127. <template slot-scope="scope">
  128. <span
  129. :style="{
  130. color: scope.row.isUploaded === true ? '#67c23a' : '#f56c6c',
  131. }"
  132. >
  133. {{ scope.row.isUploaded === true ? '已上传' : '未上传' }}
  134. </span>
  135. </template>
  136. </el-table-column>
  137. <!-- 操作列(根据“表格类型”显示不同按钮) -->
  138. <el-table-column label="操作" width="320" align="center">
  139. <template slot-scope="scope">
  140. <!-- 在线填报:所有类型均显示 -->
  141. <el-button
  142. type="text"
  143. size="small"
  144. :disabled="isViewMode"
  145. @click="handleOnlineFillClick(scope.row)"
  146. >
  147. 在线填报
  148. </el-button>
  149. <!-- 动态表:数据下载、数据上传 -->
  150. <template v-if="scope.row.tableType === '动态表'">
  151. <el-button
  152. type="text"
  153. size="small"
  154. :disabled="isViewMode"
  155. @click="handleDataDownload(scope.row)"
  156. >
  157. 数据下载
  158. </el-button>
  159. <el-button
  160. type="text"
  161. size="small"
  162. :disabled="isViewMode"
  163. @click="handleDataUpload(scope.row)"
  164. >
  165. 数据上传
  166. </el-button>
  167. </template>
  168. <!-- 固定表:模版下载、数据上传 -->
  169. <template v-else-if="scope.row.tableType === '固定表'">
  170. <el-button
  171. type="text"
  172. size="small"
  173. :disabled="isViewMode"
  174. @click="handleDataDownload(scope.row)"
  175. >
  176. 模版下载
  177. </el-button>
  178. <el-button
  179. type="text"
  180. size="small"
  181. :disabled="isViewMode"
  182. @click="handleDataUpload(scope.row)"
  183. >
  184. 数据上传
  185. </el-button>
  186. </template>
  187. <!-- 单记录:仅显示在线填报(上面已显示),不再追加其他按钮 -->
  188. </template>
  189. </el-table-column>
  190. </el-table>
  191. <input
  192. ref="dynamicUploadInput"
  193. type="file"
  194. accept=".xls,.xlsx"
  195. style="display: none"
  196. @change="handleDynamicUploadChange"
  197. />
  198. <el-pagination
  199. background
  200. layout="total, sizes, prev, pager, next"
  201. :current-page="pagination.currentPage"
  202. :page-sizes="[10, 20, 30, 50]"
  203. :page-size="pagination.pageSize"
  204. :total="pagination.total"
  205. style="margin-top: 20px; text-align: right"
  206. @current-change="$emit('handle-page-change', $event)"
  207. @size-change="$emit('handle-size-change', $event)"
  208. />
  209. </div>
  210. </template>
  211. <script>
  212. import SurveyFormDialog from './SurveyFormDialog.vue'
  213. import FixedTableDialog from './FixedTableDialog.vue'
  214. import DynamicTableDialog from './DynamicTableDialog.vue'
  215. import {
  216. getSingleRecordSurveyList,
  217. getSurveyDetail,
  218. getDynamicTableData,
  219. downloadTemplate,
  220. importData,
  221. } from '@/api/audit/survey'
  222. import { getListBySurveyTemplateIdAndVersion } from '@/api/costSurveyTemplateHeaders'
  223. export default {
  224. name: 'CostSurveyTab',
  225. components: {
  226. SurveyFormDialog,
  227. FixedTableDialog,
  228. DynamicTableDialog,
  229. },
  230. props: {
  231. paginatedData: {
  232. type: Array,
  233. default: () => [],
  234. },
  235. pagination: {
  236. type: Object,
  237. default: () => ({ currentPage: 1, pageSize: 10, total: 0 }),
  238. },
  239. isViewMode: {
  240. type: Boolean,
  241. default: false,
  242. },
  243. // 被监审单位ID
  244. auditedUnitId: {
  245. type: String,
  246. default: '',
  247. },
  248. // 上传记录ID
  249. uploadId: {
  250. type: String,
  251. default: '',
  252. },
  253. // 成本调查表模板ID
  254. surveyTemplateId: {
  255. type: String,
  256. default: '',
  257. },
  258. // 目录ID
  259. catalogId: {
  260. type: String,
  261. default: '',
  262. },
  263. // 任务ID(用于上传)
  264. taskId: {
  265. type: [String, Number],
  266. default: '',
  267. },
  268. // 监审期间(从立项信息中获取)
  269. auditPeriod: {
  270. type: [String, Array],
  271. default: null,
  272. },
  273. },
  274. data() {
  275. return {
  276. surveyFormDialogVisible: false,
  277. fixedTableDialogVisible: false,
  278. dynamicTableDialogVisible: false,
  279. currentSurveyRow: null,
  280. // 表单字段配置(可以从后台获取,或通过 props 传入)
  281. formFields: [],
  282. // 表单详情数据(用于回显)
  283. surveyDetailData: {},
  284. // 固定表数据配置
  285. tableItems: [],
  286. // 监审期间(年份数组)
  287. auditPeriods: [],
  288. // 动态表数据
  289. dynamicTableData: [],
  290. dynamicDialogKey: 0,
  291. dynamicTableLoading: false,
  292. fixedHeaders: null,
  293. // 上传相关
  294. pendingDynamicRow: null,
  295. }
  296. },
  297. mounted() {
  298. // 表单字段配置在打开弹窗时动态加载,不需要在 mounted 中初始化
  299. },
  300. methods: {
  301. // 处理在线填报点击
  302. async handleOnlineFillClick(row) {
  303. this.currentSurveyRow = row
  304. // 重置详情数据
  305. this.surveyDetailData = {}
  306. // 如果表格类型是"单记录",弹出调查表填报弹窗
  307. if (row.tableType === '单记录') {
  308. console.log(this.auditedUnitId, 'this.auditedUnitId')
  309. // 如果该行有 id,先调用接口获取详情数据
  310. if (row.id && this.auditedUnitId) {
  311. try {
  312. const params = {
  313. uploadId: row.id,
  314. auditedUnitId: this.auditedUnitId,
  315. }
  316. const res = await getSurveyDetail(params)
  317. console.log('单记录详情数据', res)
  318. if (res && res.code === 200 && res.value) {
  319. // 将详情数据转换为表单数据格式
  320. // 接口返回的数据可能是数组格式 [{rowid: 'xxx', rkey: 'xxx', rvalue: 'xxx'}, ...]
  321. // 需要转换为 {rowid: 'rvalue', ...} 的格式
  322. const detailData = {}
  323. if (Array.isArray(res.value)) {
  324. res.value.forEach((item) => {
  325. if (item.rowid && item.rvalue !== undefined) {
  326. detailData[item.rowid] = item.rvalue
  327. }
  328. })
  329. } else if (res.value) {
  330. // 如果不是数组,可能是对象格式,直接使用
  331. Object.assign(detailData, res.value)
  332. }
  333. this.surveyDetailData = detailData
  334. console.log('转换后的详情数据', this.surveyDetailData)
  335. }
  336. } catch (err) {
  337. console.error('获取单记录详情失败', err)
  338. }
  339. }
  340. // 调用接口获取单记录表单字段配置(详情数据已准备好)
  341. await this.initFormFields()
  342. // 接口调用完成后会自动打开弹窗(在 initFormFields 中处理)
  343. } else if (row.tableType === '固定表') {
  344. // 如果表格类型是"固定表",弹出固定表填报弹窗
  345. // 调用接口获取固定表配置
  346. await this.initFixedTableData()
  347. // 接口调用完成后会自动打开弹窗(在 initFixedTableData 中处理)
  348. } else if (row.tableType === '动态表') {
  349. // 如果表格类型是"动态表",弹出动态表填报弹窗
  350. this.resetDynamicDialogState()
  351. await this.initDynamicTableData()
  352. } else {
  353. // 其他类型,触发原有事件
  354. this.$emit('handle-online-fill', row)
  355. }
  356. },
  357. // 处理调查表保存
  358. handleSurveyFormSave(formData) {
  359. // 可以将保存的数据传递给父组件
  360. this.$emit('handle-survey-form-save', {
  361. row: this.currentSurveyRow,
  362. formData: formData,
  363. })
  364. },
  365. // 处理刷新事件
  366. handleRefresh() {
  367. // 触发父组件的刷新事件
  368. this.$emit('handle-survey-form-save', {
  369. row: this.currentSurveyRow,
  370. formData: {},
  371. })
  372. },
  373. // 处理固定表保存
  374. handleFixedTableSave(tableData) {
  375. // 可以将保存的数据传递给父组件
  376. this.$emit('handle-fixed-table-save', {
  377. row: this.currentSurveyRow,
  378. tableData: tableData,
  379. })
  380. },
  381. // 处理动态表保存
  382. handleDynamicTableSave(tableData) {
  383. // 将保存的数据传递给父组件
  384. this.$emit('handle-dynamic-table-save', {
  385. row: this.currentSurveyRow,
  386. tableData: tableData,
  387. })
  388. // 触发刷新事件
  389. this.handleRefresh()
  390. },
  391. // 触发动态表数据上传
  392. handleDataUpload(row) {
  393. if (this.isViewMode) return
  394. this.pendingDynamicRow = row || null
  395. this.$nextTick(() => {
  396. const input = this.$refs.dynamicUploadInput
  397. if (input) {
  398. input.value = ''
  399. input.click()
  400. }
  401. })
  402. },
  403. // 文件选择后上传
  404. async handleDynamicUploadChange(e) {
  405. const files = e && e.target && e.target.files
  406. if (!files || !files.length || !this.pendingDynamicRow) return
  407. const file = files[0]
  408. const row = this.pendingDynamicRow
  409. // 参数收集
  410. const surveyTemplateId =
  411. (row && (row.surveyTemplateId || row.templateId)) ||
  412. this.surveyTemplateId ||
  413. ''
  414. const taskId = (row && row.taskId) || this.taskId || ''
  415. const materialId = row && (row.materialId || row.materialID || row.id)
  416. const periodRecordId =
  417. row && (row.periodRecordId || row.uploadId || row.id)
  418. if (!surveyTemplateId || !taskId) {
  419. this.$message &&
  420. this.$message.warning &&
  421. this.$message.warning('缺少必要参数,无法上传')
  422. return
  423. }
  424. const formData = new FormData()
  425. formData.append('file', file)
  426. formData.append('surveyTemplateId', surveyTemplateId)
  427. formData.append('taskId', taskId)
  428. if (materialId) formData.append('materialId', materialId)
  429. if (periodRecordId) formData.append('periodRecordId', periodRecordId)
  430. let loading
  431. try {
  432. loading = this.$loading({
  433. lock: true,
  434. text: '数据上传中...',
  435. spinner: 'el-icon-loading',
  436. background: 'rgba(0, 0, 0, 0.7)',
  437. })
  438. await importData(formData)
  439. this.$message &&
  440. this.$message.success &&
  441. this.$message.success('数据上传成功')
  442. // 上传成功后触发刷新
  443. this.$emit('handle-dynamic-table-save', { row, tableData: {} })
  444. this.handleRefresh()
  445. } catch (err) {
  446. console.error('数据上传失败:', err)
  447. } finally {
  448. if (loading && loading.close) loading.close()
  449. this.pendingDynamicRow = null
  450. if (this.$refs.dynamicUploadInput)
  451. this.$refs.dynamicUploadInput.value = ''
  452. }
  453. },
  454. // 动态表-数据下载
  455. async handleDataDownload(row) {
  456. try {
  457. const loading = this.$loading({
  458. lock: true,
  459. text: '数据下载中...',
  460. spinner: 'el-icon-loading',
  461. background: 'rgba(0, 0, 0, 0.7)',
  462. })
  463. // 取 surveyTemplateId 与 versionId
  464. const surveyTemplateId =
  465. (row && (row.surveyTemplateId || row.templateId)) ||
  466. this.surveyTemplateId ||
  467. ''
  468. const versionId =
  469. (row && (row.versionId || row.version || row.templateVersionId)) ||
  470. ''
  471. // if (!surveyTemplateId || !versionId) {
  472. // loading.close()
  473. // this.$message &&
  474. // this.$message.warning &&
  475. // this.$message.warning('缺少模板或版本信息,无法下载')
  476. // return
  477. // }
  478. const params = { surveyTemplateId, versionId }
  479. const res = await downloadTemplate(params)
  480. loading.close()
  481. // 处理响应数据(可能是 axios 响应或直接 Blob)
  482. const headers = (res && res.headers) || {}
  483. const contentDisposition =
  484. headers['content-disposition'] || headers['Content-Disposition']
  485. let fileName =
  486. this.extractFileNameFromHeader(contentDisposition) ||
  487. `${row.name || '数据'}.xlsx`
  488. if (!/\.[a-zA-Z0-9]+$/.test(fileName)) {
  489. fileName += '.xlsx'
  490. }
  491. const blobData = (res && res.data) || res
  492. const blob =
  493. blobData instanceof Blob ? blobData : new Blob([blobData])
  494. const url = window.URL.createObjectURL(blob)
  495. const link = document.createElement('a')
  496. link.style.display = 'none'
  497. link.href = url
  498. link.download = fileName
  499. document.body.appendChild(link)
  500. link.click()
  501. document.body.removeChild(link)
  502. window.URL.revokeObjectURL(url)
  503. this.$message &&
  504. this.$message.success &&
  505. this.$message.success('开始下载文件')
  506. } catch (e) {
  507. console.error('数据下载失败: ', e)
  508. }
  509. },
  510. extractFileNameFromHeader(contentDisposition) {
  511. if (!contentDisposition) return ''
  512. const match = /filename[^;=\n]*=((['"])?.*?\2|[^;\n]*)/i.exec(
  513. contentDisposition
  514. )
  515. if (match && match[1]) {
  516. try {
  517. return decodeURIComponent(match[1].replace(/['"]/g, ''))
  518. } catch (e) {
  519. return match[1].replace(/['"]/g, '')
  520. }
  521. }
  522. return ''
  523. },
  524. // 初始化动态表数据
  525. async initDynamicTableData() {
  526. try {
  527. this.dynamicTableLoading = true
  528. const uploadId =
  529. (this.currentSurveyRow &&
  530. (this.currentSurveyRow.uploadId || this.currentSurveyRow.id)) ||
  531. this.uploadId ||
  532. ''
  533. const auditedUnitId =
  534. this.auditedUnitId ||
  535. (this.currentSurveyRow && this.currentSurveyRow.auditedUnitId) ||
  536. ''
  537. const catalogId =
  538. (this.currentSurveyRow && this.currentSurveyRow.catalogId) ||
  539. this.catalogId ||
  540. ''
  541. const surveyTemplateId =
  542. (this.currentSurveyRow && this.currentSurveyRow.surveyTemplateId) ||
  543. this.surveyTemplateId ||
  544. ''
  545. const params = {
  546. uploadId,
  547. auditedUnitId,
  548. catalogId,
  549. surveyTemplateId,
  550. }
  551. const res = await getDynamicTableData(params)
  552. if (res && res.code === 200) {
  553. const records = res.value?.records || res.value || []
  554. this.dynamicTableData = Array.isArray(records) ? records : []
  555. } else {
  556. // 接口调用失败,使用当前行的动态表数据或空数组
  557. this.dynamicTableData =
  558. this.currentSurveyRow?.dynamicTableData || []
  559. }
  560. // 初始化表格项配置(用于详情/编辑时显示表单)
  561. if (
  562. this.currentSurveyRow &&
  563. this.currentSurveyRow.tableItems &&
  564. this.currentSurveyRow.tableItems.length > 0
  565. ) {
  566. this.tableItems = this.currentSurveyRow.tableItems
  567. } else {
  568. this.tableItems = this.getMockTableItems()
  569. }
  570. this.dynamicTableDialogVisible = true
  571. } catch (error) {
  572. console.error('获取动态表数据失败', error)
  573. // 出错时使用当前行的动态表数据或空数组
  574. this.dynamicTableData = this.currentSurveyRow?.dynamicTableData || []
  575. this.tableItems =
  576. this.currentSurveyRow?.tableItems || this.getMockTableItems()
  577. // 出错时也打开弹窗
  578. this.dynamicTableDialogVisible = true
  579. } finally {
  580. this.dynamicTableLoading = false
  581. }
  582. },
  583. // 初始化表单字段配置
  584. async initFormFields() {
  585. // 如果是单记录类型,调用接口获取表单字段配置(包含校验规则)
  586. if (
  587. this.currentSurveyRow &&
  588. this.currentSurveyRow.tableType === '单记录' &&
  589. this.currentSurveyRow.surveyTemplateId
  590. ) {
  591. try {
  592. const params = {
  593. surveyTemplateId: this.currentSurveyRow.surveyTemplateId,
  594. }
  595. // 调用 getListBySurveyTemplateIdAndVersion 获取完整的字段配置(包含校验规则)
  596. const res = await getListBySurveyTemplateIdAndVersion(params)
  597. console.log('单记录表单字段配置(含校验规则)', res)
  598. if (res && res.code === 200) {
  599. let mapped = []
  600. if (Array.isArray(res.value)) {
  601. // 数组格式:直接映射每个字段(包含完整的校验规则)
  602. mapped = res.value
  603. .map((item, index) =>
  604. this.mapApiFieldToFormField(item, index)
  605. )
  606. .filter(Boolean)
  607. } else if (res.value && typeof res.value === 'object') {
  608. // 对象格式:从 fixedFields 和 fixedFieldids 解析(兼容旧格式)
  609. const { fixedFields, fixedFieldids } = res.value
  610. if (fixedFields && fixedFieldids) {
  611. const labels = fixedFields
  612. .split(',')
  613. .map((item) => item.trim())
  614. const ids = fixedFieldids
  615. .split(',')
  616. .map((item) => item.trim())
  617. mapped = labels.map((label, index) => ({
  618. prop: ids[index] || `field_${index}`,
  619. label: label,
  620. type: 'input',
  621. colSpan: 12,
  622. placeholder: `请输入${label}`,
  623. rules: [],
  624. defaultValue: '',
  625. disabled: false,
  626. clearable: true,
  627. multiple: false,
  628. required: false,
  629. }))
  630. }
  631. }
  632. if (mapped.length > 0) {
  633. // 使用包含完整校验规则的字段配置
  634. this.formFields = mapped
  635. console.log(
  636. '转换后的表单字段配置(含校验规则)',
  637. this.formFields
  638. )
  639. } else {
  640. // 如果没有数据,使用默认配置
  641. this.formFields = this.getMockFormFields()
  642. }
  643. } else {
  644. // 接口返回失败,使用默认配置
  645. this.formFields = this.getMockFormFields()
  646. }
  647. // 打开弹窗
  648. this.surveyFormDialogVisible = true
  649. } catch (err) {
  650. console.error('获取单记录表单字段配置失败', err)
  651. // 出错时使用默认配置
  652. this.formFields = this.getMockFormFields()
  653. this.surveyFormDialogVisible = true
  654. }
  655. } else if (this.currentSurveyRow && this.currentSurveyRow.formFields) {
  656. // 如果当前行有表单配置,则使用
  657. this.formFields = this.currentSurveyRow.formFields
  658. this.surveyFormDialogVisible = true
  659. } else {
  660. // 使用假数据作为测试(实际开发中应该从后台获取)
  661. this.formFields = this.getMockFormFields()
  662. this.surveyFormDialogVisible = true
  663. }
  664. },
  665. // 将 API 返回的字段数据映射为表单字段配置(包含校验规则)
  666. mapApiFieldToFormField(item, index = 0) {
  667. if (!item) return null
  668. const getVal = (keys, fallback) => {
  669. for (const key of keys) {
  670. if (
  671. key &&
  672. item[key] !== undefined &&
  673. item[key] !== null &&
  674. item[key] !== ''
  675. ) {
  676. return item[key]
  677. }
  678. }
  679. return fallback
  680. }
  681. const toBool = (value) => {
  682. if (value === undefined || value === null) return false
  683. if (typeof value === 'boolean') return value
  684. if (typeof value === 'number') return value === 1
  685. const str = String(value).trim().toLowerCase()
  686. return ['1', 'true', 'y', 'yes', '是'].includes(str)
  687. }
  688. const toNumber = (value) => {
  689. if (value === undefined || value === null || value === '')
  690. return undefined
  691. const num = Number(value)
  692. return Number.isNaN(num) ? undefined : num
  693. }
  694. const prop =
  695. getVal(
  696. [
  697. 'fieldName',
  698. 'field_name',
  699. 'columnName',
  700. 'column_name',
  701. 'fieldCode',
  702. ],
  703. undefined
  704. ) || `field_${index}`
  705. const label =
  706. getVal(
  707. [
  708. 'columnComment',
  709. 'column_comment',
  710. 'fieldCname',
  711. 'field_cname',
  712. 'fieldLabel',
  713. 'field_label',
  714. ],
  715. prop
  716. ) || prop
  717. const columnType =
  718. (getVal(
  719. ['columnType', 'column_type', 'fieldType', 'field_type'],
  720. ''
  721. ) || '') + ''
  722. const columnTypeLower = columnType.toLowerCase()
  723. const totalLength = toNumber(
  724. getVal(
  725. ['fieldTypeLen', 'field_typelen', 'length', 'fieldLength'],
  726. undefined
  727. )
  728. )
  729. const decimalLength = toNumber(
  730. getVal(
  731. ['fieldTypeNointLen', 'field_typenointlen', 'scale'],
  732. undefined
  733. )
  734. )
  735. const isAuditPeriod = toBool(
  736. getVal(['isAuditPeriod', 'is_audit_period'], false)
  737. )
  738. const dictCode =
  739. getVal(
  740. [
  741. 'dictCode',
  742. 'dict_code',
  743. 'dictId',
  744. 'dictid',
  745. 'dictType',
  746. 'dict_type',
  747. ],
  748. ''
  749. ) || ''
  750. const optionsRaw = getVal(['options'], [])
  751. let options = []
  752. if (Array.isArray(optionsRaw)) {
  753. options = optionsRaw
  754. } else if (typeof optionsRaw === 'string' && optionsRaw.trim() !== '') {
  755. options = optionsRaw.split(',').map((value) => ({
  756. label: value.trim(),
  757. value: value.trim(),
  758. }))
  759. }
  760. let type = getVal(['componentType', 'type'], '')
  761. if (!type) {
  762. if (dictCode || options.length > 0) {
  763. type = 'select'
  764. } else if (
  765. columnTypeLower.includes('datetime') ||
  766. columnTypeLower.includes('timestamp') ||
  767. columnTypeLower.includes('date time')
  768. ) {
  769. type = 'datetime'
  770. } else if (columnTypeLower.includes('date')) {
  771. type = 'date'
  772. } else if (columnTypeLower.includes('year')) {
  773. type = 'year'
  774. } else if (
  775. columnTypeLower.includes('int') ||
  776. columnTypeLower.includes('number') ||
  777. columnTypeLower.includes('decimal') ||
  778. columnTypeLower.includes('float') ||
  779. columnTypeLower.includes('double')
  780. ) {
  781. type = 'number'
  782. } else {
  783. type = 'input'
  784. }
  785. }
  786. const required = toBool(
  787. getVal(['isRequired', 'is_required', 'required'], false)
  788. )
  789. const multiple = toBool(
  790. getVal(['isMultiple', 'is_multiple', 'multiple'], false)
  791. )
  792. const colSpan =
  793. toNumber(
  794. getVal(['colSpan', 'colspan', 'columnSpan', 'column_span'], 12)
  795. ) || 12
  796. const placeholder =
  797. getVal(
  798. ['placeholder', 'columnComment', 'column_comment'],
  799. undefined
  800. ) || (type === 'select' ? `请选择${label}` : `请输入${label}`)
  801. const defaultValue = getVal(
  802. ['defaultValue', 'default_value', 'defaultVal', 'default_val'],
  803. undefined
  804. )
  805. const precision = toNumber(
  806. getVal(
  807. ['fieldTypeNointLen', 'field_typenointlen', 'precision'],
  808. undefined
  809. )
  810. )
  811. const min = toNumber(getVal(['min'], undefined))
  812. const max = toNumber(getVal(['max'], undefined))
  813. const format = getVal(['format'], undefined)
  814. const valueFormat =
  815. getVal(['valueFormat', 'value_format'], undefined) ||
  816. (type === 'datetime'
  817. ? 'yyyy-MM-dd HH:mm:ss'
  818. : type === 'date'
  819. ? 'yyyy-MM-dd'
  820. : type === 'year'
  821. ? 'yyyy'
  822. : undefined)
  823. const formatLength = this.extractLengthFromFormat(format)
  824. const rules = this.buildFieldRules({
  825. type,
  826. label,
  827. required,
  828. totalLength,
  829. decimalLength,
  830. formatLength,
  831. format,
  832. isAuditPeriod,
  833. })
  834. return {
  835. prop,
  836. label,
  837. type,
  838. colSpan,
  839. placeholder,
  840. dictCode,
  841. dictType: dictCode,
  842. options,
  843. required,
  844. defaultValue,
  845. multiple,
  846. precision,
  847. min,
  848. max,
  849. format,
  850. valueFormat,
  851. totalLength,
  852. decimalLength,
  853. formatLength,
  854. rules, // 包含完整的校验规则
  855. }
  856. },
  857. // 从 format 中提取长度
  858. extractLengthFromFormat(format) {
  859. if (!format) return undefined
  860. const str = String(format).trim()
  861. if (!str) return undefined
  862. const match = str.match(/\d+/)
  863. if (match && match[0]) {
  864. const len = Number(match[0])
  865. return Number.isNaN(len) ? undefined : len
  866. }
  867. return undefined
  868. },
  869. // 构建字段校验规则
  870. buildFieldRules(meta) {
  871. const {
  872. type,
  873. label,
  874. required,
  875. totalLength,
  876. decimalLength,
  877. formatLength,
  878. format,
  879. isAuditPeriod,
  880. } = meta || {}
  881. const rules = []
  882. const trigger = type === 'select' ? 'change' : 'blur'
  883. if (required) {
  884. rules.push({
  885. required: true,
  886. message: `${type === 'select' ? '请选择' : '请输入'}${label}`,
  887. trigger,
  888. })
  889. }
  890. const inputMaxLength = formatLength || totalLength
  891. if (type === 'input' && inputMaxLength) {
  892. rules.push({
  893. validator: (_, value, callback) => {
  894. if (value === undefined || value === null || value === '') {
  895. callback()
  896. return
  897. }
  898. const str = String(value)
  899. if (str.length > inputMaxLength) {
  900. callback(
  901. new Error(`${label}长度不能超过${inputMaxLength}个字符`)
  902. )
  903. } else {
  904. callback()
  905. }
  906. },
  907. trigger: 'blur',
  908. })
  909. }
  910. const numberTotal = totalLength || formatLength
  911. if (type === 'number') {
  912. rules.push({
  913. validator: (_, value, callback) => {
  914. if (value === undefined || value === null || value === '') {
  915. callback()
  916. return
  917. }
  918. if (Number.isNaN(Number(value))) {
  919. callback(new Error(`${label}必须为数字`))
  920. return
  921. }
  922. const pure = String(value).replace('-', '')
  923. if (numberTotal && pure.replace('.', '').length > numberTotal) {
  924. callback(new Error(`${label}总位数不能超过${numberTotal}`))
  925. return
  926. }
  927. if (decimalLength !== undefined && decimalLength !== null) {
  928. const decimals = pure.split('.')[1] || ''
  929. if (decimals.length > decimalLength) {
  930. callback(
  931. new Error(`${label}小数位不能超过${decimalLength}位`)
  932. )
  933. return
  934. }
  935. }
  936. callback()
  937. },
  938. trigger: 'blur',
  939. })
  940. }
  941. if (type === 'datetime' || type === 'date') {
  942. if (format) {
  943. rules.push({
  944. validator: (_, value, callback) => {
  945. if (value === undefined || value === null || value === '') {
  946. callback()
  947. return
  948. }
  949. callback()
  950. },
  951. trigger: 'change',
  952. })
  953. }
  954. }
  955. if (type === 'year' || isAuditPeriod) {
  956. rules.push({
  957. validator: (_, value, callback) => {
  958. if (value === undefined || value === null || value === '') {
  959. callback()
  960. return
  961. }
  962. const pattern = /^\d{4}$/
  963. if (!pattern.test(String(value))) {
  964. callback(new Error(`${label}必须是四位年份`))
  965. } else {
  966. callback()
  967. }
  968. },
  969. trigger: 'change',
  970. })
  971. }
  972. return rules
  973. },
  974. // 获取假数据表单字段配置(用于测试)
  975. getMockFormFields() {
  976. return [
  977. {
  978. prop: 'institutionName',
  979. label: '机构名称',
  980. type: 'input',
  981. colSpan: 12,
  982. defaultValue: '幼儿园基本情况',
  983. placeholder: '请输入机构名称',
  984. required: true,
  985. },
  986. {
  987. prop: 'institutionNature',
  988. label: '机构性质',
  989. type: 'select',
  990. colSpan: 12,
  991. dictType: 'institutionNature', // 字典类型
  992. defaultValue: '公办',
  993. placeholder: '请选择机构性质',
  994. required: true,
  995. clearable: true,
  996. },
  997. {
  998. prop: 'institutionLevel',
  999. label: '机构评定等级',
  1000. type: 'select',
  1001. colSpan: 12,
  1002. dictType: 'institutionLevel', // 字典类型
  1003. defaultValue: '省一级',
  1004. placeholder: '请选择机构评定等级',
  1005. required: true,
  1006. clearable: true,
  1007. },
  1008. {
  1009. prop: 'educationMode',
  1010. label: '机构办学方式',
  1011. type: 'select',
  1012. colSpan: 12,
  1013. dictType: 'educationMode', // 字典类型
  1014. defaultValue: '全日制',
  1015. placeholder: '请选择机构办学方式',
  1016. required: true,
  1017. clearable: true,
  1018. },
  1019. {
  1020. prop: 'institutionAddress',
  1021. label: '机构地址',
  1022. type: 'input',
  1023. colSpan: 12,
  1024. placeholder: '请输入机构地址',
  1025. required: true,
  1026. },
  1027. {
  1028. prop: 'formFiller',
  1029. label: '机构填表人',
  1030. type: 'input',
  1031. colSpan: 12,
  1032. placeholder: '请输入机构填表人',
  1033. required: true,
  1034. },
  1035. {
  1036. prop: 'financialManager',
  1037. label: '机构财务负责人',
  1038. type: 'input',
  1039. colSpan: 12,
  1040. placeholder: '请输入机构财务负责人',
  1041. required: true,
  1042. },
  1043. {
  1044. prop: 'contactPhone',
  1045. label: '机构联系电话',
  1046. type: 'input',
  1047. colSpan: 12,
  1048. placeholder: '请输入机构联系电话',
  1049. required: true,
  1050. rules: [
  1051. {
  1052. required: true,
  1053. message: '请输入机构联系电话',
  1054. trigger: 'blur',
  1055. },
  1056. {
  1057. pattern: /^1[3-9]\d{9}$/,
  1058. message: '请输入正确的手机号码',
  1059. trigger: 'blur',
  1060. },
  1061. ],
  1062. },
  1063. ]
  1064. },
  1065. // 初始化固定表数据
  1066. async initFixedTableData() {
  1067. // 如果是固定表类型,调用接口获取固定表配置
  1068. if (
  1069. this.currentSurveyRow &&
  1070. this.currentSurveyRow.tableType === '固定表' &&
  1071. this.currentSurveyRow.surveyTemplateId
  1072. ) {
  1073. try {
  1074. const params = {
  1075. surveyTemplateId: this.currentSurveyRow.surveyTemplateId,
  1076. }
  1077. const res = await getSingleRecordSurveyList(params)
  1078. console.log('固定表配置数据', res)
  1079. if (res && res.code === 200 && res.value) {
  1080. // 将接口返回的数据转换为固定表配置格式
  1081. // 固定表使用 itemlist,不使用 fixedFields 和 fixedFieldids
  1082. const { itemlist } = res.value
  1083. console.log('itemlist', itemlist)
  1084. // 如果有 itemlist,使用 itemlist 作为表格项配置
  1085. if (itemlist && Array.isArray(itemlist) && itemlist.length > 0) {
  1086. this.tableItems = itemlist.map((item, index) => ({
  1087. id: item.id || item.itemId || '',
  1088. rowid: item.rowid || item.id || item.itemId || '', // rowid 用于父子关系
  1089. seq: item.序号, // 序号就是序号
  1090. itemName: item.项目 || '', // 项目就是项目
  1091. unit: item.unit || '', // 单位是 unit
  1092. isCategory: item.isCategory || false,
  1093. categorySeq: item.categorySeq || '',
  1094. categoryId: item.categoryId || '',
  1095. parentid:
  1096. item.parentid !== undefined
  1097. ? item.parentid
  1098. : item.parentId !== undefined
  1099. ? item.parentId
  1100. : '-1', // 父项ID,默认为 '-1'(父项)
  1101. validateRules: item.validateRules || {},
  1102. linkageRules: item.linkageRules || {},
  1103. children: item.children || [],
  1104. ...item, // 保留其他字段
  1105. }))
  1106. } else {
  1107. // 如果没有 itemlist,使用假数据
  1108. this.tableItems = this.getMockTableItems()
  1109. }
  1110. } else {
  1111. // 接口返回失败,使用默认配置
  1112. this.tableItems = this.getMockTableItems()
  1113. }
  1114. } catch (err) {
  1115. console.error('获取固定表配置失败', err)
  1116. // 出错时使用默认配置
  1117. this.tableItems = this.getMockTableItems()
  1118. }
  1119. } else if (this.currentSurveyRow && this.currentSurveyRow.tableItems) {
  1120. // 如果当前行有表格配置,则使用
  1121. this.tableItems = this.currentSurveyRow.tableItems
  1122. } else {
  1123. // 使用假数据作为测试(实际开发中应该从后台获取)
  1124. this.tableItems = this.getMockTableItems()
  1125. }
  1126. // 初始化监审期间(从立项信息中获取)
  1127. if (this.auditPeriod) {
  1128. // 如果传入了监审期间,使用传入的值
  1129. if (Array.isArray(this.auditPeriod)) {
  1130. this.auditPeriods = this.auditPeriod.map((p) => String(p))
  1131. } else {
  1132. this.auditPeriods = this.parseAuditPeriod(this.auditPeriod)
  1133. }
  1134. } else if (this.currentSurveyRow && this.currentSurveyRow.auditPeriod) {
  1135. // 如果当前行有监审期间,使用当前行的
  1136. this.auditPeriods = this.parseAuditPeriod(
  1137. this.currentSurveyRow.auditPeriod
  1138. )
  1139. } else {
  1140. // 默认使用最近3年
  1141. const currentYear = new Date().getFullYear()
  1142. this.auditPeriods = [
  1143. String(currentYear - 2),
  1144. String(currentYear - 1),
  1145. String(currentYear),
  1146. ]
  1147. }
  1148. // 额外拉取表头/规则信息,并透传给弹窗
  1149. try {
  1150. const headerRes = await getListBySurveyTemplateIdAndVersion({
  1151. surveyTemplateId: this.currentSurveyRow.surveyTemplateId,
  1152. })
  1153. if (headerRes && headerRes.code === 200) {
  1154. this.fixedHeaders = headerRes.value || null
  1155. } else {
  1156. this.fixedHeaders = null
  1157. }
  1158. } catch (e) {
  1159. this.fixedHeaders = null
  1160. }
  1161. // 打开弹窗
  1162. this.fixedTableDialogVisible = true
  1163. },
  1164. // 解析监审期间字符串(如 "2022,2023,2024" 或 "2022-2024")
  1165. parseAuditPeriod(periodStr) {
  1166. if (!periodStr) return []
  1167. if (periodStr.includes(',')) {
  1168. return periodStr.split(',').map((p) => p.trim())
  1169. }
  1170. if (periodStr.includes('-')) {
  1171. const parts = periodStr.split('-')
  1172. if (parts.length === 2) {
  1173. const start = parseInt(parts[0].trim())
  1174. const end = parseInt(parts[1].trim())
  1175. const years = []
  1176. for (let year = start; year <= end; year++) {
  1177. years.push(String(year))
  1178. }
  1179. return years
  1180. }
  1181. }
  1182. return [String(periodStr)]
  1183. },
  1184. // 获取假数据表格配置(用于测试)
  1185. getMockTableItems() {
  1186. return [
  1187. {
  1188. id: '1',
  1189. itemName: '班级数',
  1190. unit: '个',
  1191. isCategory: false,
  1192. seq: 1,
  1193. validateRules: {
  1194. required: true,
  1195. type: 'number',
  1196. min: 0,
  1197. },
  1198. },
  1199. {
  1200. id: '2',
  1201. itemName: '幼儿学生人数',
  1202. unit: '人',
  1203. isCategory: false,
  1204. seq: 2,
  1205. validateRules: {
  1206. required: true,
  1207. type: 'number',
  1208. min: 0,
  1209. },
  1210. },
  1211. {
  1212. id: 'III',
  1213. itemName: '在取做保职工总人数',
  1214. unit: '人',
  1215. isCategory: true,
  1216. categorySeq: 'III',
  1217. children: [
  1218. {
  1219. id: '3-1',
  1220. itemName: '行政管理人员数',
  1221. unit: '人',
  1222. isCategory: false,
  1223. categoryId: 'III',
  1224. seq: 3,
  1225. validateRules: {
  1226. required: true,
  1227. type: 'number',
  1228. min: 0,
  1229. },
  1230. linkageRules: {
  1231. parent: 'III',
  1232. relation: 'sum',
  1233. },
  1234. },
  1235. {
  1236. id: '3-2',
  1237. itemName: '教师人数',
  1238. unit: '人',
  1239. isCategory: false,
  1240. categoryId: 'III',
  1241. seq: 4,
  1242. validateRules: {
  1243. required: true,
  1244. type: 'number',
  1245. min: 0,
  1246. },
  1247. linkageRules: {
  1248. parent: 'III',
  1249. relation: 'sum',
  1250. },
  1251. },
  1252. {
  1253. id: '3-3',
  1254. itemName: '保育员人数',
  1255. unit: '人',
  1256. isCategory: false,
  1257. categoryId: 'III',
  1258. seq: 5,
  1259. validateRules: {
  1260. required: true,
  1261. type: 'number',
  1262. min: 0,
  1263. },
  1264. linkageRules: {
  1265. parent: 'III',
  1266. relation: 'sum',
  1267. },
  1268. },
  1269. {
  1270. id: '3-4',
  1271. itemName: '医务人员',
  1272. unit: '人',
  1273. isCategory: false,
  1274. categoryId: 'III',
  1275. seq: 6,
  1276. validateRules: {
  1277. required: true,
  1278. type: 'number',
  1279. min: 0,
  1280. },
  1281. linkageRules: {
  1282. parent: 'III',
  1283. relation: 'sum',
  1284. },
  1285. },
  1286. {
  1287. id: '3-5',
  1288. itemName: '工勤人员',
  1289. unit: '人',
  1290. isCategory: false,
  1291. categoryId: 'III',
  1292. seq: 7,
  1293. validateRules: {
  1294. required: true,
  1295. type: 'number',
  1296. min: 0,
  1297. },
  1298. linkageRules: {
  1299. parent: 'III',
  1300. relation: 'sum',
  1301. },
  1302. children: [
  1303. {
  1304. id: '3-5-1',
  1305. itemName: '炊事员',
  1306. unit: '人',
  1307. isCategory: false,
  1308. categoryId: '3-5',
  1309. seq: 8,
  1310. validateRules: {
  1311. required: true,
  1312. type: 'number',
  1313. min: 0,
  1314. },
  1315. linkageRules: {
  1316. parent: '3-5',
  1317. relation: 'sum',
  1318. },
  1319. },
  1320. {
  1321. id: '3-5-2',
  1322. itemName: '司机',
  1323. unit: '人',
  1324. isCategory: false,
  1325. categoryId: '3-5',
  1326. seq: 9,
  1327. validateRules: {
  1328. required: true,
  1329. type: 'number',
  1330. min: 0,
  1331. },
  1332. linkageRules: {
  1333. parent: '3-5',
  1334. relation: 'sum',
  1335. },
  1336. },
  1337. {
  1338. id: '3-5-3',
  1339. itemName: '清洁工',
  1340. unit: '人',
  1341. isCategory: false,
  1342. categoryId: '3-5',
  1343. seq: 10,
  1344. validateRules: {
  1345. required: true,
  1346. type: 'number',
  1347. min: 0,
  1348. },
  1349. linkageRules: {
  1350. parent: '3-5',
  1351. relation: 'sum',
  1352. },
  1353. },
  1354. ],
  1355. },
  1356. {
  1357. id: '3-6',
  1358. itemName: '其他人员',
  1359. unit: '人',
  1360. isCategory: false,
  1361. categoryId: 'III',
  1362. seq: 11,
  1363. validateRules: {
  1364. required: true,
  1365. type: 'number',
  1366. min: 0,
  1367. },
  1368. linkageRules: {
  1369. parent: 'III',
  1370. relation: 'sum',
  1371. },
  1372. },
  1373. ],
  1374. },
  1375. ]
  1376. },
  1377. resetDynamicDialogState() {
  1378. this.dynamicTableDialogVisible = false
  1379. this.dynamicTableData = []
  1380. this.tableItems = []
  1381. this.dynamicDialogKey = Date.now()
  1382. },
  1383. },
  1384. }
  1385. </script>