ソースを参照

chore(cascader): use icons-vue-taro

richard1015 3 年 前
コミット
c7d5f45313

+ 317 - 0
src/packages/__VUE/cascader/cascader-item.taro.vue

@@ -0,0 +1,317 @@
+<template>
+  <nut-tabs class="nut-cascader" v-model="tabsCursor" @click="handleTabClick" title-scroll>
+    <template v-if="!initLoading && panes.length">
+      <nut-tab-pane v-for="(pane, index) in panes" :title="formatTabTitle(pane)" :key="index">
+        <view role="menu" class="nut-cascader-pane">
+          <template v-for="node in pane.nodes" :key="node.value">
+            <view
+              class="nut-cascader-item"
+              :aria-checked="isSelected(pane, node)"
+              :aria-disabled="node.disabled || undefined"
+              :class="{ active: isSelected(pane, node), disabled: node.disabled }"
+              role="menuitemradio"
+              @click="handleNode(node, false)"
+            >
+              <view class="nut-cascader-item__title">{{ node.text }}</view>
+
+              <Loading v-if="node.loading" class="nut-cascader-item__icon-loading" name="loading" />
+              <Checklist v-else class="nut-cascader-item__icon-check" name="checklist" />
+            </view>
+          </template>
+        </view>
+      </nut-tab-pane>
+    </template>
+    <template v-else>
+      <nut-tab-pane title="Loading...">
+        <view class="nut-cascader-pane"></view>
+      </nut-tab-pane>
+    </template>
+  </nut-tabs>
+</template>
+<script lang="ts">
+import { watch, ref, Ref, computed } from 'vue';
+import { createComponent } from '@/packages/utils/create';
+const { create, translate } = createComponent('cascader-item');
+import { convertListToOptions } from './helper';
+import { CascaderPane, CascaderOption, CascaderValue, convertConfig, Tabs } from './types';
+import { Loading, Checklist } from '@nutui/icons-vue-taro';
+import Tree from './tree';
+
+export default create({
+  components: { Loading, Checklist },
+  props: {
+    visible: Boolean,
+    modelValue: Array,
+    options: {
+      type: Array,
+      default: () => []
+    },
+    lazy: Boolean,
+    lazyLoad: Function,
+    valueKey: {
+      type: String,
+      default: 'value'
+    },
+    textKey: {
+      type: String,
+      default: 'text'
+    },
+    childrenKey: {
+      type: String,
+      default: 'children'
+    },
+    convertConfig: Object
+  },
+  emits: ['update:modelValue', 'change', 'pathChange'],
+
+  setup(props, { emit }) {
+    const configs = computed(() => ({
+      lazy: props.lazy,
+      lazyLoad: props.lazyLoad,
+      valueKey: props.valueKey,
+      textKey: props.textKey,
+      childrenKey: props.childrenKey,
+      convertConfig: props.convertConfig
+    }));
+
+    const tabsCursor = ref(0);
+    const initLoading = ref(false);
+    const innerValue: Ref<CascaderValue> = ref(props.modelValue as CascaderValue);
+    const tree: Ref<Tree> = ref(new Tree([], {}));
+    const panes: Ref<CascaderPane[]> = ref([]);
+    const isLazy = computed(() => configs.value.lazy && Boolean(configs.value.lazyLoad));
+
+    const lazyLoadMap = new Map();
+    let currentProcessNode: CascaderOption | null;
+    const init = async () => {
+      lazyLoadMap.clear();
+      panes.value = [];
+      tabsCursor.value = 0;
+      initLoading.value = false;
+      currentProcessNode = null;
+
+      let { options } = props;
+
+      if (configs.value.convertConfig) {
+        options = convertListToOptions(options as CascaderOption[], configs.value.convertConfig as convertConfig);
+      }
+
+      tree.value = new Tree(options as CascaderOption[], {
+        value: configs.value.valueKey,
+        text: configs.value.textKey,
+        children: configs.value.childrenKey
+      });
+
+      if (isLazy.value && !tree.value.nodes.length) {
+        await invokeLazyLoad({
+          root: true,
+          loading: true,
+          text: '',
+          value: ''
+        });
+      }
+
+      panes.value = [{ nodes: tree.value.nodes, selectedNode: null }];
+      syncValue();
+    };
+
+    const syncValue = async () => {
+      const currentValue = innerValue.value;
+
+      if (currentValue === undefined || !tree.value.nodes.length) {
+        return;
+      }
+
+      if (currentValue.length === 0) {
+        tabsCursor.value = 0;
+        panes.value = [{ nodes: tree.value.nodes, selectedNode: null }];
+        return;
+      }
+
+      let needToSync = currentValue;
+
+      if (isLazy.value && Array.isArray(currentValue) && currentValue.length) {
+        needToSync = [];
+        let parent = tree.value.nodes.find((node) => node.value === currentValue[0]);
+
+        if (parent) {
+          needToSync = [parent.value];
+          initLoading.value = true;
+
+          const last = await currentValue.slice(1).reduce(async (p: Promise<CascaderOption | void>, value) => {
+            const parent = await p;
+
+            await invokeLazyLoad(parent);
+            const node = parent?.children?.find((item) => item.value === value);
+
+            if (node) {
+              needToSync.push(value);
+            }
+
+            return Promise.resolve(node);
+          }, Promise.resolve(parent));
+
+          await invokeLazyLoad(last);
+
+          initLoading.value = false;
+        }
+      }
+
+      if (needToSync.length && currentValue === props.modelValue) {
+        const pathNodes = tree.value.getPathNodesByValue(needToSync);
+        pathNodes.map((node, index) => {
+          tabsCursor.value = index;
+          methods.handleNode(node, true);
+        });
+      }
+    };
+
+    const invokeLazyLoad = async (node?: CascaderOption | void) => {
+      if (!node) {
+        return;
+      }
+
+      if (!configs.value.lazyLoad) {
+        node.leaf = true;
+        return;
+      }
+
+      if (tree.value.isLeaf(node, isLazy.value) || tree.value.hasChildren(node, isLazy.value)) {
+        return;
+      }
+
+      node.loading = true;
+
+      const parent = node.root ? null : node;
+      let lazyLoadPromise = lazyLoadMap.get(node);
+
+      if (!lazyLoadPromise) {
+        lazyLoadPromise = new Promise((resolve) => {
+          // 外部必须resolve
+          configs.value.lazyLoad?.(node, resolve);
+        });
+        lazyLoadMap.set(node, lazyLoadPromise);
+      }
+
+      const nodes: CascaderOption[] | void = await lazyLoadPromise;
+
+      if (Array.isArray(nodes) && nodes.length > 0) {
+        tree.value.updateChildren(nodes, parent);
+      } else {
+        // 如果加载完成后没有提供子节点,作为叶子节点处理
+        node.leaf = true;
+      }
+
+      node.loading = false;
+      lazyLoadMap.delete(node);
+    };
+
+    const emitChange = (pathNodes: CascaderOption[]) => {
+      const emitValue = pathNodes.map((node) => node.value);
+
+      innerValue.value = emitValue;
+      emit('change', emitValue, pathNodes);
+      emit('update:modelValue', emitValue, pathNodes);
+    };
+
+    const methods = {
+      // 选中一个节点,静默模式不触发事件
+      async handleNode(node: CascaderOption, silent?: boolean) {
+        const { disabled, loading } = node;
+
+        if ((!silent && disabled) || !panes.value[tabsCursor.value]) {
+          return;
+        }
+
+        if (tree.value.isLeaf(node, isLazy.value)) {
+          node.leaf = true;
+          panes.value[tabsCursor.value].selectedNode = node;
+          panes.value = panes.value.slice(0, (node.level as number) + 1);
+
+          if (!silent) {
+            const pathNodes = panes.value.map((pane) => pane.selectedNode);
+
+            emitChange(pathNodes as CascaderOption[]);
+            emit('pathChange', pathNodes);
+          }
+          return;
+        }
+
+        if (tree.value.hasChildren(node, isLazy.value)) {
+          const level = (node.level as number) + 1;
+
+          panes.value[tabsCursor.value].selectedNode = node;
+          panes.value = panes.value.slice(0, level);
+          panes.value.push({
+            nodes: node.children || [],
+            selectedNode: null
+          });
+
+          tabsCursor.value = level;
+
+          if (!silent) {
+            const pathNodes = panes.value.map((pane) => pane.selectedNode);
+            emit('pathChange', pathNodes);
+          }
+          return;
+        }
+
+        currentProcessNode = node;
+
+        if (loading) {
+          return;
+        }
+
+        await invokeLazyLoad(node);
+
+        if (currentProcessNode === node) {
+          panes.value[tabsCursor.value].selectedNode = node;
+          methods.handleNode(node, silent);
+        }
+      },
+      handleTabClick(tab: Tabs) {
+        currentProcessNode = null;
+        tabsCursor.value = Number(tab.paneKey);
+      },
+      formatTabTitle(pane: CascaderPane) {
+        return pane.selectedNode ? pane.selectedNode.text : translate('select');
+      },
+      isSelected(pane: CascaderPane, node: CascaderOption) {
+        return pane?.selectedNode?.value === node.value;
+      }
+    };
+
+    watch(
+      [configs, () => props.options],
+      () => {
+        init();
+      },
+      {
+        deep: true,
+        immediate: true
+      }
+    );
+    watch(
+      () => props.modelValue,
+      (value) => {
+        if (value !== innerValue.value) {
+          innerValue.value = value as CascaderValue;
+          syncValue();
+        }
+      }
+    );
+    watch(
+      () => props.visible,
+      (val) => {
+        // console.log('watch: props.visible', val);
+        // TODO: value为空时,保留上次选择记录,修复单元测试问题
+        if (val && Array.isArray(innerValue.value) && innerValue.value.length > 0) {
+          syncValue();
+        }
+      }
+    );
+
+    return { panes, initLoading, tabsCursor, ...methods };
+  }
+});
+</script>

+ 4 - 3
src/packages/__VUE/cascader/cascader-item.vue

@@ -14,8 +14,8 @@
             >
               <view class="nut-cascader-item__title">{{ node.text }}</view>
 
-              <nut-icon v-if="node.loading" class="nut-cascader-item__icon-loading" name="loading" />
-              <nut-icon v-else class="nut-cascader-item__icon-check" name="checklist" />
+              <Loading v-if="node.loading" class="nut-cascader-item__icon-loading" name="loading" />
+              <Checklist v-else class="nut-cascader-item__icon-check" name="checklist" />
             </view>
           </template>
         </view>
@@ -34,9 +34,11 @@ import { createComponent } from '@/packages/utils/create';
 const { create, translate } = createComponent('cascader-item');
 import { convertListToOptions } from './helper';
 import { CascaderPane, CascaderOption, CascaderValue, convertConfig, Tabs } from './types';
+import { Loading, Checklist } from '@nutui/icons-vue';
 import Tree from './tree';
 
 export default create({
+  components: { Loading, Checklist },
   props: {
     visible: Boolean,
     modelValue: Array,
@@ -60,7 +62,6 @@ export default create({
     },
     convertConfig: Object
   },
-  components: {},
   emits: ['update:modelValue', 'change', 'pathChange'],
 
   setup(props, { emit }) {

+ 1 - 1
src/packages/__VUE/cascader/index.taro.vue

@@ -52,7 +52,7 @@ import { CascaderValue, CascaderOption } from './types';
 import { createComponent } from '@/packages/utils/create';
 import { popupProps } from '../popup/props';
 const { create } = createComponent('cascader');
-import CascaderItem from './cascader-item.vue';
+import CascaderItem from './cascader-item.taro.vue';
 import Popup from '../popup/index.taro.vue';
 
 export default create({