mirror of
https://github.com/pipipi-pikachu/PPTist.git
synced 2025-04-15 02:20:00 +08:00
feat: 增加一组基础组件
This commit is contained in:
parent
0502c80fa4
commit
9306c81ded
28
README.md
28
README.md
@ -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
38
package-lock.json
generated
@ -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",
|
||||
|
@ -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
98
src/components/Button.vue
Normal 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>
|
@ -1,39 +1,45 @@
|
||||
<template>
|
||||
<div class="checkbox-button-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.checkbox-button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
::v-deep(.checkbox-button) {
|
||||
border-radius: 0;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
display: inline-block;
|
||||
|
||||
& + .checkbox-button {
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
&:first-child {
|
||||
border-top-left-radius: $borderRadius;
|
||||
border-bottom-left-radius: $borderRadius;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: $borderRadius;
|
||||
border-bottom-right-radius: $borderRadius;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
<template>
|
||||
<div class="button-group">
|
||||
<slot></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script lang="ts" setup>
|
||||
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.button-group {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
|
||||
::v-deep(.button) {
|
||||
border-radius: 0;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
display: inline-block;
|
||||
|
||||
& + .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;
|
||||
border-left-width: 1px;
|
||||
}
|
||||
|
||||
&:last-child {
|
||||
border-top-right-radius: $borderRadius;
|
||||
border-bottom-right-radius: $borderRadius;
|
||||
border-right-width: 1px;
|
||||
}
|
||||
}
|
||||
}
|
||||
</style>
|
109
src/components/Checkbox.vue
Normal file
109
src/components/Checkbox.vue
Normal 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>
|
@ -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>
|
||||
</script>
|
34
src/components/Divider.vue
Normal file
34
src/components/Divider.vue
Normal 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
95
src/components/Input.vue
Normal 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
183
src/components/Message.vue
Normal 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
137
src/components/Modal.vue
Normal 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>
|
162
src/components/NumberInput.vue
Normal file
162
src/components/NumberInput.vue
Normal 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>
|
74
src/components/Popover.vue
Normal file
74
src/components/Popover.vue
Normal 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>
|
26
src/components/RadioButton.vue
Normal file
26
src/components/RadioButton.vue
Normal 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>
|
35
src/components/RadioGroup.vue
Normal file
35
src/components/RadioGroup.vue
Normal 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
160
src/components/Select.vue
Normal 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>
|
54
src/components/SelectGroup.vue
Normal file
54
src/components/SelectGroup.vue
Normal 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
272
src/components/Slider.vue
Normal 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
97
src/components/Switch.vue
Normal 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>
|
70
src/components/TextArea.vue
Normal file
70
src/components/TextArea.vue
Normal 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>
|
@ -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)
|
||||
}
|
||||
}
|
||||
|
0
src/plugins/directive/message.ts
Normal file
0
src/plugins/directive/message.ts
Normal file
67
src/plugins/directive/tooltip.scss
Normal file
67
src/plugins/directive/tooltip.scss
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
62
src/plugins/directive/tooltip.ts
Normal file
62
src/plugins/directive/tooltip.ts
Normal 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
|
@ -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 {
|
||||
|
@ -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 injectKeySlideId: InjectionKey<SlideId> = Symbol()
|
||||
export const injectKeyRadioGroupValue: InjectionKey<RadioGroupValue> = Symbol()
|
103
src/utils/message.ts
Normal file
103
src/utils/message.ts
Normal 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
|
@ -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 {
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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())
|
||||
|
@ -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,
|
||||
|
Loading…
x
Reference in New Issue
Block a user