因为公司网络问题,只得将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>