[TOC]

button组件

组件构成

import Button from "./src/button.vue";
import { App } from "vue";
Button.install = (app: App):void => {
  app.component(Button.name, Button)
};
type IWithInstall<T> = T & { install(app: App): void };
const _Button: IWithInstall<typeof Button> = Button;
export default _Button;
  • 当组件在loading时,和icon显示冲突的处理
  • 外部使用组件时,触发的事件调用应是内部emit触发
<template>
  <button :class="styleClass" @click="handleClick">
    <!-- 处理loading状态时 显示loading图标 -->
    <i v-if="loading" class="m-icon-loading"></i>
    <!-- 不存在loading状态时 才显示icon -->
    <i v-if="icon && !loading" :class="icon"></i>
    <span v-if="$slots.default"><slot></slot></span>
  </button>
</template>
<script lang="ts">
  import { defineComponent, PropType, computed } from "vue";
  type BtnType =
    | "primary"
    | "success"
    | "warning"
    | "danger"
    | "default"
    | "info";
  export default defineComponent({
    props: {
      type: {
        type: String as PropType<BtnType>,
        default: "primary",
        validator: (val: string) => {
          return [
          "primary",
          "success",
          "warning",
          "danger",
          "default",
          "info",
          ].includes(val);
      },
    },
    icon: {
      type: String,
        default: "",
    },
    disabled: Boolean,
    loading: Boolean,
    round: Boolean,
    circle: Boolean,
    },
    name: "MButton",
    emits: ['click'],
    setup(props, ctx) {
      const styleClass = computed(() => {
        return [
          "m-button",
          "m-button--" + props.type,
          { "is-disabled": props.disabled },
          { "is-loading": props.loading },
          { "is-round": props.round },
          { "is-circle": props.circle },
        ];
       });
       const handleClick = (e) => {
         ctx.emit("click", e);
       };
       return { styleClass, handleClick };
    },
  });
</script>

样式

@import "./common/var.scss";
@import "./mixins/mixin.scss";

@include b(button){ // 和namespace技术后 m-button
  display: inline-block;
  outline: none;
  border: #f3f3f3;
  user-select: none;
  padding: 10px;
  border-radius: 5px;
  vertical-align: middle;
  margin-right: 10px;
  // 样式以m-icon开头的,说明是 图标icon,将图标和内容添加间距
  & [class*="m-icon-"]{
    vertical-align: middle;
    & + span{
      margin-left: 5px;
    }
  }

  @include when(disabled){
    cursor: not-allowed;
  }
  @include when(loading){
    pointer-events: none;
  }
  @include when(round){
    border-radius:10%;
  }
  @include when(circle){
    border-radius:50%;
  }

  @include m(primary){
    @include button-type($--color-white, $--color-primary, $--color-primary);
  }
  @include m(success){
    @include button-type($--color-white, $--color-success, $--color-success);
  }
  @include m(danger){
    @include button-type($--color-white, $--color-danger, $--color-danger);
  }
  @include m(warning){
    @include button-type($--color-white, $--color-warning, $--color-warning);
  }
  @include m(info){
    @include button-type($--color-white, $--color-info, $--color-info);
  }
}

组件的使用

<template>
  <m-button
    icon="m-icon-notice"
    type="danger"
    :loading="btnIsLoading"
    @click="handleClick"
    >click</m-button>
  <m-button icon="m-icon-notice" type="danger">buton</m-button>
  <m-button icon="m-icon-notice" type="danger"></m-button>
</template>
<script>
  import { defineComponent, ref, onMounted } from "vue";

  const useButton = () => {
    const btnIsLoading = ref(true);
    const handleClick = () => {
      console.log("use click");
    };
    onMounted(()=>{
      setTimeout(() => {
        // loading状态时 按钮点击无效
        btnIsLoading.value = false;
      }, 2000);
    })
    return { handleClick, btnIsLoading };
  };

  export default defineComponent({
    setup(props) {
      return {
        ...useButton(),
      };
    },
  });
</script>
  • vue3使用compositionAPI,把使用的逻辑单独放一个函数中,函数暴露出变量及方法,useButton

    button-group组件

    button-group组件是依赖于button实现,所以把button-group.vue放当button目录下。
    button-group目录下只放index.ts

    import ButtonGroup from "../button/src/button-group.vue";
    import { App } from "vue";
    ButtonGroup.install = (app: App):void => {
      app.component(ButtonGroup.name, ButtonGroup)
    };
    type IWithInstall<T> = T & { install(app: App): void };
    const _ButtonGroup: IWithInstall<typeof ButtonGroup> = ButtonGroup;
    export default _ButtonGroup;
    
    <template>
    <div :class="styleClass">
      <slot></slot>
    </div>
    </template>
    <script lang="ts">
    import { computed, defineComponent, PropType } from "vue";
    import {BtnType} from "./button.vue"
    export default defineComponent({
    name: "MButtonGroup",
    props:{
      type: {
        type: String as PropType<BtnType>,
        default: "", // 默认type为空,取内部button的类型,如果设置则覆盖 子button中设置的type
        validator: (val: string) => {
          return [
            "primary",
            "success",
            "warning",
            "danger",
            "default",
            "info",
          ].includes(val);
        },
      },
    },
    setup(props){
      const styleClass = computed(()=>{
        return [
          "m-button-group",
          props.type ? "m-button-group--" + props.type:"",
        ];
      })
      return {
        styleClass
      }
    }
    });
    </script>
    
  • 如果button-group设置type,则以该type为主,覆盖掉内部button的type。

    样式处理

    ```css @import “./common/var.scss”; @import “./mixins/mixin.scss”;

@include b(button-group){ &>.#{$namespace}-button{ &:first-child{ border-top-right-radius: 0; border-bottom-right-radius: 0; margin-right: 1px; } &:last-child{ border-top-left-radius: 0; border-bottom-left-radius: 0; } }

@include m(primary){
    @include button-group-type($--color-white, $--color-primary, $--color-primary);
}
@include m(success){
    @include button-group-type($--color-white, $--color-success, $--color-success);
}
@include m(danger){
    @include button-group-type($--color-white, $--color-danger, $--color-danger);
}
@include m(warning){
    @include button-group-type($--color-white, $--color-warning, $--color-warning);
}
@include m(info){
    @include button-group-type($--color-white, $--color-info, $--color-info);
}

}

[代码](https://github.com/shenshuai89/ming-ui/tree/button-comp)
<a name="q0Yyn"></a>
# row和col组件
创建row和col组件项目
```css
lerna create row
lerna create col

组件结构

  • row和col组件实现布局相关,有很大的灵活性,并且不需要展示出页面内容,因此采用h函数渲染结构实现,直接创建出row.ts和col.ts文件
  • computed计算属性生成,内部生成的是ref对象,调用时需要使用value值
    import { computed, defineComponent, h } from "vue";
    export default defineComponent({
    name: 'MRow',
    props: {
      tag: {
        type: String,
        default: 'div'
      }
    },
    setup(props, { slots }) {
      const styleClass = computed(() => [
        'm-row'
      ])
      return () => h(props.tag, {
        class: styleClass.value
      }, slots.default?.())
    }
    });
    
    import { computed, defineComponent, h } from "vue"
    export default defineComponent({
    name: 'MCol',
    props: {
      tag: {
        type: String,
        default: 'div',
      }
    },
    setup(props, { slots }) {
      const styleClass = computed(() => [
        'm-col'
      ])
      return () => h(props.tag, {
        class: styleClass.value
      }, slots.default?.())
    }
    })
    

    设置样式

    ```css @import “./common/var.scss”; @import “./mixins/mixin.scss”;

@include b(row){ display: flex; flex-wrap: wrap; }

```css
@import "./common/var.scss";
@import "./mixins/mixin.scss";

@include b(col) {
  box-sizing: border-box;
}
@for $i from 1 through 24 {
    .#{$namespace}-col-span-#{$i} {
      max-width: (1/24 * $i) * 100%;
      flex: (1/24 * $i) * 100%;
    }
    .#{$namespace}-col-offset-#{$i} {
      margin-left: (1/24 * $i) * 100%;
    }
  }
@include res(xs) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-xs-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-xs-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}
@include res(sm) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-sm-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-sm-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}
@include res(md) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-md-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }

    .#{$namespace}-col-md-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}

@include res(lg) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-lg-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-lg-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}
@include res(xl) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-xl-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-xl-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}
  • col样式中使用到scss的@for循环语法
  • 定义响应式使用到scss的@if语法 map对象的map-has-key以及 6.7插值语句

    // 定义响应式 responsive
    @mixin res($key, $map: $--breakpoints) {
      // 循环断点Map,如果存在则返回
      @if map-has-key($map, $key) {
          @media only screen and #{inspect(map-get($map, $key))} {
              @content;
          }
      }
    }
    

    row组件justify对齐属性

    给row.ts设置 ```typescript import { computed, defineComponent, h, PropType, provide } from “vue”; export default defineComponent({ name: “MRow”, props: { tag: {

    type: String,
    default: "div",
    

    }, justify: {

    type: String as PropType<
      "start" | "end" | "center" | "space-round" | "space-between"
    >,
    default: "start",
    

    }, }, setup(props, { slots }) { const styleClass = computed(() => [

    "m-row",
    props.justify !== "start" ? `is-justify-${props.justify}` : "",
    

    ]);

    return () => {

    return h(
      props.tag,
      { class: styleClass.value, style: styles.value },
      slots.default?.()
    );
    

    }; }, });

然后设置row.scss,添加@when的mixin方法
```css
@import "./common/var.scss";
@import "./mixins/mixin.scss";

@include b(row){
  display: flex;
  flex-wrap: wrap;
  @include when(justify-end){
    justify-content: flex-end;
  }
  @include when(justify-center){
    justify-content: center;
  }
  @include when(justify-space-around){
    justify-content: space-around;
  }
  @include when(justify-space-between){
    justify-content: space-between;
  }
}

col组件处理响应式

定义mixin方法

// theme-chalk/src/common/var.scss
$--sm: 768px !default;
$--md: 992px !default;
$--lg: 1200px !default;
$--xl: 1920px !default;

$--breakpoints: (
  'xs' : (max-width: $--sm - 1),
  'sm' : (min-width: $--sm),
  'md' : (min-width: $--md),
  'lg' : (min-width: $--lg),
  'xl' : (min-width: $--xl)
);

// theme-chalk/src/mixin/mixin.scss
@mixin res($key, $map: $--breakpoints) {
    // 循环断点Map,如果存在则返回
    @if map-has-key($map, $key) {
        @media only screen and #{inspect(map-get($map, $key))} {
            @content;
        }
    }
}

使用scss的mixin定义的res方法计算不同分辨率下显示的样式

@include res(xs) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-xs-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-xs-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}
@include res(sm) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-sm-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-sm-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}
@include res(md) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-md-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }

    .#{$namespace}-col-md-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}

@include res(lg) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-lg-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-lg-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}
@include res(xl) {
  @for $i from 0 through 24 {
    .#{$namespace}-col-xl-#{$i} {
      max-width: (1 / 24 * $i * 100) * 1%;
      flex: 0 0 (1 / 24 * $i * 100) * 1%;
    }
    .#{$namespace}-col-xl-offset-#{$i} {
      margin-left: (1 / 24 * $i * 100) * 1%;
    }
  }
}

处理col.ts中响应式类的显示

///.../
props: {
    xs: {
      type: Number,
      default: 0,
    },
    sm: {
      type: Number,
      default: 0,
    },
    md: {
      type: Number,
      default: 0,
    },
    lg: {
      type: Number,
      default: 0,
    },
    xl: {
      type: Number,
      default: 0,
    },
  }
////.../
setup(props){
  const sizes = ["xs", "sm", "md", "lg", "xl"] as const;
  sizes.forEach((size) => {
    if (typeof props[size] === "number" && props[size] > 0) {
      allClass.push(`m-col-${size}-${props[size]}`);
    }
  });
}
///.../

gutter属性

gutter设置在row上,但是要作用在col组件,就需要使用到组件之间的数据通信,col有可能被嵌套多次,所以不是简单的父子通信,而是跨层级深层次组件通信,使用到provide和inject属性
在row.ts组件中设置gutter属性, 代码第25行

import { computed, defineComponent, h, PropType, provide } from "vue";
export default defineComponent({
  name: "MRow",
  props: {
    tag: {
      type: String,
      default: "div",
    },
    gutter: {
      type: Number,
      default: 0,
    },
    justify: {
      type: String as PropType<
        "start" | "end" | "center" | "space-round" | "space-between"
      >,
      default: "start",
    },
  },
  setup(props, { slots }) {
    const styleClass = computed(() => [
      "m-row",
      props.justify !== "start" ? `is-justify-${props.justify}` : "",
    ]);
    provide("ZRow", props.gutter);

    const styles = computed(() => {
      if (props.gutter) {
        return {
          marginLeft: -(props.gutter / 2) + "px",
          marginRight: -(props.gutter / 2) + "px",
        };
      }
    });
    return () => {
      return h(
        props.tag,
        { class: styleClass.value, style: styles.value },
        slots.default?.()
      );
    };
  },
});

在col.ts中使用gutter属性,第39行

import { computed, defineComponent, h, inject } from "vue";
export default defineComponent({
  name: "MCol",
  props: {
    tag: {
      type: String,
      default: "div",
    },
    span: {
      type: Number,
      default: 24,
    },
    offset: {
      type: Number,
      default: 0,
    },
    xs: {
      type: Number,
      default: 0,
    },
    sm: {
      type: Number,
      default: 0,
    },
    md: {
      type: Number,
      default: 0,
    },
    lg: {
      type: Number,
      default: 0,
    },
    xl: {
      type: Number,
      default: 0,
    },
  },
  setup(props, { slots }) {
    const gutter = inject("ZRow", 0);

    let styleClass = computed(() => {
      const post = ["span", "offset"] as const;
      const allClass = [];
      post.forEach((item) => {
        const size = props[item];
        if (typeof size === "number" && size > 0) {
          if(size<=24){
            allClass.push(`m-col-${item}-${props[item]}`);
          } else{
            allClass.push(`m-col-${item}-24`);
          }
        }
      });

      const sizes = ["xs", "sm", "md", "lg", "xl"] as const;
      sizes.forEach((size) => {
        if (typeof props[size] === "number" && props[size] > 0) {
          allClass.push(`m-col-${size}-${props[size]}`);
        }
      });

      return ["m-col", ...allClass];
    });

    const styles = computed(() => {
      if (gutter) {
        return {
          paddingLeft: gutter / 2 + "px",
          paddingRight: gutter / 2 + "px",
        };
      }
    });

    return () => {
      return h(
        props.tag,
        {
          class: styleClass.value,
          style: styles.value,
        },
        slots.default?.()
      );
    };
  },
});

代码:

checkbox选择框

创建checkbox和checkbox-group基本结构

lerna create checkbox
lerna create checkbox-group

import Checkbox from "./src/checkbox.vue";
import { App } from "vue";
Checkbox.install = (app: App):void => {
    app.component(Checkbox.name, Checkbox);
};
type IWithInstall<T> = T & { install(app: App): void };
const _Checkbox: IWithInstall<typeof Checkbox> = Checkbox;
export default _Checkbox;
import CheckboxGroup from "../checkbox/src/checkbox-group.vue";
import { App } from "vue";
CheckboxGroup.install = (app: App):void => {
    app.component(CheckboxGroup.name, CheckboxGroup)
};
type IWithInstall<T> = T & { install(app: App): void };
const _CheckboxGroup: IWithInstall<typeof CheckboxGroup> = CheckboxGroup;
export default _CheckboxGroup;
<template>
  <div>checkbo11x</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
  // 一定要写完整name属性,否则加载组件不成功
  name: "MCheckbox",
  setup(props) {},
});
</script>
<template>
  <div>checkbox group</div>
</template>
<script>
import { defineComponent } from "vue";
export default defineComponent({
  name: "MCheckboxGroup",
  setup(props) {},
});
</script>

实现checkbox

  • 处理外层通过v-model传递的数据
  • 处理外层绑定的checked属性
  • 处理外层的绑定的change事件

    设计checkbox组件props接口

    export interface ICheckboxProps {
      name?: string, // input中原生name属性,可以让for相互对应,实现点击文字触发响应
      label?: string | boolean | number, // v-model为array时使用
      modelValue: string | boolean | number, // 绑定的值
      indeterminate?: boolean // 是否半选
      disabled?: boolean // 禁用
      checked?: boolean // 是否选中
    }
    

    数据的双向绑定v-model

  • 接收到数据后,无法直接返回给父组件,需要经过computed进行转换

  • computed的get、set属性可以接受数据和设置数据 ```vue

    <a name="kR6OZ"></a>
    ### 属性checked的设置
    ```typescript
    // 2.checkbox的 checked 属性设置,更新checkbox的value值,同时更新checked值
    const useCheckboxStatus = (props: ICheckboxProps, model) => {
      const isChecked = computed(() => {
        const value = model.value;
        return value;
      });
      return isChecked
    }
    

    change事件传递

    • 触发响应父级的事件,使用emit把数据返回给父级 ```vue

      <a name="Yvstw"></a>
      ### 在example项目中使用组件
      ```vue
      <m-checkbox v-model="checkVal">checkbox</m-checkbox>
      

      实现checkbox-group组件

      首先看下复选框怎么使用

      <template>
        <m-checkbox-group v-model="checkgroupVal" @change="checkboxgroupchange">
          <m-checkbox v-for="c in checkArray" :key="c" :label="c"></m-checkbox>
        </m-checkbox-group>
      </template>
      <script setup>
      import { ref } from "vue";
      const checkgroupVal = ref(["北京"]);
      const checkArray = ref(["北京", "上海", "香港", "澳门"]);
      const checkboxgroupchange = () => {
        console.log("checkboxgroupchange34566");
      };
      </script>
      

      checkbox-group的功能,包裹checkbox组件,并把数据传递给checkbox

      嵌套组件间数据传递,使用到provide和inject

      checkbox-group注入数据

      <template>
        <div class="z-checkbox-group">
          <slot></slot>
        </div>
      </template>
      <script>
      import { defineComponent, computed, provide } from "vue";
      export default defineComponent({
        name: "MCheckboxGroup",
        props: {
          modelValue: Array,
        },
        emits: ["change", "update:modelValue"],
        setup(props, { emit }) {
          const modelValue = computed(() => props.modelValue);
          const changeEvent = (val) => {
            emit("change", val); // 更新change事件的val值
            emit("update:modelValue", val); //更新v-model绑定的modelValue
          };
          provide("MCheckboxGroup", {
            name: "MCheckboxGroup",
            modelValue,
            changeEvent,
          });
        },
      });
      </script>
      

      第20行,数据的提供者,给内部的checkbox组件使用

      // checkbox-group
      export interface ICheckboxGroupProvide {
        modelValue?: ComputedRef;
        changeEvent?: (val: unknown) => void;
        name?: string;
      }
      
      • 重新调节checkbox,第75接收数据
      • 第76行,判断是不是使用了group组件
      • 如果是group组件,则使用group的modelValue数据,第36行
      • 第46行,如果是group组件,则使用group的事件 ```vue ```

        串梭框