瀏覽代碼

添加 Form ApiTransfer , 修复标签页切换灰屏不显示内容问题 (#2052)

* Table BasicColumn 添加 editDynamicDisabled
Co-authored-by: Cyrus Zhou <6802207@qq.com>
使用方式同 Form FormSchema dynamicDisabled
```
export const Columns: BasicColumn[] = [
  {
    title: 'Title',
    dataIndex: 'Title',
    editRow: true,
    editComponent: 'Select',
    editDynamicDisabled: ({ record }) => record.isDisabled,
  },

* editComponentProps onChange 功能恢复
Co-authored-by: Cyrus Zhou <6802207@qq.com>
说明:
...omit(compProps, 'onChange')
这会忽略 onChange ,导致 editComponentProps onChange 被取消

如下功能将不支持:
```
editComponentProps: ({ record }) => {
  return {
    options: effectTypeData,
    onChange: () => {
    },
  };
},
```

* tableData == null 报错

* ApiSelect 第一次选择触发required错误提示问题

* 恢复 虽然可以解决第一次选择提示报错问题,但是会导致 onChange: (e: any, options: any) => 无法获得 options 的值

* 修复标签页切换灰屏不显示内容问题
Co-authored-by: Cyrus Zhou <6802207@qq.com>
问题描述页面没有用 div 包括 会提示 Component inside <Transition> renders non-element root node that cannot be animated ,
导致页灰屏必须刷新页面才可以显示内容

* 添加 Form ApiTransfer
## 使用方式
api 方式:
```
    ......
    component: 'ApiTransfer',
    componentProps: {
       api: sysUserSelector,
       labelField: 'name',
       valueField: 'id',
    },
    .....
```
数据方式:
```
     ....
     componentProps: {
      dataSource: [
        { title: 'Test01', key: '0', disabled: false, description: 'description 01' },
        { title: 'Test02', key: '1', disabled: false, description: 'description 02' },
        { title: 'Test03', key: '2', disabled: false, description: 'description 03' },
        { title: 'Test04', key: '3', disabled: false, description: 'description 04' },
        { title: 'Test05', key: '4', disabled: false, description: 'description 05' },
      ],
    },
    ....
```
Cyrus Zhou 2 年之前
父節點
當前提交
40071529d2

+ 1 - 0
src/components/Form/index.ts

@@ -12,5 +12,6 @@ export { default as ApiTreeSelect } from './src/components/ApiTreeSelect.vue';
 export { default as ApiTree } from './src/components/ApiTree.vue';
 export { default as ApiRadioGroup } from './src/components/ApiRadioGroup.vue';
 export { default as ApiCascader } from './src/components/ApiCascader.vue';
+export { default as ApiTransfer } from './src/components/ApiTransfer.vue';
 
 export { BasicForm };

+ 2 - 0
src/components/Form/src/componentMap.ts

@@ -27,6 +27,7 @@ import ApiSelect from './components/ApiSelect.vue';
 import ApiTree from './components/ApiTree.vue';
 import ApiTreeSelect from './components/ApiTreeSelect.vue';
 import ApiCascader from './components/ApiCascader.vue';
+import ApiTransfer from './components/ApiTransfer.vue';
 import { BasicUpload } from '/@/components/Upload';
 import { StrengthMeter } from '/@/components/StrengthMeter';
 import { IconPicker } from '/@/components/Icon';
@@ -57,6 +58,7 @@ componentMap.set('ApiCascader', ApiCascader);
 componentMap.set('Cascader', Cascader);
 componentMap.set('Slider', Slider);
 componentMap.set('Rate', Rate);
+componentMap.set('ApiTransfer', ApiTransfer);
 
 componentMap.set('DatePicker', DatePicker);
 componentMap.set('MonthPicker', DatePicker.MonthPicker);

+ 135 - 0
src/components/Form/src/components/ApiTransfer.vue

@@ -0,0 +1,135 @@
+<template>
+  <Transfer
+    :data-source="getdataSource"
+    show-search
+    :filter-option="filterOption"
+    :render="(item) => item.title"
+    :showSelectAll="showSelectAll"
+    :selectedKeys="selectedKeys"
+    :targetKeys="getTargetKeys"
+    :showSearch="showSearch"
+    @change="handleChange"
+  />
+</template>
+
+<script lang="ts">
+  import { computed, defineComponent, watch, ref, unref, watchEffect } from 'vue';
+  import { Transfer } from 'ant-design-vue';
+  import { isFunction } from '/@/utils/is';
+  import { get, omit } from 'lodash-es';
+  import { propTypes } from '/@/utils/propTypes';
+  import { useI18n } from '/@/hooks/web/useI18n';
+  import { TransferDirection, TransferItem } from 'ant-design-vue/lib/transfer';
+  export default defineComponent({
+    name: 'ApiTransfer',
+    components: { Transfer },
+    props: {
+      value: { type: Array<string> },
+      api: {
+        type: Function as PropType<(arg?: Recordable) => Promise<TransferItem[]>>,
+        default: null,
+      },
+      params: { type: Object },
+      dataSource: { type: Array<TransferItem> },
+      immediate: propTypes.bool.def(true),
+      alwaysLoad: propTypes.bool.def(false),
+      afterFetch: { type: Function as PropType<Fn> },
+      resultField: propTypes.string.def(''),
+      labelField: propTypes.string.def('title'),
+      valueField: propTypes.string.def('key'),
+      showSearch: { type: Boolean, default: false },
+      disabled: { type: Boolean, default: false },
+      filterOption: {
+        type: Function as PropType<(inputValue: string, item: TransferItem) => boolean>,
+      },
+      selectedKeys: { type: Array<string> },
+      showSelectAll: { type: Boolean, default: false },
+      targetKeys: { type: Array<string> },
+    },
+    emits: ['options-change', 'change'],
+    setup(props, { attrs, emit }) {
+      const _dataSource = ref<TransferItem[]>([]);
+      const _targetKeys = ref<string[]>([]);
+      const { t } = useI18n();
+
+      const getAttrs = computed(() => {
+        return {
+          ...(!props.api ? { dataSource: unref(_dataSource) } : {}),
+          ...attrs,
+        };
+      });
+      const getdataSource = computed(() => {
+        const { labelField, valueField } = props;
+
+        return unref(_dataSource).reduce((prev, next: Recordable) => {
+          if (next) {
+            prev.push({
+              ...omit(next, [labelField, valueField]),
+              title: next[labelField],
+              key: next[valueField],
+            });
+          }
+          return prev;
+        }, [] as TransferItem[]);
+      });
+      const getTargetKeys = computed<string[]>(() => {
+        if (unref(_targetKeys).length > 0) {
+          return unref(_targetKeys);
+        }
+        if (Array.isArray(props.value)) {
+          return props.value;
+        }
+        return [];
+      });
+
+      function handleChange(keys: string[], direction: TransferDirection, moveKeys: string[]) {
+        _targetKeys.value = keys;
+        console.log(direction);
+        console.log(moveKeys);
+        emit('change', keys);
+      }
+
+      watchEffect(() => {
+        props.immediate && !props.alwaysLoad && fetch();
+      });
+
+      watch(
+        () => props.params,
+        () => {
+          fetch();
+        },
+        { deep: true },
+      );
+
+      async function fetch() {
+        const api = props.api;
+        if (!api || !isFunction(api)) {
+          if (Array.isArray(props.dataSource)) {
+            _dataSource.value = props.dataSource;
+          }
+          return;
+        }
+        _dataSource.value = [];
+        try {
+          const res = await api(props.params);
+          if (Array.isArray(res)) {
+            _dataSource.value = res;
+            emitChange();
+            return;
+          }
+          if (props.resultField) {
+            _dataSource.value = get(res, props.resultField) || [];
+          }
+          emitChange();
+        } catch (error) {
+          console.warn(error);
+        } finally {
+        }
+      }
+      function emitChange() {
+        emit('options-change', unref(getdataSource));
+      }
+      return { getTargetKeys, getdataSource, t, getAttrs, handleChange };
+    },
+  });
+</script>

+ 2 - 1
src/components/Form/src/types/index.ts

@@ -113,4 +113,5 @@ export type ComponentType =
   | 'Render'
   | 'Slider'
   | 'Rate'
-  | 'Divider';
+  | 'Divider'
+  | 'ApiTransfer';

+ 6 - 2
src/layouts/page/index.vue

@@ -15,9 +15,13 @@
         appear
       >
         <keep-alive v-if="openCache" :include="getCaches">
-          <component :is="Component" :key="route.fullPath" />
+          <div :key="route.name">
+            <component :is="Component" :key="route.fullPath" />
+          </div>
         </keep-alive>
-        <component v-else :is="Component" :key="route.fullPath" />
+        <div v-else :key="route.name">
+          <component :is="Component" :key="route.fullPath" />
+        </div>
       </transition>
     </template>
   </RouterView>