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

403 lines
11 KiB

  1. <template>
  2. <div class="custom-table-wrapper">
  3. <div class="custom-table-header">
  4. <div class="custom-table-row">
  5. <div v-for="(col, index) in columns" :key="index" class="custom-table-cell header-cell"
  6. :style="{ width: col.width ? col.width + 'px' : 'auto' }">
  7. <div class="header-cell-content">
  8. <div>{{ col.label }}</div>
  9. <template v-if="col.headerSelectKey && col.headerOptions">
  10. <HandleFormItem type="select" class="header-select" :item="getHeaderItem(col)"
  11. v-model="headerSelectFields[col.headerSelectKey]" @change="onHeaderSelectChange(index, $event)" />
  12. </template>
  13. </div>
  14. </div>
  15. <!-- 默认操作栏 -->
  16. <div class="custom-table-cell header-cell" :style="{ width: '80px' }">
  17. <div class="header-cell-content">
  18. <div>操作</div>
  19. </div>
  20. </div>
  21. </div>
  22. </div>
  23. <div class="custom-table-body">
  24. <div v-for="(row, rowIndex) in localDataSource" :key="rowIndex" class="custometable-row">
  25. <div v-for="(col, colIndex) in columns" :key="colIndex" class="custom-table-cell body-cell"
  26. :style="{ width: col.width ? col.width + 'px' : 'auto' }">
  27. <div class="inner-table-cell">
  28. <div>
  29. <template v-if="col.bodyType === 'input'">
  30. <HandleFormItem type="input" class="body-input" :item="getBodyItem(col,rowIndex)" v-model="row[col.prop]" @change="onBodyValueChange(rowIndex, colIndex, $event)" />
  31. </template>
  32. <template v-else-if="col.bodyType === 'inputNumber'">
  33. <HandleFormItem type="inputNumber" class="body-input-number" :item="getBodyItem(col,rowIndex)"
  34. v-model="row[col.prop]" @change="onBodyValueChange(rowIndex, colIndex, $event)" />
  35. </template>
  36. <template v-else-if="col.bodyType === 'select'">
  37. <HandleFormItem type="select" class="body-select" :item="getBodyItem(col,rowIndex)" v-model="row[col.prop]" @change="onBodyValueChange(rowIndex, colIndex, $event)" />
  38. </template>
  39. <template v-else>
  40. {{ row[col.prop] }}
  41. </template>
  42. </div>
  43. <div class="m-l-5">
  44. <template v-if="col.bodySubType === 'inputNumber' && col.showBodySub">
  45. <HandleFormItem type="inputNumber" :item="getBodySubItem(col)"
  46. v-model="row[col.bodySubKey]" @change="onBodySubValueChange(rowIndex, colIndex, $event)" />
  47. </template>
  48. <template v-else>
  49. {{ row[col.bodySubKey] }}
  50. </template>
  51. </div>
  52. </div>
  53. </div>
  54. <!-- 默认操作栏 -->
  55. <div class="custom-table-cell body-cell" :style="{ width: '80px' }">
  56. <div class="inner-table-cell">
  57. <el-popconfirm
  58. confirm-button-text='确认'
  59. cancel-button-text='取消'
  60. icon="el-icon-info"
  61. icon-color="red"
  62. title="确认删除当前数据?"
  63. @confirm = "deleteRow(rowIndex)"
  64. >
  65. <el-button slot="reference" type="text" size="small" class="delete-button" >
  66. 删除
  67. </el-button>
  68. </el-popconfirm>
  69. </div>
  70. </div>
  71. </div>
  72. </div>
  73. </div>
  74. </template>
  75. <script>
  76. import HandleFormItem from "./HandleFormItem.vue"
  77. export default {
  78. name: 'CustomTable',
  79. components: {
  80. HandleFormItem
  81. },
  82. props: {
  83. columns: {
  84. type: Array,
  85. required: true,
  86. // 示例格式:
  87. // [
  88. // { label: '姓名', prop: 'name' },
  89. // { label: '状态', prop: 'status', type: 'select', options: [{value:1,label:'启用'},...], selected: null }
  90. // ]
  91. },
  92. formData: {
  93. type: Object,
  94. default:()=>{
  95. return {
  96. stepTableFormData: [],
  97. headerSelectFields: {}
  98. }
  99. }
  100. },
  101. },
  102. data() {
  103. return {
  104. localDataSource: [],
  105. headerSelectFields: {}
  106. }
  107. },
  108. watch: {
  109. formData:{
  110. immediate: true,
  111. handler(newData) {
  112. console.log(newData,"newData")
  113. const {stepTableFormData = [], headerSelectFields = {}} = newData;
  114. this.updateDataSource(stepTableFormData);
  115. this.headerSelectFields = JSON.parse(JSON.stringify(headerSelectFields))
  116. }
  117. },
  118. },
  119. mounted() {
  120. this.initHeaderSelectValues();
  121. },
  122. methods: {
  123. // 初始化表头选择器值
  124. initHeaderSelectValues() {
  125. const headerSelectObj = {};
  126. this.columns.map(col => {
  127. if(col.headerSelectKey){
  128. headerSelectObj[col.headerSelectKey] = col.defaultValue || col.headerOptions[0].value || ""
  129. }
  130. });
  131. this.headerSelectFields = headerSelectObj;
  132. },
  133. // 获取最新数据
  134. getFormData() {
  135. // 合并表头选择器值到 columns
  136. // 数据校验
  137. const validateResult = this.validateFormData();
  138. return new Promise((resolve,reject)=>{
  139. if(validateResult.valid){
  140. resolve({
  141. stepTableFormData: [...this.localDataSource],
  142. headerSelectFields: this.headerSelectFields,
  143. })
  144. }else{
  145. this.$message.error(validateResult.errors[0].error);
  146. reject(validateResult.errors[0].error)
  147. }
  148. })
  149. },
  150. // 表单数据校验
  151. validateFormData() {
  152. const templateStatus = this.$store.state.template.templateStatus;
  153. const errors = [];
  154. // 遍历数据行
  155. this.localDataSource.forEach((row, rowIndex) => {
  156. // 遍历列
  157. this.columns.forEach((col, colIndex) => {
  158. // 只校验 fillType 与当前模板状态匹配的字段
  159. if (col.bodyFillType === templateStatus || col.bodySubType === templateStatus) {
  160. // 检查主字段
  161. const mainValue = row[col.prop];
  162. if (this.isValueEmpty(mainValue)) {
  163. errors.push({
  164. rowIndex,
  165. colIndex,
  166. field: col.prop,
  167. label: col.label,
  168. error: `请填写${col.label}`
  169. });
  170. }
  171. // 检查子字段(如果有)
  172. if (col.bodySubKey) {
  173. const subValue = row[col.bodySubKey];
  174. if (this.isValueEmpty(subValue)) {
  175. errors.push({
  176. rowIndex,
  177. colIndex,
  178. field: col.bodySubKey,
  179. label: `${col.label}单位`,
  180. error: `请填写${col.label}单位`
  181. });
  182. }
  183. }
  184. }
  185. });
  186. });
  187. return {
  188. valid: errors.length === 0,
  189. errors: errors
  190. };
  191. },
  192. // 判断值是否为空
  193. isValueEmpty(value) {
  194. if (value === null || value === undefined || value === '') {
  195. return true;
  196. }
  197. if (typeof value === 'string' && value.trim() === '') {
  198. return true;
  199. }
  200. if (Array.isArray(value) && value.length === 0) {
  201. return true;
  202. }
  203. return false;
  204. },
  205. // 表头选择器变化
  206. onHeaderSelectChange(colIndex, value) {
  207. this.headerSelectValues[colIndex] = value;
  208. this.$emit('header-select-change', colIndex, value);
  209. },
  210. // 表体值变化
  211. onBodyValueChange(rowIndex, colIndex, value) {
  212. const col = this.columns[colIndex];
  213. this.localDataSource[rowIndex][col.prop] = value;
  214. this.$emit('body-value-change', rowIndex, colIndex, value);
  215. },
  216. // 表体子值变化
  217. onBodySubValueChange(rowIndex, colIndex, value) {
  218. const col = this.columns[colIndex];
  219. this.localDataSource[rowIndex][col.bodySubKey] = value;
  220. this.$emit('body-sub-value-change', rowIndex, colIndex, value);
  221. },
  222. getHeaderItem(col) {
  223. return {
  224. fillType: col.fillType,
  225. options: col.headerOptions,
  226. label: ""
  227. }
  228. },
  229. getBodyItem(col,rowIndex) {
  230. const currentItem = this.localDataSource[rowIndex];
  231. const item = {
  232. fillType: col.bodyFillType,
  233. options: col.bodyOptions,
  234. maxlength: col.bodyMaxlength,
  235. label: col.label,
  236. precision: currentItem[col.bodyPrecisionKey] || col.precision || 0,
  237. };
  238. if(col.bodyDisabled){
  239. item.disabled = col.bodyDisabled;
  240. }
  241. return item
  242. },
  243. getBodySubItem(col) {
  244. const item = {
  245. fillType: col.bodySubFillType,
  246. options: col.bodySubOptions,
  247. maxlength: col.bodySubMaxlength,
  248. label: "",
  249. placeholder:col.bodySubPlaceholder||"请输入"
  250. }
  251. if(col.bodySubDisabled){
  252. item.disabled = col.bodySubDisabled;
  253. }
  254. },
  255. // 删除行
  256. deleteRow(rowIndex) {
  257. this.localDataSource.splice(rowIndex, 1);
  258. this.$emit('row-delete', rowIndex);
  259. },
  260. // 更新数据方法,可在formData变更时调用,也可由父组件调用
  261. updateDataSource(dataSource = []) {
  262. // 深拷贝数据以避免直接修改原始数据
  263. this.localDataSource = JSON.parse(JSON.stringify(dataSource || []));
  264. }
  265. }
  266. };
  267. </script>
  268. <style scoped>
  269. .custom-table-wrapper {
  270. border: 1px solid #ebeef5;
  271. border-radius: 4px;
  272. overflow: hidden;
  273. font-size: 14px;
  274. color: #606266;
  275. margin-top: 20px;
  276. }
  277. .inner-table-cell{
  278. display: flex;
  279. align-items: center;
  280. justify-content: center;
  281. }
  282. .m-l-5{
  283. margin-left: 5px;
  284. }
  285. .sub-input-number{
  286. width: 145px;
  287. .el-input-number--mini{
  288. width: 145px;
  289. }
  290. }
  291. /* 表头 */
  292. .custom-table-header {
  293. background-color: #f5f7fa;
  294. border-bottom: 1px solid #ebeef5;
  295. white-space: nowrap;
  296. display: block;
  297. }
  298. .custom-table-body {
  299. max-height: 300px;
  300. /* 可根据需要调整或由父组件控制 */
  301. }
  302. .header-cell-content {
  303. display: flex;
  304. align-items: center;
  305. justify-content: center;
  306. }
  307. /* 共同行样式 */
  308. .custom-table-row {
  309. display: table;
  310. width: 100%;
  311. table-layout: fixed;
  312. }
  313. .custometable-row {
  314. display: table;
  315. width: 100%;
  316. table-layout: fixed;
  317. &:not(:last-child) {
  318. border-bottom: 1px solid #ebeef5;
  319. }
  320. }
  321. /* 单元格 */
  322. .custom-table-cell {
  323. display: table-cell;
  324. padding: 12px 10px;
  325. text-align: left;
  326. vertical-align: middle;
  327. border-right: 1px solid #ebeef5;
  328. box-sizing: border-box;
  329. }
  330. .custom-table-cell:last-child {
  331. border-right: none;
  332. }
  333. .header-cell {
  334. font-weight: bold;
  335. color: #909399;
  336. background-color: #f5f7fa;
  337. }
  338. .body-cell {
  339. color: #606266;
  340. background-color: #fff;
  341. }
  342. /* select 样式(模仿 Element UI) */
  343. .header-cell select {
  344. width: 100%;
  345. padding: 4px 8px;
  346. border: 1px solid #dcdfe6;
  347. border-radius: 4px;
  348. outline: none;
  349. background-color: #fff;
  350. font-size: 13px;
  351. color: #606266;
  352. appearance: none;
  353. /* 隐藏默认箭头(可选) */
  354. 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");
  355. background-repeat: no-repeat;
  356. background-position: right 8px center;
  357. background-size: 14px;
  358. padding-right: 28px;
  359. }
  360. /* 滚动容器:如果整体宽度超限,显示横向滚动条 */
  361. .custom-table-wrapper {
  362. display: flex;
  363. flex-direction: column;
  364. max-width: 100%;
  365. /* 父容器决定宽度 */
  366. overflow-x: auto;
  367. }
  368. .custom-table-header,
  369. .custom-table-body {
  370. min-width: 100%;
  371. }
  372. .header-select {
  373. width: 100px;
  374. margin-left: 5px;
  375. }
  376. .delete-button{
  377. color: red;
  378. }
  379. </style>