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

633 lines
21 KiB

  1. <template>
  2. <div>
  3. <div class="custom-table-wrapper">
  4. <div class="custom-table-header">
  5. <div class="custom-table-row">
  6. <div v-for="(col, index) in columns" :key="index" class="custom-table-cell header-cell"
  7. :style="{ width: col.width ? col.width + 'px' : 'auto' }">
  8. <div class="header-cell-content">
  9. <div>{{ $t(col.label) }}</div>
  10. <template
  11. v-if="col.headerSelectKey && col.headerOptions && (showHeaderSelect || templateFillType === 'preFill')">
  12. <HandleFormItem :fieldKey="prefixKey+'_'+col.headerSelectKey" :fieldItemLabel="fieldItemLabel" type="select" class="header-select" :item="getHeaderItem(col)"
  13. v-model="headerSelectFields[col.headerSelectKey]" @change="onHeaderSelectChange(col, $event)"
  14. :error="hasError(-1, index, col.headerSelectKey)"
  15. @update:error="onErrorUpdate(-1, index, col.headerSelectKey, $event)" />
  16. </template>
  17. <span v-else-if="headerSelectFields[col.headerSelectKey]" class="fill-type-icon">({{
  18. headerSelectFields[col.headerSelectKey] }})</span>
  19. </div>
  20. </div>
  21. <!-- 默认操作栏 -->
  22. <div class="custom-table-cell header-cell" :style="{ width: '245px' }" v-if="showOperation">
  23. <div class="header-cell-content">
  24. <div>操作</div>
  25. </div>
  26. </div>
  27. </div>
  28. </div>
  29. <div class="custom-table-body">
  30. <div v-for="(row, rowIndex) in localDataSource" :key="rowIndex" class="custometable-row">
  31. <div v-for="(col, colIndex) in columns" :key="colIndex" class="custom-table-cell body-cell"
  32. :style="{ width: col.width ? col.width + 'px' : 'auto' }">
  33. <div class="inner-table-cell">
  34. <div>
  35. <template v-if="col.bodyType === 'input'">
  36. <HandleFormItem :fieldKey="prefixKey+'_'+col.prop+'_'+rowIndex" :fieldItemLabel="fieldItemLabel" type="input" @blur="onBlur(rowIndex, col.prop, $event)" @copy="onCopy(rowIndex, col)"
  37. class="body-input" :item="getBodyItem(col, rowIndex)" v-model="row[col.prop]"
  38. @change="onBodyValueChange(rowIndex, colIndex, $event)"
  39. :error="hasError(rowIndex, colIndex, col.prop)"
  40. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  41. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  42. </template>
  43. <template v-else-if="col.bodyType === 'inputNumber'">
  44. <HandleFormItem :fieldKey="prefixKey+'_'+col.prop+'_'+rowIndex" :fieldItemLabel="fieldItemLabel" type="inputNumber" @copy="onCopy(rowIndex, col)" class="body-input-number"
  45. :item="getBodyItem(col, rowIndex)" v-model="row[col.prop]" @blur="onBlur(rowIndex, col.prop, $event)"
  46. @change="onBodyValueChange(rowIndex, colIndex, $event)"
  47. :error="hasError(rowIndex, colIndex, col.prop)"
  48. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  49. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  50. </template>
  51. <template v-else-if="col.bodyType === 'select'">
  52. <HandleFormItem :fieldKey="prefixKey+'_'+col.prop+'_'+rowIndex" :fieldItemLabel="fieldItemLabel" type="select" class="body-select" @blur="onBlur(rowIndex, col.prop, $event)"
  53. :item="getBodyItem(col, rowIndex)" v-model="row[col.prop]"
  54. @change="onBodyValueChange(rowIndex, colIndex, $event)"
  55. :error="hasError(rowIndex, colIndex, col.prop)"
  56. @update:error="onErrorUpdate(rowIndex, colIndex, col.prop, $event)"
  57. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.prop)" />
  58. </template>
  59. <template v-else>
  60. {{ row[col.prop] }}
  61. </template>
  62. </div>
  63. <div class="m-l-5" v-if="col.showBodySub">
  64. <template v-if="col.bodySubType === 'inputNumber'">
  65. <HandleFormItem :fieldKey="prefixKey+'_'+col.bodySubKey+'_'+rowIndex" :fieldItemLabel="fieldItemLabel" type="inputNumber" @blur="onSubBlur(rowIndex, col.bodySubKey, $event)"
  66. @copy="onCopy(rowIndex, col)" :item="getBodySubItem(col)" v-model="row[col.bodySubKey]"
  67. @change="onBodySubValueChange(rowIndex, colIndex, $event)"
  68. :error="hasError(rowIndex, colIndex, col.bodySubKey)"
  69. @update:error="onErrorUpdate(rowIndex, colIndex, col.bodySubKey, $event)"
  70. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.bodySubKey)" />
  71. </template>
  72. <template v-else-if="col.bodySubType === 'select'">
  73. <HandleFormItem :fieldKey="prefixKey+'_'+col.bodySubKey+'_'+rowIndex" :fieldItemLabel="fieldItemLabel" type="select" class="body-select" @blur="onSubBlur(rowIndex, col.bodySubKey, $event)"
  74. :item="getBodySubItem(col, rowIndex)" v-model="row[col.bodySubKey]"
  75. @change="onBodySubValueChange(rowIndex, colIndex, $event)"
  76. :error="hasError(rowIndex, colIndex, col.bodySubKey)"
  77. @update:error="onErrorUpdate(rowIndex, colIndex, col.bodySubKey, $event)"
  78. :orange-bg="hasOrangeBg(rowIndex, colIndex, col.bodySubKey)" />
  79. </template>
  80. <template v-else>
  81. {{ row[col.bodySubKey] }}
  82. </template>
  83. </div>
  84. </div>
  85. </div>
  86. <!-- 默认操作栏 -->
  87. <div class="custom-table-cell body-cell" :style="{ width: '245px' }" v-if="showOperation">
  88. <div class="inner-table-cell">
  89. <slot name="operation" :row="row" :rowIndex="rowIndex"></slot>
  90. </div>
  91. </div>
  92. </div>
  93. </div>
  94. <div v-if="localDataSource.length == 0">
  95. <div class="no-data">暂无数据</div>
  96. </div>
  97. </div>
  98. <div class="add-row" v-if="isShowAddRos()">
  99. <el-button type="primary" plain @click="onAddRow">添加行</el-button>
  100. </div>
  101. </div>
  102. </template>
  103. <script>
  104. import HandleFormItem from "./HandleFormItem.vue"
  105. export default {
  106. inject: ['templateFillType'],
  107. name: 'CustomTable',
  108. components: {
  109. HandleFormItem
  110. },
  111. props: {
  112. // 是否显示表头选择器
  113. showHeaderSelect: {
  114. type: Boolean,
  115. default: false,
  116. },
  117. showAddRow: {
  118. type: Boolean,
  119. default: true,
  120. },
  121. // 是否显示操作栏
  122. showOperation: {
  123. type: Boolean,
  124. default: true,
  125. },
  126. columns: {
  127. type: Array,
  128. required: true,
  129. // 示例格式:
  130. // [
  131. // { label: '姓名', prop: 'name' },
  132. // { label: '状态', prop: 'status', type: 'select', options: [{value:1,label:'启用'},...], selected: null }
  133. // ]
  134. },
  135. formData: {
  136. type: Object,
  137. default: () => {
  138. return {
  139. stepTableFormData: [],
  140. headerSelectFields: {}
  141. }
  142. }
  143. },
  144. fieldItemLabel: {
  145. type: String,
  146. default: '',
  147. },
  148. //循环组件的情况下需要用这个来区分字段
  149. prefixKey:{
  150. type: String,
  151. default: "",
  152. }
  153. },
  154. data() {
  155. return {
  156. localDataSource: [],
  157. headerSelectFields: {},
  158. formErrors: [], // 表单错误状态管理
  159. orangeBgCells: {} // 存储需要橙色背景的单元格 {rowIndex-colIndex: true/false}
  160. }
  161. },
  162. watch: {
  163. formData: {
  164. immediate: true,
  165. handler(newData) {
  166. console.log(newData, "newData")
  167. const { stepTableFormData = [], headerSelectFields = {} } = newData;
  168. this.updateDataSource(stepTableFormData);
  169. this.headerSelectFields = JSON.parse(JSON.stringify(headerSelectFields))
  170. }
  171. },
  172. },
  173. mounted() {
  174. // this.initHeaderSelectValues();
  175. },
  176. methods: {
  177. isShowAddRos() {
  178. if(!this.showAddRow) {
  179. return false;
  180. }
  181. return this.templateFillType === 'preFill';
  182. },
  183. // 复制值
  184. onCopy(rowIndex, col) {
  185. if (col.copyFrom) {
  186. if (!this.localDataSource[rowIndex][col.copyFrom]) {//没有值就不用复制了
  187. return
  188. }
  189. this.$set(this.localDataSource[rowIndex], col.prop, this.localDataSource[rowIndex][col.copyFrom])
  190. this.onBlur(rowIndex, col.prop, this.localDataSource[rowIndex][col.prop]);
  191. }
  192. },
  193. // 初始化表头选择器值
  194. initHeaderSelectValues() {
  195. const headerSelectObj = {};
  196. this.columns.map(col => {
  197. if (col.headerSelectKey) {
  198. headerSelectObj[col.headerSelectKey] = col.defaultValue || col.headerOptions[0].value || ""
  199. }
  200. });
  201. this.headerSelectFields = headerSelectObj;
  202. },
  203. // 直接获取表单数据,不做校验
  204. getFilledFormData() {
  205. return {
  206. stepTableFormData: [...this.localDataSource],
  207. headerSelectFields: this.headerSelectFields,
  208. };
  209. },
  210. // 获取最新数据
  211. getFormData() {
  212. // 合并表头选择器值到 columns
  213. // 数据校验
  214. const validateResult = this.validateFormData();
  215. console.log(validateResult,"validateResult")
  216. return new Promise((resolve, reject) => {
  217. if (validateResult.valid) {
  218. resolve({
  219. stepTableFormData: [...this.localDataSource],
  220. headerSelectFields: this.headerSelectFields,
  221. })
  222. } else {
  223. // this.$message.error("表单内容未填完,请填写后再提交");
  224. reject(validateResult.errors[0].error)
  225. }
  226. })
  227. },
  228. // 表单数据校验
  229. validateFormData() {
  230. const errors = [];
  231. // 清空之前的错误状态
  232. this.formErrors = [];
  233. // 校验表头的 HandleFormItem
  234. this.columns.forEach((col, colIndex) => {
  235. if (col.headerSelectKey && col.headerOptions && col.fillType === this.templateFillType) {
  236. const headerValue = this.headerSelectFields[col.headerSelectKey];
  237. if (this.isValueEmpty(headerValue)) {
  238. const errorItem = {
  239. rowIndex: -1, // 表头特殊标记
  240. colIndex,
  241. field: col.headerSelectKey,
  242. label: this.$t(col.label),
  243. error: `请选择${this.$t(col.label)}`
  244. };
  245. errors.push(errorItem);
  246. this.formErrors.push(errorItem);
  247. }
  248. }
  249. });
  250. // 遍历数据行
  251. this.localDataSource.forEach((row, rowIndex) => {
  252. // 遍历列
  253. this.columns.forEach((col, colIndex) => {
  254. // 只校验 fillType 与当前模板状态匹配的字段
  255. if (col.bodyFillType === this.templateFillType || col.bodySubFillType === this.templateFillType) {
  256. // 检查主字段
  257. const mainValue = row[col.prop];
  258. if (this.isValueEmpty(mainValue) && !col.bodyDisabled) {
  259. const errorItem = {
  260. rowIndex,
  261. colIndex,
  262. field: col.prop,
  263. label: this.$t(col.label),
  264. error: `请填写${this.$t(col.label)}`
  265. };
  266. errors.push(errorItem);
  267. this.formErrors.push(errorItem);
  268. }
  269. // 检查子字段(如果有)
  270. if (col.bodySubKey && !col.bodySubDisabled) {
  271. const subValue = row[col.bodySubKey];
  272. console.log(col, subValue, "subValue")
  273. if (this.isValueEmpty(subValue)) {
  274. const errorItem = {
  275. rowIndex,
  276. colIndex,
  277. field: col.bodySubKey,
  278. label: `${this.$t(col.label)}单位`,
  279. error: `请填写${this.$t(col.label)}单位`
  280. };
  281. errors.push(errorItem);
  282. this.formErrors.push(errorItem);
  283. }
  284. }
  285. }
  286. });
  287. });
  288. return {
  289. valid: errors.length === 0,
  290. errors: errors
  291. };
  292. },
  293. // 判断值是否为空
  294. isValueEmpty(value) {
  295. if (value === null || value === undefined || value === '') {
  296. return true;
  297. }
  298. if (typeof value === 'string' && value.trim() === '') {
  299. return true;
  300. }
  301. if (Array.isArray(value) && value.length === 0) {
  302. return true;
  303. }
  304. return false;
  305. },
  306. // 表头选择器变化
  307. onHeaderSelectChange(col, value) {
  308. this.headerSelectFields[col.headerSelectKey] = value;
  309. // 输入时清除对应表单项的错误状态
  310. this.formErrors = this.formErrors.filter(error =>
  311. !(error.rowIndex === -1 &&
  312. error.field === col.headerSelectKey)
  313. );
  314. },
  315. // 表体值变化
  316. onBodyValueChange(rowIndex, colIndex, value) {
  317. const col = this.columns[colIndex];
  318. this.localDataSource[rowIndex][col.prop] = value;
  319. // 输入时清除对应表单项的错误状态
  320. this.formErrors = this.formErrors.filter(error =>
  321. !(error.rowIndex === rowIndex &&
  322. error.colIndex === colIndex &&
  323. error.field === col.prop)
  324. );
  325. this.$emit('body-value-change', rowIndex, colIndex, value);
  326. },
  327. // 表体子值变化
  328. onBodySubValueChange(rowIndex, colIndex, value) {
  329. const col = this.columns[colIndex];
  330. this.localDataSource[rowIndex][col.bodySubKey] = value;
  331. // 输入时清除对应表单项的错误状态
  332. this.formErrors = this.formErrors.filter(error =>
  333. !(error.rowIndex === rowIndex &&
  334. error.colIndex === colIndex &&
  335. error.field === col.bodySubKey)
  336. );
  337. this.$emit('body-sub-value-change', rowIndex, colIndex, value);
  338. },
  339. getHeaderItem(col) {
  340. return {
  341. fillType: col.fillType,
  342. options: col.headerOptions,
  343. label: ""
  344. }
  345. },
  346. getBodyItem(col, rowIndex) {
  347. const currentItem = this.localDataSource[rowIndex];
  348. const item = {
  349. fillType: col.bodyFillType,
  350. options: col.bodyOptions,
  351. maxlength: col.bodyMaxlength,
  352. label: this.$t(col.label),
  353. precision: currentItem[col.bodyPrecisionKey] || col.precision || 0,
  354. copyFrom: col.copyFrom || "",
  355. compareTo: col.bodyCompareTo, // 添加 compareTo 字段
  356. };
  357. if (col.bodyDisabled) {
  358. item.disabled = col.bodyDisabled;
  359. }
  360. return item
  361. },
  362. getBodySubItem(col) {
  363. const item = {
  364. fillType: col.bodySubFillType,
  365. options: col.bodySubOptions,
  366. maxlength: col.bodySubMaxlength || 10,
  367. label: "",
  368. placeholder: col.bodySubPlaceholder || "请输入",
  369. precision: col.subPrecision || 0,
  370. compareTo: col.bodySubCompareTo, // 添加 compareTo 字段
  371. }
  372. if (col.bodySubDisabled) {
  373. item.disabled = col.bodySubDisabled;
  374. }
  375. return item
  376. },
  377. // 删除行
  378. deleteRow(rowIndex) {
  379. this.localDataSource.splice(rowIndex, 1);
  380. this.$emit('row-delete', rowIndex);
  381. },
  382. // 更新数据方法,可在formData变更时调用,也可由父组件调用
  383. updateDataSource(dataSource = []) {
  384. // 深拷贝数据以避免直接修改原始数据
  385. this.localDataSource = JSON.parse(JSON.stringify(dataSource || []));
  386. },
  387. onAddRow() {
  388. this.addRow({
  389. actSolutionVolumePrecision: 3,//小数点精度默认为3
  390. actSolutionConcentrationPrecision: 3,//小数点精度默认为3
  391. targetDiluentVolumePrecision: 3,//小数点精度默认为3
  392. targetStartSolutionVolumePrecision: 3,//小数点精度默认为3
  393. });
  394. },
  395. // 添加行
  396. addRow(row = {}) {
  397. this.localDataSource.push(row);
  398. },
  399. getDataSource() {
  400. return this.localDataSource;
  401. },
  402. // 根据行索引更新数据
  403. updateDataSourceByRowIndex(rowIndex, data) {
  404. this.localDataSource[rowIndex] = { ...this.localDataSource[rowIndex], ...data };
  405. console.log(this.localDataSource, "this.localDataSource")
  406. this.localDataSource = [...this.localDataSource];
  407. },
  408. // 判断表单项是否有错误
  409. hasError(rowIndex, colIndex, field) {
  410. return this.formErrors.some(error =>
  411. error.rowIndex === rowIndex &&
  412. error.colIndex === colIndex &&
  413. error.field === field
  414. );
  415. },
  416. // 处理错误状态更新
  417. onErrorUpdate(rowIndex, colIndex, field, isError) {
  418. if (!isError) {
  419. this.formErrors = this.formErrors.filter(error =>
  420. !(error.rowIndex === rowIndex &&
  421. error.colIndex === colIndex &&
  422. error.field === field)
  423. );
  424. }
  425. },
  426. onSubBlur(rowIndex, colKey, value) {
  427. this.$emit("blur", { rowIndex, colKey, value, item: this.localDataSource[rowIndex] });
  428. },
  429. // 检查是否需要橙色背景
  430. hasOrangeBg(rowIndex, colIndex, field) {
  431. const key = `${rowIndex}-${colIndex}-${field}`;
  432. return this.orangeBgCells[key] || false;
  433. },
  434. // 设置橙色背景状态
  435. setOrangeBg(rowIndex, colIndex, field, status) {
  436. const key = `${rowIndex}-${colIndex}-${field}`;
  437. this.$set(this.orangeBgCells, key, status);
  438. },
  439. onBlur(rowIndex, colKey) {
  440. const value = this.localDataSource[rowIndex][colKey];
  441. // 查找对应的列配置
  442. const col = this.columns.find(c => c.prop === colKey);
  443. if (col && col.bodyFillType === "actFill" && col.bodyCompareTo) {
  444. const compareToValue = this.localDataSource[rowIndex][col.bodyCompareTo];
  445. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  446. if (value !== compareToValue) {
  447. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.prop === colKey), colKey, true);
  448. } else {
  449. // 相等则移除橙色背景
  450. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.prop === colKey), colKey, false);
  451. }
  452. }
  453. this.$emit("blur", { rowIndex, colKey, value, item: this.localDataSource[rowIndex] });
  454. },
  455. onSubBlur(rowIndex, colKey, value) {
  456. // 查找对应的列配置
  457. const col = this.columns.find(c => c.bodySubKey === colKey);
  458. if (col && col.bodySubFillType === "actFill" && col.bodySubCompareTo) {
  459. const compareToValue = this.localDataSource[rowIndex][col.bodySubCompareTo];
  460. // 比较当前值和compareTo值,如果不相等则设置橙色背景
  461. if (value !== compareToValue) {
  462. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.bodySubKey === colKey), colKey, true);
  463. } else {
  464. // 相等则移除橙色背景
  465. this.setOrangeBg(rowIndex, this.columns.findIndex(c => c.bodySubKey === colKey), colKey, false);
  466. }
  467. }
  468. this.$emit("blur", { rowIndex, colKey, value, item: this.localDataSource[rowIndex] });
  469. }
  470. }
  471. };
  472. </script>
  473. <style scoped>
  474. .custom-table-wrapper {
  475. border: 1px solid #ebeef5;
  476. border-radius: 4px;
  477. overflow: hidden;
  478. font-size: 14px;
  479. color: #606266;
  480. margin-top: 20px;
  481. }
  482. .inner-table-cell {
  483. display: flex;
  484. align-items: center;
  485. justify-content: center;
  486. }
  487. .m-l-5 {
  488. margin-left: 5px;
  489. }
  490. .sub-input-number {
  491. width: 145px;
  492. .el-input-number--mini {
  493. width: 145px;
  494. }
  495. }
  496. /* 表头 */
  497. .custom-table-header {
  498. background-color: #f5f7fa;
  499. border-bottom: 1px solid #ebeef5;
  500. white-space: nowrap;
  501. display: block;
  502. }
  503. .custom-table-body {
  504. max-height: 300px;
  505. /* 可根据需要调整或由父组件控制 */
  506. }
  507. .header-cell-content {
  508. display: flex;
  509. align-items: center;
  510. justify-content: center;
  511. }
  512. /* 共同行样式 */
  513. .custom-table-row {
  514. display: table;
  515. width: 100%;
  516. table-layout: fixed;
  517. }
  518. .custometable-row {
  519. display: table;
  520. width: 100%;
  521. table-layout: fixed;
  522. &:not(:last-child) {
  523. border-bottom: 1px solid #ebeef5;
  524. }
  525. }
  526. /* 单元格 */
  527. .custom-table-cell {
  528. display: table-cell;
  529. padding: 12px 10px;
  530. text-align: left;
  531. vertical-align: middle;
  532. border-right: 1px solid #ebeef5;
  533. box-sizing: border-box;
  534. }
  535. .custom-table-cell:last-child {
  536. border-right: none;
  537. }
  538. .header-cell {
  539. font-weight: bold;
  540. color: #909399;
  541. background-color: #f5f7fa;
  542. }
  543. .body-cell {
  544. color: #606266;
  545. background-color: #fff;
  546. }
  547. /* select 样式(模仿 Element UI) */
  548. .header-cell select {
  549. width: 100%;
  550. padding: 4px 8px;
  551. border: 1px solid #dcdfe6;
  552. border-radius: 4px;
  553. outline: none;
  554. background-color: #fff;
  555. font-size: 13px;
  556. color: #606266;
  557. appearance: none;
  558. /* 隐藏默认箭头(可选) */
  559. 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");
  560. background-repeat: no-repeat;
  561. background-position: right 8px center;
  562. background-size: 14px;
  563. padding-right: 28px;
  564. }
  565. /* 滚动容器:如果整体宽度超限,显示横向滚动条 */
  566. .custom-table-wrapper {
  567. display: flex;
  568. flex-direction: column;
  569. max-width: 100%;
  570. /* 父容器决定宽度 */
  571. overflow-x: auto;
  572. }
  573. .custom-table-header,
  574. .custom-table-body {
  575. min-width: 100%;
  576. }
  577. .header-select {
  578. width: 100px;
  579. margin-left: 5px;
  580. }
  581. .no-data {
  582. text-align: center;
  583. padding: 20px 0;
  584. color: rgb(144, 147, 153)
  585. }
  586. .add-row {
  587. display: flex;
  588. justify-content: center;
  589. padding: 20px 0;
  590. margin-top: 20px;
  591. }
  592. </style>