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