华西海圻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.

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