从零搭建Vue3.0组件库之Transfer组件

一.定义组件属性

export type Key = string | number
export type DataItem = {
    key: Key
    label: string
    disabled: boolean
}
export type Props = {                    // 别名
    key: string,     // key => id;
    label: string,   // label => desc
    disabled: string // disabled => dis
}
export interface TransferProps {         // 穿梭框需要的数据 
    data: DataItem[],  // 默认类型
    modelValue: Key[], // 当前选中的是
    props: Props       // 可改名
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16

二.定义组件结构

左右panel和中间button结构

<template>
  <div class="z-transfer">
    <!-- 左边穿梭框 -->
    <TransferPanel></TransferPanel>
    <div class="z-transfer__buttons">
      <z-button type="primary" icon="z-icon-arrow-left-bold"> </z-button> 
      &nbsp;
      <z-button type="primary" icon="z-icon-arrow-right-bold"> </z-button>
    </div>
    <!-- 左边穿梭框 -->
    <TransferPanel></TransferPanel>
  </div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import TransferPanel from "./transfer-panel";
import ZButton from "@z-ui/button";
export default defineComponent({
  name: "ZTransfer",
  components: {
    TransferPanel,
    ZButton,
  },
  props: {
    data: {
      type: Array as PropType<DataItem[]>,
      default: () => [],
    },
    modelValue: {
      type: Array as PropType<Key[]>,
      default: () => [],
    },
    props: {
      type: Object as PropType<Props>,
      default: () => ({
        label: "label",
        key: "key",
        disabled: "disabled",
      }),
    },
  },
});
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43

panel面板主要有:面板头部和面板体

<template>
  <div class="z-transfer-panel">
    <p class="z-transfer-panel__header">
        列表
    </p>
    <div class="z-transfer-panel__body">
       ----
    </div>
  </div>
</template>
1
2
3
4
5
6
7
8
9
10

三.实现穿梭功能

1.左右面板数据进行拆分

<!-- 左边穿梭框 -->
<TransferPanel :data="sourceData" :props="props"></TransferPanel>
<!-- 左边穿梭框 -->
<TransferPanel :data="targetData" :props="props"></TransferPanel>
1
2
3
4
setup(props) {
    // 1.计算 左右数据
    const { propsKey, sourceData, targetData } = useComputedData(props);
    return {
      sourceData,
      targetData,
    };
},
1
2
3
4
5
6
7
8

根据所有数据和key,进行数据的筛查 useComputedData.ts

import { computed } from "@vue/runtime-core";
import { TransferProps } from "./transfer.type";

export const useComputedData = (props: TransferProps) => {
    const propsKey = computed(() => props.props.key);
    const dataObj = computed(() => {
        return props.data.reduce((memo, cur) => {
            memo[cur[propsKey.value]] = cur;
            return memo
        }, {}); // 根据key 映射原来的对象
    });
    // 通过key 进行数据筛选
    const sourceData = computed(() => {
        return props.data.filter(item => !props.modelValue.includes(item[propsKey.value]))
    });
    // 目标数据
    const targetData = computed(() => {
        return props.modelValue.reduce((arr, cur) => {
            const val = dataObj.value[cur]; // 根据key 映射值,存放到数组中
            if (val) {
                arr.push(val)
            }
            return arr
        }, []);
    });
    return {
        propsKey,
        sourceData,
        targetData
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31

2.面板渲染内容

<template>
  <div class="z-transfer-panel">
    <p class="z-transfer-panel__header">
      <z-checkbox></z-checkbox>
    </p>
    <div class="z-transfer-panel__body">
      <z-checkbox-group class="z-transfer-panel__list">
        <z-checkbox
          class="z-transfer-panel__item"
          v-for="item in data"
          :key="item[keyProp]"
          :label="item[keyProp]"
          :disabled="item[disabledProp]"
        > {{item[labelProp]}}
        </z-checkbox>
      </z-checkbox-group>
    </div>
  </div>
</template>
<script lang="ts">
import { defineComponent } from "vue";
import ZCheckbox from "@z-ui/checkbox";
import ZCheckboxGroup from "@z-ui/checkbox-group";
import {useCheck} from './useCheck'
export default defineComponent({
  components: {
    ZCheckbox,
    ZCheckboxGroup,
  },
  props: {
    data: {
      type: Array,
      default: () => [],
    },
    props:{
        type: Object as PropType<Props>
    }
  },
  setup(props) {
    const { labelProp, keyProp, disabledProp } = useCheck(props);
    return {
        labelProp,
        keyProp,
        disabledProp
    }
  },
});
</script>

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49

获取数据信息

import { computed } from "@vue/runtime-core";
export interface TransferPanelProps {
    data: any[]
    props: Props
}
export const useCheck = (props: TransferPanelProps) => {
    const labelProp = computed(() => props.props.label);
    const keyProp = computed(() => props.props.key);
    const disabledProp = computed(() => props.props.disabled);
    return {
        labelProp,
        keyProp,
        disabledProp
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

3.获取当前选中的值

<z-checkbox v-model="allChecked"  @change="handleAllCheckedChange"></z-checkbox>
1
const panelState = reactive({
    checked: [], // 选中的值
    allChecked: false, // 是否全选
});
const updateAllChecked = () => {
    const checkableDataKeys = props.data.map(item => item[keyProp.value]);
    panelState.allChecked = checkableDataKeys.length > 0 && checkableDataKeys.every(item => panelState.checked.includes(item))
}
watch(() => panelState.checked, (val) => {
    updateAllChecked(); // 更新全选状态
    emit('checked-change',val)
});
const handleAllCheckedChange = (value: Key[]) => { // 更新checked
    panelState.checked = value ? props.data.map(item => item[keyProp.value]) : []
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15

每次选中的时候,将选中的结果传递给父组件

<TransferPanel
      :data="sourceData"
      :props="props"
      @checked-change="onSourceCheckedChange"
></TransferPanel>
<TransferPanel
      :data="targetData"
      :props="props"
	  @checked-change="onTargetCheckedChange"
></TransferPanel>
const checkedState = reactive({
    leftChecked: [],
    rightChecked: [],
});
const onSourceCheckedChange = (val) => {
    checkedState.leftChecked = val;
};
const onTargetCheckedChange = (val) => {
    checkedState.rightChecked = val;
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

将左右选中的内容分别存储到对应的数组中

4.穿梭实现

const addToLeft = () => { // 减少modelValue中的值
    const currentValue = props.modelValue.slice();
    checkedState.rightChecked.forEach((item) => {
        const index = currentValue.indexOf(item);
        if (index > -1) {
            currentValue.splice(index, 1);
        }
    });
    emit("update:modelValue", currentValue);
};

const addToRight = () => {
    let currentValue = props.modelValue.slice(); // 给modelValue添加值
    const itemsToBeMoved = props.data // 在所有数据中晒出选中的
    .filter((item) =>
            checkedState.leftChecked.includes(item[propsKey.value])
           )
    .map((item) => item[propsKey.value]);
    currentValue = currentValue.concat(itemsToBeMoved);
    console.log(currentValue)
    emit("update:modelValue", currentValue);
};
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

四.修复Bug

1.穿梭后清空选中列表

watch(() => props.data, () => {
    const checked = [];
    panelState.checked = checked;
});
1
2
3
4

2.被禁用元素不支持穿梭

const checkableData = computed(() => {// 过滤禁用的数据
 	return props.data.filter(item => !item[disabledProp.value])
});
const updateAllChecked = () => { // 更新checkall
    const checkableDataKeys = checkableData.value.map(item => item[keyProp.value]);
    panelState.allChecked = checkableDataKeys.length > 0 && checkableDataKeys.every(item => panelState.checked.includes(item))
}
const handleAllCheckedChange = (value: Key[]) => { // 更新checked
    panelState.checked = value ? checkableData.value.map(item => item[keyProp.value]) : []
}
1
2
3
4
5
6
7
8
9
10