feat: 增加一组基础组件

This commit is contained in:
pipipi-pikachu 2023-09-22 00:33:15 +08:00
parent 0502c80fa4
commit 9306c81ded
32 changed files with 1971 additions and 92 deletions

View File

@ -169,20 +169,22 @@ npm run serve
- 将 npm 更换到 pnpm
- 将 Vue CLI 更换到 Vite 生态;
- (进行中)移除 Ant-Design-Vue 组件样式异常且难以控制、存在全局污染的问题、已知部分组件存在Bug如Slider、Popover、Tooltip...
- message -> https://github.com/smastrom/notivue
- Modal -> custom
- Tooltip -> https://github.com/atomiks/tippyjs
- Popover -> https://github.com/atomiks/tippyjs
- Select -> https://github.com/atomiks/tippyjs
- Message
- Tooltip
- Popover
- Modal
- Select
- Drawer -> remove
- Slider -> custom
- Button -> custom
- Input -> custom
- InputNumber -> custom
- Switch -> custom
- Radio -> custom
- Checkbox -> custom
- Divider -> custom
- Slider
- Button
- ButtonGroup
- Input
- InputNumber
- TextArea
- Switch
- RadioButton
- Checkbox
- Divider
- 支持 Iframe 引用;
- 组合元素重构:能够支持组合元素进行旋转、缩放、整体执行动画等;
- 支持多屏放映;

38
package-lock.json generated
View File

@ -22,6 +22,7 @@
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"nanoid": "^4.0.2",
"number-precision": "^1.6.0",
"pinia": "^2.1.4",
"pptxgenjs": "^3.12.0",
"pptxtojson": "^0.0.11",
@ -40,6 +41,7 @@
"svg-arc-to-cubic-bezier": "^3.2.0",
"svg-pathdata": "^6.0.3",
"tinycolor2": "^1.6.0",
"tippy.js": "^6.3.7",
"vue": "^3.3.4",
"vuedraggable": "^4.1.0"
},
@ -2665,6 +2667,11 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"node_modules/@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
},
"node_modules/@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -9682,6 +9689,11 @@
"boolbase": "^1.0.0"
}
},
"node_modules/number-precision": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/number-precision/-/number-precision-1.6.0.tgz",
"integrity": "sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ=="
},
"node_modules/object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
@ -13172,6 +13184,14 @@
"resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"node_modules/tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmmirror.com/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"dependencies": {
"@popperjs/core": "^2.9.0"
}
},
"node_modules/to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz",
@ -16615,6 +16635,11 @@
"integrity": "sha512-a5Sab1C4/icpTZVzZc5Ghpz88yQtGOyNqYXcZgOssB2uuAr+wF/MvN6bgtW32q7HHrvBki+BsZ0OuNv6EV3K9g==",
"dev": true
},
"@popperjs/core": {
"version": "2.11.8",
"resolved": "https://registry.npmmirror.com/@popperjs/core/-/core-2.11.8.tgz",
"integrity": "sha512-P1st0aksCrn9sGZhp8GMYwBnQsbvAWsZAX44oXNNvLHGqAOcoVxmjZiohstwQ7SqKnbR47akdNi+uleWD8+g6A=="
},
"@rollup/plugin-babel": {
"version": "5.3.1",
"resolved": "https://registry.npmmirror.com/@rollup/plugin-babel/-/plugin-babel-5.3.1.tgz",
@ -22331,6 +22356,11 @@
"boolbase": "^1.0.0"
}
},
"number-precision": {
"version": "1.6.0",
"resolved": "https://registry.npmmirror.com/number-precision/-/number-precision-1.6.0.tgz",
"integrity": "sha512-05OLPgbgmnixJw+VvEh18yNPUo3iyp4BEWJcrLu4X9W05KmMifN7Mu5exYvQXqxxeNWhvIF+j3Rij+HmddM/hQ=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmmirror.com/object-assign/-/object-assign-4.1.1.tgz",
@ -25086,6 +25116,14 @@
"resolved": "https://registry.npmmirror.com/tinycolor2/-/tinycolor2-1.6.0.tgz",
"integrity": "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="
},
"tippy.js": {
"version": "6.3.7",
"resolved": "https://registry.npmmirror.com/tippy.js/-/tippy.js-6.3.7.tgz",
"integrity": "sha512-E1d3oP2emgJ9dRQZdf3Kkn0qJgI6ZLpyS5z6ZkY1DF3kaQaBsGZsndEpHwx+eC+tYM41HaSNvNtLx8tU57FzTQ==",
"requires": {
"@popperjs/core": "^2.9.0"
}
},
"to-fast-properties": {
"version": "2.0.0",
"resolved": "https://registry.npmmirror.com/to-fast-properties/-/to-fast-properties-2.0.0.tgz",

View File

@ -23,6 +23,7 @@
"lodash": "^4.17.21",
"mitt": "^3.0.1",
"nanoid": "^4.0.2",
"number-precision": "^1.6.0",
"pinia": "^2.1.4",
"pptxgenjs": "^3.12.0",
"pptxtojson": "^0.0.11",
@ -41,6 +42,7 @@
"svg-arc-to-cubic-bezier": "^3.2.0",
"svg-pathdata": "^6.0.3",
"tinycolor2": "^1.6.0",
"tippy.js": "^6.3.7",
"vue": "^3.3.4",
"vuedraggable": "^4.1.0"
},

98
src/components/Button.vue Normal file
View File

@ -0,0 +1,98 @@
<template>
<button
class="button"
:class="{
'disabled': disabled,
'checked': !disabled && checked,
'default': !disabled && type === 'default',
'primary': !disabled && type === 'primary',
'checkbox': !disabled && type === 'checkbox',
'radio': !disabled && type === 'radio',
}"
@click="handleClick()"
>
<slot></slot>
</button>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
checked?: boolean
disabled?: boolean
type?: 'default' | 'primary' | 'checkbox' | 'radio'
}>(), {
checked: false,
disabled: false,
type: 'default',
})
const emit = defineEmits<{
(event: 'click'): void
}>()
const handleClick = () => {
if (props.disabled) return
emit('click')
}
</script>
<style lang="scss" scoped>
.button {
height: 32px;
line-height: 32px;
outline: 0;
font-size: 13px;
padding: 0 15px;
text-align: center;
color: $textColor;
border-radius: $borderRadius;
user-select: none;
cursor: pointer;
&.default {
background-color: #fff;
border: 1px solid #d9d9d9;
color: $textColor;
&:hover {
color: $themeColor;
border-color: $themeColor;
}
}
&.primary {
background-color: $themeColor;
border: 1px solid $themeColor;
color: #fff;
&:hover {
background-color: $themeHoverColor;
border-color: $themeHoverColor;
}
}
&.checkbox, &.radio {
background-color: #fff;
border: 1px solid #d9d9d9;
color: $textColor;
&:not(.checked):hover {
color: $themeColor;
}
}
&.checked {
color: #fff;
background-color: $themeColor;
border-color: $themeColor;
&:hover {
background: $themeHoverColor;
border-color: $themeHoverColor;
}
}
&.disabled {
background-color: #f5f5f5;
border: 1px solid #d9d9d9;
color: #b7b7b7;
cursor: default;
}
}
</style>

View File

@ -1,5 +1,5 @@
<template>
<div class="checkbox-button-group">
<div class="button-group">
<slot></slot>
</div>
</template>
@ -9,20 +9,26 @@
</script>
<style lang="scss" scoped>
.checkbox-button-group {
.button-group {
display: flex;
align-items: center;
::v-deep(.checkbox-button) {
::v-deep(.button) {
border-radius: 0;
border-left-width: 0;
border-right-width: 0;
display: inline-block;
& + .checkbox-button {
& + .button {
border-left-width: 1px;
}
&:hover {
&.default + .button.default {
border-left-color: $themeColor;
}
}
&:first-child {
border-top-left-radius: $borderRadius;
border-bottom-left-radius: $borderRadius;

109
src/components/Checkbox.vue Normal file
View File

@ -0,0 +1,109 @@
<template>
<label
class="checkbox"
:class="{
'checked': value,
'disabled': disabled,
}"
@change="$event => handleChange($event)"
>
<span class="checkbox-input"></span>
<input class="checkbox-original" type="checkbox">
<span class="checkbox-label">
<slot></slot>
</span>
</label>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
value: boolean
disabled?: boolean
}>(), {
disabled: false,
})
const emit = defineEmits<{
(event: 'update:value', payload: boolean): void
}>()
const handleChange = (e: Event) => {
if (props.disabled) return
emit('update:value', (e.target as HTMLInputElement).checked)
}
</script>
<style lang="scss" scoped>
.checkbox {
height: 20px;
display: flex;
align-items: center;
cursor: pointer;
&:not(.disabled).checked {
.checkbox-input {
background-color: $themeColor;
border-color: $themeColor;
}
.checkbox-input::after {
transform: rotate(45deg) scaleY(1);
}
.checkbox-label {
color: $themeColor;
}
}
&.disabled {
color: #b7b7b7;
cursor: default;
.checkbox-input {
background-color: #f5f5f5;
}
}
}
.checkbox-input {
display: inline-block;
position: relative;
border: 1px solid #d9d9d9;
border-radius: $borderRadius;
width: 16px;
height: 16px;
background-color: #fff;
vertical-align: middle;
transition: border-color .25s cubic-bezier(.71, -.46, .29, 1.46), background-color .25s cubic-bezier(.71, -.46, .29, 1.46);
z-index: 1;
&::after {
content: '';
border: 2px solid #fff;
border-left: 0;
border-top: 0;
height: 9px;
left: 4px;
position: absolute;
top: 1px;
transform: rotate(45deg) scaleY(0);
width: 6px;
transition: transform .15s ease-in .05s;
transform-origin: center;
}
}
.checkbox-original {
opacity: 0;
outline: 0;
position: absolute;
margin: 0;
width: 0;
height: 0;
z-index: -1;
}
.checkbox-label {
margin-left: 5px;
line-height: 20px;
font-size: 13px;
user-select: none;
}
</style>

View File

@ -1,43 +1,21 @@
<template>
<button class="checkbox-button" :class="{ 'checked': checked }">
<Button
:checked="checked"
:disabled="disabled"
type="checkbox"
>
<slot></slot>
</button>
</Button>
</template>
<script lang="ts" setup>
import Button from './Button.vue'
withDefaults(defineProps<{
checked?: boolean
disabled?: boolean
}>(), {
checked: false,
disabled: false,
})
</script>
<style lang="scss" scoped>
.checkbox-button {
outline: 0;
background-color: #fff;
border: 1px solid #d9d9d9;
font-size: 13px;
padding: 0 15px;
height: 32px;
line-height: 32px;
text-align: center;
color: $textColor;
cursor: pointer;
&:hover {
color: $themeColor;
}
&.checked {
color: #fff;
background-color: $themeColor;
border-color: $themeColor;
&:hover {
background: rgba($color: $themeColor, $alpha: .9);
border-color: rgba($color: $themeColor, $alpha: .9);
}
}
}
</style>

View File

@ -0,0 +1,34 @@
<template>
<div :class="['divider', type]"
:style="{
margin: type === 'horizontal' ? `${margin || 24}px 0` : `0 ${margin || 8}px`
}"
></div>
</template>
<script lang="ts" setup>
withDefaults(defineProps<{
type?: 'horizontal' | 'vertical'
margin?: number
}>(), {
type: 'horizontal',
margin: 0,
})
</script>
<style lang="scss" scoped>
.divider {
&.horizontal {
width: 100%;
margin: 24px 0;
border-block-start: 1px solid rgba(5, 5, 5, .06);
}
&.vertical {
position: relative;
height: 1em;
display: inline-block;
margin: 0 8px;
border-inline-start: 1px solid rgba(5, 5, 5, .06);
}
}
</style>

95
src/components/Input.vue Normal file
View File

@ -0,0 +1,95 @@
<template>
<div
class="input"
:class="{
'disabled': disabled,
'focused': focused,
}"
>
<span class="prefix">
<slot name="prefix"></slot>
</span>
<input
type="text"
:disabled="disabled"
:value="value"
:placeholder="placeholder"
@input="$event => handleInput($event)"
@focus="focused = true"
@blur="focused = false"
/>
<span class="suffix">
<slot name="suffix"></slot>
</span>
</div>
</template>
<script lang="ts" setup>
import { ref } from 'vue'
withDefaults(defineProps<{
value: string
disabled?: boolean
placeholder?: string
}>(), {
disabled: false,
placeholder: '',
})
const emit = defineEmits<{
(event: 'update:value', payload: string): void
}>()
const focused = ref(false)
const handleInput = (e: Event) => {
emit('update:value', (e.target as HTMLInputElement).value)
}
</script>
<style lang="scss" scoped>
.input {
background-color: #fff;
border: 1px solid #d9d9d9;
padding: 0 5px;
border-radius: $borderRadius;
transition: border-color .25s;
font-size: 13px;
display: inline-flex;
input {
min-width: 0;
height: 30px;
outline: 0;
border: 0;
line-height: 30px;
vertical-align: top;
color: $textColor;
padding: 0 5px;
flex: 1;
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
&::placeholder {
color: #bfbfbf;
}
}
&:not(.disabled):hover, &.focused {
border-color: $themeColor;
}
&.disabled {
background-color: #f5f5f5;
border-color: #dcdcdc;
color: #b7b7b7;
}
.prefix, .suffix {
display: flex;
justify-content: center;
align-items: center;
line-height: 30px;
user-select: none;
}
}
</style>

183
src/components/Message.vue Normal file
View File

@ -0,0 +1,183 @@
<template>
<Transition
name="message-fade"
appear
mode="in-out"
@beforeLeave="emit('close')"
@afterLeave="emit('destroy')"
>
<div class="message" :id="id" v-if="visible">
<div class="message-container"
@mouseenter="clearTimer()"
@mouseleave="startTimer()"
>
<div class="icons">
<IconAttention theme="filled" size="18" fill="#faad14" v-if="type === 'warning'" />
<IconCheckOne theme="filled" size="18" fill="#52c41a" v-if="type === 'success'" />
<IconCloseOne theme="filled" size="18" fill="#ff4d4f" v-if="type === 'error'" />
<IconInfo theme="filled" size="18" fill="#1677ff" v-if="type === 'info'" />
</div>
<div class="content">
<div class="title" v-if="title">{{ title }}</div>
<div class="description">{{ message }}</div>
</div>
<div class="control">
<span
class="close-btn"
@click="close()"
v-if="closable"
>
<IconCloseSmall />
</span>
</div>
</div>
</div>
</Transition>
</template>
<script lang="ts" setup>
import { onMounted, ref, onBeforeMount } from 'vue'
import { icons } from '@/plugins/icon'
const {
IconAttention,
IconCheckOne,
IconCloseOne,
IconInfo,
IconCloseSmall,
} = icons
const props = withDefaults(defineProps<{
id: string
message: string
type?: string
title?: string
duration?: number
closable?: boolean
}>(), {
type: 'success',
title: '',
duration: 3000,
closable: false,
})
const emit = defineEmits<{
(event: 'close'): void
(event: 'destroy'): void
}>()
const visible = ref(true)
const timer = ref<number | null>(null)
const startTimer = () => {
if (props.duration <= 0) return
timer.value = setTimeout(close, props.duration)
}
const clearTimer = () => {
if (timer.value) clearTimeout(timer.value)
}
const close = () => visible.value = false
onBeforeMount(() => {
clearTimer()
})
onMounted(() => {
startTimer()
})
defineExpose({
close,
})
</script>
<style lang="scss" scoped>
.message {
max-width: 500px;
& + & {
margin-top: 15px;
}
}
.message-container {
min-width: 150px;
display: flex;
align-items: center;
padding: 10px;
font-size: 13px;
overflow: hidden;
border-radius: $borderRadius;
box-shadow: 0 1px 8px rgba(0, 0, 0, .15);
background: #fff;
pointer-events: all;
position: relative;
.icons {
display: flex;
align-items: center;
margin-right: 10px;
}
.title {
font-size: 14px;
font-weight: 700;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.content {
width: 100%;
}
.description {
line-height: 1.5;
color: $textColor;
}
.title + .description {
margin-top: 5px;
}
.control {
position: relative;
height: 100%;
margin-left: 10px;
}
.close-btn {
font-size: 15px;
color: #666;
display: flex;
align-items: center;
cursor: pointer;
&:hover {
color: $themeColor;
}
}
}
.message-fade-enter-active {
animation: message-fade-in-down .3s;
}
.message-fade-leave-active {
animation: message-fade-out .3s;
}
@keyframes message-fade-in-down {
0% {
opacity: 0;
transform: translateY(-20px);
}
100% {
opacity: 1;
transform: translateY(0);
}
}
@keyframes message-fade-out {
0% {
opacity: 1;
margin-top: 0;
}
100% {
opacity: 0;
margin-top: -45px;
}
}
</style>

137
src/components/Modal.vue Normal file
View File

@ -0,0 +1,137 @@
<template>
<Transition name="modal-fade">
<div class="modal" ref="modalRef" v-show="visible" tabindex="-1" @keyup.esc="onEsc()">
<div class="mask" @click="onClickMask()"></div>
<Transition name="modal-zoom">
<div class="modal-content" v-show="visible" :style="contentStyle">
<span class="close-btn" v-if="closeButton" @click="emit('update:visible', false)"><IconClose /></span>
<slot></slot>
</div>
</Transition>
</div>
</Transition>
</template>
<script lang="ts" setup>
import { computed, nextTick, ref, watch, type CSSProperties } from 'vue'
import { icons } from '@/plugins/icon'
const { IconClose } = icons
const props = withDefaults(defineProps<{
visible: boolean
width?: number
closeButton?: boolean
closeOnClickMask?: boolean
closeOnEsc?: boolean
contentStyle?: CSSProperties
}>(), {
width: 480,
closeButton: false,
closeOnClickMask: true,
closeOnEsc: true,
})
const modalRef = ref<HTMLDivElement>()
const emit = defineEmits<{
(event: 'update:visible', payload: boolean): void
}>()
const contentStyle = computed(() => {
return {
width: props.width + 'px',
...(props.contentStyle || {})
}
})
watch(() => props.visible, () => {
if (props.visible) {
nextTick(() => modalRef.value!.focus())
}
})
const onEsc = () => {
if (props.visible && props.closeOnEsc) emit('update:visible', false)
}
const onClickMask = () => {
if (props.closeOnClickMask) emit('update:visible', false)
}
</script>
<style lang="scss" scoped>
.modal, .mask {
top: 0;
left: 0;
width: 100%;
height: 100%;
z-index: 5000;
}
.modal {
position: fixed;
display: flex;
justify-content: center;
align-items: center;
outline: 0;
border: 0;
}
.mask {
position: absolute;
background: rgba(0, 0, 0, .25);
}
.modal-content {
z-index: 5001;
padding: 10px;
background: #fff;
border-radius: $borderRadius;
box-shadow: 0 1px 3px rgba(0, 0, 0, .2);
position: relative;
}
.close-btn {
position: absolute;
top: 10px;
right: 12px;
width: 16px;
height: 16px;
cursor: pointer;
}
.modal-fade-enter-active {
animation: modal-fade-enter .25s both ease-in;
}
.modal-fade-leave-active {
animation: modal-fade-leave .25s both ease-out;
}
.modal-zoom-enter-active {
animation: modal-zoom-enter .25s both cubic-bezier(.4, 0, 0, 1.5);
}
.modal-zoom-leave-active {
animation: modal-zoom-leave .25s both;
}
@keyframes modal-fade-enter {
from {
opacity: 0;
}
}
@keyframes modal-fade-leave {
to {
opacity: 0;
}
}
@keyframes modal-zoom-enter {
from {
transform: scale3d(.3, .3, .3);
}
}
@keyframes modal-zoom-leave {
to {
transform: scale3d(.3, .3, .3);
}
}
</style>

View File

@ -0,0 +1,162 @@
<template>
<div
class="number-input"
:class="{
'disabled': disabled,
'focused': focused,
}"
>
<span class="prefix">
<slot name="prefix"></slot>
</span>
<div class="input-wrap">
<input
type="text"
:disabled="disabled"
v-model="number"
:placeholder="placeholder"
@focus="focused = true"
@blur="focused = false"
/>
<div class="handlers">
<span class="handler" @click="number += step">
<svg fill="currentColor" width="1em" height="1em" viewBox="64 64 896 896"><path d="M890.5 755.3L537.9 269.2c-12.8-17.6-39-17.6-51.7 0L133.5 755.3A8 8 0 00140 768h75c5.1 0 9.9-2.5 12.9-6.6L512 369.8l284.1 391.6c3 4.1 7.8 6.6 12.9 6.6h75c6.5 0 10.3-7.4 6.5-12.7z"></path></svg>
</span>
<span class="handler" @click="number -= step">
<svg fill="currentColor" width="1em" height="1em" viewBox="64 64 896 896"><path d="M884 256h-75c-5.1 0-9.9 2.5-12.9 6.6L512 654.2 227.9 262.6c-3-4.1-7.8-6.6-12.9-6.6h-75c-6.5 0-10.3 7.4-6.5 12.7l352.6 486.1c12.8 17.6 39 17.6 51.7 0l352.6-486.1c3.9-5.3.1-12.7-6.4-12.7z"></path></svg>
</span>
</div>
</div>
<span class="suffix">
<slot name="suffix"></slot>
</span>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
const props = withDefaults(defineProps<{
value: number
disabled?: boolean
placeholder?: string
min?: number
max?: number
step?: number
}>(), {
disabled: false,
placeholder: '',
min: 0,
max: Infinity,
step: 1,
})
const emit = defineEmits<{
(event: 'update:value', payload: number): void
}>()
const number = ref(0)
const focused = ref(false)
watch(() => props.value, () => {
if (props.value !== number.value) {
number.value = props.value
}
})
watch(number, () => {
let value = +number.value
if (isNaN(value)) value = 0
else if (value > props.max) value = props.max
else if (value < props.min) value = props.min
number.value = value
emit('update:value', number.value)
})
</script>
<style lang="scss" scoped>
.number-input {
background-color: #fff;
border: 1px solid #d9d9d9;
padding: 0 0 0 5px;
border-radius: $borderRadius;
transition: border-color .25s;
font-size: 13px;
display: inline-flex;
.input-wrap {
flex: 1;
color: $textColor;
padding: 0 0 0 5px;
position: relative;
}
&:not(.disabled) .input-wrap:hover .handlers {
opacity: 1;
}
.handlers {
width: 20px;
position: absolute;
top: 0;
bottom: 0;
right: 0;
display: flex;
flex-direction: column;
font-size: 6px;
color: #999;
opacity: 0;
user-select: none;
transition: opacity .25s;
.handler {
width: 100%;
height: 50%;
display: flex;
justify-content: center;
align-items: center;
border-left: 1px solid #d9d9d9;
cursor: pointer;
& + .handler {
border-top: 1px solid #d9d9d9;
}
&:hover {
color: $themeColor;
}
}
}
input {
width: 100%;
min-width: 0;
padding: 0;
height: 30px;
line-height: 30px;
outline: 0;
border: 0;
font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
&::placeholder {
color: #bfbfbf;
}
}
&:not(.disabled):hover, &.focused {
border-color: $themeColor;
}
&.disabled {
background-color: #f5f5f5;
border-color: #dcdcdc;
color: #b7b7b7;
}
.prefix, .suffix {
display: flex;
justify-content: center;
align-items: center;
line-height: 30px;
user-select: none;
}
}
</style>

View File

@ -0,0 +1,74 @@
<template>
<div class="popover" ref="triggerRef">
<div class="popover-content" :style="contentStyle" ref="contentRef">
<slot name="content"></slot>
</div>
<slot></slot>
</div>
</template>
<script lang="ts" setup>
import { type CSSProperties, onMounted, ref, watch, computed } from 'vue'
import tippy, { type Instance, type Placement } from 'tippy.js'
import 'tippy.js/animations/scale.css'
const props = withDefaults(defineProps<{
value: boolean
trigger?: 'click' | 'mouseenter'
placement?: Placement
appendTo?: HTMLElement | 'parent'
contentStyle?: CSSProperties
}>(), {
trigger: 'click',
placement: 'auto',
})
const emit = defineEmits<{
(event: 'update:value', payload: boolean): void
}>()
const instance = ref<Instance>()
const triggerRef = ref<HTMLElement>()
const contentRef = ref<HTMLElement>()
const contentStyle = computed(() => {
return props.contentStyle || {}
})
watch(() => props.value, () => {
if (!instance.value) return
if (props.value) instance.value.show()
else instance.value.hide()
})
onMounted(() => {
instance.value = tippy(triggerRef.value!, {
content: contentRef.value!,
allowHTML: true,
trigger: props.trigger,
placement: props.placement,
interactive: true,
appendTo: props.appendTo || document.body,
maxWidth: 'none',
offset: [0, 8],
animation: 'scale',
onShown() {
if (!props.value) emit('update:value', true)
},
onHidden() {
if (props.value) emit('update:value', false)
},
})
})
</script>
<style lang="scss" scoped>
.popover-content {
background-color: #fff;
padding: 10px;
border: 1px solid $borderColor;
box-shadow: $boxShadow;
border-radius: 2px;
}
</style>

View File

@ -0,0 +1,26 @@
<template>
<Button
:checked="!disabled && _value === value"
:disabled="disabled"
type="radio"
@click="!disabled && updateValue(value)"
>
<slot></slot>
</Button>
</template>
<script lang="ts" setup>
import { inject } from 'vue'
import { injectKeyRadioGroupValue, type RadioGroupValue } from '@/types/injectKey'
import Button from './Button.vue'
const { value: _value, updateValue } = inject(injectKeyRadioGroupValue) as RadioGroupValue
withDefaults(defineProps<{
value: string
disabled?: boolean
}>(), {
disabled: false,
})
</script>

View File

@ -0,0 +1,35 @@
<template>
<ButtonGroup class="radio-group">
<slot></slot>
</ButtonGroup>
</template>
<script lang="ts" setup>
import { computed, provide } from 'vue'
import { injectKeyRadioGroupValue } from '@/types/injectKey'
import ButtonGroup from './ButtonGroup.vue'
const props = withDefaults(defineProps<{
value: string
disabled?: boolean
}>(), {
disabled: false,
})
const emit = defineEmits<{
(event: 'update:value', payload: string): void
}>()
const updateValue = (value: string) => {
if (props.disabled) return
emit('update:value', value)
}
const value = computed(() => props.value)
provide(injectKeyRadioGroupValue, {
value,
updateValue,
})
</script>

160
src/components/Select.vue Normal file
View File

@ -0,0 +1,160 @@
<template>
<div class="select-wrap" v-if="disabled">
<div class="select disabled" ref="selectRef">
<div class="selector">{{ value }}</div>
<div class="icon">
<slot name="icon">
<IconDown :size="14" />
</slot>
</div>
</div>
</div>
<Popover
class="select-wrap"
trigger="click"
v-model:value="popoverVisible"
placement="bottom"
:contentStyle="{
padding: 0,
boxShadow: '0 6px 16px 0 rgba(0, 0, 0, 0.08)',
}"
v-else
>
<template #content>
<div class="options" :style="{ width: width + 2 + 'px' }">
<div class="option"
:class="{ 'disabled': option.disabled }"
v-for="option in options"
:key="option.key"
@click="handleSelect(option)"
>{{ option.value }}</div>
</div>
</template>
<div class="select" ref="selectRef">
<div class="selector">{{ value }}</div>
<div class="icon">
<slot name="icon">
<IconDown :size="14" />
</slot>
</div>
</div>
</Popover>
</template>
<script lang="ts" setup>
import { onMounted, onUnmounted, ref } from 'vue'
import Popover from './Popover.vue'
interface SelectOption {
key: string
value: string | number
disabled?: boolean
}
withDefaults(defineProps<{
value: string | number
options: SelectOption[]
disabled?: boolean
}>(), {
disabled: false,
})
const emit = defineEmits<{
(event: 'update:value', payload: string | number): void
}>()
const popoverVisible = ref(false)
const selectRef = ref<HTMLElement>()
const width = ref(0)
const updateWidth = () => {
if (!selectRef.value) return
width.value = selectRef.value.clientWidth
}
const resizeObserver = new ResizeObserver(updateWidth)
onMounted(() => {
if (!selectRef.value) return
resizeObserver.observe(selectRef.value)
})
onUnmounted(() => {
if (!selectRef.value) return
resizeObserver.unobserve(selectRef.value)
})
const handleSelect = (option: SelectOption) => {
if (option.disabled) return
emit('update:value', option.value)
popoverVisible.value = false
}
</script>
<style lang="scss" scoped>
.select {
width: 100%;
height: 32px;
padding-right: 32px;
border-radius: $borderRadius;
transition: border-color .25s;
font-size: 13px;
user-select: none;
background-color: #fff;
border: 1px solid #d9d9d9;
position: relative;
cursor: pointer;
&:not(.disabled):hover {
border-color: $themeColor;
}
&.disabled {
background-color: #f5f5f5;
border-color: #dcdcdc;
color: #b7b7b7;
cursor: default;
}
.selector {
min-width: 50px;
height: 30px;
line-height: 30px;
padding-left: 10px;
@include ellipsis-oneline();
}
}
.options {
max-height: 260px;
padding: 5px;
overflow: auto;
text-align: left;
font-size: 13px;
user-select: none;
}
.option {
height: 32px;
line-height: 32px;
padding: 0 5px;
cursor: pointer;
@include ellipsis-oneline();
&.disabled {
background-color: #f5f5f5;
color: #b7b7b7;
cursor: default;
}
&:not(.disabled):hover {
background-color: rgba($color: $themeColor, $alpha: .05);
}
}
.icon {
width: 32px;
height: 30px;
color: #bfbfbf;
position: absolute;
top: 0;
right: 0;
display: flex;
justify-content: center;
align-items: center;
}
</style>

View File

@ -0,0 +1,54 @@
<template>
<div class="select-group">
<slot></slot>
</div>
</template>
<script lang="ts" setup>
</script>
<style lang="scss" scoped>
.select-group {
display: flex;
align-items: center;
::v-deep(.select-wrap) {
.select {
border-radius: 0;
border-left-width: 0;
border-right-width: 0;
}
& + .select-wrap {
.select {
border-left-width: 1px;
}
}
&:hover {
& + .select-wrap {
.select {
border-left-color: $themeColor;
}
}
}
&:first-child {
.select {
border-top-left-radius: $borderRadius;
border-bottom-left-radius: $borderRadius;
border-left-width: 1px;
}
}
&:last-child {
.select {
border-top-right-radius: $borderRadius;
border-bottom-right-radius: $borderRadius;
border-right-width: 1px;
}
}
}
}
</style>

272
src/components/Slider.vue Normal file
View File

@ -0,0 +1,272 @@
<template>
<div class="slider" :class="{ 'disabled': disabled }" ref="sliderRef" @mousedown="$event => handleMousedown($event)">
<div class="bar">
<template v-if="!range">
<div class="track" :style="{ width: `${percentage}%` }"></div>
<div class="thumb" :style="{ left: `${percentage}%` }" :data-tooltip="tooltipValue"></div>
</template>
<template v-else>
<div class="track" :style="{ width: `${end - start}%`, left: `${start}%` }"></div>
<div class="thumb" :style="{ left: `${start}%` }" :data-tooltip="tooltipValue"></div>
<div class="thumb" :style="{ left: `${end}%` }" :data-tooltip="tooltipValue"></div>
</template>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, watch } from 'vue'
import NP from 'number-precision'
const getBoundingClientRectViewLeft = (element: HTMLElement) => {
return element.getBoundingClientRect().left
}
const props = withDefaults(defineProps<{
value: number | [number, number]
disabled?: boolean
min?: number
max?: number
step?: number
range?: boolean
}>(), {
disabled: false,
min: 0,
max: 100,
step: 1,
range: false,
})
const emit = defineEmits<{
(event: 'update:value', payload: number | [number, number]): void
}>()
const sliderRef = ref<HTMLElement>()
const percentage = ref(0)
const start = ref(0)
const end = ref(0)
const handler = ref<'start' | 'end'>('end')
const tooltipValue = ref(0)
watch(() => props.value, () => {
if (typeof props.value === 'number') {
percentage.value = (props.value - props.min) / (props.max - props.min) * 100
}
else {
start.value = (props.value[0] - props.min) / (props.max - props.min) * 100
end.value = (props.value[1] - props.min) / (props.max - props.min) * 100
}
}, {
immediate: true,
})
const getPercentage = (e: MouseEvent | TouchEvent) => {
if (!sliderRef.value) return 0
const clientX = 'clientX' in e ? e.clientX : e.changedTouches[0].clientX
let progress = (clientX - getBoundingClientRectViewLeft(sliderRef.value)) / sliderRef.value.clientWidth
progress = Math.max(progress, 0)
progress = Math.min(progress, 1)
let _percentage = progress * 100
const step = props.step / (props.max - props.min) * 100
const remainder = _percentage % step
if (remainder > 0) {
if (remainder <= step / 2) _percentage = _percentage - remainder
else _percentage = _percentage - remainder + step
}
return _percentage
}
const getNewValue = (percentage: number) => {
let diff = percentage / 100 * (props.max - props.min)
if (props.step >= 1) diff = Math.fround(diff)
else {
const str = props.step.toString()
const match = str.match(/^[0.]*([1-9])/)
if (match) {
const targetNumber = match[1]
const position = str.indexOf(targetNumber) - 1
if (position > 0) {
const accuracy = Math.pow(10, position)
diff = Math.fround(diff * accuracy) / accuracy
}
}
}
return NP.plus(diff, props.min)
}
//
const updateRange = (e: MouseEvent | TouchEvent) => {
const value = getPercentage(e)
if (handler.value === 'start') start.value = value
else end.value = value
tooltipValue.value = getNewValue(value)
}
const updateRangeEnd = (e: MouseEvent | TouchEvent) => {
updatePercentage(e)
const newValue = getNewValue(percentage.value)
const oldValueArr = props.value as [number, number]
const newValueArr: [number, number] = handler.value === 'start' ? [newValue, oldValueArr[1]] : [oldValueArr[0], newValue]
if (newValueArr[0] > newValueArr[1]) {
[newValueArr[0], newValueArr[1]] = [newValueArr[1], newValueArr[0]]
}
emit('update:value', newValueArr)
document.removeEventListener('mousemove', updateRange)
document.removeEventListener('touchmove', updateRange)
document.removeEventListener('mouseup', updateRangeEnd)
document.removeEventListener('touchend', updateRangeEnd)
}
//
const updatePercentage = (e: MouseEvent | TouchEvent) => {
percentage.value = getPercentage(e)
tooltipValue.value = getNewValue(percentage.value)
}
const updatePercentageEnd = (e: MouseEvent | TouchEvent) => {
updatePercentage(e)
const newValue = getNewValue(percentage.value)
emit('update:value', newValue)
document.removeEventListener('mousemove', updatePercentage)
document.removeEventListener('touchmove', updatePercentage)
document.removeEventListener('mouseup', updatePercentageEnd)
document.removeEventListener('touchend', updatePercentageEnd)
}
const handleMousedown = (e: MouseEvent | TouchEvent) => {
if (props.disabled) return
if (props.range) {
const _percentage = getPercentage(e)
if (Math.abs(_percentage - start.value) < Math.abs(_percentage - end.value)) {
handler.value = 'start'
}
else handler.value = 'end'
document.addEventListener('mousemove', updateRange)
document.addEventListener('touchmove', updateRange)
document.addEventListener('mouseup', updateRangeEnd)
document.addEventListener('touchend', updateRangeEnd)
}
else {
document.addEventListener('mousemove', updatePercentage)
document.addEventListener('touchmove', updatePercentage)
document.addEventListener('mouseup', updatePercentageEnd)
document.addEventListener('touchend', updatePercentageEnd)
}
}
</script>
<style scoped lang="scss">
.slider {
width: 100%;
height: 12px;
padding: 4px 0;
user-select: none;
&.disabled {
.track {
background-color: #b4b4b4;
}
.thumb {
outline: 2px solid #b4b4b4;
}
}
}
.slider:not(.disabled) {
cursor: pointer;
.bar {
&:hover {
background-color: #f0f0f0;
}
}
.track {
&:hover {
background-color: $themeHoverColor;
}
}
.thumb {
&:hover, &:active {
outline: 4px solid $themeColor;
}
}
}
.bar {
height: 4px;
border-radius: 2px;
position: relative;
background-color: #f5f5f5;
user-select: none;
transition: background-color .2s;
}
.track {
position: absolute;
top: 0;
left: 0;
height: 100%;
background-color: $themeColor;
transition: background-color .2s;
}
.thumb {
position: absolute;
top: 50%;
left: 0;
width: 10px;
height: 10px;
background-color: #fff;
outline: 2px solid $themeColor;
transform: translate(-50%, -50%);
border-radius: 50%;
&:hover, &:active {
&::before, &::after {
display: block;
}
}
&::before {
content: attr(data-tooltip);
min-width: 44px;
display: none;
position: absolute;
left: 50%;
bottom: 24px;
transform: translateX(-50%);
z-index: 1000;
background-color: #262626;
text-align: center;
color: #fff;
border-radius: 2px;
padding: 8px 5px;
font-size: 12px;
}
&::after {
content: '';
display: none;
position: absolute;
left: 50%;
bottom: 15px;
transform: translateX(-50%);
z-index: 1000;
border: 5px solid transparent;
border-top-color: #262626;
}
}
</style>

97
src/components/Switch.vue Normal file
View File

@ -0,0 +1,97 @@
<template>
<span
class="switch"
:class="{
'active': value,
'disabled': disabled,
}"
@click="handleChange()"
>
<span class="switch-core"></span>
<span class="switch-text">
<slot></slot>
</span>
</span>
</template>
<script lang="ts" setup>
const props = withDefaults(defineProps<{
value: boolean
disabled?: boolean
}>(), {
disabled: false,
})
const emit = defineEmits<{
(event: 'update:value', payload: boolean): void
}>()
const handleChange = () => {
if (props.disabled) return
emit('update:value', !props.value)
}
</script>
<style lang="scss" scoped>
.switch {
color: $textColor;
cursor: pointer;
&:not(.disabled).active {
.switch-core {
border-color: $themeColor;
background-color: $themeColor;
&::after {
left: 100%;
margin-left: -17px;
}
}
}
&.disabled {
cursor: default;
.switch-text {
color: #b7b7b7;
}
.switch-core::after {
background-color: #f5f5f5;
}
}
}
.switch-text {
margin-left: 5px;
display: inline-block;
height: 20px;
line-height: 20px;
font-size: 13px;
user-select: none;
}
.switch-core {
margin: 0;
display: inline-block;
position: relative;
width: 40px;
height: 20px;
border: 1px solid #d9d9d9;
outline: none;
border-radius: 10px;
box-sizing: border-box;
background: #d9d9d9;
transition: border-color .3s, background-color .3s;
vertical-align: middle;
&::after {
content: '';
position: absolute;
top: 1px;
left: 1px;
border-radius: 100%;
transition: all .3s;
width: 16px;
height: 16px;
background-color: #fff;
}
}
</style>

View File

@ -0,0 +1,70 @@
<template>
<textarea
class="textarea"
:class="{
'disabled': disabled,
'resizable': resizable,
}"
:disabled="disabled"
:value="value"
:rows="rows"
:placeholder="placeholder"
@input="$event => handleInput($event)"
></textarea>
</template>
<script lang="ts" setup>
withDefaults(defineProps<{
value: string
rows?: number
disabled?: boolean
resizable?: boolean
placeholder?: string
}>(), {
rows: 4,
disabled: false,
resizable: false,
placeholder: '',
})
const emit = defineEmits<{
(event: 'update:value', payload: string): void
}>()
const handleInput = (e: Event) => {
emit('update:value', (e.target as HTMLInputElement).value)
}
</script>
<style lang="scss" scoped>
.textarea {
outline: 0;
width: 100%;
background-color: #fff;
border: 1px solid #d9d9d9;
border-radius: $borderRadius;
padding: 10px;
transition: border-color .25s;
resize: none;
font-family: -apple-system,BlinkMacSystemFont, 'Segoe UI',Roboto,'Helvetica Neue',Arial,'Noto Sans',sans-serif,'Apple Color Emoji','Segoe UI Emoji','Segoe UI Symbol','Noto Color Emoji';
&:focus {
border-color: $themeColor;
background-color: #fff;
}
&.resizable {
resize: vertical;
}
&.disabled {
background-color: #f5f5f5;
border-color: #dcdcdc;
color: #b7b7b7;
}
&::placeholder {
color: #bfbfbf;
}
}
</style>

View File

@ -2,10 +2,12 @@ import type { App } from 'vue'
import Contextmenu from './contextmenu'
import ClickOutside from './clickOutside'
import Tooltip from './tooltip'
export default {
install(app: App) {
app.directive('contextmenu', Contextmenu)
app.directive('click-outside', ClickOutside)
app.directive('tooltip', Tooltip)
}
}

View File

View File

@ -0,0 +1,67 @@
.tippy-box[data-theme~='tooltip'] {
background-color: #262626;
color: #fff;
border-radius: 2px;
padding: 8px;
font-size: 12px;
line-height: 1.5;
.tippy-arrow {
width: 12px;
height: 12px;
color: #262626;
&::before {
content: '';
position: absolute;
border-color: transparent;
border-style: solid;
}
}
&[data-placement^='top'] > .tippy-arrow {
bottom: 0;
&::before {
bottom: -5px;
left: 0;
border-width: 6px 6px 0;
border-top-color: initial;
transform-origin: center top;
}
}
&[data-placement^='bottom'] > .tippy-arrow {
top: 0;
&::before {
top: -5px;
left: 0;
border-width: 0 6px 6px;
border-bottom-color: initial;
transform-origin: center bottom;
}
}
&[data-placement^='left'] > .tippy-arrow {
right: 0;
&::before {
border-width: 6px 0 6px 6px;
border-left-color: initial;
right: -5px;
transform-origin: center left;
}
}
&[data-placement^='right'] > .tippy-arrow {
left: 0;
&::before {
left: -5px;
border-width: 6px 6px 6px 0;
border-right-color: initial;
transform-origin: center right;
}
}
}

View File

@ -0,0 +1,62 @@
import type { Directive, DirectiveBinding } from 'vue'
import tippy, { type Instance, type Placement } from 'tippy.js'
import './tooltip.scss'
const TOOLTIP_INSTANCE = 'TOOLTIP_INSTANCE'
interface CustomHTMLElement extends HTMLElement {
[TOOLTIP_INSTANCE]?: Instance
}
type Delay = number | [number | null, number | null]
interface BindingValue {
content: string
placement?: Placement
delay?: Delay
}
const TooltipDirective: Directive = {
mounted(el: CustomHTMLElement, binding: DirectiveBinding<BindingValue | string>) {
let content = ''
let placement: Placement = 'auto'
let delay: Delay = [300, 0]
if (typeof binding.value === 'string') {
content = binding.value
}
else {
content = binding.value.content
if (binding.value.placement !== undefined) placement = binding.value.placement
if (binding.value.delay !== undefined) delay = binding.value.delay
}
el[TOOLTIP_INSTANCE] = tippy(el, {
content,
theme: 'tooltip',
duration: 100,
animation: 'scale',
allowHTML: true,
placement,
delay,
})
},
updated(el: CustomHTMLElement, binding: DirectiveBinding<BindingValue | string>) {
let content = ''
if (typeof binding.value === 'string') {
content = binding.value
}
else {
content = binding.value.content
}
if (el[TOOLTIP_INSTANCE]) el[TOOLTIP_INSTANCE].setContent(content)
},
unmounted(el: CustomHTMLElement) {
if (el[TOOLTIP_INSTANCE]) el[TOOLTIP_INSTANCE].destroy()
},
}
export default TooltipDirective

View File

@ -116,6 +116,10 @@ import {
Right,
MoveOne,
HamburgerButton,
Attention,
CheckOne,
CloseOne,
Info,
} from '@icon-park/vue-next'
export interface Icons {
@ -237,6 +241,10 @@ export const icons: Icons = {
IconRight: Right,
IconMoveOne: MoveOne,
IconHamburgerButton: HamburgerButton,
IconAttention: Attention,
IconCheckOne: CheckOne,
IconCloseOne: CloseOne,
IconInfo: Info,
}
export default {

View File

@ -2,6 +2,11 @@ import type { InjectionKey, Ref } from 'vue'
export type SlideScale = Ref<number>
export type SlideId = Ref<string>
export type RadioGroupValue = {
value: Ref<string>
updateValue: (value: string) => void
}
export const injectKeySlideScale: InjectionKey<SlideScale> = Symbol()
export const injectKeySlideId: InjectionKey<SlideId> = Symbol()
export const injectKeyRadioGroupValue: InjectionKey<RadioGroupValue> = Symbol()

103
src/utils/message.ts Normal file
View File

@ -0,0 +1,103 @@
import { createVNode, render, type AppContext } from 'vue'
import MessageComponent from '@/components/Message.vue'
export interface MessageOptions {
type?: 'info' | 'success' | 'warning' | 'error'
title?: string
message?: string
duration?: number
closable?: boolean
ctx?: AppContext
onClose?: () => void
}
export type MessageTypeOptions = Omit<MessageOptions, 'type' | 'message'>
export interface MessageIntance {
id: string
close: () => void
}
export type MessageFn = (message: string, options?: MessageTypeOptions) => MessageIntance
export interface Message {
(options: MessageOptions): MessageIntance
info: MessageFn
success: MessageFn
error: MessageFn
warning: MessageFn
closeAll: () => void
_context?: AppContext | null
}
const instances: MessageIntance[] = []
let wrap: HTMLDivElement | null = null
let seed = 0
const defaultOptions: MessageOptions = {
duration: 3000,
}
const message: Message = (options: MessageOptions) => {
const id = 'message-' + seed++
const props = {
...defaultOptions,
...options,
id,
}
if (!wrap) {
wrap = document.createElement('div')
wrap.className = 'message-wrap'
wrap.style.cssText = `
width: 100%;
position: fixed;
top: 0;
left: 0;
z-index: 1010;
pointer-events: none;
display: flex;
flex-direction: column;
box-sizing: border-box;
padding: 15px;
background-color: rgba(255, 255, 255, 0);
transition: all 1s ease-in-out;
align-items: center;
`
document.body.appendChild(wrap)
}
const vm = createVNode(MessageComponent, props, null)
const div = document.createElement('div')
vm.appContext = options.ctx || message._context || null
vm.props!.onClose = options.onClose
vm.props!.onDestroy = () => {
if (wrap && wrap.childNodes.length <= 1) {
wrap.remove()
wrap = null
}
render(null, div)
}
render(vm, div)
wrap.appendChild(div.firstElementChild!)
const instance = {
id,
close: () => vm?.component?.exposed?.close(),
}
instances.push(instance)
return instance
}
message.success = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'success', message: msg })
message.info = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'info', message: msg })
message.warning = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'warning', message: msg })
message.error = (msg: string, options?: MessageTypeOptions) => message({ ...options, type: 'error', message: msg })
message.closeAll = function() {
for (let i = instances.length - 1; i >= 0; i--) {
instances[i].close()
}
}
export default message

View File

@ -279,7 +279,7 @@ import ElementFlip from '../common/ElementFlip.vue'
import ColorButton from '../common/ColorButton.vue'
import TextColorButton from '../common/TextColorButton.vue'
import CheckboxButton from '@/components/CheckboxButton.vue'
import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import CheckboxButtonGroup from '@/components/ButtonGroup.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import ShapeItemThumbnail from '@/views/Editor/CanvasTool/ShapeItemThumbnail.vue'
import {

View File

@ -196,7 +196,7 @@ import ElementOutline from '../common/ElementOutline.vue'
import ColorButton from '../common/ColorButton.vue'
import TextColorButton from '../common/TextColorButton.vue'
import CheckboxButton from '@/components/CheckboxButton.vue'
import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import CheckboxButtonGroup from '@/components/ButtonGroup.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import {
Divider,

View File

@ -347,7 +347,7 @@ import ElementShadow from '../common/ElementShadow.vue'
import ColorButton from '../common/ColorButton.vue'
import TextColorButton from '../common/TextColorButton.vue'
import CheckboxButton from '@/components/CheckboxButton.vue'
import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import CheckboxButtonGroup from '@/components/ButtonGroup.vue'
import ColorPicker from '@/components/ColorPicker/index.vue'
import {
Divider,

View File

@ -1,6 +1,6 @@
<template>
<div class="element-flip">
<CheckboxButtonGroup class="row">
<ButtonGroup class="row">
<CheckboxButton
style="flex: 1;"
:checked="flipV"
@ -11,7 +11,7 @@
:checked="flipH"
@click="updateFlip({ flipH: !flipH })"
><IconFlipHorizontally /> 水平翻转</CheckboxButton>
</CheckboxButtonGroup>
</ButtonGroup>
</div>
</template>
@ -23,7 +23,7 @@ import type { ImageOrShapeFlip } from '@/types/slides'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import CheckboxButton from '@/components/CheckboxButton.vue'
import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import ButtonGroup from '@/components/ButtonGroup.vue'
const slidesStore = useSlidesStore()
const { handleElement } = storeToRefs(useMainStore())

View File

@ -133,7 +133,7 @@ import useAddSlidesOrElements from '@/hooks/useAddSlidesOrElements'
import useHistorySnapshot from '@/hooks/useHistorySnapshot'
import CheckboxButton from '@/components/CheckboxButton.vue'
import CheckboxButtonGroup from '@/components/CheckboxButtonGroup.vue'
import CheckboxButtonGroup from '@/components/ButtonGroup.vue'
import {
Divider,
Button,