vben
2021-08-24 56a966cfbf8db5b29a42185f0f25a0e800c30dbb
提交 | 用户 | 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 ?? '' : ''">
V 9         {{ getValues ? getValues : '&nbsp;' }}
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 (e?.target && Reflect.has(e.target, 'value')) {
9c2f3f 194           currentValueRef.value = (e as ChangeEvent).target.value;
fab7a6 195         } else if (component === 'Checkbox') {
9c2f3f 196           currentValueRef.value = (e as ChangeEvent).target.checked;
V 197         } else if (isString(e) || isBoolean(e) || isNumber(e)) {
198           currentValueRef.value = e;
199         }
829b36 200         const onChange = props.column?.editComponentProps?.onChange;
201         if (onChange && isFunction(onChange)) onChange(...arguments);
fab7a6 202
4f8e1c 203         table.emit?.('edit-change', {
V 204           column: props.column,
205           value: unref(currentValueRef),
206           record: toRaw(props.record),
207         });
9c2f3f 208         handleSubmiRule();
V 209       }
210
211       async function handleSubmiRule() {
212         const { column, record } = props;
213         const { editRule } = column;
214         const currentValue = unref(currentValueRef);
215
216         if (editRule) {
217           if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
218             ruleVisible.value = true;
219             const component = unref(getComponent);
33a335 220             ruleMessage.value = createPlaceholderMessage(component);
9c2f3f 221             return false;
V 222           }
223           if (isFunction(editRule)) {
224             const res = await editRule(currentValue, record as Recordable);
225             if (!!res) {
226               ruleMessage.value = res;
227               ruleVisible.value = true;
228               return false;
229             } else {
230               ruleMessage.value = '';
231               return true;
232             }
233           }
234         }
235         ruleMessage.value = '';
236         return true;
237       }
238
4f8e1c 239       async function handleSubmit(needEmit = true, valid = true) {
V 240         if (valid) {
241           const isPass = await handleSubmiRule();
242           if (!isPass) return false;
243         }
244
4ae39c 245         const { column, index, record } = props;
246         if (!record) return false;
9c2f3f 247         const { key, dataIndex } = column;
1a85df 248         const value = unref(currentValueRef);
9c2f3f 249         if (!key || !dataIndex) return;
4f8e1c 250
9c2f3f 251         const dataKey = (dataIndex || key) as string;
V 252
2c867b 253         if (!record.editable) {
254           const { getBindValues } = table;
255
256           const { beforeEditSubmit, columns } = unref(getBindValues);
257
258           if (beforeEditSubmit && isFunction(beforeEditSubmit)) {
259             spinning.value = true;
260             const keys: string[] = columns
261               .map((_column) => _column.dataIndex)
262               .filter((field) => !!field) as string[];
263             let result: any = true;
264             try {
265               result = await beforeEditSubmit({
266                 record: pick(record, keys),
267                 index,
268                 key,
269                 value,
270               });
271             } catch (e) {
272               result = false;
273             } finally {
274               spinning.value = false;
275             }
276             if (result === false) {
277               return;
278             }
279           }
280         }
281
4ae39c 282         set(record, dataKey, value);
283         //const record = await table.updateTableData(index, dataKey, value);
9edc28 284         needEmit && table.emit?.('edit-end', { record, index, key, value });
9c2f3f 285         isEdit.value = false;
V 286       }
287
64533f 288       async function handleEnter() {
V 289         if (props.column?.editRow) {
290           return;
291         }
292         handleSubmit();
293       }
294
797189 295       function handleSubmitClick() {
296         handleSubmit();
297       }
298
9c2f3f 299       function handleCancel() {
V 300         isEdit.value = false;
301         currentValueRef.value = defaultValueRef.value;
8d2223 302         const { column, index, record } = props;
303         const { key, dataIndex } = column;
304         table.emit?.('edit-cancel', {
305           record,
306           index,
307           key: dataIndex || key,
308           value: unref(currentValueRef),
309         });
9c2f3f 310       }
V 311
312       function onClickOutside() {
313         if (props.column?.editable || unref(getRowEditable)) {
314           return;
315         }
316         const component = unref(getComponent);
317
318         if (component.includes('Input')) {
319           handleCancel();
320         }
321       }
322
52af1d 323       // only ApiSelect or TreeSelect
9c2f3f 324       function handleOptionsChange(options: LabelValueOptions) {
52af1d 325         const { replaceFields } = props.column?.editComponentProps ?? {};
326         const component = unref(getComponent);
327         if (component === 'ApiTreeSelect') {
328           const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
329           let listOptions: Recordable[] = treeToList(options, { children });
330           listOptions = listOptions.map((item) => {
331             return {
332               label: item[title],
333               value: item[value],
334             };
335           });
336           optionsRef.value = listOptions as LabelValueOptions;
337         } else {
338           optionsRef.value = options;
339         }
9c2f3f 340       }
V 341
342       function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
343         if (props.record) {
344           /* eslint-disable  */
345           isArray(props.record[cbs])
aa596a 346             ? props.record[cbs]?.push(handle)
9c2f3f 347             : (props.record[cbs] = [handle]);
V 348         }
349       }
350
351       if (props.record) {
352         initCbs('submitCbs', handleSubmit);
353         initCbs('validCbs', handleSubmiRule);
354         initCbs('cancelCbs', handleCancel);
355
fe2bcf 356         if (props.column.dataIndex) {
357           if (!props.record.editValueRefs) props.record.editValueRefs = {};
358           props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
359         }
9c2f3f 360         /* eslint-disable  */
V 361         props.record.onCancelEdit = () => {
362           isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
363         };
364         /* eslint-disable */
365         props.record.onSubmitEdit = async () => {
366           if (isArray(props.record?.submitCbs)) {
aa596a 367             const validFns = (props.record?.validCbs || []).map((fn) => fn());
9c2f3f 368
aa596a 369             const res = await Promise.all(validFns);
4f8e1c 370
9c2f3f 371             const pass = res.every((item) => !!item);
V 372
373             if (!pass) return;
374             const submitFns = props.record?.submitCbs || [];
4f8e1c 375             submitFns.forEach((fn) => fn(false, false));
de5bf7 376             table.emit?.('edit-row-end');
9c2f3f 377             return true;
V 378           }
379         };
380       }
381
382       return {
383         isEdit,
384         prefixCls,
385         handleEdit,
386         currentValueRef,
387         handleSubmit,
388         handleChange,
389         handleCancel,
390         elRef,
391         getComponent,
392         getRule,
393         onClickOutside,
394         ruleMessage,
395         getRuleVisible,
396         getComponentProps,
397         handleOptionsChange,
398         getWrapperStyle,
8eaf57 399         getWrapperClass,
9c2f3f 400         getRowEditable,
9ea257 401         getValues,
64533f 402         handleEnter,
797189 403         handleSubmitClick,
2c867b 404         spinning,
9c2f3f 405       };
V 406     },
407   });
408 </script>
409 <style lang="less">
410   @prefix-cls: ~'@{namespace}-editable-cell';
411
8eaf57 412   .edit-cell-align-left {
413     text-align: left;
414
415     input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
416       text-align: left;
417     }
418   }
419
420   .edit-cell-align-center {
421     text-align: center;
422
423     input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
424       text-align: center;
425     }
426   }
427
428   .edit-cell-align-right {
429     text-align: right;
430
431     input:not(.ant-calendar-picker-input, .ant-time-picker-input) {
432       text-align: right;
433     }
434   }
435
9c2f3f 436   .edit-cell-rule-popover {
V 437     .ant-popover-inner-content {
438       padding: 4px 8px;
439       color: @error-color;
440       // border: 1px solid @error-color;
441       border-radius: 2px;
442     }
443   }
444   .@{prefix-cls} {
445     position: relative;
446
447     &__wrapper {
448       display: flex;
449       align-items: center;
450       justify-content: center;
4f8e1c 451
V 452       > .ant-select {
453         min-width: calc(100% - 50px);
454       }
9c2f3f 455     }
V 456
457     &__icon {
458       &:hover {
459         transform: scale(1.2);
460
461         svg {
462           color: @primary-color;
463         }
464       }
465     }
466
4bb506 467     .ellipsis-cell {
468       .cell-content {
469         overflow-wrap: break-word;
470         word-break: break-word;
471         overflow: hidden;
472         white-space: nowrap;
473         text-overflow: ellipsis;
474       }
475     }
476
9c2f3f 477     &__normal {
V 478       &-icon {
479         position: absolute;
480         top: 4px;
481         right: 0;
482         display: none;
483         width: 20px;
484         cursor: pointer;
485       }
486     }
487
488     &:hover {
489       .@{prefix-cls}__normal-icon {
490         display: inline-block;
491       }
492     }
493   }
494 </style>