华西海圻ELN前端工程
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

1109 lines
36 KiB

  1. <template>
  2. <div>
  3. <div class="custom-table-wrapper" :class="{ 'no-border': !isBorder }">
  4. <div class="custom-table-header" v-if="isBorder">
  5. <div class="custom-table-row">
  6. <div v-if="showSort" class="custom-table-cell header-cell sort-cell">
  7. 序号
  8. </div>
  9. <div v-for="(col, index) in columns" :key="index" class="custom-table-cell header-cell"
  10. :style="getCellWidth(col)">
  11. <div class="header-cell-content">
  12. <div>{{ $t(col.label) }}</div>
  13. <template
  14. v-if="col.headerSelectKey && col.headerOptions && (showHeaderSelect || templateFillType === 'preFill')">
  15. <HandleFormItem :fieldKey="prefixKey + '_' + col.headerSelectKey"
  16. :fieldItemLabel="fieldItemLabel" type="select" class="header-select"
  17. :item="getHeaderItem(col)" v-model="headerSelectFields[col.headerSelectKey]"
  18. @change="onHeaderSelectChange(col, $event)"
  19. :error="hasError(-1, index, col.headerSelectKey)"
  20. @update:error="onErrorUpdate(-1, index, col.headerSelectKey, $event)" />
  21. </template>
  22. <div v-else-if="headerSelectFields[col.headerSelectKey]" class="fill-type-icon"
  23. :style="{ width: (templateFillType !== 'actFill') ? '60px' : 'auto' }">({{
  24. headerSelectFields[col.headerSelectKey] }})</div>
  25. </div>
  26. </div>
  27. <!-- 默认操作栏 -->
  28. <div class="custom-table-cell header-cell" :style="{ width: operationWidth }" v-if="showOperation">
  29. <div class="header-cell-content">
  30. <div>操作</div>
  31. </div>
  32. </div>
  33. </div>
  34. </div>
  35. <div class="custom-table-body">
  36. <div v-for="(row, rowIndex) in localDataSource" :key="rowIndex" class="custometable-row">
  37. <div v-if="showSort" class="custom-table-cell body-cell sort-cell">
  38. {{ rowIndex + 1 }}
  39. </div>
  40. <div v-for="(col, colIndex) in columns" :key="colIndex" class="custom-table-cell body-cell"
  41. :style="getCellWidth(col)">
  42. <div class="inner-table-cell">
  43. <div class="flex1" :class="{ 'item-center': !isBorder && col.label }">
  44. <div v-if="!isBorder && col.label" class="mr-5">
  45. {{ $t(col.label) }}
  46. </div>
  47. <template v-if="col.bodyType === 'input'">
  48. <HandleFormItem :fieldKey="prefixKey + '_' + col.prop + '_' + rowIndex"
  49. :fieldItemLabel="fieldItemLabel" type="input"
  50. @blur="onBlur(rowIndex, col.prop, $event)" @copy="onCopy(rowIndex, col)"
  51. class="body-input" :item="getBodyItem(col, rowIndex)" v-model="row[col.prop]"
  52. @change="onBodyValueChange(rowIndex, colIndex, $event)"
  53. :error="hasError(rowIndex, colIndex, col.prop)"
  54. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  55. @beforeSaveRecord="(data, callback) => beforeSaveRecord(data, callback, rowIndex, col, row)"
  56. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  57. </template>
  58. <template v-else-if="col.bodyType === 'inputNumber'">
  59. <HandleFormItem :fieldKey="prefixKey + '_' + col.prop + '_' + rowIndex"
  60. :fieldItemLabel="fieldItemLabel" type="inputNumber"
  61. @copy="onCopy(rowIndex, col)" class="body-input-number"
  62. :item="getBodyItem(col, rowIndex)" v-model="row[col.prop]"
  63. @blur="onBlur(rowIndex, col.prop, $event)"
  64. @change="onBodyValueChange(rowIndex, colIndex, $event)"
  65. :error="hasError(rowIndex, colIndex, col.prop)"
  66. @beforeSaveRecord="(data, callback) => beforeSaveRecord(data, callback, rowIndex, col, row)"
  67. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  68. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  69. </template>
  70. <template v-else-if="col.bodyType === 'select'">
  71. <div class="flex flex1">
  72. <HandleFormItem :fieldKey="prefixKey + '_' + col.prop + '_' + rowIndex"
  73. :fieldItemLabel="fieldItemLabel" type="select" class="body-select"
  74. @blur="onBlur(rowIndex, col.prop, $event)"
  75. :item="getBodyItem(col, rowIndex)" v-model="row[col.prop]"
  76. @change="onBodyValueChange(rowIndex, colIndex, $event, row, 'select')"
  77. :error="hasError(rowIndex, colIndex, col.prop)"
  78. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  79. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  80. </div>
  81. </template>
  82. <div class="flex flex1" v-else-if="col.bodyType === 'clickable'">
  83. <HandleFormItem :fieldKey="prefixKey + '_' + col.prop + '_' + rowIndex"
  84. :fieldItemLabel="fieldItemLabel" type="clickable" class="body-clickable"
  85. :item="getBodyItem(col, rowIndex)" :value="row[col.prop]"
  86. :error="hasError(rowIndex, colIndex, col.prop)"
  87. @clickable="handleClickable(col, rowIndex, colIndex, row)"
  88. @resetRecord="resetRecord(rowIndex, colIndex, col.prop)"
  89. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  90. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  91. </div>
  92. <div class="flex flex1" v-else-if="isRegent(col, 'bodyType')">
  93. <HandleFormItem :fieldKey="prefixKey + '_' + col.prop + '_' + rowIndex"
  94. :fieldItemLabel="fieldItemLabel" :type="col.bodyType" class="body-clickable"
  95. sourceFrom="customTable" :item="getBodyItem(col, rowIndex)"
  96. :value="row[col.prop]" :error="hasError(rowIndex, colIndex, col.prop)"
  97. @onRegentSubmit="(data, inputValue) => onRegentSubmit(data, inputValue, col, rowIndex, colIndex, row)"
  98. @beforeReagentSubmit="(data, callback) => onBeforeReagentSubmit(data, callback, col, row)"
  99. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  100. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  101. </div>
  102. <template v-else-if="col.bodyType === 'span'">
  103. <div class="body-span">
  104. {{ row[col.prop] }}
  105. </div>
  106. </template>
  107. <template v-else-if="col.bodyType === 'checkboxTag'">
  108. <div class="flex flex-wrap" :class="{ 'row-error-border': hasError(rowIndex, colIndex, col.prop) }">
  109. <HandleFormItem
  110. :fieldKey="prefixKey + '_' + col.prop + '_' + rowIndex"
  111. :fieldItemLabel="fieldItemLabel" type="checkboxTag"
  112. :value="row[col.prop]"
  113. :item="getBodyItem(col, rowIndex)"
  114. @change="onCheckboxTagChange(rowIndex, colIndex, col, $event)"
  115. @deleteTag="onDeleteCheckboxTag(rowIndex, col, $event)"
  116. :error="hasError(rowIndex, colIndex, col.prop)"
  117. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)" />
  118. </div>
  119. </template>
  120. </div>
  121. <div v-show="isShowOther(row[col.prop], col)" class="flex flex1">
  122. <div class="other-title">{{ col.otherLabel ? $t(col.otherLabel) :
  123. $t("template.common.other") }}
  124. </div>
  125. <div class="flex flex1">
  126. <HandleFormItem :field-item-label="fieldItemLabel"
  127. :field-key="prefixKey + '_' + col.otherCode"
  128. @blur="onBlur(rowIndex, col.prop, $event)" :item="getOtherItem(col)"
  129. v-model="row[col.otherCode]"
  130. :error="hasError(rowIndex, colIndex, col.otherCode)"
  131. @update:error="onErrorUpdate(rowIndex, colIndex, col.otherCode, $event)"
  132. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.otherCode)" />
  133. </div>
  134. </div>
  135. <div class="m-l-5" :class="{ 'flex1': col.bodySubType !== 'button' }"
  136. v-if="isShowBodySub(col, row)">
  137. <template v-if="col.bodySubType === 'inputNumber'">
  138. <HandleFormItem :fieldKey="prefixKey + '_' + col.bodySubKey + '_' + rowIndex"
  139. :fieldItemLabel="fieldItemLabel" type="inputNumber"
  140. @blur="onSubBlur(rowIndex, col.bodySubKey, $event)"
  141. @copy="onCopy(rowIndex, col)" :item="getBodySubItem(col)"
  142. v-model="row[col.bodySubKey]"
  143. @change="onBodySubValueChange(rowIndex, colIndex, $event,)"
  144. :error="hasError(rowIndex, colIndex, col.bodySubKey)"
  145. @update:error="onErrorUpdate(rowIndex, colIndex, col.bodySubKey, $event)"
  146. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.bodySubKey)" />
  147. </template>
  148. <template v-if="col.bodySubType === 'input'">
  149. <HandleFormItem :fieldKey="prefixKey + '_' + col.bodySubKey + '_' + rowIndex"
  150. :fieldItemLabel="fieldItemLabel" type="input"
  151. @blur="onSubBlur(rowIndex, col.bodySubKey, $event)"
  152. @copy="onCopy(rowIndex, col)" :item="getBodySubItem(col)"
  153. v-model="row[col.bodySubKey]"
  154. @change="onBodySubValueChange(rowIndex, colIndex, $event,)"
  155. :error="hasError(rowIndex, colIndex, col.bodySubKey)"
  156. @update:error="onErrorUpdate(rowIndex, colIndex, col.bodySubKey, $event)"
  157. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.bodySubKey)" />
  158. </template>
  159. <template v-else-if="col.bodySubType === 'select'">
  160. <HandleFormItem :fieldKey="prefixKey + '_' + col.bodySubKey + '_' + rowIndex"
  161. :fieldItemLabel="fieldItemLabel" type="select" class="body-select"
  162. @blur="onSubBlur(rowIndex, col.bodySubKey, $event)"
  163. :item="getBodySubItem(col, rowIndex)" v-model="row[col.bodySubKey]"
  164. @change="onBodySubValueChange(rowIndex, colIndex, $event, row, 'select')"
  165. :error="hasError(rowIndex, colIndex, col.bodySubKey)"
  166. @update:error="onErrorUpdate(rowIndex, colIndex, col.bodySubKey, $event)"
  167. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.bodySubKey)" />
  168. </template>
  169. <template v-else-if="col.bodySubType === 'span'">
  170. <div class="body-span">
  171. {{ row[col.bodySubKey] }}
  172. </div>
  173. </template>
  174. <template v-else-if="col.bodySubType === 'button'">
  175. <HandleFormItem class="ml-10" type="button" :item="getBodyButtonItem(col, rowIndex)"
  176. :value="row[col.bodySubKey]"
  177. @clickButton="(e, data) => handleClickButton(e, data, col.bodySubKey, rowIndex, colIndex)" />
  178. </template>
  179. </div>
  180. </div>
  181. </div>
  182. <!-- 默认操作栏 -->
  183. <div class="custom-table-cell body-cell" :style="{ width: isBorder ? operationWidth : 'auto' }"
  184. v-if="showOperation">
  185. <div class="inner-table-cell">
  186. <slot name="operation" :row="row" :rowIndex="rowIndex" :columns="getOperationColumns()">
  187. </slot>
  188. </div>
  189. </div>
  190. </div>
  191. </div>
  192. <div v-if="localDataSource.length == 0">
  193. <div class="no-data">暂无数据</div>
  194. </div>
  195. </div>
  196. <div class="add-row" v-if="isShowAddRos()">
  197. <el-button type="primary" plain @click="onAddRow">添加行</el-button>
  198. </div>
  199. </div>
  200. </template>
  201. <script>
  202. import HandleFormItem from "./HandleFormItem.vue";
  203. import { isEqual } from "@/utils/index.js";
  204. import { isShowOther } from "@/utils/formPackageCommon.js";
  205. import { EventBus } from "@/utils/eventBus";
  206. import { getuuid } from "@/utils/index.js";
  207. import { isRegent } from "@/utils/index.js";
  208. import { isValueEmpty } from '@/utils/index.js';
  209. import _ from "lodash";
  210. export default {
  211. inject: ['templateFillType', 'getZdxgjl', 'updateZdxgjl'],
  212. name: 'CustomTable',
  213. components: {
  214. HandleFormItem
  215. },
  216. props: {
  217. operationWidth: {
  218. type: String,
  219. default: '245px',
  220. },
  221. // 是否显示表头选择器
  222. showHeaderSelect: {
  223. type: Boolean,
  224. default: false,
  225. },
  226. showAddRow: {
  227. type: Boolean,
  228. default: undefined,
  229. },
  230. // 是否显示操作栏
  231. showOperation: {
  232. type: Boolean,
  233. default: true,
  234. },
  235. columns: {
  236. type: Array,
  237. required: true,
  238. // 示例格式:
  239. // [
  240. // { label: '姓名', prop: 'name' },
  241. // { label: '状态', prop: 'status', type: 'select', options: [{value:1,label:'启用'},...], selected: null }
  242. // ]
  243. },
  244. formData: {
  245. type: Object,
  246. default: () => {
  247. return {
  248. stepTableFormData: [],
  249. headerSelectFields: {}
  250. }
  251. }
  252. },
  253. fieldItemLabel: {
  254. type: String,
  255. default: '',
  256. },
  257. //循环组件的情况下需要用这个来区分字段
  258. prefixKey: {
  259. type: String,
  260. default: "",
  261. },
  262. isBorder: {//是否无边框,无边框的没有表头和border
  263. type: Boolean,
  264. default: true,
  265. },
  266. showSort: {
  267. type: Boolean,
  268. default: false,
  269. },
  270. },
  271. data() {
  272. return {
  273. localDataSource: [],
  274. headerSelectFields: {},
  275. formErrors: [], // 表单错误状态管理
  276. orangeBgCells: {}, // 存储需要橙色背景的单元格 {rowIndex-colIndex: true/false}
  277. isShowOther,
  278. oldLocalDataSource: [],
  279. uuid: getuuid(),
  280. isRegent,
  281. }
  282. },
  283. watch: {
  284. formData: {
  285. immediate: true,
  286. handler(newData) {
  287. const { stepTableFormData = [], headerSelectFields = {} } = newData;
  288. this.updateDataSource(stepTableFormData);
  289. this.headerSelectFields = JSON.parse(JSON.stringify(headerSelectFields));
  290. // 在数据加载后检查 compareTo 逻辑
  291. this.checkCompareToOnDataLoad();
  292. }
  293. },
  294. localDataSource: {
  295. immediate: true,
  296. deep: true,
  297. handler(newVal, oldVal) {
  298. // if(newVal.length == 0){
  299. // return
  300. // }
  301. // this.localDataSource = [...newVal];
  302. }
  303. }
  304. },
  305. mounted() {
  306. },
  307. unmounted() {
  308. this.oldLocalDataSource = [];
  309. },
  310. methods: {
  311. // 删除checkboxTag
  312. onDeleteCheckboxTag(rowIndex, col, tagIndex) {
  313. this.localDataSource[rowIndex][col.prop].splice(tagIndex, 1);
  314. this.$emit("onDeleteTag", rowIndex, col, tagIndex);
  315. this.justUpdateFilledFormData();
  316. },
  317. onCheckboxTagChange(rowIndex, colIndex, col, value) {
  318. console.log(value,"vvv")
  319. // value 现在是整个数组
  320. this.localDataSource[rowIndex][col.prop] = value;
  321. // 根据校验规则判断是否清除错误状态
  322. let isValid = false;
  323. if (this.templateFillType === "actFill") {
  324. // actFill时,检查是否有checked为true的项
  325. isValid = value && value.some(tag => tag.checked === true);
  326. } else if (this.templateFillType === "preFill") {
  327. // preFill时,检查所有tagValue是否不为空
  328. isValid = value && value.every(tag => tag.tagValue && tag.tagValue.trim() !== '');
  329. }
  330. this.onErrorUpdate(rowIndex, colIndex, col.prop, !isValid);
  331. this.$emit("onCheckboxTagChange", rowIndex, col, value)
  332. },
  333. handleClickButton(e, data, key, rowIndex, colIndex) {
  334. this.$emit("clickButton", key, rowIndex, colIndex, e, data,)
  335. },
  336. beforeSaveRecord(data, callback, rowIndex, col, row) {
  337. this.$emit("beforeSaveRecord", { inputData: data, callback, rowIndex, key: col.prop, rowData: row, dataSource: this.localDataSource })
  338. },
  339. getCellWidth(col) {
  340. const { templateFillType } = this;
  341. let width = col.width ? col.width + 'px' : 'auto';
  342. if (templateFillType !== "actFill" && templateFillType !== "preFill") {
  343. width = (col.showWidth) ? col.showWidth + 'px' : (col.width ? col.width + 'px' : 'auto')
  344. }
  345. return { width }
  346. },
  347. //取消按钮 重置记录
  348. resetRecord(rowIndex, colIndex,) {
  349. if (this.localDataSource.length) {
  350. this.localDataSource = [...this.oldLocalDataSource];
  351. this.oldLocalDataSource = [];
  352. }
  353. },
  354. //获取操作栏的列
  355. getOperationColumns() {
  356. return { columnsData: this.columns, headerSelectFields: this.headerSelectFields }
  357. },
  358. //获取其他下拉框的配置
  359. getOtherItem(sItem) {
  360. return {
  361. label: sItem.otherLabel ? this.$t(sItem.otherLabel) : this.$t("template.common.other"),
  362. fillType: sItem.bodyFillType,
  363. maxlength: sItem.otherMaxlength || 50,
  364. parentLabel: sItem.label,
  365. type: "input"
  366. }
  367. },
  368. isShowBodySub(col, row) {
  369. if (col.hasOwnProperty("showBodySub")) {
  370. return col.showBodySub
  371. } else if (col.bodySubType === 'span' && !row[col.bodySubKey]) {//如果是span没有值的话就隐藏
  372. return false;
  373. }
  374. return col.bodySubType && col.bodySubKey;
  375. },
  376. // 点击事件
  377. handleClickable(col, rowIndex, colIndex, row) {
  378. console.log("clickable", rowIndex, colIndex, col, row)
  379. if (this.templateFillType !== 'actFill') {
  380. return
  381. }
  382. this.$emit("clickable", col, rowIndex, row)
  383. },
  384. onBeforeReagentSubmit(data, callback, col, row) {
  385. if (this.templateFillType !== 'actFill') {
  386. return
  387. }
  388. this.$emit("beforeReagentSubmit", { selectData: data, callback, key: col.prop, rowData: row })
  389. },
  390. onRegentSubmit(data, inputValue, col, rowIndex, colIndex, row) {
  391. if (this.templateFillType !== 'actFill') {
  392. return
  393. }
  394. this.updateDataSourceByRowIndex(rowIndex, { [col.prop]: inputValue })
  395. this.$emit("onRegentSubmit", { selectInfo: data, key: col.prop, col, rowIndex, colIndex, rowData: row })
  396. },
  397. isShowAddRos() {
  398. if (this.showAddRow !== undefined) {
  399. return this.showAddRow
  400. }
  401. return this.templateFillType === 'preFill';
  402. },
  403. // 复制值
  404. onCopy(rowIndex, col) {
  405. if (col.copyFrom) {
  406. if (isValueEmpty(this.localDataSource[rowIndex][col.copyFrom])) {//没有值就不用复制了
  407. return
  408. }
  409. this.updateDataSourceByRowIndex(rowIndex, { [col.prop]: this.localDataSource[rowIndex][col.copyFrom] }, "clickable")
  410. this.onBlur(rowIndex, col.prop, this.localDataSource[rowIndex][col.prop]);
  411. }
  412. },
  413. // 初始化表头选择器值
  414. initHeaderSelectValues() {
  415. const headerSelectObj = {};
  416. this.columns.map(col => {
  417. if (col.headerSelectKey) {
  418. headerSelectObj[col.headerSelectKey] = col.defaultValue || col.headerOptions[0].value || ""
  419. }
  420. });
  421. this.headerSelectFields = headerSelectObj;
  422. },
  423. // 直接获取表单数据,不做校验
  424. getFilledFormData() {
  425. return {
  426. stepTableFormData: [...this.localDataSource],
  427. headerSelectFields: this.headerSelectFields,
  428. };
  429. },
  430. // 获取最新数据
  431. getFormData() {
  432. // 合并表头选择器值到 columns
  433. // 数据校验
  434. const validateResult = this.validateFormData();
  435. return new Promise((resolve, reject) => {
  436. if (validateResult.valid) {
  437. resolve({
  438. stepTableFormData: [...this.localDataSource],
  439. headerSelectFields: this.headerSelectFields,
  440. })
  441. } else {
  442. // this.$message.error("表单内容未填完,请填写后再提交");
  443. reject(validateResult.errors[0].error)
  444. }
  445. })
  446. },
  447. // 表单数据校验
  448. validateFormData() {
  449. const errors = [];
  450. // 清空之前的错误状态
  451. this.formErrors = [];
  452. // 校验表头的 HandleFormItem
  453. this.columns.forEach((col, colIndex) => {
  454. if (col.headerSelectKey && col.headerOptions && col.fillType === this.templateFillType) {
  455. const headerValue = this.headerSelectFields[col.headerSelectKey];
  456. if (isValueEmpty(headerValue)) {
  457. const errorItem = {
  458. rowIndex: -1, // 表头特殊标记
  459. colIndex,
  460. field: col.headerSelectKey,
  461. label: this.$t(col.label),
  462. error: `请选择${this.$t(col.label)}`
  463. };
  464. errors.push(errorItem);
  465. this.formErrors.push(errorItem);
  466. }
  467. }
  468. });
  469. // 遍历数据行
  470. this.localDataSource.forEach((row, rowIndex) => {
  471. // 遍历列
  472. this.columns.forEach((col, colIndex) => {
  473. // 只校验 fillType 与当前模板状态匹配的字段
  474. if (col.bodyFillType === this.templateFillType || col.bodySubFillType === this.templateFillType) {
  475. // 检查主字段
  476. const mainValue = row[col.prop];
  477. if (col.bodyType === "checkboxTag") {
  478. // checkboxTag类型的校验逻辑
  479. if (this.templateFillType === "actFill") {
  480. // actFill时,检查是否有checked为true的项
  481. const hasChecked = mainValue && mainValue.some(tag => tag.checked === true);
  482. if (!hasChecked && !col.bodyDisabled) {
  483. const errorItem = {
  484. rowIndex,
  485. colIndex,
  486. field: col.prop,
  487. label: this.$t(col.label),
  488. error: `请勾选${this.$t(col.label)}`
  489. };
  490. errors.push(errorItem);
  491. this.formErrors.push(errorItem);
  492. }
  493. } else if (this.templateFillType === "preFill") {
  494. // preFill时,检查所有tagValue是否不为空
  495. const allTagValuesFilled = mainValue && mainValue.every(tag => tag.tagValue && tag.tagValue.trim() !== '');
  496. if (!allTagValuesFilled && !col.bodyDisabled) {
  497. const errorItem = {
  498. rowIndex,
  499. colIndex,
  500. field: col.prop,
  501. label: this.$t(col.label),
  502. error: `请填写${this.$t(col.label)}`
  503. };
  504. errors.push(errorItem);
  505. this.formErrors.push(errorItem);
  506. }
  507. }
  508. } else {
  509. if (isValueEmpty(mainValue) && !col.bodyDisabled && col.bodyType !== 'span' && col.bodyType !== 'button') {
  510. const errorItem = {
  511. rowIndex,
  512. colIndex,
  513. field: col.prop,
  514. label: this.$t(col.label),
  515. error: `请填写${this.$t(col.label)}`
  516. };
  517. errors.push(errorItem);
  518. this.formErrors.push(errorItem);
  519. }
  520. // 检查子字段(如果有)
  521. if (col.bodySubKey && !col.bodySubDisabled && col.bodySubType !== 'span' && col.bodySubType !== "button") {
  522. const subValue = row[col.bodySubKey];
  523. console.log(col, subValue, "subValue")
  524. if (isValueEmpty(subValue)) {
  525. const errorItem = {
  526. rowIndex,
  527. colIndex,
  528. field: col.bodySubKey,
  529. label: `${this.$t(col.label)}单位`,
  530. error: `请填写${this.$t(col.label)}单位`
  531. };
  532. errors.push(errorItem);
  533. this.formErrors.push(errorItem);
  534. }
  535. }
  536. }
  537. console.log(col.otherCode, "col.otherCode")
  538. // 检查其他输入框
  539. if (col.otherCode) {
  540. const isSelectedOther = this.isShowOther(mainValue);
  541. if (!isSelectedOther) {
  542. return;
  543. }
  544. const otherValue = row[col.otherCode];
  545. if (isValueEmpty(otherValue)) {
  546. const errorItem = {
  547. rowIndex,
  548. colIndex,
  549. field: col.otherCode,
  550. label: `${this.$t(col.label)}单位`,
  551. error: `请填写${this.$t(col.otherLabel) ? this.$t(col.otherLabel) : '其他'}信息`
  552. };
  553. errors.push(errorItem);
  554. this.formErrors.push(errorItem);
  555. }
  556. }
  557. }
  558. });
  559. });
  560. console.log(errors, this.localDataSource, "errors")
  561. return {
  562. valid: errors.length === 0,
  563. errors: errors
  564. };
  565. },
  566. // 判断值是否为空
  567. isValueEmpty(value) {
  568. if (value === null || value === undefined || value === '') {
  569. return true;
  570. }
  571. if (typeof value === 'string' && value.trim() === '') {
  572. return true;
  573. }
  574. if (Array.isArray(value) && value.length === 0) {
  575. return true;
  576. }
  577. return false;
  578. },
  579. // 表头选择器变化
  580. onHeaderSelectChange(col, value) {
  581. if (col.headerSelectTo) {
  582. this.headerSelectFields[col.headerSelectTo] = value;
  583. }
  584. this.headerSelectFields[col.headerSelectKey] = value;
  585. this.$emit('headerSelectChange', { key: col.headerSelectKey, headerSelectFields: this.headerSelectFields, dataSource: this.localDataSource });
  586. // 输入时清除对应表单项的错误状态
  587. this.formErrors = this.formErrors.filter(error =>
  588. !(error.rowIndex === -1 &&
  589. error.field === col.headerSelectKey)
  590. );
  591. },
  592. // 检查并应用 compareTo 逻辑
  593. checkCompareToLogic(rowIndex, colIndex, colKey, value) {
  594. const col = this.columns[colIndex];
  595. // 检查主字段的 compareTo 逻辑
  596. if (col && col.bodyFillType === "actFill" && col.compareTo) {
  597. const compareToValue = this.localDataSource[rowIndex][col.compareTo];
  598. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  599. if (value !== compareToValue) {
  600. this.setOrangeBg(rowIndex, colIndex, colKey, true);
  601. } else {
  602. // 相等则移除橙色背景
  603. this.setOrangeBg(rowIndex, colIndex, colKey, false);
  604. }
  605. }
  606. },
  607. // 在数据加载时检查 compareTo 逻辑
  608. checkCompareToOnDataLoad() {
  609. // 遍历所有行和列,检查 compareTo 逻辑
  610. this.localDataSource.forEach((row, rowIndex) => {
  611. this.columns.forEach((col, colIndex) => {
  612. const currentValue = row[col.prop];
  613. const compareToValue = row[col.compareTo];
  614. if (col.compareTo && !isValueEmpty(currentValue) && !isValueEmpty(compareToValue)) {
  615. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  616. if (!isEqual(currentValue, compareToValue)) {
  617. this.setOrangeBg(rowIndex, colIndex, col.prop, true);
  618. } else {
  619. // 相等则移除橙色背景
  620. this.setOrangeBg(rowIndex, colIndex, col.prop, false);
  621. }
  622. }
  623. // 检查子字段的 compareTo 逻辑
  624. if (col.bodySubFillType === "actFill" && col.bodySubCompareTo) {
  625. const currentValue = row[col.bodySubKey];
  626. const compareToValue = row[col.bodySubCompareTo];
  627. if (!isValueEmpty(currentValue) && !isValueEmpty(compareToValue)) {
  628. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  629. if (!isEqual(currentValue, compareToValue)) {
  630. this.setOrangeBg(rowIndex, colIndex, col.bodySubKey, true);
  631. } else {
  632. // 相等则移除橙色背景
  633. this.setOrangeBg(rowIndex, colIndex, col.bodySubKey, false);
  634. }
  635. }
  636. }
  637. });
  638. });
  639. },
  640. // 表体值变化
  641. onBodyValueChange(rowIndex, colIndex, value, row, type) {
  642. const col = this.columns[colIndex];
  643. this.localDataSource[rowIndex][col.prop] = value;
  644. // 检查并应用 compareTo 逻辑
  645. this.checkCompareToLogic(rowIndex, colIndex, col.prop, value);
  646. // 输入时清除对应表单项的错误状态
  647. this.formErrors = this.formErrors.filter(error =>
  648. !(error.rowIndex === rowIndex &&
  649. error.colIndex === colIndex &&
  650. error.field === col.prop)
  651. );
  652. if (type === "select") {
  653. this.$emit('bodySelectChange', { rowIndex, item: row, colIndex, value, key: col.prop, dataSource: this.localDataSource, headerSelectFields: this.headerSelectFields });
  654. }
  655. },
  656. // 表体子值变化
  657. onBodySubValueChange(rowIndex, colIndex, value, row, type) {
  658. const col = this.columns[colIndex];
  659. this.localDataSource[rowIndex][col.bodySubKey] = value;
  660. // 检查子字段的 compareTo 逻辑
  661. if (col && col.bodySubFillType === "actFill" && col.bodySubCompareTo) {
  662. const compareToValue = this.localDataSource[rowIndex][col.bodySubCompareTo];
  663. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  664. if (value !== compareToValue) {
  665. this.setOrangeBg(rowIndex, colIndex, col.bodySubKey, true);
  666. } else {
  667. // 相等则移除橙色背景
  668. this.setOrangeBg(rowIndex, colIndex, col.bodySubKey, false);
  669. }
  670. }
  671. // 输入时清除对应表单项的错误状态
  672. this.formErrors = this.formErrors.filter(error =>
  673. !(error.rowIndex === rowIndex &&
  674. error.colIndex === colIndex &&
  675. error.field === col.bodySubKey)
  676. );
  677. if (type === "select") {
  678. this.$emit('bodySelectChange', { rowIndex, item: row, colIndex, value, key: col.bodySubKey, dataSource: this.localDataSource, headerSelectFields: this.headerSelectFields });
  679. }
  680. },
  681. getHeaderItem(col) {
  682. return {
  683. fillType: col.fillType,
  684. options: col.headerOptions,
  685. label: ""
  686. }
  687. },
  688. getBodyItem(col, rowIndex) {
  689. const currentItem = this.localDataSource[rowIndex];
  690. const item = {
  691. fillType: col.bodyFillType,
  692. options: col.bodyOptions,
  693. maxlength: col.bodyMaxlength,
  694. label: this.$t(col.label),
  695. precision: currentItem[col.bodyPrecisionKey] || col.precision,
  696. copyFrom: col.copyFrom || "",
  697. compareTo: col.compareTo, // 添加 compareTo 字段
  698. type: col.bodyType || "input",
  699. filledCodes: col.filledCodes,
  700. };
  701. if (col.bodyDisabled) {
  702. item.disabled = col.bodyDisabled;
  703. }
  704. return item
  705. },
  706. getBodyButtonItem(col,) {
  707. return {
  708. buttonName: col.bodySubButtonName,
  709. fillType: col.bodySubFillType,
  710. type: "button",
  711. }
  712. },
  713. getBodySubItem(col) {
  714. const item = {
  715. fillType: col.bodySubFillType,
  716. options: col.bodySubOptions,
  717. maxlength: col.bodySubMaxlength || 10,
  718. label: "",
  719. placeholder: col.bodySubPlaceholder || (col.bodySubType === 'select' ? '请选择' : '请输入'),
  720. precision: col.subPrecision,
  721. compareTo: col.bodySubCompareTo, // 添加 compareTo 字段
  722. type: col.bodySubType || "input",
  723. }
  724. if (col.bodySubDisabled) {
  725. item.disabled = col.bodySubDisabled;
  726. }
  727. return item
  728. },
  729. // 删除行
  730. deleteRow(rowIndex) {
  731. this.localDataSource.splice(rowIndex, 1);
  732. this.$emit('row-delete', rowIndex);
  733. },
  734. // 更新数据方法,可在formData变更时调用,也可由父组件调用
  735. updateDataSource(dataSource = []) {
  736. this.oldLocalDataSource = JSON.parse(JSON.stringify(this.localDataSource));
  737. // 深拷贝数据以避免直接修改原始数据
  738. this.localDataSource = JSON.parse(JSON.stringify(dataSource || []));
  739. this.checkCompareToOnDataLoad();
  740. },
  741. // 根据行索引更新数据 autoUpdateRecord 是否自动更新记录
  742. updateDataSourceByRowIndex(rowIndex, data) {
  743. this.oldLocalDataSource = JSON.parse(JSON.stringify(this.localDataSource));
  744. this.localDataSource[rowIndex] = { ...this.localDataSource[rowIndex], ...data };
  745. this.localDataSource = [...this.localDataSource];
  746. this.checkCompareToOnDataLoad();
  747. this.justUpdateFilledFormData();
  748. },
  749. // 比较newData和oldData的值是否相等,只要有一对不相等就返回false
  750. compareOldAndCurrentFormFields(newData, oldData) {
  751. for (const key in newData) {
  752. const oldValue = newData[key];
  753. const currentValue = oldData[key];
  754. if (JSON.stringify(oldValue) !== JSON.stringify(currentValue)) {
  755. return false;
  756. } else {
  757. return false;
  758. }
  759. }
  760. return true;
  761. },
  762. // 只是更新已填写的表单数据,不触发校验
  763. justUpdateFilledFormData(){
  764. const params = {
  765. type: "fieldChanged",
  766. newRecord: null,
  767. resourceList: null,
  768. }
  769. EventBus.$emit('onModifyRecord', params,)
  770. },
  771. onAddRow() {
  772. if (this.$listeners && this.$listeners['onAddRow']) {
  773. this.$emit('onAddRow');
  774. return;
  775. }
  776. this.addRow({
  777. actSolutionVolumePrecision: 3,//小数点精度默认为3
  778. actSolutionConcentrationPrecision: 3,//小数点精度默认为3
  779. targetDiluentVolumePrecision: 3,//小数点精度默认为3
  780. targetStartSolutionVolumePrecision: 3,//小数点精度默认为3
  781. });
  782. this.justUpdateFilledFormData()
  783. },
  784. // 添加行
  785. addRow(row = {}) {
  786. this.localDataSource.push(row);
  787. },
  788. addRows(rows = []) {
  789. this.localDataSource.push(...rows);
  790. },
  791. getDataSource() {
  792. return this.localDataSource;
  793. },
  794. // 判断表单项是否有错误
  795. hasError(rowIndex, colIndex, field) {
  796. return this.formErrors.some(error =>
  797. error.rowIndex === rowIndex &&
  798. error.colIndex === colIndex &&
  799. error.field === field
  800. );
  801. },
  802. // 处理错误状态更新
  803. onErrorUpdate(rowIndex, colIndex, field, isError) {
  804. if (!isError) {
  805. this.formErrors = this.formErrors.filter(error =>
  806. !(error.rowIndex === rowIndex &&
  807. error.colIndex === colIndex &&
  808. error.field === field)
  809. );
  810. }
  811. },
  812. // onSubBlur(rowIndex, colKey, value) {
  813. // this.$emit("blur", { rowIndex, colKey, value, item: this.localDataSource[rowIndex] });
  814. // },
  815. // 检查是否需要橙色背景
  816. hasOrangeBg(rowIndex, colIndex, field) {
  817. const key = `${rowIndex}-${colIndex}-${field}`;
  818. return this.orangeBgCells[key] || false;
  819. },
  820. // 设置橙色背景状态
  821. setOrangeBg(rowIndex, colIndex, field, status) {
  822. const key = `${rowIndex}-${colIndex}-${field}`;
  823. this.$set(this.orangeBgCells, key, status);
  824. },
  825. onBlur(rowIndex, colKey) {
  826. const value = this.localDataSource[rowIndex][colKey];
  827. // 查找对应的列配置
  828. const col = this.columns.find(c => c.prop === colKey);
  829. if (col && col.bodyFillType === "actFill" && col.compareTo) {
  830. const compareToValue = this.localDataSource[rowIndex][col.compareTo];
  831. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  832. if (value !== compareToValue) {
  833. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.prop === colKey), colKey, true);
  834. } else {
  835. // 相等则移除橙色背景
  836. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.prop === colKey), colKey, false);
  837. }
  838. }
  839. this.$emit("blur", { rowIndex, colKey, value, dataSource: this.localDataSource, headerSelectFields: this.headerSelectFields, item: this.localDataSource[rowIndex] });
  840. },
  841. onSubBlur(rowIndex, colKey, value) {
  842. // 查找对应的列配置
  843. const col = this.columns.find(c => c.bodySubKey === colKey);
  844. if (col && col.bodySubFillType === "actFill" && col.bodySubCompareTo) {
  845. const compareToValue = this.localDataSource[rowIndex][col.bodySubCompareTo];
  846. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  847. if (value !== compareToValue) {
  848. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.bodySubKey === colKey), colKey, true);
  849. } else {
  850. // 相等则移除橙色背景
  851. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.bodySubKey === colKey), colKey, false);
  852. }
  853. }
  854. this.$emit("blur", { rowIndex, colKey, value, item: this.localDataSource[rowIndex] });
  855. }
  856. }
  857. };
  858. </script>
  859. <style scoped>
  860. .custom-table-wrapper {
  861. border: 1px solid #ebeef5;
  862. border-radius: 4px;
  863. overflow: hidden;
  864. font-size: 14px;
  865. color: #606266;
  866. margin-top: 20px;
  867. &.no-border {
  868. box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
  869. border-radius: 5px 5px;
  870. border: none;
  871. .custom-table-cell {
  872. border-right: none;
  873. }
  874. .custometable-row {
  875. display: flex;
  876. border-bottom: none;
  877. }
  878. }
  879. }
  880. .inner-table-cell {
  881. display: flex;
  882. align-items: center;
  883. justify-content: center;
  884. }
  885. .m-l-5 {
  886. margin-left: 5px;
  887. }
  888. .sub-input-number {
  889. width: 145px;
  890. .el-input-number--mini {
  891. width: 145px;
  892. }
  893. }
  894. /* 表头 */
  895. .custom-table-header {
  896. background-color: #f5f7fa;
  897. border-bottom: 1px solid #ebeef5;
  898. white-space: nowrap;
  899. display: block;
  900. }
  901. .custom-table-body {
  902. max-height: 500px;
  903. overflow-y: auto;
  904. /* 可根据需要调整或由父组件控制 */
  905. }
  906. .header-cell-content {
  907. display: flex;
  908. align-items: center;
  909. justify-content: center;
  910. }
  911. /* 共同行样式 */
  912. .custom-table-row {
  913. display: table;
  914. width: 100%;
  915. table-layout: fixed;
  916. }
  917. .custometable-row {
  918. display: table;
  919. width: 100%;
  920. table-layout: fixed;
  921. &:not(:last-child) {
  922. border-bottom: 1px solid #ebeef5;
  923. }
  924. }
  925. /* 单元格 */
  926. .custom-table-cell {
  927. display: table-cell;
  928. padding: 12px 10px;
  929. text-align: left;
  930. vertical-align: middle;
  931. border-right: 1px solid #ebeef5;
  932. page-break-inside: avoid;
  933. box-sizing: border-box;
  934. }
  935. .custom-table-cell:last-child {
  936. border-right: none;
  937. }
  938. .header-cell {
  939. color: #909399;
  940. background-color: #f5f7fa;
  941. font-size: 12px;
  942. word-break: break-word;
  943. white-space: normal;
  944. }
  945. .body-cell {
  946. color: #606266;
  947. page-break-inside: avoid;
  948. background-color: #fff;
  949. }
  950. /* select 样式(模仿 Element UI) */
  951. .header-cell select {
  952. width: 100%;
  953. padding: 4px 8px;
  954. border: 1px solid #dcdfe6;
  955. border-radius: 4px;
  956. outline: none;
  957. background-color: #fff;
  958. font-size: 13px;
  959. color: #606266;
  960. appearance: none;
  961. /* 隐藏默认箭头(可选) */
  962. background-image: url("data:image/svg+xml;charset=UTF-8,%3csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='currentColor' stroke-width='2' stroke-linecap='round' stroke-linejoin='round'%3e%3cpolyline points='6,9 12,15 18,9'%3e%3c/polyline%3e%3c/svg%3e");
  963. background-repeat: no-repeat;
  964. background-position: right 8px center;
  965. background-size: 14px;
  966. padding-right: 28px;
  967. }
  968. /* 滚动容器:如果整体宽度超限,显示横向滚动条 */
  969. .custom-table-wrapper {
  970. display: flex;
  971. flex-direction: column;
  972. max-width: 100%;
  973. /* 父容器决定宽度 */
  974. overflow-x: auto;
  975. }
  976. .custom-table-header,
  977. .custom-table-body {
  978. min-width: 100%;
  979. }
  980. .header-select {
  981. width: 100px;
  982. margin-left: 5px;
  983. }
  984. .no-data {
  985. text-align: center;
  986. padding: 20px 0;
  987. color: rgb(144, 147, 153)
  988. }
  989. .add-row {
  990. display: flex;
  991. justify-content: center;
  992. padding: 20px 0;
  993. margin-top: 20px;
  994. }
  995. .flex1 {
  996. flex: 1;
  997. }
  998. .flex {
  999. display: flex;
  1000. }
  1001. .other-title {
  1002. text-align: right;
  1003. margin: 0 10px;
  1004. font-size: 14px;
  1005. font-weight: normal;
  1006. color: #606266;
  1007. width: auto;
  1008. }
  1009. .body-span {
  1010. text-align: center;
  1011. }
  1012. .item-center {
  1013. display: flex;
  1014. align-items: center;
  1015. }
  1016. .mr-5 {
  1017. margin-right: 5px;
  1018. }
  1019. .sort-cell {
  1020. text-align: center;
  1021. width: 100px;
  1022. }
  1023. .flex-wrap {
  1024. flex-wrap: wrap;
  1025. gap:10px;
  1026. }
  1027. .row-error-border {
  1028. box-shadow: 0 0 6px #ffc3c3;
  1029. padding: 8px;
  1030. border-radius: 4px;
  1031. border: 1px solid #ff5d5d;
  1032. }
  1033. </style>