浏览代码

feat(checkbox): 增加选择限制,反选,半选功能

suzigang 3 年之前
父节点
当前提交
5ad63b6e72

+ 0 - 12
src/packages/__VUE/checkbox/__tests__/checkbox.spec.ts

@@ -145,15 +145,3 @@ test('should emit "update:modelValue" event when checkbox is clicked', async ()
   wrapper.trigger('click');
   expect(wrapper.emitted('update:modelValue')![1]).toEqual([false]);
 });
-
-// test('should emit change event when modelValue is changed', async () => {
-//   const wrapper = mount(Checkbox);
-
-//   wrapper.trigger('click');
-//   await wrapper.setProps({ modelValue: true });
-//   expect(wrapper.emitted('change')![0]).toEqual([true]);
-
-//   wrapper.trigger('click');
-//   await wrapper.setProps({ modelValue: false });
-//   expect(wrapper.emitted('change')![1]).toEqual([false]);
-// });

+ 158 - 0
src/packages/__VUE/checkbox/common.ts

@@ -0,0 +1,158 @@
+import { h, computed, inject, getCurrentInstance, onMounted, reactive, watch } from 'vue';
+
+export const component = (componentName: string, nutIcon: object) => {
+  return {
+    components: {
+      nutIcon
+    },
+    props: {
+      modelValue: {
+        type: Boolean,
+        default: false
+      },
+      disabled: {
+        type: Boolean,
+        default: false
+      },
+      textPosition: {
+        type: String,
+        default: 'right'
+      },
+      iconSize: {
+        type: [String, Number],
+        default: ''
+      },
+      iconName: {
+        type: String,
+        default: 'check-normal'
+      },
+      iconActiveName: {
+        type: String,
+        default: 'checked'
+      },
+      iconIndeterminateName: {
+        type: String,
+        default: 'check-disabled'
+      },
+      label: {
+        type: String,
+        default: ''
+      },
+      iconClassPrefix: {
+        type: String,
+        default: 'nut-icon'
+      },
+      iconFontClassName: {
+        type: String,
+        default: 'nutui-iconfont'
+      },
+      indeterminate: {
+        type: Boolean,
+        default: false
+      }
+    },
+    emits: ['change', 'update:modelValue'],
+    setup(props: any, { emit, slots }: any) {
+      const parent: any = inject('parent', null);
+      const state = reactive({
+        partialSelect: props.indeterminate
+      });
+
+      const hasParent = computed(() => !!parent);
+
+      const pValue = computed(() => {
+        if (hasParent.value) {
+          return parent.value.value.includes(props.label);
+        } else {
+          return props.modelValue;
+        }
+      });
+
+      const pDisabled = computed(() => {
+        return hasParent.value ? (parent.disabled.value ? parent.disabled.value : props.disabled) : props.disabled;
+      });
+
+      const checked = computed(() => !!props.modelValue);
+
+      const color = computed(() => {
+        return !pDisabled.value
+          ? !pValue.value
+            ? 'nut-checkbox__icon--unchecked'
+            : state.partialSelect
+            ? 'nut-checkbox__icon--indeterminate'
+            : 'nut-checkbox__icon'
+          : 'nut-checkbox__icon--disable';
+      });
+
+      const emitChange = (value: string | boolean, label?: string) => {
+        emit('update:modelValue', value);
+        emit('change', value, label);
+      };
+
+      const renderIcon = () => {
+        const { iconName, iconSize, iconActiveName, iconClassPrefix, iconFontClassName, iconIndeterminateName } = props;
+        return h(nutIcon, {
+          name: !pValue.value ? iconName : state.partialSelect ? iconIndeterminateName : iconActiveName,
+          size: iconSize,
+          class: color.value,
+          classPrefix: iconClassPrefix,
+          fontClassName: iconFontClassName
+        });
+      };
+
+      const renderLabel = () => {
+        return h(
+          'view',
+          {
+            class: `${componentName}__label ${pDisabled.value ? `${componentName}__label--disabled` : ''}`
+          },
+          slots.default?.()
+        );
+      };
+
+      const handleClick = (e: MouseEvent | TouchEvent) => {
+        if (pDisabled.value) return;
+        if (checked.value && state.partialSelect) {
+          state.partialSelect = false;
+          emitChange(checked.value, slots.default?.()[0].children as string);
+          return;
+        }
+        emitChange(!checked.value, slots.default?.()[0].children as string);
+        if (hasParent.value) {
+          let value = parent.value.value;
+          let max = parent.max.value;
+          let { label } = props;
+          const index = value.indexOf(label);
+          if (index > -1) {
+            value.splice(index, 1);
+          } else if (index <= -1 && (value.length < max || !max)) {
+            value.push(label);
+          }
+          parent.updateValue(value);
+        }
+      };
+
+      onMounted(() => {
+        hasParent.value && parent['relation'](getCurrentInstance());
+      });
+
+      watch(
+        () => props.indeterminate,
+        (newVal) => {
+          state.partialSelect = newVal;
+        }
+      );
+
+      return () => {
+        return h(
+          'view',
+          {
+            class: `${componentName} ${props.textPosition === 'left' ? `${componentName}--reverse` : ''}`,
+            onClick: handleClick
+          },
+          [renderIcon(), renderLabel()]
+        );
+      };
+    }
+  };
+};

+ 98 - 10
src/packages/__VUE/checkbox/demo.vue

@@ -16,7 +16,13 @@
         <div>{{ checkbox1 }}</div>
       </nut-cell>
     </nut-cell-group>
-
+    <nut-cell-group :title="translate('semi')">
+      <nut-cell>
+        <nut-checkbox v-model="checkbox9" :indeterminate="true" :label="translate('checkbox')">{{
+          translate('checkbox')
+        }}</nut-checkbox>
+      </nut-cell>
+    </nut-cell-group>
     <nut-cell-group :title="translate('disable')">
       <nut-cell>
         <nut-checkbox v-model="checkbox3" disabled>{{ translate('unselectDisable') }}</nut-checkbox>
@@ -81,13 +87,51 @@
         <nut-button type="primary" @click="toggleAll(true)" style="margin: 0 20px 0 0">{{
           translate('selectAll')
         }}</nut-button>
-        <nut-button type="info" @click="toggleAll(false)">{{ translate('cancel') }}</nut-button>
+        <nut-button type="info" @click="toggleAll(false)" style="margin: 0 20px 0 0">{{
+          translate('cancel')
+        }}</nut-button>
+        <nut-button type="warning" @click="toggleReverse()">{{ translate('selectReverse') }}</nut-button>
       </nut-cell>
     </nut-cell-group>
+    <nut-cell-group :title="translate('useGroupLimit')">
+      <nut-cell>
+        <nut-checkboxgroup v-model="checkboxgroup4" :max="2">
+          <nut-checkbox label="1" style="margin: 2px 20px 0 0">{{ translate('combine') }}</nut-checkbox>
+          <nut-checkbox label="2">{{ translate('combine') }}</nut-checkbox>
+          <nut-checkbox label="3" style="margin: 2px 20px 0 0">{{ translate('combine') }}</nut-checkbox>
+          <nut-checkbox label="4">{{ translate('combine') }}</nut-checkbox>
+        </nut-checkboxgroup>
+      </nut-cell>
+      <nut-cell>
+        <div class="demo-check">{{ translate('selected') }}</div>
+        <div>{{ checkboxgroup4 }}</div>
+      </nut-cell>
+    </nut-cell-group>
+    <nut-cell-group :title="translate('useGroupInte')">
+      <nut-cell>
+        <nut-checkbox :indeterminate="indeterminate" v-model="checkbox10" @change="changeBox5">{{
+          translate('selectAll')
+        }}</nut-checkbox>
+      </nut-cell>
+      <nut-checkboxgroup v-model="checkboxgroup5" ref="group2" @change="changeBox6">
+        <nut-cell
+          ><nut-checkbox label="1" style="margin: 2px 20px 0 0">{{ translate('combine') }}</nut-checkbox></nut-cell
+        >
+        <nut-cell
+          ><nut-checkbox label="2">{{ translate('combine') }}</nut-checkbox></nut-cell
+        >
+        <nut-cell
+          ><nut-checkbox label="3">{{ translate('combine') }}</nut-checkbox></nut-cell
+        >
+        <nut-cell
+          ><nut-checkbox label="4">{{ translate('combine') }}</nut-checkbox></nut-cell
+        >
+      </nut-checkboxgroup>
+    </nut-cell-group>
   </div>
 </template>
 <script lang="ts">
-import { reactive, ref, toRefs, onMounted } from 'vue';
+import { reactive, ref, toRefs, onMounted, Ref } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { Toast } from '@/packages/nutui.vue';
 const { createDemo, translate } = createComponent('checkbox');
@@ -95,44 +139,53 @@ import { useTranslate } from '@/sites/assets/util/useTranslate';
 useTranslate({
   'zh-CN': {
     basic: '基本用法-左右',
+    semi: '半选状态',
     disable: '禁用状态',
     size: '自定义尺寸',
     icon: '自定义图标',
     change: '点击触发change事件',
     useGroup: 'checkboxGroup使用',
+    useGroupLimit: 'checkboxGroup使用,限制最大可选数(2个)',
     disableGroup: 'checkboxGroup禁用',
     selectGroup: 'checkboxGroup 全选/取消',
     selectAll: '全选',
     cancel: '取消',
+    selectReverse: '反选',
     combine: '组合复选框',
     selected: '当前选中值',
     select: '选中',
     checkbox: '复选框',
     selectedDisable: '选中时禁用状态',
-    unselectDisable: '未选时禁用状态'
+    unselectDisable: '未选时禁用状态',
+    useGroupInte: '全选/半选/取消'
   },
   'en-US': {
     basic: 'Basic usage - left and right',
+    semi: 'Semi selective',
     disable: 'Disabled state',
     size: 'Custom size',
     icon: 'Custom icon',
     change: 'Click to trigger the change event',
     useGroup: 'use checkboxGroup',
+    useGroupLimit: 'use checkboxGroup, Limit the maximum number of options (2)',
     disableGroup: 'disable checkboxGroup',
     selectGroup: 'Checkboxgroup select all / cancel',
     selectAll: 'selectAll',
     cancel: 'cancel',
+    selectReverse: 'reverse',
     combine: 'Combined check box',
     selected: 'Currently selected value',
     select: 'selected value',
     checkbox: 'check box',
     selectedDisable: 'Disabled when selected',
-    unselectDisable: 'Disabled when not selected'
+    unselectDisable: 'Disabled when not selected',
+    useGroupInte: 'Select all / half / cancel'
   }
 });
 export default createDemo({
   setup(props, context) {
-    const group = ref(null);
+    const group = ref(null) as Ref;
+    const group2 = ref(null) as Ref;
     const data = reactive({
       checkbox1: true,
       checkbox2: false,
@@ -142,13 +195,22 @@ export default createDemo({
       checkbox6: false,
       checkbox7: false,
       checkbox8: false,
+      checkbox9: true,
+      checkbox10: false,
       checkboxgroup1: ['2', '3'],
       checkboxgroup2: ['2'],
       checkboxgroup3: ['2'],
+      checkboxgroup4: ['2'],
+      checkboxgroup5: [],
       checkboxsource: [
         { label: '1', value: translate('combine') },
-        { label: '2', value: translate('combine') }
-      ]
+        { label: '2', value: translate('combine') },
+        { label: '3', value: translate('combine') },
+        { label: '4', value: translate('combine') },
+        { label: '5', value: translate('combine') },
+        { label: '6', value: translate('combine') }
+      ],
+      indeterminate: true
     });
     const changeBox1 = (state: boolean, label: string) => {
       console.log(state, label);
@@ -163,11 +225,33 @@ export default createDemo({
     };
 
     const changeBox4 = (label: any[]) => {
-      Toast.text(`${label.length === data.checkboxsource.length ? translate('selectAll') : translate('cancel')}`);
+      console.log(label);
+    };
+
+    const changeBox5 = (value: boolean) => {
+      group2.value.toggleAll(value);
+    };
+
+    const changeBox6 = (label: string[]) => {
+      if (label.length === 4) {
+        data.indeterminate = false;
+        data.checkbox10 = true;
+      } else if (label.length && label.length < 4) {
+        data.indeterminate = true;
+        data.checkbox10 = true;
+      } else {
+        data.checkbox10 = false;
+      }
     };
 
     const toggleAll = (f: boolean) => {
-      (group.value as any).toggleAll(f);
+      Toast.text(`${f ? translate('selectAll') : translate('cancel')}`);
+      group.value.toggleAll(f);
+    };
+
+    const toggleReverse = () => {
+      Toast.text(`反选`);
+      group.value.toggleReverse();
     };
 
     return {
@@ -175,8 +259,12 @@ export default createDemo({
       changeBox2,
       changeBox3,
       changeBox4,
+      changeBox5,
+      changeBox6,
       toggleAll,
+      toggleReverse,
       group,
+      group2,
       translate,
       ...toRefs(data)
     };

+ 144 - 4
src/packages/__VUE/checkbox/doc.en-US.md

@@ -52,6 +52,32 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
 
 :::
 
+## Semi selective
+
+:::demo
+
+```html
+<template>
+  <nut-cell>
+    <nut-checkbox v-model="checkbox9" :indeterminate="true" label="check box">check box</nut-checkbox>
+  </nut-cell>
+</template>
+<script lang="ts">
+  import { reactive, toRefs } from 'vue';
+  export default {
+    props: {},
+    setup() {
+      const state = reactive({
+        checkbox9: true
+      });
+      return { ...toRefs(state) };
+    }
+  };
+</script>
+```
+
+:::
+
 ## Disabled state
 
 :::demo
@@ -203,8 +229,9 @@ When the value changes, the `change` event will be triggered
     <nut-checkbox v-for="item in checkboxsource" :key="item.label" :label="item.label">{{item.value}}</nut-checkbox>
   </nut-checkboxgroup>
   <span class="btn">
-    <nut-button type="primary" @click="toggleAll(true)">Select all</nut-button>
-    <nut-button type="primary" @click="toggleAll(false)">cancel</nut-button>
+    <nut-button type="primary" @click="toggleAll(true)" style="margin: 0 20px 0 0">Select all</nut-button>
+    <nut-button type="primary" @click="toggleAll(false)" style="margin: 0 20px 0 0">cancel</nut-button>
+    <nut-button type="warning" @click="toggleReverse()">reverse selection</nut-button>
   </span>
 </template>
 <script lang="ts">
@@ -217,7 +244,11 @@ When the value changes, the `change` event will be triggered
         checkboxgroup3: ['2'],
         checkboxsource: [
           {label: '1', value: 'Combined check box'},
-          {label: '2', value: 'Combined check box'}
+          {label: '2', value: 'Combined check box'},
+          {label: '3', value: 'Combined check box'},
+          {label: '4', value: 'Combined check box'},
+          {label: '5', value: 'Combined check box'},
+          {label: '6', value: 'Combined check box'}
         ]
       });
 
@@ -230,6 +261,11 @@ When the value changes, the `change` event will be triggered
         (group.value as any).toggleAll(f);
       };
 
+      const toggleReverse = () => {
+        Toast.text(`reverse selection`);
+        group.value.toggleReverse();
+      };
+
       return { ...toRefs(state), group, changeBox4, toggleAll };
     }
   };
@@ -238,6 +274,100 @@ When the value changes, the `change` event will be triggered
 
 :::
 
+## use checkboxGroup, Limit the maximum number of options (2)
+
+:::demo
+
+```html
+<template>
+  <nut-cell-group title="use checkboxGroup, Limit the maximum number of options (2)">
+    <nut-cell>
+      <nut-checkboxgroup v-model="checkboxgroup4" :max="2">
+        <nut-checkbox label="1" style="margin: 2px 20px 0 0">Combined check box</nut-checkbox>
+        <nut-checkbox label="2">Combined check box</nut-checkbox>
+        <nut-checkbox label="3" style="margin: 2px 20px 0 0">Combined check box</nut-checkbox>
+        <nut-checkbox label="4">Combined check box</nut-checkbox>
+      </nut-checkboxgroup>
+    </nut-cell>
+    <nut-cell>
+      <div class="demo-check">selected</div>
+      <div>{{ checkboxgroup4 }}</div>
+    </nut-cell>
+  </nut-cell-group>
+</template>
+<script lang="ts">
+  import { reactive, toRefs } from 'vue';
+  import { Toast } from '@nutui/nutui';
+  export default {
+    props: {},
+    setup() {
+      const state = reactive({
+        checkboxgroup4: ['2']
+      });
+
+      return { ...toRefs(state) };
+    }
+  };
+</script>
+```
+
+:::
+
+## Select all / half / cancel
+
+:::demo
+
+```html
+<template>
+  <nut-cell-group title="Select all / half / cancel">
+    <nut-cell>
+      <nut-checkbox :indeterminate="indeterminate" v-model="checkbox10" @change="changeBox5">selectAll</nut-checkbox>
+    </nut-cell>
+    <nut-checkboxgroup v-model="checkboxgroup5" ref="group2" @change="changeBox6">
+      <nut-cell><nut-checkbox label="1" style="margin: 2px 20px 0 0">Combined check box</nut-checkbox></nut-cell>
+      <nut-cell><nut-checkbox label="2">Combined check box</nut-checkbox></nut-cell>
+      <nut-cell><nut-checkbox label="3">Combined check box</nut-checkbox></nut-cell>
+      <nut-cell><nut-checkbox label="4">Combined check box</nut-checkbox></nut-cell>
+    </nut-checkboxgroup>
+  </nut-cell-group>
+</template>
+<script lang="ts">
+  import { reactive, toRefs,ref, Ref } from 'vue';
+  import { Toast } from '@nutui/nutui';
+  export default {
+    props: {},
+    setup() {
+      const group2 = ref(null) as Ref;
+      const state = reactive({
+        indeterminate: true,
+        checkbox10: false,
+        checkboxgroup5: [],
+      });
+
+      const changeBox5 = (value: boolean) => {
+        group2.value.toggleAll(value);
+      };
+
+      const changeBox6 = (label: string[]) => {
+        if(label.length === 4) {
+          state.indeterminate = false;
+          state.checkbox10 = true;
+        } else if(label.length && label.length < 4){
+          state.indeterminate = true;
+          state.checkbox10 = true;
+        } else {
+          state.checkbox10 = false;
+        }
+      };
+
+      return { ...toRefs(state), group2, changeBox5, changeBox6 };
+    }
+  };
+</script>
+```
+
+:::
+
 ## Checkbox
 
 | Attribute | Description | Type   | Default 
@@ -247,10 +377,12 @@ When the value changes, the `change` event will be triggered
 | text-position | The position of the text, optional value:`left`,`right` | String | `right` 
 | icon-size | [Icon Size](#/en-US/icon) | String、Number | `18` 
 | icon-name | [Icon Name](#/en-US/icon),Before selection (it is suggested to modify it together with `icon-active-name`) | String | `'check-normal'` 
-| icon-active-name | [Icon Name](#/en-US/icon),After selection (it is suggested to modify it together with `icon-name`) | String | `'checked'` 
+| icon-active-name | [Icon Name](#/en-US/icon),After selection (it is suggested to modify it together with `icon-name`) | String | `'checked'`
+| icon-indeterminate-name | [Icon Name](#/en-US/icon),Semi selected state | String | `'check-disabled'` 
 | icon-class-prefix | Custom icon class name prefix, used to use custom icons        | String                  | `nut-icon` 
 | icon-font-class-name | Basic class name of custom icon font        | String                  | `nutui-iconfont` 
 | label | Text content of the check box | String | - 
+| indeterminate | Whether half selection status is currently supported. It is generally used in select all operation       | Boolean                  | `false` |
 
 
 ## CheckboxGroup
@@ -259,6 +391,7 @@ When the value changes, the `change` event will be triggered
 |----- | ----- | ----- | ----- 
 | v-model | Identifier of the currently selected item, corresponding to `label`  | Array | - 
 | disabled | Whether to disable the selection, which will be used for all check boxes under it | Boolean | `false` 
+| max | Limit the number of choices. It cannot be used with select all / cancel / invert selection. `0 'means there is no limit | Number | `0`
 
 
 
@@ -273,3 +406,10 @@ When the value changes, the `change` event will be triggered
 | Event | Description                  | Arguments   
 |----- | ----- | ----- 
 | change | Triggered when the value changes | label,`label` returns an array representing the collection of currently selected items 
+
+## CheckboxGroup API
+
+| methodName | Description | Arguments 
+|----- | ----- | ----- 
+| toggleAll | Select all / cancel | `f`,`true`,to select all,`false`,cancel the selection
+| toggleReverse | Reverse selection | -

+ 146 - 5
src/packages/__VUE/checkbox/doc.md

@@ -52,6 +52,32 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
 
 :::
 
+## 半选状态
+
+:::demo
+
+```html
+<template>
+  <nut-cell>
+    <nut-checkbox v-model="checkbox9" :indeterminate="true" label="复选框">复选框</nut-checkbox>
+  </nut-cell>
+</template>
+<script lang="ts">
+  import { reactive, toRefs } from 'vue';
+  export default {
+    props: {},
+    setup() {
+      const state = reactive({
+        checkbox9: true
+      });
+      return { ...toRefs(state) };
+    }
+  };
+</script>
+```
+
+:::
+
 ## 禁用状态
 
 :::demo
@@ -203,8 +229,9 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
     <nut-checkbox v-for="item in checkboxsource" :key="item.label" :label="item.label">{{item.value}}</nut-checkbox>
   </nut-checkboxgroup>
   <span class="btn">
-    <nut-button type="primary" @click="toggleAll(true)">全选</nut-button>
-    <nut-button type="primary" @click="toggleAll(false)">取消</nut-button>
+    <nut-button type="primary" @click="toggleAll(true)" style="margin: 0 20px 0 0">全选</nut-button>
+    <nut-button type="primary" @click="toggleAll(false)" style="margin: 0 20px 0 0">取消</nut-button>
+    <nut-button type="warning" @click="toggleReverse()">反选</nut-button>
   </span>
 </template>
 <script lang="ts">
@@ -217,7 +244,11 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
         checkboxgroup3: ['2'],
         checkboxsource: [
           {label: '1', value: '组合复选框'},
-          {label: '2', value: '组合复选框'}
+          {label: '2', value: '组合复选框'},
+          {label: '3', value: '组合复选框'},
+          {label: '4', value: '组合复选框'},
+          {label: '5', value: '组合复选框'},
+          {label: '6', value: '组合复选框'}
         ]
       });
 
@@ -230,6 +261,11 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
         (group.value as any).toggleAll(f);
       };
 
+      const toggleReverse = () => {
+        Toast.text(`反选`);
+        group.value.toggleReverse();
+      };
+
       return { ...toRefs(state), group, changeBox4, toggleAll };
     }
   };
@@ -238,6 +274,102 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
 
 :::
 
+## checkboxGroup使用,限制最大可选数(2个)
+
+:::demo
+
+```html
+<template>
+  <nut-cell-group title="checkboxGroup使用,限制最大可选数(2个)">
+    <nut-cell>
+      <nut-checkboxgroup v-model="checkboxgroup4" :max="2">
+        <nut-checkbox label="1" style="margin: 2px 20px 0 0">组合复选框</nut-checkbox>
+        <nut-checkbox label="2">组合复选框</nut-checkbox>
+        <nut-checkbox label="3" style="margin: 2px 20px 0 0">组合复选框</nut-checkbox>
+        <nut-checkbox label="4">组合复选框</nut-checkbox>
+      </nut-checkboxgroup>
+    </nut-cell>
+    <nut-cell>
+      <div class="demo-check">选中</div>
+      <div>{{ checkboxgroup4 }}</div>
+    </nut-cell>
+  </nut-cell-group>
+</template>
+<script lang="ts">
+  import { reactive, toRefs } from 'vue';
+  import { Toast } from '@nutui/nutui';
+  export default {
+    props: {},
+    setup() {
+      const state = reactive({
+        checkboxgroup4: ['2']
+      });
+
+      return { ...toRefs(state) };
+    }
+  };
+</script>
+```
+
+:::
+
+
+## 全选/半选/取消
+
+:::demo
+
+```html
+<template>
+  <nut-cell-group title="全选/半选/取消">
+    <nut-cell>
+      <nut-checkbox :indeterminate="indeterminate" v-model="checkbox10" @change="changeBox5">全选</nut-checkbox>
+    </nut-cell>
+    <nut-checkboxgroup v-model="checkboxgroup5" ref="group2" @change="changeBox6">
+      <nut-cell><nut-checkbox label="1" style="margin: 2px 20px 0 0">组合复选框</nut-checkbox></nut-cell>
+      <nut-cell><nut-checkbox label="2">组合复选框</nut-checkbox></nut-cell>
+      <nut-cell><nut-checkbox label="3">组合复选框</nut-checkbox></nut-cell>
+      <nut-cell><nut-checkbox label="4">组合复选框</nut-checkbox></nut-cell>
+    </nut-checkboxgroup>
+  </nut-cell-group>
+</template>
+<script lang="ts">
+  import { reactive, toRefs,ref, Ref } from 'vue';
+  import { Toast } from '@nutui/nutui';
+  export default {
+    props: {},
+    setup() {
+      const group2 = ref(null) as Ref;
+      const state = reactive({
+        indeterminate: true,
+        checkbox10: false,
+        checkboxgroup5: [],
+      });
+
+      const changeBox5 = (value: boolean) => {
+        group2.value.toggleAll(value);
+      };
+
+      const changeBox6 = (label: string[]) => {
+        if(label.length === 4) {
+          state.indeterminate = false;
+          state.checkbox10 = true;
+        } else if(label.length && label.length < 4){
+          state.indeterminate = true;
+          state.checkbox10 = true;
+        } else {
+          state.checkbox10 = false;
+        }
+      };
+
+      return { ...toRefs(state), group2, changeBox5, changeBox6 };
+    }
+  };
+</script>
+```
+
+:::
+
+
 ## Checkbox
 
 | 字段 | 说明 | 类型 | 默认值
@@ -248,17 +380,18 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
 | icon-size | [图标尺寸](#/icon) | String、Number | `18`
 | icon-name | [图标名称](#/icon),选中前(建议和`icon-active-name`一起修改) | String | `'check-normal'`
 | icon-active-name | [图标名称](#/icon),选中后(建议和`icon-name`一起修改) | String | `'checked'`
+| icon-indeterminate-name | [图标名称](#/icon),半选状态 | String | `'check-disabled'`
 | icon-class-prefix | 自定义 icon 类名前缀,用于使用自定义图标        | String                  | `nut-icon` |
 | icon-font-class-name | 自定义 icon 字体基础类名        | String                  | `nutui-iconfont` |
 | label | 复选框的文本内容 | String | -
-
-
+| indeterminate | 当前是否支持半选状态,一般用在全选操作中        | Boolean                  | `false` |
 ## CheckboxGroup
 
 | 字段 | 说明 | 类型 | 默认值
 |----- | ----- | ----- | ----- 
 | v-model | 当前选中项的标识符,和 `label` 相对应  | Array | -
 | disabled | 是否禁用选择,将用于其下的全部复选框 | Boolean | `false`
+| max | 限制选择的数量,不能和`全选/取消/反选`一起使用, `0`表示没有限制 | Number | `0`
 
 
 
@@ -273,3 +406,11 @@ app.use(Checkbox).use(CheckboxGroup).use(Icon);
 | 字段 | 说明 | 回调参数 
 |----- | ----- | ----- 
 | change | 值变化时触发 | label,`label`返回一个数组,表示当前选中项的集合
+
+
+## CheckboxGroup API
+
+| 方法名 | 说明 | 参数 
+|----- | ----- | ----- 
+| toggleAll | 全选/取消 | `f`,传 `true`,表示全选,传 `false`,表示取消全选
+| toggleReverse | 反选 | -

+ 6 - 0
src/packages/__VUE/checkbox/index.scss

@@ -24,8 +24,14 @@
   }
   &__icon--unchecked {
     color: $checkbox-icon-disable-color;
+    font-size: $checkbox-icon-font-size;
+  }
+  &__icon--indeterminate {
+    color: $primary-color;
+    font-size: $checkbox-icon-font-size;
   }
   &__icon--disable {
     color: $help-color;
+    font-size: $checkbox-icon-font-size;
   }
 }

+ 2 - 129
src/packages/__VUE/checkbox/index.taro.vue

@@ -1,134 +1,7 @@
 <script lang="ts">
-import { h, computed, inject, getCurrentInstance, onMounted } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 const { create, componentName } = createComponent('checkbox');
+import { component } from './common';
 import nutIcon from '../icon/index.taro.vue';
-export default create({
-  components: {
-    nutIcon
-  },
-  props: {
-    modelValue: {
-      type: Boolean,
-      default: false
-    },
-    disabled: {
-      type: Boolean,
-      default: false
-    },
-    textPosition: {
-      type: String,
-      default: 'right'
-    },
-    iconSize: {
-      type: [String, Number],
-      default: ''
-    },
-    iconName: {
-      type: String,
-      default: 'check-normal'
-    },
-    iconActiveName: {
-      type: String,
-      default: 'checked'
-    },
-    label: {
-      type: String,
-      default: ''
-    },
-    iconClassPrefix: {
-      type: String,
-      default: 'nut-icon'
-    },
-    iconFontClassName: {
-      type: String,
-      default: 'nutui-iconfont'
-    }
-  },
-  emits: ['change', 'update:modelValue'],
-  setup(props, { emit, slots }) {
-    const parent: any = inject('parent', null);
-
-    const hasParent = computed(() => !!parent);
-
-    const pValue = computed(() => {
-      if (hasParent.value) {
-        return parent.value.value.includes(props.label);
-      } else {
-        return props.modelValue;
-      }
-    });
-
-    const pDisabled = computed(() => {
-      return hasParent.value ? parent.disabled.value : props.disabled;
-    });
-
-    const checked = computed(() => !!props.modelValue);
-
-    const color = computed(() => {
-      return !pDisabled.value
-        ? !pValue.value
-          ? 'nut-checkbox__icon--unchecked'
-          : 'nut-checkbox__icon'
-        : 'nut-checkbox__icon--disable';
-    });
-
-    const emitChange = (value: string | boolean, label?: string) => {
-      emit('update:modelValue', value);
-      emit('change', value, label);
-    };
-
-    const renderIcon = () => {
-      const { iconName, iconSize, iconActiveName, iconClassPrefix, iconFontClassName } = props;
-      return h(nutIcon, {
-        name: !pValue.value ? iconName : iconActiveName,
-        size: iconSize,
-        class: color.value,
-        classPrefix: iconClassPrefix,
-        fontClassName: iconFontClassName
-      });
-    };
-
-    const renderLabel = () => {
-      return h(
-        'view',
-        {
-          class: `${componentName}__label ${pDisabled.value ? `${componentName}__label--disabled` : ''}`
-        },
-        slots.default?.()
-      );
-    };
-
-    const handleClick = (e: MouseEvent | TouchEvent) => {
-      if (pDisabled.value) return;
-      emitChange(!checked.value, slots.default?.()[0].children as string);
-      if (hasParent.value) {
-        let value = parent.value.value;
-        let { label } = props;
-        const index = value.indexOf(label);
-        if (index > -1) {
-          value.splice(index, 1);
-        } else {
-          value.push(label);
-        }
-        parent.updateValue(value);
-      }
-    };
-
-    onMounted(() => {
-      hasParent.value && parent['relation'](getCurrentInstance());
-    });
-
-    return () => {
-      return h(
-        'view',
-        {
-          class: `${componentName} ${props.textPosition === 'left' ? `${componentName}--reverse` : ''}`,
-          onClick: handleClick
-        },
-        [renderIcon(), renderLabel()]
-      );
-    };
-  }
-});
+export default create(component(componentName, nutIcon));
 </script>

+ 2 - 130
src/packages/__VUE/checkbox/index.vue

@@ -1,135 +1,7 @@
 <script lang="ts">
-import { h, computed, inject, getCurrentInstance, onMounted } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 const { create, componentName } = createComponent('checkbox');
+import { component } from './common';
 import nutIcon from '../icon/index.vue';
-
-export default create({
-  components: {
-    nutIcon
-  },
-  props: {
-    modelValue: {
-      type: Boolean,
-      default: false
-    },
-    disabled: {
-      type: Boolean,
-      default: false
-    },
-    textPosition: {
-      type: String,
-      default: 'right'
-    },
-    iconSize: {
-      type: [String, Number],
-      default: ''
-    },
-    iconName: {
-      type: String,
-      default: 'check-normal'
-    },
-    iconActiveName: {
-      type: String,
-      default: 'checked'
-    },
-    label: {
-      type: String,
-      default: ''
-    },
-    iconClassPrefix: {
-      type: String,
-      default: 'nut-icon'
-    },
-    iconFontClassName: {
-      type: String,
-      default: 'nutui-iconfont'
-    }
-  },
-  emits: ['change', 'update:modelValue'],
-  setup(props, { emit, slots }) {
-    const parent: any = inject('parent', null);
-
-    const hasParent = computed(() => !!parent);
-
-    const pValue = computed(() => {
-      if (hasParent.value) {
-        return parent.value.value.includes(props.label);
-      } else {
-        return props.modelValue;
-      }
-    });
-
-    const pDisabled = computed(() => {
-      return hasParent.value ? parent.disabled.value : props.disabled;
-    });
-
-    const checked = computed(() => !!props.modelValue);
-
-    const color = computed(() => {
-      return !pDisabled.value
-        ? !pValue.value
-          ? 'nut-checkbox__icon--unchecked'
-          : 'nut-checkbox__icon'
-        : 'nut-checkbox__icon--disable';
-    });
-
-    const emitChange = (value: string | boolean, label?: string) => {
-      emit('update:modelValue', value);
-      emit('change', value, label);
-    };
-
-    const renderIcon = () => {
-      const { iconName, iconSize, iconActiveName, iconClassPrefix, iconFontClassName } = props;
-      return h(nutIcon, {
-        name: !pValue.value ? iconName : iconActiveName,
-        size: iconSize,
-        class: color.value,
-        classPrefix: iconClassPrefix,
-        fontClassName: iconFontClassName
-      });
-    };
-
-    const renderLabel = () => {
-      return h(
-        'view',
-        {
-          class: `${componentName}__label ${pDisabled.value ? `${componentName}__label--disabled` : ''}`
-        },
-        slots.default?.()
-      );
-    };
-
-    const handleClick = (e: MouseEvent | TouchEvent) => {
-      if (pDisabled.value) return;
-      emitChange(!checked.value, slots.default?.()[0].children as string);
-      if (hasParent.value) {
-        let value = parent.value.value;
-        let { label } = props;
-        const index = value.indexOf(label);
-        if (index > -1) {
-          value.splice(index, 1);
-        } else {
-          value.push(label);
-        }
-        parent.updateValue(value);
-      }
-    };
-
-    onMounted(() => {
-      hasParent.value && parent['relation'](getCurrentInstance());
-    });
-
-    return () => {
-      return h(
-        'view',
-        {
-          class: `${componentName} ${props.textPosition === 'left' ? `${componentName}--reverse` : ''}`,
-          onClick: handleClick
-        },
-        [renderIcon(), renderLabel()]
-      );
-    };
-  }
-});
+export default create(component(componentName, nutIcon));
 </script>

+ 25 - 3
src/packages/__VUE/checkboxgroup/index.vue

@@ -13,6 +13,10 @@ export default create({
     disabled: {
       type: Boolean,
       default: false
+    },
+    max: {
+      type: Number,
+      default: 0
     }
   },
   emits: ['change', 'update:modelValue'],
@@ -33,18 +37,36 @@ export default create({
     };
 
     const toggleAll = (checked: boolean) => {
-      let values: any[] = [];
+      let values: string[] = [];
       if (!!checked) {
         state.children.forEach((item: any) => {
-          values.push(item?.label);
+          if (!item?.disabled) {
+            values.push(item?.label);
+          }
         });
       }
       emit('update:modelValue', values);
     };
 
+    const toggleReverse = () => {
+      let values = props.modelValue.slice();
+      state.children.forEach((item: any) => {
+        let findIndex = values.findIndex((value: any) => value === item.label);
+        if (findIndex > -1) {
+          values.splice(findIndex, 1);
+        } else {
+          if (!item?.disabled) {
+            values.push(item?.label);
+          }
+        }
+      });
+      emit('update:modelValue', values);
+    };
+
     provide('parent', {
       value: computed(() => props.modelValue),
       disabled: computed(() => props.disabled),
+      max: computed(() => props.max),
       updateValue,
       relation
     });
@@ -56,7 +78,7 @@ export default create({
       }
     );
 
-    useExpose({ toggleAll });
+    useExpose({ toggleAll, toggleReverse });
 
     return () => {
       return h(

+ 1 - 5
src/packages/__VUE/elevator/index.vue

@@ -37,7 +37,7 @@
   </view>
 </template>
 <script lang="ts">
-import { computed, reactive, toRefs, nextTick, ref, Ref, watch, onMounted, onUnmounted } from 'vue';
+import { computed, reactive, toRefs, nextTick, ref, Ref, watch, onMounted } from 'vue';
 import { createComponent } from '@/packages/utils/create';
 import { useExpose } from '@/packages/utils/useExpose/index';
 const { componentName, create } = createComponent('elevator');
@@ -200,10 +200,6 @@ export default create({
       listview.value.addEventListener('scroll', listViewScroll);
     });
 
-    onUnmounted(() => {
-      listview.value.removeEventListener('scroll', listViewScroll);
-    });
-
     useExpose({
       scrollTo
     });

+ 75 - 9
src/sites/mobile-taro/vue/src/dentry/pages/checkbox/index.vue

@@ -12,7 +12,11 @@
         <div>{{ checkbox1 }}</div>
       </nut-cell>
     </nut-cell-group>
-
+    <nut-cell-group title="半选状态">
+      <nut-cell>
+        <nut-checkbox v-model="checkbox9" :indeterminate="true" label="复选框">复选框</nut-checkbox>
+      </nut-cell>
+    </nut-cell-group>
     <nut-cell-group title="禁用状态">
       <nut-cell>
         <nut-checkbox v-model="checkbox3" disabled>未选时禁用状态</nut-checkbox>
@@ -73,16 +77,43 @@
       </nut-cell>
       <nut-cell>
         <nut-button type="primary" @click="toggleAll(true)" style="margin: 0 10px 0 0">全选</nut-button>
-        <nut-button type="info" @click="toggleAll(false)">取消</nut-button>
+        <nut-button type="info" @click="toggleAll(false)" style="margin: 0 10px 0 0">取消</nut-button>
+        <nut-button type="warning" @click="toggleReverse()">反选</nut-button>
+      </nut-cell>
+    </nut-cell-group>
+    <nut-cell-group title="checkboxGroup使用,限制最大可选数(2个)">
+      <nut-cell>
+        <nut-checkboxgroup v-model="checkboxgroup4" :max="2">
+          <nut-checkbox label="1" style="margin: 2px 20px 0 0">组合复选框</nut-checkbox>
+          <nut-checkbox label="2">组合复选框</nut-checkbox>
+          <nut-checkbox label="3" style="margin: 2px 20px 0 0">组合复选框</nut-checkbox>
+          <nut-checkbox label="4">组合复选框</nut-checkbox>
+        </nut-checkboxgroup>
+      </nut-cell>
+      <nut-cell>
+        <div class="demo-check">当前选中值</div>
+        <div>{{ checkboxgroup4 }}</div>
+      </nut-cell>
+    </nut-cell-group>
+    <nut-cell-group title="全选/半选/取消">
+      <nut-cell>
+        <nut-checkbox :indeterminate="indeterminate" v-model="checkbox10" @change="changeBox5">全选</nut-checkbox>
       </nut-cell>
+      <nut-checkboxgroup v-model="checkboxgroup5" ref="group2" @change="changeBox6">
+        <nut-cell><nut-checkbox label="1" style="margin: 2px 20px 0 0">组合复选框</nut-checkbox></nut-cell>
+        <nut-cell><nut-checkbox label="2">组合复选框</nut-checkbox></nut-cell>
+        <nut-cell><nut-checkbox label="3">组合复选框</nut-checkbox></nut-cell>
+        <nut-cell><nut-checkbox label="4">组合复选框</nut-checkbox></nut-cell>
+      </nut-checkboxgroup>
     </nut-cell-group>
   </div>
 </template>
 <script lang="ts">
-import { reactive, ref, toRefs, onMounted } from 'vue';
+import { reactive, ref, toRefs, Ref } from 'vue';
 export default {
   setup(props, context) {
-    const group = ref(null);
+    const group = ref(null) as Ref;
+    const group2 = ref(null) as Ref;
     const data = reactive({
       checkbox1: true,
       checkbox2: false,
@@ -92,13 +123,22 @@ export default {
       checkbox6: false,
       checkbox7: false,
       checkbox8: false,
+      checkbox9: true,
+      checkbox10: false,
       checkboxgroup1: ['2', '3'],
       checkboxgroup2: ['2'],
       checkboxgroup3: ['2'],
+      checkboxgroup4: ['2'],
+      checkboxgroup5: [],
       checkboxsource: [
         { label: '1', value: '组合复选框' },
-        { label: '2', value: '组合复选框' }
-      ]
+        { label: '2', value: '组合复选框' },
+        { label: '3', value: '组合复选框' },
+        { label: '4', value: '组合复选框' },
+        { label: '5', value: '组合复选框' },
+        { label: '6', value: '组合复选框' }
+      ],
+      indeterminate: true
     });
     const changeBox1 = (state: boolean, label: string) => {
       console.log(state, label);
@@ -109,15 +149,37 @@ export default {
     };
 
     const changeBox3 = (state: boolean, label: string) => {
-      console.log(`您${state ? '选中' : '取消'}了${label}`);
+      console.log(`${state ? '选中' : '取消'} ${label}`);
     };
 
     const changeBox4 = (label: any[]) => {
-      console.log(`${label.length === data.checkboxsource.length ? '全选' : '取消全选'}`);
+      console.log(label);
+    };
+
+    const changeBox5 = (value: boolean) => {
+      group2.value.toggleAll(value);
+    };
+
+    const changeBox6 = (label: string[]) => {
+      if (label.length === 4) {
+        data.indeterminate = false;
+        data.checkbox10 = true;
+      } else if (label.length && label.length < 4) {
+        data.indeterminate = true;
+        data.checkbox10 = true;
+      } else {
+        data.checkbox10 = false;
+      }
     };
 
     const toggleAll = (f: boolean) => {
-      (group.value as any).toggleAll(f);
+      console.log(`${f ? '全选' : '取消'}`);
+      group.value.toggleAll(f);
+    };
+
+    const toggleReverse = () => {
+      console.log(`反选`);
+      group.value.toggleReverse();
     };
 
     return {
@@ -125,8 +187,12 @@ export default {
       changeBox2,
       changeBox3,
       changeBox4,
+      changeBox5,
+      changeBox6,
       toggleAll,
+      toggleReverse,
       group,
+      group2,
       ...toRefs(data)
     };
   }