雪忆
2024-07-29 ecfe66a0199606241c73a52519bbe800c9aa31f8
src/components/Table/src/components/editable/EditableCell.vue
@@ -1,63 +1,39 @@
<template>
  <div :class="prefixCls">
    <div v-show="!isEdit" :class="`${prefixCls}__normal`" @click="handleEdit">
      {{ getValues || '&nbsp;' }}
      <FormOutlined :class="`${prefixCls}__normal-icon`" v-if="!column.editRow" />
    </div>
    <div v-if="isEdit" :class="`${prefixCls}__wrapper`" v-click-outside="onClickOutside">
      <CellComponent
        v-bind="getComponentProps"
        :component="getComponent"
        :style="getWrapperStyle"
        :popoverVisible="getRuleVisible"
        :rule="getRule"
        :ruleMessage="ruleMessage"
        :class="getWrapperClass"
        size="small"
        ref="elRef"
        @change="handleChange"
        @options-change="handleOptionsChange"
        @pressEnter="handleEnter"
      />
      <div :class="`${prefixCls}__action`" v-if="!getRowEditable">
        <CheckOutlined :class="[`${prefixCls}__icon`, 'mx-2']" @click="handleSubmit" />
        <CloseOutlined :class="`${prefixCls}__icon `" @click="handleCancel" />
      </div>
    </div>
  </div>
</template>
<script lang="ts">
<script lang="tsx">
  import type { CSSProperties, PropType } from 'vue';
  import { computed, defineComponent, nextTick, ref, toRaw, unref, watchEffect } from 'vue';
  import type { BasicColumn } from '../../types/table';
  import type { EditRecordRow } from './index';
  import { defineComponent, ref, unref, nextTick, computed, watchEffect, toRaw } from 'vue';
  import { FormOutlined, CloseOutlined, CheckOutlined } from '@ant-design/icons-vue';
  import { CheckOutlined, CloseOutlined, FormOutlined } from '@ant-design/icons-vue';
  import { CellComponent } from './CellComponent';
  import { useDesign } from '/@/hooks/web/useDesign';
  import { useDesign } from '@/hooks/web/useDesign';
  import { useTableContext } from '../../hooks/useTableContext';
  import clickOutside from '/@/directives/clickOutside';
  import clickOutside from '@/directives/clickOutside';
  import { propTypes } from '/@/utils/propTypes';
  import { isString, isBoolean, isFunction, isNumber, isArray } from '/@/utils/is';
  import { propTypes } from '@/utils/propTypes';
  import { isArray, isBoolean, isFunction, isNumber, isString } from '@/utils/is';
  import { createPlaceholderMessage } from './helper';
  import { pick, set } from 'lodash-es';
  import { treeToList } from '@/utils/helper/treeHelper';
  import { Spin } from 'ant-design-vue';
  import { parseRowKey } from '../../helper';
  import { warn } from '@/utils/log';
  export default defineComponent({
    name: 'EditableCell',
    components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent },
    components: { FormOutlined, CloseOutlined, CheckOutlined, CellComponent, Spin },
    directives: {
      clickOutside,
    },
    props: {
      value: {
        type: [String, Number, Boolean, Object] as PropType<string | number | boolean | Recordable>,
        type: [String, Number, Boolean, Object] as PropType<
          string | number | boolean | Record<string, any>
        >,
        default: '',
      },
      record: {
        type: Object as PropType<EditRecordRow>,
        type: Object as any,
      },
      column: {
        type: Object as PropType<BasicColumn>,
@@ -71,9 +47,10 @@
      const elRef = ref();
      const ruleVisible = ref(false);
      const ruleMessage = ref('');
      const optionsRef = ref<LabelValueOptions>([]);
      const optionsRef = ref([]);
      const currentValueRef = ref<any>(props.value);
      const defaultValueRef = ref<any>(props.value);
      const spinning = ref<boolean>(false);
      const { prefixCls } = useDesign('editable-cell');
@@ -89,31 +66,74 @@
        return ['Checkbox', 'Switch'].includes(component);
      });
      const getComponentProps = computed(() => {
        const compProps = props.column?.editComponentProps ?? {};
        const component = unref(getComponent);
        const apiSelectProps: Recordable = {};
        if (component === 'ApiSelect') {
          apiSelectProps.cache = true;
      const getDisable = computed(() => {
        const { editDynamicDisabled } = props.column;
        let disabled = false;
        if (isBoolean(editDynamicDisabled)) {
          disabled = editDynamicDisabled;
        }
        if (isFunction(editDynamicDisabled)) {
          const { record } = props;
          disabled = editDynamicDisabled({ record, currentValue: currentValueRef.value });
        }
        return disabled;
      });
      const getComponentProps = computed(() => {
        const isCheckValue = unref(getIsCheckComp);
        let compProps = props.column?.editComponentProps ?? ({} as any);
        const { checkedValue, unCheckedValue } = compProps;
        const valueField = isCheckValue ? 'checked' : 'value';
        const val = unref(currentValueRef);
        const value = isCheckValue ? (isNumber(val) && isBoolean(val) ? val : !!val) : val;
        let value = val;
        if (isCheckValue) {
          if (typeof checkedValue !== 'undefined') {
            value = val === checkedValue ? checkedValue : unCheckedValue;
          } else if (typeof unCheckedValue !== 'undefined') {
            value = val === unCheckedValue ? unCheckedValue : checkedValue;
          } else {
            value = isNumber(val) || isBoolean(val) ? val : !!val;
          }
        }
        const { record, column, index } = props;
        if (isFunction(compProps)) {
          compProps = compProps({ text: val, record, column, index }) ?? {};
        }
        // 用临时变量存储 onChange方法 用于 handleChange方法 获取,并删除原始onChange, 防止存在两个 onChange
        compProps.onChangeTemp = compProps.onChange;
        delete compProps.onChange;
        const component = unref(getComponent);
        const apiSelectProps: Record<string, any> = {};
        if (component === 'ApiSelect') {
          apiSelectProps.cache = true;
        }
        upEditDynamicDisabled(record, column, value);
        return {
          size: 'small',
          getPopupContainer: () => unref(table?.wrapRef.value) ?? document.body,
          placeholder: createPlaceholderMessage(unref(getComponent)),
          ...apiSelectProps,
          ...compProps,
          [valueField]: value,
        };
          disabled: unref(getDisable),
        } as any;
      });
      function upEditDynamicDisabled(record, column, value) {
        if (!record) return false;
        const { key, dataIndex } = column;
        if (!key && !dataIndex) return;
        const dataKey = (dataIndex || key) as string;
        set(record, dataKey, value);
      }
      const getValues = computed(() => {
        const { editComponentProps, editValueMap } = props.column;
        const { editValueMap } = props.column;
        const value = unref(currentValueRef);
@@ -122,14 +142,19 @@
        }
        const component = unref(getComponent);
        if (!component.includes('Select')) {
        if (!component.includes('Select') && !component.includes('Radio')) {
          return value;
        }
        const options: LabelValueOptions = editComponentProps?.options ?? (unref(optionsRef) || []);
        const options = unref(getComponentProps)?.options ?? (unref(optionsRef) || []);
        const option = options.find((item) => `${item.value}` === `${value}`);
        return option?.label ?? value;
      });
      const getRowEditable = computed(() => {
        const { editable } = props.record || {};
        return !!editable;
      });
      const getWrapperStyle = computed((): CSSProperties => {
@@ -146,13 +171,9 @@
        return `edit-cell-align-${align}`;
      });
      const getRowEditable = computed(() => {
        const { editable } = props.record || {};
        return !!editable;
      });
      watchEffect(() => {
        defaultValueRef.value = props.value;
        // defaultValueRef.value = props.value;
        currentValueRef.value = props.value;
      });
      watchEffect(() => {
@@ -162,8 +183,9 @@
        }
      });
      function handleEdit() {
        if (unref(getRowEditable) || unref(props.column?.editRow)) return;
      function handleEdit(e) {
        e.stopPropagation();
        if (unref(getRowEditable) || unref(props.column?.editRow) || unref(getDisable)) return;
        ruleMessage.value = '';
        isEdit.value = true;
        nextTick(() => {
@@ -172,27 +194,31 @@
        });
      }
      async function handleChange(e: any) {
      async function handleChange(e: any, ...rest: any[]) {
        const component = unref(getComponent);
        if (!e) {
          currentValueRef.value = e;
        } else if (e?.target && Reflect.has(e.target, 'value')) {
          currentValueRef.value = (e as ChangeEvent).target.value;
        } else if (component === 'Checkbox') {
          currentValueRef.value = (e as ChangeEvent).target.checked;
        } else if (isString(e) || isBoolean(e) || isNumber(e)) {
          currentValueRef.value = e.target.checked;
        } else if (component === 'Switch') {
          currentValueRef.value = e;
        } else if (e?.target && Reflect.has(e.target, 'value')) {
          currentValueRef.value = e.target.value;
        } else if (isString(e) || isBoolean(e) || isNumber(e) || isArray(e)) {
          currentValueRef.value = e;
        }
        const onChange = unref(getComponentProps)?.onChangeTemp;
        if (onChange && isFunction(onChange)) onChange(e, ...rest);
        table.emit?.('edit-change', {
          column: props.column,
          value: unref(currentValueRef),
          record: toRaw(props.record),
        });
        handleSubmiRule();
        handleSubmitRule();
      }
      async function handleSubmiRule() {
      async function handleSubmitRule() {
        const { column, record } = props;
        const { editRule } = column;
        const currentValue = unref(currentValueRef);
@@ -201,13 +227,12 @@
          if (isBoolean(editRule) && !currentValue && !isNumber(currentValue)) {
            ruleVisible.value = true;
            const component = unref(getComponent);
            const message = createPlaceholderMessage(component);
            ruleMessage.value = message;
            ruleMessage.value = createPlaceholderMessage(component);
            return false;
          }
          if (isFunction(editRule)) {
            const res = await editRule(currentValue, record as Recordable);
            if (!!res) {
            const res = await editRule(currentValue, record);
            if (res) {
              ruleMessage.value = res;
              ruleVisible.value = true;
              return false;
@@ -223,19 +248,54 @@
      async function handleSubmit(needEmit = true, valid = true) {
        if (valid) {
          const isPass = await handleSubmiRule();
          const isPass = await handleSubmitRule();
          if (!isPass) return false;
        }
        const { column, index } = props;
        const { column, index, record } = props;
        if (!record) return false;
        const { key, dataIndex } = column;
        const value = unref(currentValueRef);
        if (!key || !dataIndex) return;
        if (!key && !dataIndex) return;
        const dataKey = (dataIndex || key) as string;
        const record = await table.updateTableData(index, dataKey, value);
        needEmit && table.emit?.('edit-end', { record, index, key, value });
        if (!record.editable) {
          const { getBindValues } = table;
          const { beforeEditSubmit, columns, rowKey } = unref(getBindValues);
          const rowKeyParsed = parseRowKey(rowKey, record);
          if (beforeEditSubmit && isFunction(beforeEditSubmit)) {
            spinning.value = true;
            const keys: string[] = columns
              .map((_column) => _column.dataIndex)
              .filter((field) => !!field) as string[];
            let result: any = true;
            try {
              result = await beforeEditSubmit({
                record: pick(record, [rowKeyParsed, ...keys]),
                index,
                key: dataKey as string,
                value,
              });
            } catch (e) {
              result = false;
              warn(e);
            } finally {
              spinning.value = false;
            }
            if (result === false) {
              return;
            }
          }
        }
        set(record, dataKey, value);
        defaultValueRef.value = value;
        //const record = await table.updateTableData(index, dataKey, value);
        needEmit && table.emit?.('edit-end', { record, index, key: dataKey, value });
        isEdit.value = false;
      }
@@ -243,6 +303,10 @@
        if (props.column?.editRow) {
          return;
        }
        handleSubmit();
      }
      function handleSubmitClick() {
        handleSubmit();
      }
@@ -270,12 +334,26 @@
        }
      }
      // only ApiSelect
      function handleOptionsChange(options: LabelValueOptions) {
        optionsRef.value = options;
      // only ApiSelect or TreeSelect
      function handleOptionsChange(options) {
        const { replaceFields } = unref(getComponentProps);
        const component = unref(getComponent);
        if (component === 'ApiTreeSelect') {
          const { title = 'title', value = 'value', children = 'children' } = replaceFields || {};
          let listOptions = treeToList(options, { children });
          listOptions = listOptions.map((item) => {
            return {
              label: item[title],
              value: item[value],
            };
          });
          optionsRef.value = listOptions;
        } else {
          optionsRef.value = options;
        }
      }
      function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle: Fn) {
      function initCbs(cbs: 'submitCbs' | 'validCbs' | 'cancelCbs', handle) {
        if (props.record) {
          /* eslint-disable  */
          isArray(props.record[cbs])
@@ -286,27 +364,19 @@
      if (props.record) {
        initCbs('submitCbs', handleSubmit);
        initCbs('validCbs', handleSubmiRule);
        initCbs('validCbs', handleSubmitRule);
        initCbs('cancelCbs', handleCancel);
        if (props.column.dataIndex) {
          if (!props.record.editValueRefs) props.record.editValueRefs = {};
          props.record.editValueRefs[props.column.dataIndex] = currentValueRef;
          props.record.editValueRefs[props.column.dataIndex as any] = currentValueRef;
        }
        /* eslint-disable  */
        props.record.onCancelEdit = () => {
          isArray(props.record?.cancelCbs) && props.record?.cancelCbs.forEach((fn) => fn());
        };
        /* eslint-disable */
        props.record.onSubmitEdit = async () => {
          if (isArray(props.record?.submitCbs)) {
            const validFns = (props.record?.validCbs || []).map((fn) => fn());
            const res = await Promise.all(validFns);
            const pass = res.every((item) => !!item);
            if (!pass) return;
            if (!props.record?.onValid?.()) return;
            const submitFns = props.record?.submitCbs || [];
            submitFns.forEach((fn) => fn(false, false));
            table.emit?.('edit-row-end');
@@ -336,8 +406,68 @@
        getRowEditable,
        getValues,
        handleEnter,
        // getSize,
        handleSubmitClick,
        spinning,
        getDisable,
      };
    },
    render() {
      return (
        <div class={this.prefixCls}>
          <div
            v-show={!this.isEdit}
            class={{ [`${this.prefixCls}__normal`]: true, 'ellipsis-cell': this.column.ellipsis }}
            onClick={this.handleEdit}
          >
            <div class="cell-content" title={this.column.ellipsis ? this.getValues ?? '' : ''}>
              {this.column.editRender
                ? this.column.editRender({
                    text: this.value,
                    record: this.record as Recordable,
                    column: this.column,
                    index: this.index,
                    currentValue: this.currentValueRef,
                  })
                : this.getValues ?? '\u00A0'}
            </div>
            {!this.column.editRow && !this.getDisable && (
              <FormOutlined class={`${this.prefixCls}__normal-icon`} />
            )}
          </div>
          {this.isEdit && (
            <Spin spinning={this.spinning} onClick={(e) => e.stopPropagation()}>
              <div
                class={`${this.prefixCls}__wrapper`}
                v-click-outside={this.onClickOutside}
                onClick={(e) => e.stopPropagation()}
              >
                <CellComponent
                  {...this.getComponentProps}
                  component={this.getComponent}
                  style={this.getWrapperStyle}
                  popoverVisible={this.getRuleVisible}
                  rule={this.getRule}
                  ruleMessage={this.ruleMessage}
                  class={this.getWrapperClass}
                  ref="elRef"
                  onChange={this.handleChange}
                  onOptionsChange={this.handleOptionsChange}
                  onPressEnter={this.handleEnter}
                />
                {!this.getRowEditable && (
                  <div class={`${this.prefixCls}__action`}>
                    <CheckOutlined
                      class={[`${this.prefixCls}__icon`, 'mx-2']}
                      onClick={this.handleSubmitClick}
                    />
                    <CloseOutlined class={`${this.prefixCls}__icon `} onClick={this.handleCancel} />
                  </div>
                )}
              </div>
            </Spin>
          )}
        </div>
      );
    },
  });
</script>
@@ -371,13 +501,14 @@
  .edit-cell-rule-popover {
    .ant-popover-inner-content {
      padding: 4px 8px;
      color: @error-color;
      // border: 1px solid @error-color;
      border-radius: 2px;
      color: @error-color;
    }
  }
  .@{prefix-cls} {
    position: relative;
    min-height: 24px; // 设置高度让其始终可被hover
    &__wrapper {
      display: flex;
@@ -399,12 +530,22 @@
      }
    }
    .ellipsis-cell {
      .cell-content {
        overflow: hidden;
        text-overflow: ellipsis;
        word-break: break-word;
        white-space: nowrap;
        overflow-wrap: break-word;
      }
    }
    &__normal {
      &-icon {
        display: none;
        position: absolute;
        top: 4px;
        right: 0;
        display: none;
        width: 20px;
        cursor: pointer;
      }