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 checkboxlerna 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属性
- 
设计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 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; }
 
- 触发响应父级的事件,使用emit把数据返回给父级
```vue
 
                         
                                

