https://github.com/yanghuanrong/RelaxPlus,学习(上)

449 阅读2分钟

因为公司网络问题,只得将GitHub找到的优秀源码放到帖子上,在公司不忙时来学习。

util工具目录

relax-plus\utils\calendar.js

export function on(target, event, fn) {
  if (target && event && fn) {
    target.addEventListener(event, fn)
  }
}

export function off(target, event, fn) {
  if (target && event && fn) {
    target.removeEventListener(event, fn)
  }
}

export function remove(target, className) {
  if (target && className) {
    target.classList.remove(className)
  }
}

export function add(target, className) {
  if (target && className) {
    target.classList.add(className)
  }
}

relax-plus\utils\component.js

import { h, render } from 'vue'

export function createComponent(component, props) {
  const vnode = h(component, props)
  render(vnode, document.createElement('div'))
  return vnode.component
}

relax-plus\utils\dom.js

export function on(target, event, fn) {
  if (target && event && fn) {
    target.addEventListener(event, fn)
  }
}

export function off(target, event, fn) {
  if (target && event && fn) {
    target.removeEventListener(event, fn)
  }
}

export function remove(target, className) {
  if (target && className) {
    target.classList.remove(className)
  }
}

export function add(target, className) {
  if (target && className) {
    target.classList.add(className)
  }
}

relax-plus\utils\emiter.js

import { getCurrentInstance } from 'vue'
import mitt from './mitt'

const emitter = mitt()
const wrapper = Symbol('wrapper')

const useEmit = function() {
  const currentComponentInstance = getCurrentInstance()

  function on(type, fn) {
    const event = (e) => {
      const { type, emitComponentInstance, value } = e

      if (type === 'dispatch') {
        if (isChildComponent(emitComponentInstance, currentComponentInstance)) {
          fn && fn(...value)
        }
      } else if (type === 'broadcast') {
        if (isChildComponent(currentComponentInstance, emitComponentInstance)) {
          fn && fn(...value)
        }
      } else {
        fn && fn(...value)
      }
    }

    event[wrapper] = fn
    emitter.on(type, event)
  }

  function dispatch(type, ...args) {
    emitter.emit(type, {
      type: 'dispatch',
      emitComponentInstance: currentComponentInstance,
      value: args,
    })
  }

  function broadcast(type, ...args) {
    emitter.emit(type, {
      type: 'broadcast',
      emitComponentInstance: currentComponentInstance,
      value: args,
    })
  }

  return {
    on,
    dispatch,
    broadcast,
  }
}

function isChildComponent(componentChild, componentParent) {
  const parentUId = componentParent.uid
  while (componentChild && componentChild?.parent?.uid !== parentUId) {
    componentChild = componentChild.parent
  }
  return Boolean(componentChild)
}

export default useEmit

relax-plus\utils\isType.js

export const isNull = (targe) => toString.call(targe) === '[object Null]'
export const isObject = (targe) => toString.call(targe) === '[object Object]'
export const isNumber = (targe) => toString.call(targe) === '[object Number]'
export const isString = (targe) => toString.call(targe) === '[object String]'
export const isUndefined = (targe) =>
  toString.call(targe) === '[object Undefined]'
export const isBoolean = (targe) => toString.call(targe) === '[object Boolean]'
export const isArray = (targe) => toString.call(targe) === '[object Array]'
export const isFunction = (targe) =>
  toString.call(targe) === '[object Function]'

relax-plus\utils\mitt.js

export default function() {
  const all = new Map()
  const cached = {}

  function on(type, handler) {
    const handlers = all.get(type)
    const added = handlers && handlers.push(handler)
    if (!added) {
      all.set(type, [handler])
    }

    if (cached[type] instanceof Array) {
      handler.apply(null, cached[type])
    }
  }

  function emit(type, evt) {
    ;(all.get(type) || []).slice().map((handler) => {
      handler(evt)
    })
    cached[type] = Array.prototype.slice.call(arguments, 1)
  }

  function off(type, handler) {
    const handlers = all.get(type)
    if (handlers) {
      handlers.splice(handlers.indexOf(handler), 1)
    }
  }

  return {
    on,
    emit,
    off,
  }
}

relax-plus\utils\togger.js

import { onMounted, ref, getCurrentInstance, reactive, onUnmounted } from 'vue'

export default function useToggle() {
  const rect = reactive({})
  const trigger = ref(null)
  const isShow = ref(false)
  const currentInstance = getCurrentInstance()
  const clientHeight = document.documentElement.clientHeight

  let elHeight = 0
  let top1 = 0
  let top2 = 0
  let parent = null

  const focus = (e) => {
    parent = e.target
    const el = e.target.getBoundingClientRect()
    const scrollTop = document.documentElement.scrollTop
    const parentHeight = el.top + el.height

    top1 = parentHeight + scrollTop
    top2 = top1 - elHeight - el.height - 5

    const top = parentHeight + elHeight > clientHeight ? top2 : top1
    rect.transformOrigin =
      parentHeight + elHeight > clientHeight ? 'center bottom' : 'center top'
    rect.top = top + 'px'
    rect.left = el.left + 'px'
    rect.minWidth = el.width + 'px'
    rect.minHeight = elHeight + 'px'
  }

  const show = () => {
    isShow.value = true
  }
  const hide = () => {
    isShow.value = false
  }
  const toggle = () => {
    if (isShow.value) {
      hide()
    } else {
      show()
    }
  }

  const isHide = (e) => {
    const el = currentInstance.vnode.el
    if (!el.contains(e.target)) {
      hide()
    }
  }

  onMounted(() => {
    const el = trigger.value
    el.style.top = '-100%'
    el.style.display = 'block'
    elHeight = el.offsetHeight
    el.style.top = ''
    el.style.display = 'none'

    window.addEventListener('scroll', () => {
      if (isShow.value && parent) {
        const Rect = parent.getBoundingClientRect()

        if (Rect.top + Rect.height + elHeight > clientHeight) {
          rect.top = top2 + 'px'
          rect.transformOrigin = 'center bottom'
        } else {
          rect.top = top1 + 'px'
          rect.transformOrigin = 'center top'
        }
      }
    })

    document.addEventListener('click', isHide)
  })

  onUnmounted(() => {
    document.removeEventListener('click', isHide)
  })

  return {
    rect,
    trigger,
    focus,
    toggle,
    isShow,
    hide,
  }
}

Badge

<template>
  <span :class="'x-badge'">
    <template v-if="status !== ''">
      <i :class="[`x-badge-${status}`, shine && 'x-badge-shine']"></i>
      {{ text }}
    </template>
    <sup class="x-badge-count" v-else>{{ count }}</sup>
  </span>
</template>

<script>
export default {
  name: 'Badge',
  props: {
    text: [String, Number],
    status: {
      type: String,
      default: '',
      validator: (value) =>
        [
          '',
          'success',
          'primary',
          'warning',
          'info',
          'error',
          'default',
        ].includes(value),
    },
    count: [String, Number],
    shine: Boolean,
  },
}
</script>
<style>
.x-badge{

  .x-badge-success{
    --color: rgb(var(--success));
  }
  
  .x-badge-warning{
    --color: rgb(var(--warning));
  }
  
  .x-badge-error{
    --color: rgb(var(--danger));
  }
  
  .x-badge-default{
    --color: rgb(var(--default));
  }

  .x-badge-warning, 
  .x-badge-success, 
  .x-badge-error,
  .x-badge-default{
      width: 6px;
      height: 6px;
      border-radius: 50%;
      display: inline-block;
      margin-right: 5px;
      background-color: var(--color);
  }

  .x-badge-shine{
    position: relative;

    &::after{
      content: "";
      position: absolute;
      left: 0;
      top: 0;
      width: 100%;
      height: 100%;
      border-radius: 50%;
      display: block;
      background-color: var(--color);
      animation: shine 2.5s cubic-bezier(.47, -.2, .13, 1.35) infinite;
    }
  }

  .x-badge-count{
    background-color: rgb(var(--danger));
    border-radius: 8px;
    font-size: 12px;
    line-height: 16px;
    padding: 0 5px;
    color: rgb(var(--white));
  }

  @keyframes shine {
    0% {
      transform: scale(1);
      opacity: 0.48;
    }

    100% {
      transform: scale(3);
      opacity: 0;
    }
  }
}
</style>

Button

<template>
  <button class="x-btn" :class="className" :disabled="isDisabled">
    <span v-if="load" class="x-load"></span>
    <span class="x-btn-content" :style="style">
      <i v-if="icon !== ''" :class="icon" />
      <span v-if="$slots.default">
        <slot></slot>
      </span>
    </span>
  </button>
</template>

<script>
import {
  toRefs,
  computed,
  getCurrentInstance,
  ref,
  watch,
  watchEffect,
  onMounted,
  nextTick,
} from 'vue';
export default {
  name: 'Button',
  props: {
    type: {
      type: String,
      default: 'default',
      validator: (value) =>
        [
          'success',
          'primary',
          'warning',
          'info',
          'danger',
          'default',
          'text',
        ].includes(value),
    },
    size: {
      type: String,
      default: 'md',
      validator: (value) => ['lg', 'sm', 'md'].includes(value),
    },
    icon: String,
    plain: Boolean,
    round: Boolean,
    circle: Boolean,
    block: Boolean,
    disabled: Boolean,
    loading: Boolean,
  },
  setup(props) {
    const instance = getCurrentInstance();
    const { loading, icon } = toRefs(props);
    const load = ref(loading.value);

    const isDisabled = computed(() => props.disabled || load.value);

    watchEffect(() => {
      load.value = loading.value;
    });

    const oldClick = instance.attrs.onClick;
    async function modified() {
      const cb = oldClick();
      if (cb && typeof cb.then === 'function') {
        load.value = true;
        cb && (await cb);
        load.value = false;
      }
    }
    oldClick && (instance.attrs.onClick = modified);

    const className = useClass({
      props,
      load,
      icon,
    });

    const style = computed(() =>
      load.value
        ? {
            opacity: '0',
            transform: 'scale(2.2)',
          }
        : {}
    );

    return {
      className,
      icon,
      style,
      isDisabled,
      load,
    };
  },
};

const useClass = ({ props, load: loading }) => {
  return computed(() => {
    return [
      props.type && `x-btn-${props.type}`,
      props.size !== '' || props.size ? `x-btn-${props.size}` : '',
      {
        'is-plain': props.plain,
        'is-round': props.round,
        'is-circle': props.circle,
        'is-block': props.block,
        disabled: props.disabled,
      },
      loading.value && 'x-btn-loading',
    ];
  });
};
</script>
<style lang="less">
.x-btn {
  --color: var(--default);

  display: inline-block;
  position: relative;
  max-width: 100%;
  margin: 0;
  padding: 7px 15px;
  transition: all 0.25s ease-in-out;
  border: 1px solid transparent;
  border-radius: 2px;
  font-size: 12px;
  font-weight: 400;
  text-align: center;
  text-overflow: ellipsis;
  white-space: nowrap;
  cursor: pointer;
  vertical-align: middle;
  appearance: none;
  user-select: none;
  text-decoration: none;
  color: rgb(var(--white));
  border-color: rgb(var(--color), 0.8);
  background-color: rgb(var(--color), 0.9);

  &:hover {
    background-color: rgb(var(--color), 1);
  }

  &:active {
    border-color: rgb(var(--color), 1);
    background-color: rgb(var(--color), .5);
    box-shadow: 0 0 0 2px rgb(var(--color), .5);
  }

  &:focus {
    background-image: none;
    outline: 0;
  }

  &.x-btn-default {
    --color: var(--default);
    color: rgb(var(--black), .7);

    &.is-plain{
      color: rgb(var(--mix-color));
    }
  }
  &.x-btn-success {
    --color: var(--success);
  }
  &.x-btn-primary {
    --color: var(--primary);
  }
  &.x-btn-danger {
    --color: var(--danger);
  }
  &.x-btn-info {
    --color: var(--info);
  }
  &.x-btn-warning {
    --color: var(--warning);
  }

  &.is-plain {
    border-width: 1px;
    border-style: solid;
    background-color: transparent;
    box-shadow: none;
    color: rgb(var(--color));

    &:hover{
      background-color: rgb(var(--color), .1);
    }
    &:active {
      box-shadow: 0 0 0 2px rgb(var(--color), .3);
    }
  }

  &.x-btn-lg {
    padding: 8px 33px;
    font-size: 14px;
  }

  &.x-btn-sm {
    padding: 3px 10px;
    font-size: 12px;
    .x-load{
      width: 1.5em;
      height: 1.5em;
    }
  }

  &.x-btn-text{
    padding-left: 2px;
    padding-right: 2px;
    color: var(--color);
    background-color: transparent;
    border: none;

    &:hover{
      color: rgb(var(--primary));
    }
    &:active{
      box-shadow: none;
    }
  }
  
  &.is-round {
    border-radius: 2em
  }

  &.is-circle {
    border-radius: 50%;
    padding: 8px 9px;
  }

  &.is-block {
    display: block;
    width: 100%;
  }
  
  &.disabled,
  &:disabled {
    box-shadow: none;
    opacity: 0.5;
    cursor: not-allowed;

    &:hover,
    &:focus,
    &:active {
      transform: none;
      box-shadow: none;
      opacity: 0.5;
    }
  }

  &.x-btn-loading{
    &:disabled{
      cursor: inherit;
      box-shadow: none;
      opacity: 0.5;
  
      &:hover,
      &:focus,
      &:active {
        transform: none;
        box-shadow: none;
        opacity: 0.5;
      }
    }
  }
  
  &>.x-btn-content{
    display: inline-block;
    transition: all .35s ease;
    transform: scale(1);
    opacity: 1;
  }
  
  .x-load {
    display: inline-block;
    width: 2em;
    height: 2em;
    color: inherit;
    vertical-align: middle;
    pointer-events: none;
    border: 0 solid transparent;
    border-radius: 50%;
    position: absolute;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
  
    &:before,
    &:after {
      content: '';
      border: .2em solid currentcolor;
      border-radius: 50%;
      width: inherit;
      height: inherit;
      position: absolute;
      top: 0;
      left: 0;
      animation: x-load 1s linear infinite;
      opacity: 0;
    }
    
    &:after {
        animation-delay: .5s;
    }
  }
  
  @keyframes x-load {
      0% {
          transform: scale(0);
          opacity: 0;
      }
      50% {
          opacity: 1;
      }
      100% {
          transform: scale(1);
          opacity: 0;
      }
  }


  & [class*=x-icon-]+span {
    margin-left: 5px;
  }
}

</style>

Calendar

<template>
  <div class="x-calendar">
    <div class="x-calendar-head">
      <div class="x-calendar-month">
        <div class="x-calendar-btn" @click="changePrevMonth">
          <i class="x-icon-chevron-left"></i>
        </div>
        <span>
          {{ nowTime.year }}年{{
            nowTime.month + 1 < 10
              ? '0' + (nowTime.month + 1)
              : nowTime.month + 1
          }}月
        </span>
        <div class="x-calendar-btn" @click="changeNextMonth">
          <i class="x-icon-chevron-right"></i>
        </div>
      </div>
      <div class="x-calendar-btn" @click="changeNowMonth">
        <i class="x-icon-circle" style="margin-right: 5px"></i>今天
      </div>
    </div>
    <div class="x-calendar-group" :data-month="nowTime.month + 1 + '月'">
      <div class="x-calendar-week" v-for="(item, i) in week" :key="i">
        {{ item }}
      </div>
      <div
        class="x-calendar-cell"
        :title="`${item.y}年${item.m}月${item.d}日`"
        v-for="(item, i) in cell"
        :key="i"
        :class="[
          item.class,
          {
            today: today(item),
            active: isAactiveDay(item),
          },
        ]"
        @click="changeDay(item)"
      >
        <div class="x-calendar-cell__box">
          <div class="x-calendar-cell__day">{{ item.d }}</div>
          <slot name="dateCell" :data="item"></slot>
        </div>
      </div>
    </div>
  </div>
</template>

<script>
import useCalendar from '../../utils/calendar'

export default {
  name: 'Calendar',
  setup() {
    const calendar = useCalendar()

    return {
      ...calendar,
    }
  },
}
</script>
<style lang="less">
.x-calendar {
  width: 100%;
  user-select: none;
  border: 1px solid var(--line-color);
  border-radius: 8px;

  .x-calendar-head{
    display: flex;
    justify-content: space-between;
    padding: 30px 10px;
    padding-bottom: 10px;
    line-height: 30px;

    .x-calendar-month{
      display: flex;
      font-size: 18px;

      span{
        padding: 5px 10px;
      }
    }

    .x-calendar-btn{
      padding: 5px 10px;
      border-radius: 3px;
      cursor: pointer;
      transition: background-color .2s ease;
      
      &:hover{
        background-color: var(--hover-background-color)
      };
    }
  }

  .x-calendar-group{
    display: flex;
    flex-wrap: wrap;
    position: relative;

    &::before{
      content: attr(data-month);
      position: absolute;
      right: 0;
      bottom: 0;
      font-size: 24em;
      line-height: 1.2;
      opacity: 0.02;
      pointer-events: none;
      font-family: auto;
      font-weight: bold;
    }

    .x-calendar-week{
      list-style: none;
      width: calc(100% / 7);
      height: 40px;
      line-height: 40px;
      text-align: center;
      color: var(--sub-text-color);
    }

    .x-calendar-cell{
      list-style: none;
      width: calc(100% / 7);
      height: 100px;
      border-top: 1px solid var(--line-color);
      border-right: 1px solid var(--line-color);
      position: relative;
      transition: all .2s ease-in;
      cursor: pointer;

      &.today{
        .x-calendar-cell__day{
          background-color: rgb(var(--primary));
          color: rgb(var(--white));
          border-radius: 50%;
        }
      }

      &:hover{
        background-color: rgb(var(--primary), .05);
      }

      &.active{
        z-index: 2;
        cursor: auto;
        box-shadow: 0 0 4px rgb(var(--primary), .8);
        &::after{
          border-color: rgb(var(--primary));
        }
        &:hover{
          background-color: transparent;
        }
      }

      
      &::after{
        content: "";
        position: absolute;
        left: -1px;
        right: -1px;
        bottom: -1px;
        top: -1px;
        border: 1px solid transparent;
        pointer-events: none;
      }

      &:nth-of-type(7n){
        border-right: 0;
      }
      
      &:nth-last-of-type(1){
        &::after{
          border-bottom-right-radius: 8px;
        }
      }
      &:nth-last-of-type(7){
        &::after{
          border-bottom-left-radius: 8px;
        }
      }

      .x-calendar-cell__day{
        position: absolute;
        right: 10px;
        top: 10px;
        width: 20px;
        height: 20px;
        text-align: center;
        color: var(--main-text-color);
      }

      .x-calendar-cell__box{
        padding: 10px;
        height: inherit;
        overflow: auto;
        padding-right: 25px;
      }

      &.x-prev-day,&.x-next-day{
        .x-calendar-cell__day{
          color: var(--sub-text-color);
          opacity: 0.5;
        }
      }
    }
  }
}

<style>

Carousel

<template>
  <div
    class="x-carousel"
    :style="`width:${width}; height: ${height}`"
    @mouseover="mouseover"
    @mouseleave="mouseleave"
  >
    <div class="x-carousel-left" @click="setCarousel(-1, 'slide-left')">
      <i class="x-icon-chevron-left"></i>
    </div>
    <div class="x-carousel-right" @click="setCarousel(1, 'slide-right')">
      <i class="x-icon-chevron-right"></i>
    </div>
    <div class="x-carousel-warp">
      <slot></slot>
    </div>
    <div class="x-carousel-dot">
      <i
        v-for="(item, i) in items"
        :key="i"
        :class="{ active: inActive === i }"
        @click="handerDot(i)"
      ></i>
    </div>
  </div>
</template>

<script>
import { onUnmounted, provide, reactive, ref, toRefs, watchEffect } from 'vue'
import emitter from '../../utils/emiter'

export default {
  name: 'Carousel',
  props: {
    width: String,
    height: String,
    autoplay: Boolean,
    interval: {
      type: Number,
      default: 3,
    },
  },
  setup(props) {
    const inActive = ref(0)
    const inUid = ref(0)
    const items = reactive([])
    const transitionName = ref('slide-right')
    const { autoplay, interval } = toRefs(props)
    provide('carousel-active', inUid)
    provide('carousel-name', transitionName)
    const { on } = emitter()

    on('carousel-item', (uid) => {
      items.push(uid)
    })

    watchEffect(() => {
      inUid.value = items[inActive.value]
    })

    const setCarousel = (i, name) => {
      inActive.value += i
      transitionName.value = name

      if (inActive.value < 0) {
        inActive.value = items.length - 1
      }
      if (inActive.value >= items.length) {
        inActive.value = 0
      }
    }

    let time = null
    const timeStop = () => {
      if (time) {
        clearInterval(time)
        time = null
      }
    }
    const timeStar = () => {
      if (autoplay && autoplay.value && !time) {
        time = setInterval(() => {
          setCarousel(1, 'slide-right')
        }, interval.value * 1000)
      }
    }

    const mouseover = () => {
      timeStop()
    }

    const mouseleave = () => {
      timeStar()
    }

    const handerDot = (i) => {
      const now = i - inActive.value
      setCarousel(now, now < 0 ? 'slide-left' : 'slide-right')
    }

    timeStar()
    onUnmounted(() => {
      timeStop()
    })

    return {
      setCarousel,
      inActive,
      items,
      mouseleave,
      mouseover,
      handerDot,
    }
  },
}
</script>
<style lang="less">
.x-carousel{
  position: relative;

  &:hover{
    .x-carousel-left, .x-carousel-right{
      opacity: 1;
      visibility: visible;
      transform: translate(0, -50%);
    }
  }

  .x-carousel-left, .x-carousel-right{
    position: absolute;
    width: 40px;
    height: 40px;
    top: 50%;
    transform: translateY(-50%);
    background-color: rgb(var(--black), .1);
    z-index: 4;
    border-radius: 50%;
    display: flex;
    align-items: center;
    justify-content: center;
    font-size: 18px;
    color: rgb(var(--white));
    cursor: pointer;
    opacity: 0;
    visibility: hidden;
    transition-property: opacity,transform,visibility;
    transition-duration: .2s;
    transition-timing-function: ease;
  }
  .x-carousel-left{
    left: 10px;
    transform: translate(-10%, -50%);
  }
  .x-carousel-right{
    right: 10px;
    transform: translate(10%, -50%);
  }
  .x-carousel-item{
    position: absolute;
  }
  .x-carousel-dot{
    position: absolute;
    bottom: 10px;
    left: 50%;
    transform: translateX(-50%);
    z-index: 1;
    display: flex;

    i{
      margin: 0 5px;
      display: block;
      border-radius: 2em;
      width: 8px;
      height: 8px;
      background-color: rgb(var(--white));
      opacity: 0.7;
      transition: all .3s ease;
      cursor: pointer;

      &.active{
        opacity: 1;
        width: 16px;
      }
    }

  }
  .x-carousel-warp{
    height: inherit;
    position: relative;
    overflow: hidden;
  }
}
</style>

CarouselItem

<template>
  <transition :name="inName" mode="out-in">
    <div class="x-carousel-item" v-show="isShow">
      <slot></slot>
    </div>
  </transition>
</template>

<script>
import { computed, getCurrentInstance, inject } from 'vue'
import emitter from '../../utils/emiter'
export default {
  name: 'CarouselItem',
  setup() {
    const inActive = inject('carousel-active')
    const inName = inject('carousel-name')
    const instance = getCurrentInstance()
    const { dispatch } = emitter()
    dispatch('carousel-item', instance.uid)

    const isShow = computed(() => inActive.value === instance.uid)

    return {
      isShow,
      inName,
    }
  },
}
</script>

Checkbox

<template>
  <label
    class="x-checkbox"
    :class="{
      'x-checkbox-checked': isCheked,
      'x-checkbox-disabled': disabled,
    }"
  >
    <input
      type="checkbox"
      :label="label"
      :disabled="disabled"
      :checked="isCheked"
      @change.stop="handerClick"
    />
    <span>
      <template v-if="$slots.default">
        <slot></slot>
      </template>
      <template v-else>
        {{ label }}
      </template>
    </span>
  </label>
</template>

<script>
import { inject, reactive, watchEffect, computed } from 'vue'
import { isArray } from '../../utils/isType'
export default {
  name: 'Checkbox',
  props: {
    label: [String, Number, Boolean],
    modelValue: Boolean,
    checked: Boolean,
    disabled: Boolean,
  },
  setup(props, { emit }) {
    const checkboxGroup = inject('checkboxGroup', { props: {} })

    const state = reactive({
      modelValue: null,
    })

    watchEffect(() => {
      state.modelValue =
        checkboxGroup.props.modelValue || props.modelValue || props.checked
    })

    const model = computed({
      get() {
        return state.modelValue
      },
      set({ checked, label }) {
        if (isArray(model.value)) {
          const modelValue = model.value
          const labelIndex = modelValue.indexOf(label)

          labelIndex === -1 && checked === true && modelValue.push(label)
          labelIndex !== -1 &&
            checked === false &&
            modelValue.splice(labelIndex, 1)

          state.modelValue = modelValue
          emit('update:modelValue', modelValue)
        } else {
          state.modelValue = checked
          emit('update:modelValue', checked)
        }
      },
    })

    const isCheked = computed(() => {
      if (isArray(model.value)) {
        return model.value.indexOf(props.label) !== -1
      } else {
        return model.value
      }
    })

    const handerClick = (e) => {
      model.value = {
        checked: e.target.checked,
        label: props.label,
      }
      emit('change', model.value)
    }

    return {
      handerClick,
      isCheked,
    }
  },
}
</script>
<style lang="less">
.x-checkbox {
  --color: var(--primary);
  cursor: pointer;
  margin-right: 10px;
  user-select: none;
  line-height: 1.3;

  &:hover {
    input[type="checkbox"]+span::before {
      border-color: rgb(var(--primary));
    }
  }

  input[type="checkbox"]+span {
    display: inline-block;
    padding-left: 6px;
    position: relative;
    font-weight: normal;

    &::before {
      content: "";
      background-color: rgb(--white);
      border-radius: 2px;
      border: 1px solid var(--line-color);
      display: inline-block;
      left: 0;
      margin-left: -14px;
      position: absolute;
      transition: 0.3s ease-in-out;
      width: 16px;
      height: 16px;
      outline: none !important;
    }

    &::after {
      content: "";
      position: absolute;
      top: 3px;
      left: -8px;
      display: table;
      width: 4px;
      height: 8px;
      border: 1px solid rgb(var(--white));
      border-top-width: 0;
      border-left-width: 0;
      transform: rotate(45deg) scale(0);
      opacity: 0;
      transition: all .4s ease;
    }

    &:active::before {
      box-shadow: 0 0 0 2px rgb(var(--color), .5);
    }
  }

  input[type="checkbox"] {
    cursor: pointer;
    opacity: 0;
    z-index: 1;
    outline: none !important;
  }

  &.x-checkbox-checked {
    input[type="checkbox"]+span {
      &::after {
        opacity: 1;
        transform: rotate(45deg) scale(1);
      }

      &::before {
        background-color: rgb(var(--color));
        border-color: rgb(var(--color));
      }
    }

    &.x-checkbox-disabled {
      input[type="checkbox"]+span {
        &::before{
          background-color: var(--line-color);
        }
        &::after{
          border: 1px solid rgb(var(--mix-color), .4);
          border-top-width: 0;
          border-left-width: 0;
          transform: rotate(45deg);
          border-color: rgb(var(--mix-color), .4);
        }
      }
    }
  }

  &.x-checkbox-disabled {
    cursor: not-allowed;

    input[type="checkbox"]+span {
      opacity: 0.65;

      &::before {
        background-color: var(--line-color);
        border-color: var(--line-color);
      }

      &:active::before {
        box-shadow: none;
      }
    }

    
  }
}
</style>

CheckboxGroup

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
import { getCurrentInstance, provide } from 'vue'

export default {
  name: 'CheckboxGroup',
  props: {
    modelValue: Array,
  },
  setup() {
    provide('checkboxGroup', getCurrentInstance())
  },
}
</script>

Col

<template>
  <component :is="tag" :class="classes" :style="style">
    <slot></slot>
  </component>
</template>

<script>
import { inject, computed } from 'vue'
import { isNumber, isString } from '../../utils/isType'
export default {
  name: 'Col',
  props: {
    tag: {
      type: String,
      default: 'div',
    },
    span: {
      type: Number,
      default: 24,
    },
    offset: Number,
    order: Number,
    xs: [Number, Object],
    sm: [Number, Object],
    md: [Number, Object],
    lg: [Number, Object],
  },
  setup(props) {
    const Row = inject('Row', { props: {} })
    let classes = ['x-col']

    let isSpan = true
    ;['xs', 'sm', 'md', 'lg'].forEach((item) => {
      if (isNumber(props[item])) {
        isSpan = false
        classes.push(`x-col-${item}-${props[item]}`)
      } else if (isString(props[item])) {
        isSpan = false
        props[item].span && classes.push(`x-col-${item}-${props[item].span}`)
        props[item].offset &&
          classes.push(`x-col-offset-${item}-${props[item].span}`)
      }
    })

    if (isSpan) {
      classes = [`x-col-sp-${props.span}`]
      props.offset && classes.push(`x-col-offset-sp-${props.offset}`)
    }

    if (Row.type === 'flex') {
      props.order && classes.push(`x-col-order-${props.order}`)
    }

    const style = computed(() => {
      const ret = {}
      if (Row.gutter) {
        ret.paddingLeft = `${Row.gutter / 2}px`
        ret.paddingRight = ret.paddingLeft
      }
      return ret
    })

    return {
      tag: props.tag,
      classes,
      style,
    }
  },
}
</script>

DatePicker

<template>
  <div class="x-date-edit">
    <Input
      readonly
      :placeholder="placeholder"
      icon-before="x-icon-calendar"
      v-model="state"
      @click.prevent="toggle"
      :class="{
        'is-focus': isShow,
        'is-blur': !isShow,
      }"
      :disabled="disabled"
      clearable
      block
      @focus="focus"
    />

    <teleport to="body">
      <transition name="scaleY" ref="trigger">
        <div
          class="x-trigger x-datePicker"
          @click.stop
          :style="rect"
          v-show="isShow"
        >
          <div class="x-datePicker-head">
            <div class="x-datePicker-btn">
              <span class="x-icon-chevrons-left" @click="changePrevYear"></span>
              <span class="x-icon-chevron-left" @click="changePrevMonth"></span>
            </div>
            <span>
              {{ head }}
            </span>
            <div class="x-datePicker-btn">
              <span
                class="x-icon-chevron-right"
                @click="changeNextMonth"
              ></span>
              <span
                class="x-icon-chevrons-right"
                @click="changeNextYear"
              ></span>
            </div>
          </div>

          <div class="x-datePicker-group">
            <div class="x-datePicker-week" v-for="(item, i) in week" :key="i">
              {{ item }}
            </div>
            <div
              class="x-datePicker-cell"
              :title="`${item.y}年${item.m}月${item.d}日`"
              v-for="(item, i) in cell"
              :key="i"
              :class="[
                item.class,
                {
                  today: today(item),
                  active: isAactiveDay(item),
                },
              ]"
              @click="changeDay(item), clickDay(item)"
            >
              <div class="x-datePicker-cell__box">{{ item.d }}</div>
            </div>
          </div>
          <div class="x-calendar-foot" v-if="!onetap">
            <div class="x-calendar-quick">
              <Button type="text" size="sm" @click="changeToday">今天</Button>
            </div>
            <Button type="primary" size="sm" @click="changeClickDay"
              >确定</Button
            >
          </div>
        </div>
      </transition>
    </teleport>
  </div>
</template>

<script>
import { nextTick, ref, toRefs, watch } from 'vue'
import Input from '../input/index'
import useToggle from '../../utils/togger'
import useCalendar from '../../utils/calendar'
import Button from '../button/button.vue'

export default {
  name: 'DatePicker',
  components: {
    Input,
    Button,
  },
  props: {
    modelValue: String,
    placeholder: String,
    onetap: Boolean,
    disabled: Boolean,
  },
  setup(props, { emit }) {
    const { modelValue } = toRefs(props)
    const toggle = useToggle()
    const { hide, isShow } = toggle
    const calendar = useCalendar(props)
    const { nowTime, checkTime, repair } = calendar

    const state = ref('')
    const head = ref(headFormat(''))

    function headFormat(value) {
      const [y, m, d] =
        value !== ''
          ? value.split('-')
          : [nowTime.year, repair(nowTime.month + 1), repair(nowTime.day)]

      return `${y}${m}${d}日`
    }

    watch(checkTime, (value) => {
      head.value = headFormat(value)
    })

    const clickDay = (item) => {
      if (props.onetap) {
        const { y, m, d } = item
        state.value = `${y}-${m}-${d}`
        nextTick(() => {
          hide()
        })
      }
    }

    watch(state, (value) => {
      if (value === '') {
        calendar.changeNowMonth()
        emit('update:modelValue', value)
      } else {
        checkTime.value = value
      }
    })

    watch(isShow, (value) => {
      if (value) {
        if (modelValue.value === '') {
          calendar.changeNowMonth()
        } else {
          const [y, m, d] = modelValue.value.split('-')
          calendar.changeDay({ y, m, d })
          nowTime.year = parseInt(y)
          nowTime.month = parseInt(m) - 1
        }
      } else {
        emit('update:modelValue', state.value)
      }
    })

    const changeClickDay = () => {
      const reg = /[0-9]+/g
      const [y, m, d] = head.value.match(reg)
      state.value = `${y}-${m}-${d}`
      hide()
    }

    const changeToday = () => {
      const date = new Date()
      let y = date.getFullYear()
      let m = repair(date.getMonth() + 1)
      let d = repair(date.getDate())

      state.value = `${y}-${m}-${d}`
      hide()
    }

    return {
      ...toggle,
      ...calendar,
      state,
      head,
      clickDay,
      changeClickDay,
      changeToday,
    }
  },
}
</script>
<style lang="less">
.x-date-edit{
  display: inline-block;
}
.x-datePicker{
  width: 240px;
  padding: 10px;
  user-select: none;

  .x-datePicker-head{
    display: flex;
    justify-content: space-between;
    padding-bottom: 10px;

    .x-datePicker-btn{
      span{
        cursor: pointer;
        padding: 0 3px;
        border-radius: 3px;
        transition: all .25s ease;

        &:hover{
          background-color: var(--hover-background-color);
        }
      }
    }
  }

  .x-calendar-foot{
    padding-top: 10px;
    display: flex;
    justify-content: space-between;
  }

  .x-datePicker-group{
    display: flex;
    flex-wrap: wrap;
    width: 100%;
    text-align: center;

    .x-datePicker-week{
      list-style: none;
      width: calc(100% / 7);
      color: var(--sub-text-color);
    }

    .x-datePicker-cell{
      list-style: none;
      width: calc(100% / 7);
      position: relative;
      transition: all .2s ease-in;
      padding: 5px;

      .x-datePicker-cell__box{
        border: 1px solid transparent;
        cursor: pointer;
        border-radius: 3px;
        transition: all .2s ease;
          
        &:hover{
          background-color: rgb(var(--primary), .15);
        }
      }
      &.today{
        .x-datePicker-cell__box{
          color: rgb(var(--primary));
          background-color: rgb(var(--primary), .05);
          border: 1px solid rgb(var(--primary));
        }
      }
      &.active{
        .x-datePicker-cell__box{
          border: 1px solid rgb(var(--primary));
          background-color: rgb(var(--primary));
          color: rgb(var(--white));
        }
      }

      &.x-prev-day,&.x-next-day{
        .x-datePicker-cell__box{
          color: var(--sub-text-color);
          opacity: 0.5;
        }
      }

  
    }
  }

}
</style>

Drawer

<template>
  <div class="x-drawer">
    <transition name="fade">
      <div v-show="isShow" class="x-mask" @click="handleClose"></div>
    </transition>

    <transition name="drawer-left">
      <div
        class="x-drawer__view"
        v-if="isShow"
        :style="{ width: `${width}px` }"
      >
        <div class="x-drawer__header">
          <span class="x-drawer__title" v-if="title">{{ title }}</span>
        </div>
        <div class="x-drawer__body">
          <slot></slot>
        </div>
        <div class="x-drawer__footer" v-if="!$slots.footer">
          <Button type="primary" plain @click="handleClose">关闭</Button>
          <Button type="primary" @click="handleConfirm">确定</Button>
        </div>
        <slot name="footer" v-else></slot>
      </div>
    </transition>
  </div>
</template>

<script>
import { toRefs, watch, ref } from 'vue';
import Button from '../button';
export default {
  name: 'Drawer',
  components: {
    Button,
  },
  props: {
    width: {
      type: Number,
      default: 400,
    },
    modelValue: Boolean,
    title: String,
  },
  emits: ['close', 'ok', 'update:modelValue', 'loading'],
  setup(props, { emit }) {
    const { modelValue } = toRefs(props);
    const isShow = ref(modelValue.value);
    const handleClose = () => {
      emit('update:modelValue', false);
      emit('close');
    };
    const handleConfirm = () => {
      emit('ok');
    };
    watch(modelValue, (value) => {
      isShow.value = value;
    });
    return {
      isShow,
      handleClose,
      handleConfirm,
    };
  },
};
</script>
<style lang="less">
.el-dialog__body.min {
  height: calc(100vh - 60px - 62px);
  overflow: auto;
}
.x-drawer {
  .x-drawer__view {
    width: 442px;
    position: fixed;
    z-index: 11;
    height: 100vh;
    top: 0;
    right: 0;
    background: var(--main-background-color);
    box-shadow: -2px 0 8px var(--shadow);
    display: flex;
    flex-direction: column;

    .x-drawer__header {
      padding: 16px 24px;
      display: flex;
      justify-content: space-between;
      font-size: 18px;
      font-weight: bold;
      border-bottom: 1px solid var(--line-color);
    }
    .x-drawer__body {
      flex: 1;
      padding: 16px 24px;
      overflow: auto;
      color: var(--sub-text-color);
    }

    .x-drawer__footer {
      padding: 16px 24px;
      text-align: right;
      border-top: 1px solid var(--line-color);

      .x-btn {
        margin: 0;
        margin-left: 16px;
      }
    }
  }
  .x-mask {
    position: fixed;
    left: 0;
    top: 0;
    right: 0;
    bottom: 0;
    background: rgba(0, 0, 0, 0.3);
    z-index: 10;
  }
}

</style>

Icon

<template>
  <i :class="type"></i>
</template>

<script>
export default {
  name: 'Icon',
  props: {
    type: String,
  },
}
</script>
<style lang="less">
@font-face {
  font-family: feather;
  src: url("./fonts/feather.eot");
  src: url("./fonts/feather.eot") format("embedded-opentype"),url(./fonts/feather.woff) format("woff"),url(./fonts/feather.ttf) format("truetype"),url(./fonts/feather.svg) format("svg");
}

.x-icon {
  font-family: feather!important;
  font-style: normal;
  font-weight: 400;
  display: inline-block;
  font-variant: normal;
  text-transform: none;
  line-height: 1;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
}

.x-icon-alert-octagon:before {
  .x-icon;
  content: "\e81b";
}

.x-icon-alert-circle:before {
  .x-icon;
  content: "\e81c";
}

.x-icon-activity:before {
  .x-icon;
  content: "\e81d";
}

.x-icon-alert-triangle:before {
  .x-icon;
  content: "\e81e";
}

.x-icon-align-center:before {
  .x-icon;
  content: "\e81f";
}

.x-icon-airplay:before {
  .x-icon;
  content: "\e820";
}

.x-icon-align-justify:before {
  .x-icon;
  content: "\e821";
}

.x-icon-align-left:before {
  .x-icon;
  content: "\e822";
}

.x-icon-align-right:before {
  .x-icon;
  content: "\e823";
}

.x-icon-arrow-down-left:before {
  .x-icon;
  content: "\e824";
}

.x-icon-arrow-down-right:before {
  .x-icon;
  content: "\e825";
}

.x-icon-anchor:before {
  .x-icon;
  content: "\e826";
}

.x-icon-aperture:before {
  .x-icon;
  content: "\e827";
}

.x-icon-arrow-left:before {
  .x-icon;
  content: "\e828";
}

.x-icon-arrow-right:before {
  .x-icon;
  content: "\e829";
}

.x-icon-arrow-down:before {
  .x-icon;
  content: "\e82a";
}

.x-icon-arrow-up-left:before {
  .x-icon;
  content: "\e82b";
}

.x-icon-arrow-up-right:before {
  .x-icon;
  content: "\e82c";
}

.x-icon-arrow-up:before {
  .x-icon;
  content: "\e82d";
}

.x-icon-award:before {
  .x-icon;
  content: "\e82e";
}

.x-icon-bar-chart:before {
  .x-icon;
  content: "\e82f";
}

.x-icon-at-sign:before {
  .x-icon;
  content: "\e830";
}

.x-icon-bar-chart-2:before {
  .x-icon;
  content: "\e831";
}

.x-icon-battery-charging:before {
  .x-icon;
  content: "\e832";
}

.x-icon-bell-off:before {
  .x-icon;
  content: "\e833";
}

.x-icon-battery:before {
  .x-icon;
  content: "\e834";
}

.x-icon-bluetooth:before {
  .x-icon;
  content: "\e835";
}

.x-icon-bell:before {
  .x-icon;
  content: "\e836";
}

.x-icon-book:before {
  .x-icon;
  content: "\e837";
}

.x-icon-briefcase:before {
  .x-icon;
  content: "\e838";
}

.x-icon-camera-off:before {
  .x-icon;
  content: "\e839";
}

.x-icon-calendar:before {
  .x-icon;
  content: "\e83a";
}

.x-icon-bookmark:before {
  .x-icon;
  content: "\e83b";
}

.x-icon-box:before {
  .x-icon;
  content: "\e83c";
}

.x-icon-camera:before {
  .x-icon;
  content: "\e83d";
}

.x-icon-check-circle:before {
  .x-icon;
  content: "\e83e";
}

.x-icon-check:before {
  .x-icon;
  content: "\e83f";
}

.x-icon-check-square:before {
  .x-icon;
  content: "\e840";
}

.x-icon-cast:before {
  .x-icon;
  content: "\e841";
}

.x-icon-chevron-down:before {
  .x-icon;
  content: "\e842";
}

.x-icon-chevron-left:before {
  .x-icon;
  content: "\e843";
}

.x-icon-chevron-right:before {
  .x-icon;
  content: "\e844";
}

.x-icon-chevron-up:before {
  .x-icon;
  content: "\e845";
}

.x-icon-chevrons-down:before {
  .x-icon;
  content: "\e846";
}

.x-icon-chevrons-right:before {
  .x-icon;
  content: "\e847";
}

.x-icon-chevrons-up:before {
  .x-icon;
  content: "\e848";
}

.x-icon-chevrons-left:before {
  .x-icon;
  content: "\e849";
}

.x-icon-circle:before {
  .x-icon;
  content: "\e84a";
}

.x-icon-clipboard:before {
  .x-icon;
  content: "\e84b";
}

.x-icon-chrome:before {
  .x-icon;
  content: "\e84c";
}

.x-icon-clock:before {
  .x-icon;
  content: "\e84d";
}

.x-icon-cloud-lightning:before {
  .x-icon;
  content: "\e84e";
}

.x-icon-cloud-drizzle:before {
  .x-icon;
  content: "\e84f";
}

.x-icon-cloud-rain:before {
  .x-icon;
  content: "\e850";
}

.x-icon-cloud-off:before {
  .x-icon;
  content: "\e851";
}

.x-icon-codepen:before {
  .x-icon;
  content: "\e852";
}

.x-icon-cloud-snow:before {
  .x-icon;
  content: "\e853";
}

.x-icon-compass:before {
  .x-icon;
  content: "\e854";
}

.x-icon-copy:before {
  .x-icon;
  content: "\e855";
}

.x-icon-corner-down-right:before {
  .x-icon;
  content: "\e856";
}

.x-icon-corner-down-left:before {
  .x-icon;
  content: "\e857";
}

.x-icon-corner-left-down:before {
  .x-icon;
  content: "\e858";
}

.x-icon-corner-left-up:before {
  .x-icon;
  content: "\e859";
}

.x-icon-corner-up-left:before {
  .x-icon;
  content: "\e85a";
}

.x-icon-corner-up-right:before {
  .x-icon;
  content: "\e85b";
}

.x-icon-corner-right-down:before {
  .x-icon;
  content: "\e85c";
}

.x-icon-corner-right-up:before {
  .x-icon;
  content: "\e85d";
}

.x-icon-cpu:before {
  .x-icon;
  content: "\e85e";
}

.x-icon-credit-card:before {
  .x-icon;
  content: "\e85f";
}

.x-icon-crosshair:before {
  .x-icon;
  content: "\e860";
}

.x-icon-disc:before {
  .x-icon;
  content: "\e861";
}

.x-icon-delete:before {
  .x-icon;
  content: "\e862";
}

.x-icon-download-cloud:before {
  .x-icon;
  content: "\e863";
}

.x-icon-download:before {
  .x-icon;
  content: "\e864";
}

.x-icon-droplet:before {
  .x-icon;
  content: "\e865";
}

.x-icon-edit-2:before {
  .x-icon;
  content: "\e866";
}

.x-icon-edit:before {
  .x-icon;
  content: "\e867";
}

.x-icon-edit-1:before {
  .x-icon;
  content: "\e868";
}

.x-icon-external-link:before {
  .x-icon;
  content: "\e869";
}

.x-icon-eye:before {
  .x-icon;
  content: "\e86a";
}

.x-icon-feather:before {
  .x-icon;
  content: "\e86b";
}

.x-icon-facebook:before {
  .x-icon;
  content: "\e86c";
}

.x-icon-file-minus:before {
  .x-icon;
  content: "\e86d";
}

.x-icon-eye-off:before {
  .x-icon;
  content: "\e86e";
}

.x-icon-fast-forward:before {
  .x-icon;
  content: "\e86f";
}

.x-icon-file-text:before {
  .x-icon;
  content: "\e870";
}

.x-icon-film:before {
  .x-icon;
  content: "\e871";
}

.x-icon-file:before {
  .x-icon;
  content: "\e872";
}

.x-icon-file-plus:before {
  .x-icon;
  content: "\e873";
}

.x-icon-folder:before {
  .x-icon;
  content: "\e874";
}

.x-icon-filter:before {
  .x-icon;
  content: "\e875";
}

.x-icon-flag:before {
  .x-icon;
  content: "\e876";
}

.x-icon-globe:before {
  .x-icon;
  content: "\e877";
}

.x-icon-grid:before {
  .x-icon;
  content: "\e878";
}

.x-icon-heart:before {
  .x-icon;
  content: "\e879";
}

.x-icon-home:before {
  .x-icon;
  content: "\e87a";
}

.x-icon-github:before {
  .x-icon;
  content: "\e87b";
}

.x-icon-image:before {
  .x-icon;
  content: "\e87c";
}

.x-icon-inbox:before {
  .x-icon;
  content: "\e87d";
}

.x-icon-layers:before {
  .x-icon;
  content: "\e87e";
}

.x-icon-info:before {
  .x-icon;
  content: "\e87f";
}

.x-icon-instagram:before {
  .x-icon;
  content: "\e880";
}

.x-icon-layout:before {
  .x-icon;
  content: "\e881";
}

.x-icon-link-2:before {
  .x-icon;
  content: "\e882";
}

.x-icon-life-buoy:before {
  .x-icon;
  content: "\e883";
}

.x-icon-link:before {
  .x-icon;
  content: "\e884";
}

.x-icon-log-in:before {
  .x-icon;
  content: "\e885";
}

.x-icon-list:before {
  .x-icon;
  content: "\e886";
}

.x-icon-lock:before {
  .x-icon;
  content: "\e887";
}

.x-icon-log-out:before {
  .x-icon;
  content: "\e888";
}

.x-icon-loader:before {
  .x-icon;
  content: "\e889";
}

.x-icon-mail:before {
  .x-icon;
  content: "\e88a";
}

.x-icon-maximize-2:before {
  .x-icon;
  content: "\e88b";
}

.x-icon-map:before {
  .x-icon;
  content: "\e88c";
}

.x-icon-map-pin:before {
  .x-icon;
  content: "\e88e";
}

.x-icon-menu:before {
  .x-icon;
  content: "\e88f";
}

.x-icon-message-circle:before {
  .x-icon;
  content: "\e890";
}

.x-icon-message-square:before {
  .x-icon;
  content: "\e891";
}

.x-icon-minimize-2:before {
  .x-icon;
  content: "\e892";
}

.x-icon-mic-off:before {
  .x-icon;
  content: "\e893";
}

.x-icon-minus-circle:before {
  .x-icon;
  content: "\e894";
}

.x-icon-mic:before {
  .x-icon;
  content: "\e895";
}

.x-icon-minus-square:before {
  .x-icon;
  content: "\e896";
}

.x-icon-minus:before {
  .x-icon;
  content: "\e897";
}

.x-icon-moon:before {
  .x-icon;
  content: "\e898";
}

.x-icon-monitor:before {
  .x-icon;
  content: "\e899";
}

.x-icon-more-vertical:before {
  .x-icon;
  content: "\e89a";
}

.x-icon-more-horizontal:before {
  .x-icon;
  content: "\e89b";
}

.x-icon-move:before {
  .x-icon;
  content: "\e89c";
}

.x-icon-music:before {
  .x-icon;
  content: "\e89d";
}

.x-icon-navigation-2:before {
  .x-icon;
  content: "\e89e";
}

.x-icon-navigation:before {
  .x-icon;
  content: "\e89f";
}

.x-icon-octagon:before {
  .x-icon;
  content: "\e8a0";
}

.x-icon-package:before {
  .x-icon;
  content: "\e8a1";
}

.x-icon-pause-circle:before {
  .x-icon;
  content: "\e8a2";
}

.x-icon-pause:before {
  .x-icon;
  content: "\e8a3";
}

.x-icon-percent:before {
  .x-icon;
  content: "\e8a4";
}

.x-icon-phone-call:before {
  .x-icon;
  content: "\e8a5";
}

.x-icon-phone-forwarded:before {
  .x-icon;
  content: "\e8a6";
}

.x-icon-phone-missed:before {
  .x-icon;
  content: "\e8a7";
}

.x-icon-phone-off:before {
  .x-icon;
  content: "\e8a8";
}

.x-icon-phone-incoming:before {
  .x-icon;
  content: "\e8a9";
}

.x-icon-phone:before {
  .x-icon;
  content: "\e8aa";
}

.x-icon-phone-outgoing:before {
  .x-icon;
  content: "\e8ab";
}

.x-icon-pie-chart:before {
  .x-icon;
  content: "\e8ac";
}

.x-icon-play-circle:before {
  .x-icon;
  content: "\e8ad";
}

.x-icon-play:before {
  .x-icon;
  content: "\e8ae";
}

.x-icon-plus-square:before {
  .x-icon;
  content: "\e8af";
}

.x-icon-plus-circle:before {
  .x-icon;
  content: "\e8b0";
}

.x-icon-plus:before {
  .x-icon;
  content: "\e8b1";
}

.x-icon-pocket:before {
  .x-icon;
  content: "\e8b2";
}

.x-icon-printer:before {
  .x-icon;
  content: "\e8b3";
}

.x-icon-power:before {
  .x-icon;
  content: "\e8b4";
}

.x-icon-radio:before {
  .x-icon;
  content: "\e8b5";
}

.x-icon-repeat:before {
  .x-icon;
  content: "\e8b6";
}

.x-icon-refresh-ccw:before {
  .x-icon;
  content: "\e8b7";
}

.x-icon-rewind:before {
  .x-icon;
  content: "\e8b8";
}

.x-icon-rotate-ccw:before {
  .x-icon;
  content: "\e8b9";
}

.x-icon-refresh-cw:before {
  .x-icon;
  content: "\e8ba";
}

.x-icon-rotate-cw:before {
  .x-icon;
  content: "\e8bb";
}

.x-icon-save:before {
  .x-icon;
  content: "\e8bc";
}

.x-icon-search:before {
  .x-icon;
  content: "\e8bd";
}

.x-icon-server:before {
  .x-icon;
  content: "\e8be";
}

.x-icon-scissors:before {
  .x-icon;
  content: "\e8bf";
}

.x-icon-share-2:before {
  .x-icon;
  content: "\e8c0";
}

.x-icon-share:before {
  .x-icon;
  content: "\e8c1";
}

.x-icon-shield:before {
  .x-icon;
  content: "\e8c2";
}

.x-icon-settings:before {
  .x-icon;
  content: "\e8c3";
}

.x-icon-skip-back:before {
  .x-icon;
  content: "\e8c4";
}

.x-icon-shuffle:before {
  .x-icon;
  content: "\e8c5";
}

.x-icon-sidebar:before {
  .x-icon;
  content: "\e8c6";
}

.x-icon-skip-forward:before {
  .x-icon;
  content: "\e8c7";
}

.x-icon-slack:before {
  .x-icon;
  content: "\e8c8";
}

.x-icon-slash:before {
  .x-icon;
  content: "\e8c9";
}

.x-icon-smartphone:before {
  .x-icon;
  content: "\e8ca";
}

.x-icon-square:before {
  .x-icon;
  content: "\e8cb";
}

.x-icon-speaker:before {
  .x-icon;
  content: "\e8cc";
}

.x-icon-star:before {
  .x-icon;
  content: "\e8cd";
}

.x-icon-stop-circle:before {
  .x-icon;
  content: "\e8ce";
}

.x-icon-sun:before {
  .x-icon;
  content: "\e8cf";
}

.x-icon-sunrise:before {
  .x-icon;
  content: "\e8d0";
}

.x-icon-tablet:before {
  .x-icon;
  content: "\e8d1";
}

.x-icon-tag:before {
  .x-icon;
  content: "\e8d2";
}

.x-icon-sunset:before {
  .x-icon;
  content: "\e8d3";
}

.x-icon-target:before {
  .x-icon;
  content: "\e8d4";
}

.x-icon-thermometer:before {
  .x-icon;
  content: "\e8d5";
}

.x-icon-thumbs-up:before {
  .x-icon;
  content: "\e8d6";
}

.x-icon-thumbs-down:before {
  .x-icon;
  content: "\e8d7";
}

.x-icon-toggle-left:before {
  .x-icon;
  content: "\e8d8";
}

.x-icon-toggle-right:before {
  .x-icon;
  content: "\e8d9";
}

.x-icon-trash-2:before {
  .x-icon;
  content: "\e8da";
}

.x-icon-trash:before {
  .x-icon;
  content: "\e8db";
}

.x-icon-trending-up:before {
  .x-icon;
  content: "\e8dc";
}

.x-icon-trending-down:before {
  .x-icon;
  content: "\e8dd";
}

.x-icon-triangle:before {
  .x-icon;
  content: "\e8de";
}

.x-icon-type:before {
  .x-icon;
  content: "\e8df";
}

.x-icon-twitter:before {
  .x-icon;
  content: "\e8e0";
}

.x-icon-upload:before {
  .x-icon;
  content: "\e8e1";
}

.x-icon-umbrella:before {
  .x-icon;
  content: "\e8e2";
}

.x-icon-upload-cloud:before {
  .x-icon;
  content: "\e8e3";
}

.x-icon-unlock:before {
  .x-icon;
  content: "\e8e4";
}

.x-icon-user-check:before {
  .x-icon;
  content: "\e8e5";
}

.x-icon-user-minus:before {
  .x-icon;
  content: "\e8e6";
}

.x-icon-user-plus:before {
  .x-icon;
  content: "\e8e7";
}

.x-icon-user-x:before {
  .x-icon;
  content: "\e8e8";
}

.x-icon-user:before {
  .x-icon;
  content: "\e8e9";
}

.x-icon-users:before {
  .x-icon;
  content: "\e8ea";
}

.x-icon-video-off:before {
  .x-icon;
  content: "\e8eb";
}

.x-icon-video:before {
  .x-icon;
  content: "\e8ec";
}

.x-icon-voicemail:before {
  .x-icon;
  content: "\e8ed";
}

.x-icon-volume-x:before {
  .x-icon;
  content: "\e8ee";
}

.x-icon-volume-2:before {
  .x-icon;
  content: "\e8ef";
}

.x-icon-volume-1:before {
  .x-icon;
  content: "\e8f0";
}

.x-icon-volume:before {
  .x-icon;
  content: "\e8f1";
}

.x-icon-watch:before {
  .x-icon;
  content: "\e8f2";
}

.x-icon-wifi:before {
  .x-icon;
  content: "\e8f3";
}

.x-icon-x-square:before {
  .x-icon;
  content: "\e8f4";
}

.x-icon-wind:before {
  .x-icon;
  content: "\e8f5";
}

.x-icon-x:before {
  .x-icon;
  content: "\e8f6";
}

.x-icon-x-circle:before {
  .x-icon;
  content: "\e8f7";
}

.x-icon-zap:before {
  .x-icon;
  content: "\e8f8";
}

.x-icon-zoom-in:before {
  .x-icon;
  content: "\e8f9";
}

.x-icon-zoom-out:before {
  .x-icon;
  content: "\e8fa";
}

.x-icon-command:before {
  .x-icon;
  content: "\e8fb";
}

.x-icon-cloud:before {
  .x-icon;
  content: "\e8fc";
}

.x-icon-hash:before {
  .x-icon;
  content: "\e8fd";
}

.x-icon-headphones:before {
  .x-icon;
  content: "\e8fe";
}

.x-icon-underline:before {
  .x-icon;
  content: "\e8ff";
}

.x-icon-italic:before {
  .x-icon;
  content: "\e900";
}

.x-icon-bold:before {
  .x-icon;
  content: "\e901";
}

.x-icon-crop:before {
  .x-icon;
  content: "\e902";
}

.x-icon-help-circle:before {
  .x-icon;
  content: "\e903";
}

.x-icon-paperclip:before {
  .x-icon;
  content: "\e904";
}

.x-icon-shopping-cart:before {
  .x-icon;
  content: "\e905";
}

.x-icon-tv:before {
  .x-icon;
  content: "\e906";
}

.x-icon-wifi-off:before {
  .x-icon;
  content: "\e907";
}

.x-icon-minimize:before {
  .x-icon;
  content: "\e88d";
}

.x-icon-maximize:before {
  .x-icon;
  content: "\e908";
}

.x-icon-gitlab:before {
  .x-icon;
  content: "\e909";
}

.x-icon-sliders:before {
  .x-icon;
  content: "\e90a";
}

.x-icon-star-on:before {
  .x-icon;
  content: "\e90b";
}

.x-icon-heart-on:before {
  .x-icon;
  content: "\e90c";
}

</style>

Input

<template>
  <div
    class="x-from-input"
    :class="{
      'x-input-icon-before': iconBefore && iconBefore !== '',
      'x-input-icon-after': (iconAfter && iconAfter !== '') || clearable,
      'x-input-block': block,
    }"
  >
    <template v-if="type !== 'textarea'">
      <input
        class="x-input"
        v-bind="$attrs"
        :type="type"
        @input="handerInput"
        :value="text"
      />
      <i
        class="x-after"
        v-if="iconAfter && iconAfter !== ''"
        :class="iconAfter"
      ></i>
      <i
        class="x-before"
        v-if="iconBefore && iconBefore !== ''"
        :class="iconBefore"
      ></i>
      <transition name="fade">
        <span
          class="x-icon-x"
          v-if="clearable && textLength > 0"
          @click="handerInput()"
        ></span>
      </transition>
    </template>
    <template v-else>
      <textarea
        class="x-textarea"
        v-bind="$attrs"
        @input="handerInput"
        :value="text"
        :maxlength="maxlength"
      >
      </textarea>
      <span class="x-textarea-maxlength">
        {{ textLength }}/{{ maxlength }}
      </span>
    </template>
  </div>
</template>

<script>
import { computed, ref, toRefs, watchEffect } from 'vue'
export default {
  name: 'Input',
  inheritAttrs: false,
  props: {
    type: String,
    iconBefore: String,
    iconAfter: String,
    maxlength: Number,
    block: Boolean,
    clearable: Boolean,
    modelValue: [String, Number],
  },
  setup(props, { emit }) {
    const { modelValue } = toRefs(props)
    const text = ref('')

    watchEffect(() => {
      text.value = (modelValue && modelValue.value) || ''
    })

    const handerInput = (e) => {
      text.value = e ? e.target.value : ''
      emit('update:modelValue', text.value)
    }

    const textLength = computed(() => text.value.length)

    return {
      handerInput,
      text,
      textLength,
    }
  },
}
</script>
<style lang="less">
  .x-from-input {
    position: relative;
    display: inline-block;

    &.x-input-icon-before {
      .x-input {
        padding-left: 25px;
      }

      .x-before {
        left: 8px;
      }
    }

    &.x-input-icon-after {
      .x-input {
        padding-right: 25px;
      }

      .x-after {
        right: 8px;
      }
    }

    .x-after,.x-before,.x-icon-x{
      top: 50%;
      transform: translateY(-50%);
      color: var(--line-color);
      position: absolute;
      transition: color .25s ease;
    }

    .x-icon-x{
      right: 6px;
      cursor: pointer;
      width: 15px;
      height: 15px;
      display: flex;
      align-items: center;
      justify-content: center;
      background-color: var(--line-color);
      border-radius: 50%;
      transition: all .25s ease;
      color: rgb(var(--white));

      &:hover{
        background-color: rgb(var(--danger));
      }
    }

    &.x-input-block{
      display: block;
      width: inherit;

      .x-input{
        display: block;
        width: inherit;
      }
    }

    .x-textarea {
      padding-bottom: 2.3em;
    }

    .x-textarea-maxlength {
      position: absolute;
      left: 10px;
      bottom: 10px;
      font-size: 12px;
      color: var(--line-color);
    }

  }

  .x-input,
  .x-textarea {
    padding: 7px 10px;
    border: 1px solid var(--line-color);
    border-radius: 4px;
    font-size: 12px;
    transition-property: border-color, box-shadow;
    transition-duration: 0.25s;
    transition-timing-function: ease-in-out;
    color: var(--text);
    background-color: transparent;

    &:disabled {
      cursor: no-drop;
      background-color: var(--disabled);
      color: rgb(var(--text), .3);

      &:hover {
        border-color: var(--line-color);
      }
    }

    &:focus,
    &.is-focus {
      border-color: rgb(var(--primary));
      box-shadow: 0 0 0 2px rgb(var(--primary), .3);

      &:focus+i{
        color: rgb(var(--primary)) !important;
      }
    }

    &.is-blur {
      border-color: var(--line-color);
      box-shadow: none;

      &::placeholder {
        color: var(--line-color);
      }
    }

    &:hover {
      border-color: rgb(var(--primary));
    }

    &.x-input-lg{
      padding: 12px 30px 12px 15px;
      font-size: 14px;
    }
  
    &.x-input-sm{
      padding: 4px 6px;
      font-size: 12px;
    }

    &::placeholder {
      color: var(--line-color);
    }
  }
</style>

Menu

<template>
  <ul class="x-menu" :class="`x-menu-${mode}`">
    <slot></slot>
  </ul>
</template>

<script>
import { getCurrentInstance, provide, ref } from 'vue'
import emiter from '../../utils/emiter'
export default {
  name: 'Menu',
  props: {
    uniqueOpened: Boolean,
    mode: {
      type: String,
      default: 'vertical',
      validator: (value) => ['vertical', 'horizontal'].includes(value),
    },
  },
  setup() {
    const instance = getCurrentInstance()
    instance.currName = ref(null)
    provide('menu', instance)

    const { on } = emiter()

    on('item-click', (item) => {
      instance.currName.value = item
    })
  },
}
</script>
<style lang="less">
.x-menu {
  list-style: none;
  margin: 0;
  padding: 0;
  text-align: left;
  border-right: 1px solid var(--line-color);
  background-color: var(--main-background-color);

  &.x-menu-horizontal{
    display: flex;
    border-right: 0;
    border-bottom: 1px solid var(--line-color);

    .x-submenu{
      z-index: 1;

      &.is-active {
        color: rgb(var(--primary));
        transition: color .4s ease;
        
        &::after{
          content: "";
          position: absolute;
          bottom: -1px;
          right: 0;
          display: block;
          background-color: rgb(var(--primary));
          height: 3px;
          width: 100%;
        }
      }

      &>.x-menu{
        position: absolute;
        left: 0;
        background-color: var(--main-background-color);
        border-radius: 4px;
        box-shadow: 0 0 8px var(--shadow);
        overflow: hidden;
        min-width: 100%;
        margin-top: 3px;
        transform-origin: center top;
      }
    }

    &>.x-menu-item{

      &.active{
        background-color: transparent;

        &:after{
          width: 100%;
          height: 3px;
        }
      
      }
      
    }

    .x-menu-group {
      .x-menu-item {
        padding: 8px 20px;

        &.active {
          &::after{
            display: none;
          }
        }
      }

      .x-menu-title {
        padding: 8px 20px;
      }
    }

  }

  
  .x-menu{
    border-right: 0;
  }

  &>.x-menu-item:hover {
    color: rgb(var(--primary));
  }

  &>.x-menu-item {
    cursor: pointer;
    display: block;
    padding: 12px 20px;
    color: var(--main-text-color);
    font-size: 14px;
    position: relative;

    &.active {
      color: rgb(var(--primary));
      background-color: rgba(var(--primary), 0.1);
      transition: color .4s ease;
      
      &::after{
        content: "";
        position: absolute;
        bottom: -1px;
        right: 0;
        display: block;
        background-color: rgb(var(--primary));
        height: 100%;
        width: 3px;
      }
    }

    & .x-menu-item {
      padding: 8px 20px 8px 43px;
      font-size: 14px;
    }

    &.x-menu-group {
      padding: 0;
    }
  }

  .x-submenu {
    color: var(--main-text-color);
    position: relative;

    &>.x-menu-title {
      cursor: pointer;
      padding: 12px 20px;
      position: relative;
      font-size: 14px;
      display: flex;
      align-items: center;
      justify-content: space-between;

      i.x-arrow{
        margin-top: 10px;
        margin-left: 10px;
      }

      &:hover{
        color: rgba(var(--primary), 1);
      }
 
    }

  }

  .x-menu-group {
    .x-menu-item;

    .x-menu-title {
      font-size: 12px;
      position: relative;
      cursor: auto;
      color: var(--sub-text-color);
      padding: 8px 20px 8px 43px;
    }
  }
}

</style>

MenuItem

<template>
  <li
    class="x-menu-item"
    @click.stop="handleClick"
    :class="{ active: isActive }"
  >
    <slot></slot>
  </li>
</template>

<script>
import { toRefs, inject, computed } from 'vue'
import emiter from '../../utils/emiter'

export default {
  name: 'MenuItem',
  props: {
    name: [String, Number],
  },
  setup(props) {
    const { name } = toRefs(props)
    const { dispatch } = emiter()
    const menu = inject('menu', { props: {} })

    const handleClick = () => {
      dispatch('item-click', name.value)
    }

    const isActive = computed(() => menu.currName.value === name.value)

    return {
      handleClick,
      isActive,
    }
  },
}
</script>

MenuItemGroup

<template>
  <li class="x-menu-group">
    <div class="x-menu-title" v-if="title && title !== ''" @click.stop>
      {{ title }}
    </div>
    <ul class="x-menu">
      <slot></slot>
    </ul>
  </li>
</template>

<script>
export default {
  name: 'MenuItemGroup',
  props: {
    title: String,
  },
}
</script>

Message

<template>
  <transition name="slideY-fade" @after-leave="afterLeave" appear>
    <div class="x-message" v-show="isShow">
      <span><i :class="icon[type]" />{{ content }}</span>
    </div>
  </transition>
</template>

<script>
import { ref, getCurrentInstance } from 'vue'
export default {
  name: 'Message',
  props: {
    content: [String, Number, Boolean],
    type: String,
    duration: {
      type: Number,
      default: 1.5,
    },
  },
  setup(props) {
    const { duration } = props
    const instance = getCurrentInstance()

    const isShow = ref(true)

    if (duration > 0) {
      setTimeout(close, duration * 1000)
    }

    function close() {
      isShow.value = false
    }

    const afterLeave = () => {
      instance.vnode.el.parentElement?.removeChild(instance.vnode.el)
    }

    const icon = {
      info: 'x-icon-info info',
      error: 'x-icon-x-circle error',
      success: 'x-icon-check-circle success',
      warning: 'x-icon-alert-triangle warning',
      loading: 'x-icon-loader loading',
    }

    return {
      icon,
      isShow,
      close,
      afterLeave,
    }
  },
}
</script>
<style lang="less">
.x-message{
  font-size: 14px;
  position: fixed;
  z-index: 1010;
  top: 16px;
  left: 50%;
  background-color: var(--main-background-color);
  box-shadow: 0 4px 10px var(--shadow);
  padding: 5px 20px;
  border-radius: 3px;
  pointer-events: none;
  font-family: Arial, Helvetica, sans-serif;
  transform: translate(-50%, 0);
  
  span{
    font-size: 14px;
    position: relative;
    padding-left: 23px;
    display: block;
  }

  i[class^=x-icon]{
    font-size: 16px;
    position: absolute;
    left: 0;
    top: -2px;

    &.loading{
      color: rgb(var(--info));
      animation: rotating linear 1.5s infinite;
      transform-origin: center;
    }
  }
}

@keyframes rotating {
  0% {
    transform: rotate(0);
  }

  100% {
    transform: rotate(360deg);
  }
}
</style>

Modal

<template>
  <div style="display: inline-block">
    <teleport to="body" :disabled="!teleprot">
      <transition name="fade" appear>
        <div class="x-mask" @click="maskcancel" v-if="isShow"></div>
      </transition>
      <div class="x-modal" :class="{ confirm: type !== '' }">
        <transition
          name="scale"
          @before-enter="setOrigin"
          @before-leave="setOrigin"
          @after-leave="afterLeave"
          appear
        >
          <div
            class="x-modal-content"
            v-show="isShow"
            :class="{ 'x-modal-confirm-wrap': type !== '' }"
            :style="modalStyle"
          >
            <div class="x-modal-close" v-if="closable" @click="cancel">
              <i class="x-icon-x"></i>
            </div>

            <div class="x-modal-head">
              <template v-if="!$slots.head">
                <i v-if="type !== ''" :class="iconType[type]"></i>
                {{ title }}
              </template>
              <slot v-else name="head"></slot>
            </div>

            <div class="x-modal-body">
              <template v-if="type !== ''">
                {{ content }}
              </template>
              <slot v-else></slot>
            </div>

            <div class="x-modal-footer">
              <template v-if="!$slots.footer">
                <Button
                  class="x-modal-btn"
                  v-if="!((type !== '') & (type !== 'confirm'))"
                  plain
                  @click="cancel"
                  >{{ cancelText }}</Button
                >
                <Button
                  class="x-modal-btn"
                  type="primary"
                  :loading="loading"
                  @click="ok"
                  >{{ okText }}</Button
                >
              </template>
              <slot v-else name="footer"></slot>
            </div>
          </div>
        </transition>
      </div>
    </teleport>
  </div>
</template>

<script>
import {
  toRefs,
  ref,
  watch,
  nextTick,
  onMounted,
  getCurrentInstance,
  computed,
} from 'vue'
import Button from '../button/index'

export default {
  name: 'Modal',
  inheritAttrs: false,
  components: {
    Button,
  },
  props: {
    onOk: Function,
    onCancel: Function,
    okText: {
      type: String,
      default: '确定',
    },
    cancelText: {
      type: String,
      default: '取消',
    },
    type: {
      type: String,
      default: '',
    },
    content: String,
    teleprot: {
      type: Boolean,
      default: true,
    },
    mouseClick: Object,
    modelValue: Boolean,
    title: String,
    loading: Boolean,
    style: [String, Array, Object],
    closable: {
      type: Boolean,
      default: true,
    },
    width: {
      type: Number,
      default: 500,
    },
    top: {
      type: Number,
      default: 100,
    },
    maskClosable: {
      type: Boolean,
      default: true,
    },
  },
  emits: ['cancel', 'ok', 'update:modelValue', 'loading'],
  setup(props, { emit }) {
    const {
      loading,
      modelValue,
      closable,
      maskClosable,
      top,
      width,
      teleprot,
    } = toRefs(props)
    const { onOk, onCancel, mouseClick, style } = props
    const isShow = ref(modelValue.value)

    const maskcancel = () => {
      if (maskClosable.value) {
        cancel()
      }
    }

    const cancel = () => {
      isShow.value = false
      emit('update:modelValue', isShow.value)
      emit('cancel')
      onCancel && onCancel()
    }

    const ok = () => {
      emit('ok')
      onOk && onOk()

      nextTick(() => {
        if (!loading.value) {
          isShow.value = false
          emit('update:modelValue', isShow.value)
        }
      })
    }

    watch(loading, (value) => {
      if (!value) {
        cancel()
      }
    })

    watch(modelValue, (value) => {
      isShow.value = value
      if (value) {
        document.body.style.width = 'calc(100% - 17px)'
        document.body.style.overflow = 'hidden'
      } else {
        document.body.style.overflow = ''
        document.body.style.paddingRight = '0'
      }
    })

    let mousePosition = mouseClick

    onMounted(() => {
      if (closable.value) {
        document.addEventListener('keydown', ({ key }) => {
          if (key === 'Escape' && modelValue.value) {
            cancel()
          }
        })
      }

      const getClickPosition = (e) => {
        if (!modelValue.value) {
          mousePosition = {
            x: e.clientX,
            y: e.clientY,
          }
          setTimeout(() => (mousePosition = null), 100)
        }
      }
      document.addEventListener('click', getClickPosition, true)
    })

    const instance = getCurrentInstance()

    const modalStyle = computed(() => {
      const dest = {
        width: width.value + 'px',
        top: top.value + 'px',
        ...style,
      }
      return dest
    })

    const setOrigin = (el) => {
      if (mousePosition) {
        const { x, y } = mousePosition
        const width =
          (document.documentElement.clientWidth -
            parseFloat(modalStyle.value.width)) /
          2
        const top = parseFloat(modalStyle.value.top)
        el.style.transformOrigin = `${x - width}px ${y - top}px 0`
      }
    }

    const afterLeave = () => {
      document.body.style.overflow = ''
      if (!teleprot.value) {
        instance.vnode.el.parentElement?.removeChild(instance.vnode.el)
      }
    }

    const iconType = {
      info: 'x-icon-info info',
      error: 'x-icon-x-circle error',
      success: 'x-icon-check-circle success',
      warning: 'x-icon-alert-triangle warning',
      confirm: 'x-icon-help-circle warning',
    }

    return {
      isShow,
      maskcancel,
      cancel,
      ok,
      iconType,
      modalStyle,
      setOrigin,
      afterLeave,
    }
  },
}
</script>
<style lang="less">
.x-modal {
  position: fixed;
  left: 0;
  top: 0;
  right: 0;
  bottom: 0;
  z-index: 1005;
  pointer-events: none;

  .x-modal-content {
    position: relative;
    top: 100px;
    margin: 0 auto;
    width: 500px;
    padding: 15px 20px;
    border-radius: 3px;
    background-color: var(--main-background-color);
    box-shadow: 0 0 2px var(--shadow);
    pointer-events: all;

    .x-modal-close{
      position: absolute;
      right: 5px;
      top: 5px;
      width: 24px;
      height: 24px;
      text-align: center;
      line-height: 30px;
      font-size: 16px;
      cursor: pointer;
      color: #999;
    }

    .x-modal-head {
      color: currentColor;
      font-size: 16px;

      i[class^='x-icon']{
        margin-right: 5px;
      }
    }

    .x-modal-body{
      padding: 10px 0;
      font-size: 12px;
      color: var(--sub-text-color);
    }

    .x-modal-footer {
      text-align: right;

      .x-modal-btn{
        margin-left: 10px;
      }
    }
  }

}

</style>

Notice

<template>
  <transition
    name="notice"
    @before-leave="beforeLeave"
    @leave="leave"
    @after-leave="afterLeave"
    appear
  >
    <div
      class="x-notice-content-rect"
      v-show="isShow"
      @mouseover="endTime"
      @mouseout="starTime"
    >
      <div class="x-notice-content">
        <div class="x-notice-bar" v-if="duration" :class="type">
          <i :style="{ width: bar + '%' }"></i>
        </div>
        <span class="x-notice-icon" v-if="type">
          <i :class="[icon || iconType[type], type]" />
        </span>
        <div class="x-notice-title">{{ title }}</div>
        <div class="x-notice-description" v-if="content">{{ content }}</div>
        <span class="x-notice-close" @click="close">
          <i class="x-icon-x"></i>
        </span>
      </div>
    </div>
  </transition>
</template>

<script>
import { ref, getCurrentInstance } from 'vue'
export default {
  name: 'Notice',
  props: {
    icon: String,
    title: String,
    content: String,
    type: String,
    duration: {
      type: Number,
      default: 4.5,
    },
  },
  setup(props) {
    const { duration } = props
    const instance = getCurrentInstance()
    const isShow = ref(true)
    const bar = ref(0)

    let s = 100
    const t = duration * 1000
    let time
    const progress = () => {
      if (s <= t) {
        bar.value = (s / t) * 100
        s += 100
      } else {
        endTime()
        close()
      }
    }

    const starTime = () => {
      if (duration > 0) {
        time = setInterval(progress, 100)
      }
    }
    const endTime = () => {
      clearInterval(time)
      time = null
    }

    if (duration > 0) {
      starTime()
    }

    function close() {
      isShow.value = false
    }

    const beforeLeave = (el) => {
      el.style.height = el.offsetHeight + 'px'
    }
    const leave = (el) => {
      el.style.transition = 'all .4s ease'

      if (el.offsetHeight !== 0) {
        el.style.height = 0
        el.style.paddingTop = 0
        el.style.paddingBottom = 0
      }
    }
    const afterLeave = (el) => {
      el.style.height = el.offsetHeight + 'px'
      el.style.overflow = ''
      instance.vnode.el.parentElement?.removeChild(instance.vnode.el)
    }

    const iconType = {
      info: 'x-icon-info',
      error: 'x-icon-x-circle',
      success: 'x-icon-check-circle',
      warning: 'x-icon-alert-triangle',
    }

    return {
      iconType,
      isShow,
      close,
      bar,
      duration,
      starTime,
      endTime,

      beforeLeave,
      leave,
      afterLeave,
    }
  },
}
</script>
<style lang="less">
.x-notice-wrap{
  position: fixed;
  z-index: 1000;
  top: 24px;
  right: 40px;
  bottom: auto;

  .x-notice-content-rect{
    line-height: 1.5;
    padding: 8px;
    width: 360px;
    overflow: hidden;
    cursor: default;

    .x-notice-bar{
      --color: var(--default);
      
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 1px;
      background-color: rgb(var(--color), .2);

      i{
        background-color: rgb(var(--color));
        display: block;
        height: inherit;
        width: 0;
        transition: width .3s ease;
      }

      &.info{
        --color: var(--info);
      }
      &.success{
        --color: var(--success);
      }
      &.error{
        --color: var(--danger);
      }
      &.warning{
        --color: var(--warning);
      }
    }

    .x-notice-content{
      background: var(--main-background-color);
      box-shadow: 0 4px 10px var(--shadow);
      border-radius: 4px;
      padding: 10px 24px;
      position: relative;
    }

    .x-notice-icon{
      position: absolute;
      left: 20px;
      top: 7px;
      font-size: 18px;

      &+.x-notice-title{
        margin-left: 30px;

        &+.x-notice-description{
          margin-left: 30px;
        }
      }
    }

    .x-notice-title{
      font-size: 14px;
      color: currentColor;
    }

    .x-notice-description{
      font-size: 12px;
      color: var(--sub-text-color);
      margin-top: 5px;
      line-height: 20px;
    }

    .x-notice-close{
      position: absolute;
      right: 10px;
      top: 8px;
      color: #999;
      cursor: pointer;
      transition: color .2s;
      font-size: 14px;

      &:hover{
        color: #666;
        text-shadow: 0 0 12px fade(#000, 15%);
      }
    }
  }
}

</style>

Radio

<template>
  <label
    class="x-radio"
    :class="{
      'x-radio-checked': isCheked,
      'x-radio-disabled': disabled,
    }"
  >
    <input
      type="radio"
      :name="value"
      :checked="isCheked"
      :value="label"
      :disabled="disabled"
      @change.stop="handerClick"
    />
    <span>
      <template v-if="$slots.default">
        <slot></slot>
      </template>
      <template v-else>
        {{ label }}
      </template>
    </span>
  </label>
</template>

<script>
import { inject, computed, toRefs } from 'vue'
import emitter from '../../utils/emiter'
export default {
  name: 'Radio',
  props: {
    label: [String, Number, Boolean],
    modelValue: Boolean,
    checked: Boolean,
    disabled: Boolean,
    value: [String, Number],
    modelValue: [String, Boolean, Number],
  },
  setup(props, { emit }) {
    const { modelValue, checked } = toRefs(props)
    const radioGroup = inject('radioGroup', { props: {} })

    const { dispatch } = emitter()

    const handerClick = (e) => {
      dispatch('radio', props.value)
      emit('update:modelValue', props.value)
    }

    const isCheked = computed(() => {
      return (
        radioGroup.props.modelValue === props.value ||
        modelValue?.value === props.value ||
        checked.value
      )
    })

    return {
      handerClick,
      isCheked,
    }
  },
}
</script>
<style lang="less">
.x-radio {
  --color: var(--danger);
  cursor: pointer;
  margin-right: 10px;
  user-select: none;
  line-height: 1.3;

  &:hover {
    input[type="radio"]+span::before {
      border-color: rgb(var(--danger));
    }
  }

  input[type="radio"]+span {
    display: inline-block;
    padding-left: 6px;
    position: relative;
    font-weight: normal;

    &::before {
      content: "";
      background-color: rgb(--white);
      border-radius: 50%;
      border: 1px solid var(--line-color);
      display: inline-block;
      left: 0;
      margin-left: -14px;
      position: absolute;
      transition: 0.3s ease-in-out;
      width: 16px;
      height: 16px;
      outline: none !important;
    }

    &::after {
      content: "";
      position: absolute;
      top: 3px;
      left: -8px;
      display: table;
      width: 4px;
      height: 8px;
      border: 1px solid rgb(var(--white));
      border-top-width: 0;
      border-left-width: 0;
      transform: rotate(45deg) scale(0);
      opacity: 0;
      transition: all .4s ease;
    }

    &:active::before {
      box-shadow: 0 0 0 2px rgb(var(--color), .5);
    }
  }

  input[type="radio"] {
    cursor: pointer;
    opacity: 0;
    z-index: 1;
    outline: none !important;
  }

  &.x-radio-checked {
    input[type="radio"]+span {
      &::after {
        opacity: 1;
        transform: rotate(45deg) scale(1);
      }

      &::before {
        background-color: rgb(var(--color));
        border-color: rgb(var(--color));
      }
    }

    &.x-radio-disabled {
      input[type="radio"]+span {
        &::before{
          background-color: var(--line-color);
        }
        &::after{
          border: 1px solid rgb(var(--mix-color), .4);
          border-top-width: 0;
          border-left-width: 0;
          transform: rotate(45deg);
          border-color: rgb(var(--mix-color), .4);
        }
      }
    }
  }

  &.x-radio-disabled {
    cursor: not-allowed;

    input[type="radio"]+span {
      opacity: 0.65;

      &::before {
        background-color: var(--line-color);
        border-color: var(--line-color);
      }

      &:active::before {
        box-shadow: none;
      }
    }

    
  }
}
</style>

RadioGroup

<template>
  <div>
    <slot></slot>
  </div>
</template>

<script>
import { getCurrentInstance, provide } from 'vue'
import emitter from '../../utils/emiter'
export default {
  name: 'RadioGroup',
  props: {
    modelValue: [String, Boolean, Number],
  },
  setup(props, { emit }) {
    provide('radioGroup', getCurrentInstance())
    const { on } = emitter()

    on('radio', (value) => {
      emit('update:modelValue', value)
    })
  },
}
</script>

Row

<template>
  <component :is="tag" :class="classes" :style="style">
    <slot></slot>
  </component>
</template>

<script>
import { computed, getCurrentInstance, provide } from 'vue'
export default {
  name: 'Row',
  props: {
    tag: {
      type: String,
      default: 'div',
    },
    gutter: {
      type: Number,
      default: 0,
    },
    type: String,
    justify: {
      type: String,
      default: 'start',
      validator: (value) =>
        ['start', 'center', 'end', 'space-between', 'space-around'].includes(
          value
        ),
    },
    align: {
      type: String,
      default: 'top',
      validator: (value) => ['top', 'center', 'bottom'].includes(value),
    },
  },
  setup(props) {
    const classes = ['x-row']

    if (props.type === 'flex') {
      classes.push('x-row-flex')
      props.justify && classes.push(`x-row-flex-justify-${props.justify}`)
      props.align && classes.push(`x-row-flex-align-${props.align}`)
    }

    const style = computed(() => {
      const ret = {}
      if (props.gutter) {
        ret.marginLeft = `-${props.gutter / 2}px`
        ret.marginRight = ret.marginLeft
      }
      return ret
    })

    provide('Row', getCurrentInstance())

    return {
      tag: props.tag,
      classes,
      style,
    }
  },
}
</script>

Scroll

<template>
  <div
    class="x-scroll"
    :style="{ height: `${viewHeight}px` }"
    @mouseenter="mouseover"
    @mouseleave="mouseout"
  >
    <div
      class="x-scroll-content"
      :style="{ paddingRight: `${size}px` }"
      @scroll="viewScroll"
    >
      <slot></slot>
    </div>
    <transition name="fade">
      <div
        class="x-scroll-bar"
        v-show="!alwaysVisible || isShow"
        :style="{ width: `${size}px` }"
        @mousedown="thumbDrag($event)"
      >
        <div
          class="x-scroll-thumb"
          ref="thumb"
          :style="{
            height: `${BarHeight}px`,
            top: `${BarTop}px`,
            borderRadius: `${size}px`,
          }"
        ></div>
      </div>
    </transition>
  </div>
</template>

<script>
import {
  getCurrentInstance,
  onMounted,
  onUnmounted,
  ref,
  toRefs,
  watch,
  watchEffect,
} from 'vue'
export default {
  name: 'Scroll',
  props: {
    height: Number,
    to: Number,
    alwaysVisible: {
      type: Boolean,
      default: true,
    },
    size: {
      type: Number,
      default: 6,
    },
  },
  emits: ['onScroll', 'update:to'],
  setup(props, { emit }) {
    const instance = getCurrentInstance()
    const BarHeight = ref(30)
    const BarTop = ref(0)
    const thumb = ref(null)
    const { to, height } = toRefs(props)
    const viewHeight = ref(0)
    const isShow = ref(false)

    const viewScroll = () => {
      const el = instance.vnode.el
      const view = el.children[0]
      const catchTop = view.scrollTop / (view.scrollHeight - view.offsetHeight)
      BarTop.value = catchTop * (view.offsetHeight - thumb.value.offsetHeight)
      emit('onScroll', catchTop)
      emit('update:to', view.scrollTop)
    }

    if (to) {
      watch(to, (val) => {
        const el = instance.vnode.el
        const view = el.children[0]
        view.scrollTop = val
      })
    }

    if (height) {
      watchEffect(() => {
        viewHeight.value = height.value
      })
    }

    let observer
    onMounted(() => {
      const el = instance.vnode.el

      if (to) {
        const view = el.children[0]
        view.scrollTop = to.value
      }

      if (!height) {
        viewHeight.value = el.parentNode.offsetHeight
        window.addEventListener('resize', () => {
          viewHeight.value = el.parentNode.offsetHeight
        })
      }

      observer = new MutationObserver(() => {
        BarHeight.value =
          (el.offsetHeight * el.offsetHeight) / el.children[0].scrollHeight
        if (BarHeight.value <= 30) {
          BarHeight.value = 30
        }

        if (el.children[0].scrollHeight <= el.offsetHeight) {
          BarHeight.value = 0
        }
      })

      observer.observe(el, {
        childList: true, // 子节点的变动(新增、删除或者更改)
        attributes: true, // 属性的变动
        characterData: true, // 节点内容或节点文本的变动
        subtree: true, // 是否将观察器应用于该节点的所有后代节点
      })
      // console.log(el.offsetHeight, el.children[0].scrollHeight)
    })

    onUnmounted(() => {
      observer.disconnect()
    })

    let isDrag = false
    let isArea = false

    const mouseover = () => {
      isArea = true
      isShow.value = true
    }
    const mouseout = () => {
      isArea = false
      if (!isDrag) {
        isShow.value = false
      }
    }

    const thumbDrag = (e) => {
      e.preventDefault()
      const el = instance.vnode.el
      const view = el.children[0]
      const touchY = e.clientY - BarTop.value
      const element = e.target

      if (element.className === 'x-scroll-bar') {
        const top = e.clientY - element.getBoundingClientRect().top
        view.scrollTop =
          view.scrollHeight * (top / element.offsetHeight) -
          element.offsetHeight / 2
      } else {
        const move = (ev) => {
          isDrag = true
          const bt =
            (ev.clientY - touchY) /
            (view.offsetHeight - thumb.value.offsetHeight)
          const top = (view.scrollHeight - view.offsetHeight) * bt
          view.scrollTop = top
        }
        document.addEventListener('mousemove', move)
        document.addEventListener('mouseup', () => {
          isDrag = false
          if (!isArea) {
            isShow.value = false
          }
          document.removeEventListener('mousemove', move)
        })
      }
    }

    return {
      BarHeight,
      BarTop,
      viewScroll,
      thumb,
      viewHeight,
      thumbDrag,
      isShow,
      mouseover,
      mouseout,
    }
  },
}
</script>
<style lang="less">
.x-scroll {
  overflow: hidden;
  position: relative;
  overflow: hidden;
  height: 100%;

  .x-scroll-content {
    position: absolute;
    left: 0;
    top: 0;
    right: -17px;
    bottom: 0;
    overflow-x: hidden;
    overflow-y: scroll;
  }

  .x-scroll-bar {
    position: absolute;
    right: 0;
    top: 0;
    bottom: 0;
    width: 7px;

    .x-scroll-thumb {
      position: relative;
      width: 100%;
      background: var(--scroll);
      cursor: pointer;
      opacity: 0.6;
      transition: opacity .2s ease-in;

      &:hover{
        opacity: 1;
      }
    }
  }
}
</style>