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

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