lzdjack
2022-01-18 b63f7d17dee2c0332e753ee445d61db63bd28236
提交 | 用户 | age
b63f7d 1 <script lang="tsx">
9c2f3f 2   import type { CSSProperties, PropType } from 'vue';
33a335 3   import { computed, defineComponent, nextTick, ref, toRaw, unref, watchEffect } from 'vue';
9c2f3f 4   import type { BasicColumn } from '../../types/table';
4f8e1c 5   import type { EditRecordRow } from './index';
33a335 6   import { CheckOutlined, CloseOutlined, FormOutlined } from '@ant-design/icons-vue';
4f8e1c 7   import { CellComponent } from './CellComponent';
9c2f3f 8
V 9   import { useDesign } from '/@/hooks/web/useDesign';
4f8e1c 10   import { useTableContext } from '../../hooks/useTableContext';
V 11
9c2f3f 12   import clickOutside from '/@/directives/clickOutside';
V 13
14   import { propTypes } from '/@/utils/propTypes';
33a335 15   import { isArray, isBoolean, isFunction, isNumber, isString } from '/@/utils/is';
9c2f3f 16   import { createPlaceholderMessage } from './helper';
2c867b 17   import { omit, pick, set } from 'lodash-es';
52af1d 18   import { treeToList } from '/@/utils/helper/treeHelper';
2c867b 19   import { Spin } from 'ant-design-vue';
9c2f3f 20
V 21   export default defineComponent({
22     name: 'EditableCell',
b63f7d 23     components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, Spin },
9edc28 24     directives: {
V 25       clickOutside,
26     },
9c2f3f 27     props: {
V 28       value: {
29         type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
30         default: '',
31       },
32       record: {
33         type: Object as PropType<EditRecordRow>,
34       },
35       column: {
36         type: Object as PropType<BasicColumn>,
8b2e0f 37         default: () => ({}),
9c2f3f 38       },
V 39       index: propTypes.number,
40     },
41     setup(props) {
42       const table = useTableContext();
43       const isEdit = ref(false);
3576d0 44       const elRef = ref();
9c2f3f 45       const ruleVisible = ref(false);
V 46       const ruleMessage = ref('');
47       const optionsRef = ref<LabelValueOptions>([]);
48       const currentValueRef = ref<any>(props.value);
49       const defaultValueRef = ref<any>(props.value);
2c867b 50       const spinning = ref<boolean>(false);
9c2f3f 51
V 52       const { prefixCls } = useDesign('editable-cell');
53
54       const getComponent = computed(() => props.column?.editComponent || 'Input');
55       const getRule = computed(() => props.column?.editRule);
56
57       const getRuleVisible = computed(() => {
58         return unref(ruleMessage) && unref(ruleVisible);
59       });
60
61       const getIsCheckComp = computed(() => {
62         const component = unref(getComponent);
63         return ['Checkbox', 'Switch'].includes(component);
64       });
65
66       const getComponentProps = computed(() => {
67         const isCheckValue = unref(getIsCheckComp);
68
69         const valueField = isCheckValue ? 'checked' : 'value';
70         const val = unref(currentValueRef);
71
72         const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
73
b63f7d 74         let compProps = props.column?.editComponentProps ?? {};
L 75         const { record, column, index } = props;
76
77         if (isFunction(compProps)) {
78           compProps = compProps({ text: val, record, column, index }) ?? {};
79         }
80         const component = unref(getComponent);
81         const apiSelectProps: Recordable = {};
82         if (component === 'ApiSelect') {
83           apiSelectProps.cache = true;
84         }
85
9c2f3f 86         return {
797189 87           size: 'small',
a07ab6 88           getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
9c2f3f 89           placeholder: createPlaceholderMessage(unref(getComponent)),
V 90           ...apiSelectProps,
829b36 91           ...omit(compProps, 'onChange'),
9c2f3f 92           [valueField]: value,
b63f7d 93         } as any;
9c2f3f 94       });
V 95
96       const getValues = computed(() => {
b63f7d 97         const { editValueMap } = props.column;
9c2f3f 98
V 99         const value = unref(currentValueRef);
100
101         if (editValueMap && isFunction(editValueMap)) {
102           return editValueMap(value);
103         }
104
105         const component = unref(getComponent);
106         if (!component.includes('Select')) {
107           return value;
108         }
4f8e1c 109
b63f7d 110         const options: LabelValueOptions =
L 111           unref(getComponentProps)?.options ?? (unref(optionsRef) || []);
9c2f3f 112         const option = options.find((item) => `${item.value}` === `${value}`);
4f8e1c 113
V 114         return option?.label ?? value;
9c2f3f 115       });
V 116
3ef508 117       const getWrapperStyle = computed((): CSSProperties => {
Z 118         if (unref(getIsCheckComp) || unref(getRowEditable)) {
119           return {};
9c2f3f 120         }
3ef508 121         return {
Z 122           width: 'calc(100% - 48px)',
123         };
8eaf57 124       });
125
126       const getWrapperClass = computed(() => {
127         const { align = 'center' } = props.column;
128         return `edit-cell-align-${align}`;
3ef508 129       });
9c2f3f 130
V 131       const getRowEditable = computed(() => {
132         const { editable } = props.record || {};
133         return !!editable;
134       });
135
136       watchEffect(() => {
137         defaultValueRef.value = props.value;
61ce25 138         currentValueRef.value = props.value;
9c2f3f 139       });
V 140
141       watchEffect(() => {
142         const { editable } = props.column;
143         if (isBoolean(editable) || isBoolean(unref(getRowEditable))) {
144           isEdit.value = !!editable || unref(getRowEditable);
145         }
146       });
147
148       function handleEdit() {
149         if (unref(getRowEditable) || unref(props.column?.editRow)) return;
150         ruleMessage.value = '';
151         isEdit.value = true;
152         nextTick(() => {
153           const el = unref(elRef);
154           el?.focus?.();
155         });
156       }
157
158       async function handleChange(e: any) {
159         const component = unref(getComponent);
fab7a6 160         if (!e) {
V 161           currentValueRef.value = e;
162         } else if (e?.target && Reflect.has(e.target, 'value')) {
9c2f3f 163           currentValueRef.value = (e as ChangeEvent).target.value;
fab7a6 164         } else if (component === 'Checkbox') {
9c2f3f 165           currentValueRef.value = (e as ChangeEvent).target.checked;
V 166         } else if (isString(e) || isBoolean(e) || isNumber(e)) {
167           currentValueRef.value = e;
168         }
b63f7d 169         const onChange = unref(getComponentProps)?.onChange;
829b36 170         if (onChange && isFunction(onChange)) onChange(...arguments);
fab7a6 171
4f8e1c 172         table.emit?.('edit-change', {
V 173           column: props.column,
174           value: unref(currentValueRef),
175           record: toRaw(props.record),
176         });
9c2f3f 177         handleSubmiRule();
V 178       }
179
180       async function handleSubmiRule() {
181         const { column, record } = props;
182         const { editRule } = column;
183         const currentValue = unref(currentValueRef);
184
185         if (editRule) {
186           if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
187             ruleVisible.value = true;
188             const component = unref(getComponent);
33a335 189             ruleMessage.value = createPlaceholderMessage(component);
9c2f3f 190             return false;
V 191           }
192           if (isFunction(editRule)) {
193             const res = await editRule(currentValue, record as Recordable);
194             if (!!res) {
195               ruleMessage.value = res;
196               ruleVisible.value = true;
197               return false;
198             } else {
199               ruleMessage.value = '';
200               return true;
201             }
202           }
203         }
204         ruleMessage.value = '';
205         return true;
206       }
207
4f8e1c 208       async function handleSubmit(needEmit = true, valid = true) {
V 209         if (valid) {
210           const isPass = await handleSubmiRule();
211           if (!isPass) return false;
212         }
213
4ae39c 214         const { column, index, record } = props;
215         if (!record) return false;
9c2f3f 216         const { key, dataIndex } = column;
1a85df 217         const value = unref(currentValueRef);
ba2c1a 218         if (!key && !dataIndex) return;
4f8e1c 219
9c2f3f 220         const dataKey = (dataIndex || key) as string;
V 221
2c867b 222         if (!record.editable) {
223           const { getBindValues } = table;
224
225           const { beforeEditSubmit, columns } = unref(getBindValues);
226
227           if (beforeEditSubmit && isFunction(beforeEditSubmit)) {
228             spinning.value = true;
229             const keys: string[] = columns
230               .map((_column) => _column.dataIndex)
231               .filter((field) => !!field) as string[];
232             let result: any = true;
233             try {
234               result = await beforeEditSubmit({
235                 record: pick(record, keys),
236                 index,
e82baf 237                 key: dataKey as string,
2c867b 238                 value,
239               });
240             } catch (e) {
241               result = false;
242             } finally {
243               spinning.value = false;
244             }
245             if (result === false) {
246               return;
247             }
248           }
249         }
250
4ae39c 251         set(record, dataKey, value);
252         //const record = await table.updateTableData(index, dataKey, value);
e82baf 253         needEmit && table.emit?.('edit-end', { record, index, key: dataKey, value });
9c2f3f 254         isEdit.value = false;
V 255       }
256
64533f 257       async function handleEnter() {
V 258         if (props.column?.editRow) {
259           return;
260         }
261         handleSubmit();
262       }
263
797189 264       function handleSubmitClick() {
265         handleSubmit();
266       }
267
9c2f3f 268       function handleCancel() {
V 269         isEdit.value = false;
270         currentValueRef.value = defaultValueRef.value;
8d2223 271         const { column, index, record } = props;
272         const { key, dataIndex } = column;
273         table.emit?.('edit-cancel', {
274           record,
275           index,
276           key: dataIndex || key,
277           value: unref(currentValueRef),
278         });
9c2f3f 279       }
V 280
281       function onClickOutside() {
282         if (props.column?.editable || unref(getRowEditable)) {
283           return;
284         }
285         const component = unref(getComponent);
286
287         if (component.includes('Input')) {
288           handleCancel();
289         }
290       }
291
52af1d 292       // only ApiSelect or TreeSelect
9c2f3f 293       function handleOptionsChange(options: LabelValueOptions) {
b63f7d 294         const { replaceFields } = unref(getComponentProps);
52af1d 295         const component = unref(getComponent);
296         if (component === 'ApiTreeSelect') {
297           const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
298           let listOptions: Recordable[] = treeToList(options, { children });
299           listOptions = listOptions.map((item) => {
300             return {
301               label: item[title],
302               value: item[value],
303             };
304           });
305           optionsRef.value = listOptions as LabelValueOptions;
306         } else {
307           optionsRef.value = options;
308         }
9c2f3f 309       }
V 310
311       function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
312         if (props.record) {
313           /* eslint-disable  */
314           isArray(props.record[cbs])
aa596a 315             ? props.record[cbs]?.push(handle)
9c2f3f 316             : (props.record[cbs] = [handle]);
V 317         }
318       }
319
320       if (props.record) {
321         initCbs('submitCbs', handleSubmit);
322         initCbs('validCbs', handleSubmiRule);
323         initCbs('cancelCbs', handleCancel);
324
fe2bcf 325         if (props.column.dataIndex) {
326           if (!props.record.editValueRefs) props.record.editValueRefs = {};
b63f7d 327           props.record.editValueRefs[props.column.dataIndex as any] = currentValueRef;
fe2bcf 328         }
9c2f3f 329         /* eslint-disable  */
V 330         props.record.onCancelEdit = () => {
331           isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
332         };
333         /* eslint-disable */
334         props.record.onSubmitEdit = async () => {
335           if (isArray(props.record?.submitCbs)) {
ee7c31 336             if (!props.record?.onValid?.()) return;
9c2f3f 337             const submitFns = props.record?.submitCbs || [];
4f8e1c 338             submitFns.forEach((fn) => fn(false, false));
de5bf7 339             table.emit?.('edit-row-end');
9c2f3f 340             return true;
V 341           }
342         };
343       }
344
345       return {
346         isEdit,
347         prefixCls,
348         handleEdit,
349         currentValueRef,
350         handleSubmit,
351         handleChange,
352         handleCancel,
353         elRef,
354         getComponent,
355         getRule,
356         onClickOutside,
357         ruleMessage,
358         getRuleVisible,
359         getComponentProps,
360         handleOptionsChange,
361         getWrapperStyle,
8eaf57 362         getWrapperClass,
9c2f3f 363         getRowEditable,
9ea257 364         getValues,
64533f 365         handleEnter,
797189 366         handleSubmitClick,
2c867b 367         spinning,
9c2f3f 368       };
V 369     },
b63f7d 370     render() {
L 371       return (
372         <div class={this.prefixCls}>
373           <div
374             v-show={!this.isEdit}
375             class={{ [`${this.prefixCls}__normal`]: true, 'ellipsis-cell': this.column.ellipsis }}
376             onClick={this.handleEdit}
377           >
378             <div class="cell-content" title={this.column.ellipsis ? this.getValues ?? '' : ''}>
379               {this.column.editRender
380                 ? this.column.editRender({
381                     text: this.value,
382                     record: this.record as Recordable,
383                     column: this.column,
384                     index: this.index,
385                   })
386                 : this.getValues
387                 ? this.getValues
388                 : '\u00A0'}
389             </div>
390             {!this.column.editRow && <FormOutlined class={`${this.prefixCls}__normal-icon`} />}
391           </div>
392           {this.isEdit && (
393             <Spin spinning={this.spinning}>
394               <div class={`${this.prefixCls}__wrapper`} v-click-outside={this.onClickOutside}>
395                 <CellComponent
396                   {...this.getComponentProps}
397                   component={this.getComponent}
398                   style={this.getWrapperStyle}
399                   popoverVisible={this.getRuleVisible}
400                   rule={this.getRule}
401                   ruleMessage={this.ruleMessage}
402                   class={this.getWrapperClass}
403                   ref="elRef"
404                   onChange={this.handleChange}
405                   onOptionsChange={this.handleOptionsChange}
406                   onPressEnter={this.handleEnter}
407                 />
408                 {!this.getRowEditable && (
409                   <div class={`${this.prefixCls}__action`}>
410                     <CheckOutlined
411                       class={[`${this.prefixCls}__icon`, 'mx-2']}
412                       onClick={this.handleSubmitClick}
413                     />
414                     <CloseOutlined class={`${this.prefixCls}__icon `} onClick={this.handleCancel} />
415                   </div>
416                 )}
417               </div>
418             </Spin>
419           )}
420         </div>
421       );
422     },
9c2f3f 423   });
V 424 </script>
425 <style lang="less">
426   @prefix-cls: ~'@{namespace}-editable-cell';
427
8eaf57 428   .edit-cell-align-left {
429     text-align: left;
430
431     input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
432       text-align: left;
433     }
434   }
435
436   .edit-cell-align-center {
437     text-align: center;
438
439     input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
440       text-align: center;
441     }
442   }
443
444   .edit-cell-align-right {
445     text-align: right;
446
447     input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
448       text-align: right;
449     }
450   }
451
9c2f3f 452   .edit-cell-rule-popover {
V 453     .ant-popover-inner-content {
454       padding: 4px 8px;
455       color: @error-color;
456       // border: 1px solid @error-color;
457       border-radius: 2px;
458     }
459   }
460   .@{prefix-cls} {
461     position: relative;
462
463     &__wrapper {
464       display: flex;
465       align-items: center;
466       justify-content: center;
4f8e1c 467
V 468       > .ant-select {
469         min-width: calc(100% - 50px);
470       }
9c2f3f 471     }
V 472
473     &__icon {
474       &:hover {
475         transform: scale(1.2);
476
477         svg {
478           color: @primary-color;
479         }
480       }
481     }
482
4bb506 483     .ellipsis-cell {
484       .cell-content {
485         overflow-wrap: break-word;
486         word-break: break-word;
487         overflow: hidden;
488         white-space: nowrap;
489         text-overflow: ellipsis;
490       }
491     }
492
9c2f3f 493     &__normal {
V 494       &-icon {
495         position: absolute;
496         top: 4px;
497         right: 0;
498         display: none;
499         width: 20px;
500         cursor: pointer;
501       }
502     }
503
504     &:hover {
505       .@{prefix-cls}__normal-icon {
506         display: inline-block;
507       }
508     }
509   }
510 </style>