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