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.tsimport 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属性
-
设计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