refactor(deps): migrate to nuxt v4
This commit is contained in:
2
app/components/uni/Button/index.d.ts
vendored
Normal file
2
app/components/uni/Button/index.d.ts
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
type ButtonType = 'normal' | 'primary' | 'danger'
|
||||
type ButtonSize = 'base' | 'medium' | 'small'
|
||||
140
app/components/uni/Button/index.vue
Normal file
140
app/components/uni/Button/index.vue
Normal file
@@ -0,0 +1,140 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const emit = defineEmits(['click'])
|
||||
const props = defineProps({
|
||||
type: {
|
||||
type: String as PropType<ButtonType>,
|
||||
default: 'normal',
|
||||
},
|
||||
attrType: {
|
||||
type: String as PropType<'button' | 'submit' | 'reset'>,
|
||||
default: 'button',
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<ButtonSize>,
|
||||
default: 'base',
|
||||
},
|
||||
block: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
icon: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const buttonTypeClass = computed(() => {
|
||||
let ret = `uni-button--normal`
|
||||
if (props.type !== 'normal') ret += ` uni-button--${props.type}`
|
||||
return ret
|
||||
})
|
||||
|
||||
const buttonSizeClass = computed(() => {
|
||||
return `uni-button--${props.size}`
|
||||
})
|
||||
|
||||
const buttonIcon = computed(() => {
|
||||
if (props.icon) return props.icon
|
||||
return null
|
||||
})
|
||||
|
||||
const handleClick = (e: any) => {
|
||||
emit('click', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="w-fit flex justify-center items-center rounded-md font-bold border shadow-sm transition focus:ring-4"
|
||||
:class="{
|
||||
'w-full': block,
|
||||
'uni-button--disabled': disabled || loading,
|
||||
[buttonTypeClass]: buttonTypeClass,
|
||||
[buttonSizeClass]: buttonSizeClass,
|
||||
}"
|
||||
@click="handleClick"
|
||||
:disabled="disabled || loading"
|
||||
:type="attrType"
|
||||
>
|
||||
<Transition name="icon">
|
||||
<UniIconSpinner v-if="loading" />
|
||||
<Icon
|
||||
v-else-if="buttonIcon"
|
||||
:name="buttonIcon"
|
||||
:key="buttonIcon"
|
||||
/>
|
||||
<span
|
||||
v-else
|
||||
class="mr-2"
|
||||
>
|
||||
<slot name="icon" />
|
||||
</span>
|
||||
</Transition>
|
||||
<div
|
||||
class="flex items-center whitespace-nowrap leading-snug"
|
||||
:class="{ 'ml-2': buttonIcon || loading }"
|
||||
>
|
||||
<slot />
|
||||
</div>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-enter-active,
|
||||
.icon-leave-active {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.icon-enter-from,
|
||||
.icon-leave-to {
|
||||
opacity: 0;
|
||||
width: 0;
|
||||
}
|
||||
|
||||
.uni-button--normal {
|
||||
@apply bg-neutral-50 hover:bg-neutral-100 active:bg-neutral-200 dark:bg-neutral-800 dark:hover:bg-neutral-700 dark:active:bg-neutral-600;
|
||||
@apply ring-neutral-200/50 dark:ring-neutral-800/50;
|
||||
@apply border-neutral-300 dark:border-neutral-700;
|
||||
@apply text-neutral-700 dark:text-neutral-300;
|
||||
}
|
||||
|
||||
.uni-button--primary {
|
||||
@apply text-blue-600 dark:text-blue-600;
|
||||
}
|
||||
|
||||
.uni-button--danger {
|
||||
@apply text-red-500 dark:text-red-500;
|
||||
}
|
||||
|
||||
.uni-button--disabled {
|
||||
@apply bg-neutral-100 dark:bg-neutral-900 hover:bg-neutral-100 hover:dark:bg-neutral-900;
|
||||
@apply ring-transparent;
|
||||
@apply border-transparent;
|
||||
@apply text-neutral-400 dark:text-neutral-600;
|
||||
}
|
||||
|
||||
.uni-button--base {
|
||||
@apply text-base;
|
||||
@apply px-4 py-2;
|
||||
}
|
||||
|
||||
.uni-button--medium {
|
||||
@apply text-sm;
|
||||
@apply px-3 py-1.5;
|
||||
}
|
||||
|
||||
.uni-button--small {
|
||||
@apply text-sm;
|
||||
@apply px-2 py-1;
|
||||
}
|
||||
</style>
|
||||
157
app/components/uni/Copyable/index.vue
Normal file
157
app/components/uni/Copyable/index.vue
Normal file
@@ -0,0 +1,157 @@
|
||||
<script setup lang="ts">
|
||||
import { useMessage } from '~/composables/uni/useMessage'
|
||||
|
||||
const props = defineProps({
|
||||
hideIcon: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
iconSize: {
|
||||
type: String,
|
||||
default: '1em',
|
||||
},
|
||||
text: {
|
||||
type: String,
|
||||
required: true,
|
||||
},
|
||||
})
|
||||
|
||||
const message = useMessage()
|
||||
|
||||
const copied = ref(false)
|
||||
const copied_timeout = ref()
|
||||
|
||||
const fuck_copy = () => {
|
||||
navigator.clipboard
|
||||
.writeText(props.text || '')
|
||||
.then(() => {
|
||||
copied.value = true
|
||||
if (copied_timeout.value) clearInterval(copied_timeout.value)
|
||||
copied_timeout.value = setTimeout(() => (copied.value = false), 1500)
|
||||
})
|
||||
.catch((e) => {
|
||||
message.error(`复制失败`)
|
||||
})
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="inline-flex items-center gap-0.5 cursor-pointer"
|
||||
@click="fuck_copy"
|
||||
>
|
||||
<slot />
|
||||
<Transition
|
||||
v-if="!hideIcon"
|
||||
name="icon"
|
||||
mode="out-in"
|
||||
>
|
||||
<svg
|
||||
v-if="!copied"
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="iconSize"
|
||||
:height="iconSize"
|
||||
viewBox="0 0 24 24"
|
||||
class="text-neutral-500"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
stroke="currentColor"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path
|
||||
d="M8 10a2 2 0 0 1 2-2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2h-8a2 2 0 0 1-2-2z"
|
||||
/>
|
||||
<path d="M16 8V6a2 2 0 0 0-2-2H6a2 2 0 0 0-2 2v8a2 2 0 0 0 2 2h2" />
|
||||
</g>
|
||||
</svg>
|
||||
<svg
|
||||
v-else
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
:width="iconSize"
|
||||
:height="iconSize"
|
||||
viewBox="0 0 24 24"
|
||||
class="text-green-600"
|
||||
>
|
||||
<defs>
|
||||
<mask id="lineMdCheckAll0">
|
||||
<g
|
||||
fill="none"
|
||||
stroke="#fff"
|
||||
stroke-dasharray="22"
|
||||
stroke-dashoffset="22"
|
||||
stroke-linecap="round"
|
||||
stroke-linejoin="round"
|
||||
stroke-width="2"
|
||||
>
|
||||
<path d="M2 13.5l4 4l10.75 -10.75">
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="stroke-dashoffset"
|
||||
dur="0.2s"
|
||||
values="22;0"
|
||||
/>
|
||||
</path>
|
||||
<path
|
||||
stroke="#000"
|
||||
stroke-width="4"
|
||||
d="M7.5 13.5l4 4l10.75 -10.75"
|
||||
opacity="0"
|
||||
>
|
||||
<set
|
||||
attributeName="opacity"
|
||||
begin="0.2s"
|
||||
to="1"
|
||||
/>
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="stroke-dashoffset"
|
||||
begin="0.2s"
|
||||
dur="0.2s"
|
||||
values="22;0"
|
||||
/>
|
||||
</path>
|
||||
<path
|
||||
d="M7.5 13.5l4 4l10.75 -10.75"
|
||||
opacity="0"
|
||||
>
|
||||
<set
|
||||
attributeName="opacity"
|
||||
begin="0.2s"
|
||||
to="1"
|
||||
/>
|
||||
<animate
|
||||
fill="freeze"
|
||||
attributeName="stroke-dashoffset"
|
||||
begin="0.2s"
|
||||
dur="0.2s"
|
||||
values="22;0"
|
||||
/>
|
||||
</path>
|
||||
</g>
|
||||
</mask>
|
||||
</defs>
|
||||
<rect
|
||||
width="24"
|
||||
height="24"
|
||||
fill="currentColor"
|
||||
mask="url(#lineMdCheckAll0)"
|
||||
/>
|
||||
</svg>
|
||||
</Transition>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-enter-active,
|
||||
.icon-leave-active {
|
||||
@apply transition duration-300;
|
||||
}
|
||||
|
||||
.icon-enter-from,
|
||||
.icon-leave-to {
|
||||
@apply opacity-0;
|
||||
}
|
||||
</style>
|
||||
134
app/components/uni/FileDnD/index.vue
Normal file
134
app/components/uni/FileDnD/index.vue
Normal file
@@ -0,0 +1,134 @@
|
||||
<script lang="ts" setup>
|
||||
const emit = defineEmits(['change'])
|
||||
const props = defineProps({
|
||||
placeholder: {
|
||||
type: String,
|
||||
default: '点击或拖拽文件到此处',
|
||||
},
|
||||
placeholderDragover: {
|
||||
type: String,
|
||||
default: '松开选择文件',
|
||||
},
|
||||
multiple: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
accept: {
|
||||
type: String,
|
||||
default: '',
|
||||
},
|
||||
})
|
||||
|
||||
const inputRef = ref<HTMLInputElement | null>(null)
|
||||
const dragover = ref(false)
|
||||
|
||||
const selectedFiles = ref<File[]>([])
|
||||
|
||||
const onIncomeFiles = (files?: FileList | null) => {
|
||||
if (files && files.length > 0) {
|
||||
let wantedFiles = Array.from(files).filter((file) => {
|
||||
if (props.accept) {
|
||||
const accept = props.accept.split(',').map((type) => type.trim())
|
||||
return accept.includes(file.type)
|
||||
}
|
||||
return true
|
||||
})
|
||||
if (wantedFiles.length === 0) {
|
||||
console.error('no acceptable file')
|
||||
return
|
||||
}
|
||||
selectedFiles.value = props.multiple ? wantedFiles : [wantedFiles[0]]
|
||||
emit('change', files)
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
:class="{
|
||||
'bg-neutral-300 dark:bg-neutral-900 border-primary-300 dark:border-primary-800 shadow-inner':
|
||||
dragover,
|
||||
}"
|
||||
class="w-full h-44 relative rounded-md border-2 border-dashed border-neutral-200 dark:border-neutral-800 bg-inherit cursor-pointer select-none transition duration-200 hover:border-primary-300 dark:hover:border-primary-800 overflow-hidden"
|
||||
@click="inputRef?.click()"
|
||||
@dragover.prevent="dragover = true"
|
||||
@dragleave.prevent="dragover = false"
|
||||
@drop.prevent="
|
||||
($event) => {
|
||||
dragover = false
|
||||
if (!$event.dataTransfer?.files) return
|
||||
onIncomeFiles($event.dataTransfer?.files)
|
||||
}
|
||||
"
|
||||
>
|
||||
<input
|
||||
ref="inputRef"
|
||||
:accept="accept"
|
||||
:multiple="multiple"
|
||||
class="hidden"
|
||||
type="file"
|
||||
@change="onIncomeFiles(inputRef?.files)"
|
||||
/>
|
||||
<div
|
||||
:class="{
|
||||
'pb-6': selectedFiles.length > 0,
|
||||
}"
|
||||
class="w-full h-full flex flex-col justify-center items-center gap-2 transition-all"
|
||||
>
|
||||
<Icon
|
||||
:name="dragover ? 'i-tabler-drag-drop' : 'i-tabler-upload'"
|
||||
class="text-4xl text-neutral-400 dark:text-neutral-500"
|
||||
/>
|
||||
<p class="text-xs font-medium text-neutral-500 dark:text-neutral-400">
|
||||
{{ dragover ? '松开选择文件' : '点击或拖拽文件到此处' }}
|
||||
</p>
|
||||
</div>
|
||||
<div
|
||||
v-if="selectedFiles.length > 0"
|
||||
class="absolute inset-x-0 bottom-0 pl-2 pr-0.5 py-0.5 flex justify-between items-center bg-neutral-100 dark:bg-neutral-900 border-t dark:border-neutral-800"
|
||||
>
|
||||
<div class="flex-1 pr-4 overflow-hidden flex items-center gap-1">
|
||||
<Icon
|
||||
:name="
|
||||
selectedFiles.length === 1 ? 'i-tabler-file' : 'i-tabler-files'
|
||||
"
|
||||
class="text-neutral-500 dark:text-neutral-400"
|
||||
/>
|
||||
<p
|
||||
:title="
|
||||
selectedFiles
|
||||
.slice(0, 3)
|
||||
.map((file) => file.name)
|
||||
.join(', ')
|
||||
"
|
||||
class="text-2xs font-medium overflow-hidden text-ellipsis whitespace-nowrap"
|
||||
>
|
||||
{{
|
||||
selectedFiles
|
||||
.slice(0, 3)
|
||||
.map((file) => file.name)
|
||||
.join(', ')
|
||||
}}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<UButton
|
||||
color="red"
|
||||
size="xs"
|
||||
square
|
||||
variant="ghost"
|
||||
@click.stop="
|
||||
() => {
|
||||
selectedFiles = []
|
||||
inputRef!.value = ''
|
||||
}
|
||||
"
|
||||
>
|
||||
<Icon name="i-tabler-x" />
|
||||
</UButton>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
21
app/components/uni/Icon/CircleError.vue
Normal file
21
app/components/uni/Icon/CircleError.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-6.489 5.8a1 1 0 0 0-1.218 1.567L10.585 12l-1.292 1.293l-.083.094a1 1 0 0 0 1.497 1.32L12 13.415l1.293 1.292l.094.083a1 1 0 0 0 1.32-1.497L13.415 12l1.292-1.293l.083-.094a1 1 0 0 0-1.497-1.32L12 10.585l-1.293-1.292l-.094-.083z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
15
app/components/uni/Icon/CircleInfo.vue
Normal file
15
app/components/uni/Icon/CircleInfo.vue
Normal file
@@ -0,0 +1,15 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
fillRule="evenodd"
|
||||
d="M22 12c0 5.523-4.477 10-10 10S2 17.523 2 12S6.477 2 12 2s10 4.477 10 10Zm-10 5.75a.75.75 0 0 0 .75-.75v-6a.75.75 0 0 0-1.5 0v6c0 .414.336.75.75.75ZM12 7a1 1 0 1 1 0 2a1 1 0 0 1 0-2Z"
|
||||
clipRule="evenodd"
|
||||
></path>
|
||||
</svg>
|
||||
</template>
|
||||
21
app/components/uni/Icon/CircleSuccess.vue
Normal file
21
app/components/uni/Icon/CircleSuccess.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M17 3.34a10 10 0 1 1-14.995 8.984L2 12l.005-.324A10 10 0 0 1 17 3.34zm-1.293 5.953a1 1 0 0 0-1.32-.083l-.094.083L11 12.585l-1.293-1.292l-.094-.083a1 1 0 0 0-1.403 1.403l.083.094l2 2l.094.083a1 1 0 0 0 1.226 0l.094-.083l4-4l.083-.094a1 1 0 0 0-.083-1.32z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
21
app/components/uni/Icon/CircleWarning.vue
Normal file
21
app/components/uni/Icon/CircleWarning.vue
Normal file
@@ -0,0 +1,21 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<g
|
||||
fill="none"
|
||||
strokeLinecap="round"
|
||||
strokeLinejoin="round"
|
||||
strokeWidth="2"
|
||||
>
|
||||
<path d="M0 0h24v24H0z"></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12 2c5.523 0 10 4.477 10 10a10 10 0 0 1-19.995.324L2 12l.004-.28C2.152 6.327 6.57 2 12 2zm.01 13l-.127.007a1 1 0 0 0 0 1.986L12 17l.127-.007a1 1 0 0 0 0-1.986L12.01 15zM12 7a1 1 0 0 0-.993.883L11 8v4l.007.117a1 1 0 0 0 1.986 0L13 12V8l-.007-.117A1 1 0 0 0 12 7z"
|
||||
></path>
|
||||
</g>
|
||||
</svg>
|
||||
</template>
|
||||
26
app/components/uni/Icon/Spinner.vue
Normal file
26
app/components/uni/Icon/Spinner.vue
Normal file
@@ -0,0 +1,26 @@
|
||||
<template>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
width="1em"
|
||||
height="1em"
|
||||
viewBox="0 0 24 24"
|
||||
>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,1A11,11,0,1,0,23,12,11,11,0,0,0,12,1Zm0,19a8,8,0,1,1,8-8A8,8,0,0,1,12,20Z"
|
||||
opacity=".25"
|
||||
></path>
|
||||
<path
|
||||
fill="currentColor"
|
||||
d="M12,4a8,8,0,0,1,7.89,6.7A1.53,1.53,0,0,0,21.38,12h0a1.5,1.5,0,0,0,1.48-1.75,11,11,0,0,0-21.72,0A1.5,1.5,0,0,0,2.62,12h0a1.53,1.53,0,0,0,1.49-1.3A8,8,0,0,1,12,4Z"
|
||||
>
|
||||
<animateTransform
|
||||
attributeName="transform"
|
||||
dur="0.75s"
|
||||
repeatCount="indefinite"
|
||||
type="rotate"
|
||||
values="0 12 12;360 12 12"
|
||||
></animateTransform>
|
||||
</path>
|
||||
</svg>
|
||||
</template>
|
||||
117
app/components/uni/Input/index.vue
Normal file
117
app/components/uni/Input/index.vue
Normal file
@@ -0,0 +1,117 @@
|
||||
<script lang="ts" setup>
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const emit = defineEmits(['input', 'change', 'update:modelValue'])
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number] as PropType<string | number | undefined>,
|
||||
required: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
type: {
|
||||
type: String as PropType<
|
||||
'text' | 'password' | 'number' | 'email' | 'tel' | 'date'
|
||||
>,
|
||||
required: false,
|
||||
default: 'text',
|
||||
},
|
||||
justify: {
|
||||
type: String as PropType<'start' | 'end'>,
|
||||
required: false,
|
||||
default: 'end',
|
||||
},
|
||||
pattern: {
|
||||
type: [String, RegExp],
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
})
|
||||
|
||||
const inputValue = ref(props.modelValue)
|
||||
const isError = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
inputValue.value = value
|
||||
if (props.pattern && value) {
|
||||
const pattern =
|
||||
typeof props.pattern === 'string'
|
||||
? new RegExp(props.pattern)
|
||||
: props.pattern
|
||||
isError.value = !pattern.test(value as string)
|
||||
pattern.lastIndex = 0
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
if (props.disabled) return
|
||||
const value = e.target.value
|
||||
|
||||
if (props.pattern && value && props.type !== 'date') {
|
||||
const pattern =
|
||||
typeof props.pattern === 'string'
|
||||
? new RegExp(props.pattern)
|
||||
: props.pattern
|
||||
isError.value = !pattern.test(value)
|
||||
pattern.lastIndex = 0
|
||||
inputValue.value = value
|
||||
if (isError.value) return
|
||||
}
|
||||
|
||||
inputValue.value = value
|
||||
isError.value = false
|
||||
|
||||
emit('update:modelValue', e.target.value)
|
||||
emit('input', e)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col space-y-1"
|
||||
:class="{
|
||||
'justify-start': justify === 'start',
|
||||
'justify-end': justify === 'end',
|
||||
}"
|
||||
>
|
||||
<p
|
||||
class="block w-fit text-neutral-700 dark:text-neutral-300 text-sm font-bold font-['Nunito']"
|
||||
v-if="label"
|
||||
>
|
||||
{{ label }}
|
||||
</p>
|
||||
<div class="relative">
|
||||
<input
|
||||
class="relative w-full flex items-center gap-2.5 p-2 pr-2 rounded-md overflow-hidden border transition bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-800 focus:border-neutral-400 dark:focus:border-neutral-700 focus:ring-4 focus:ring-opacity-50 focus:ring-neutral-200 dark:focus:ring-neutral-800 outline-none placeholder-neutral-400 dark:placeholder-neutral-500 shadow-sm"
|
||||
:class="{
|
||||
'!border-red-500': isError,
|
||||
'bg-neutral-100 dark:bg-neutral-900 text-neutral-400 dark:text-neutral-600':
|
||||
disabled,
|
||||
}"
|
||||
:value="inputValue"
|
||||
@input="handleInput"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
:type="type"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
113
app/components/uni/Message/Provider.vue
Normal file
113
app/components/uni/Message/Provider.vue
Normal file
@@ -0,0 +1,113 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
Message,
|
||||
MessageApi,
|
||||
MessageProviderApi,
|
||||
MessageType,
|
||||
} from '~/components/uni/Message/index'
|
||||
|
||||
const props = defineProps({
|
||||
max: {
|
||||
type: Number,
|
||||
default: 5,
|
||||
},
|
||||
})
|
||||
|
||||
const nuxtApp = useNuxtApp()
|
||||
const messageList = ref<Message[]>([])
|
||||
|
||||
const createMessage = (
|
||||
content: string,
|
||||
type: MessageType,
|
||||
duration: number = 3000
|
||||
) => {
|
||||
const { max } = props
|
||||
messageList.value.push({
|
||||
id: (Date.now() + Math.random() * 100).toString(32).toUpperCase(),
|
||||
content,
|
||||
type,
|
||||
duration,
|
||||
})
|
||||
if (messageList.value.length > max) {
|
||||
messageList.value.shift()
|
||||
}
|
||||
}
|
||||
|
||||
const providerApi: MessageProviderApi = {
|
||||
destroy: (id: string) => {
|
||||
messageList.value.splice(
|
||||
messageList.value.findIndex((message) => message.id === id),
|
||||
1
|
||||
)
|
||||
},
|
||||
}
|
||||
|
||||
const api: MessageApi = {
|
||||
info: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'info', duration)
|
||||
},
|
||||
success: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'success', duration)
|
||||
},
|
||||
warning: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'warning', duration)
|
||||
},
|
||||
error: (content: string, duration: number = 3000) => {
|
||||
createMessage(content, 'error', duration)
|
||||
},
|
||||
destroyAll: function (): void {
|
||||
throw new Error('Function not implemented.')
|
||||
},
|
||||
}
|
||||
|
||||
nuxtApp.vueApp.provide('uni-message-provider', providerApi)
|
||||
nuxtApp.vueApp.provide('uni-message', api)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<slot />
|
||||
<teleport to="body">
|
||||
<div id="message-provider">
|
||||
<div class="message-wrapper">
|
||||
<TransitionGroup name="message">
|
||||
<UniMessage
|
||||
v-for="(message, k) in messageList"
|
||||
:key="message.id"
|
||||
:message="message"
|
||||
/>
|
||||
</TransitionGroup>
|
||||
</div>
|
||||
</div>
|
||||
</teleport>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
#message-provider .message-wrapper {
|
||||
@apply z-[50000] fixed inset-0 flex flex-col items-center pointer-events-none;
|
||||
}
|
||||
|
||||
.message-move,
|
||||
.message-leave-active {
|
||||
transition: all 0.6s ease;
|
||||
}
|
||||
|
||||
.message-enter-active {
|
||||
transition: all 0.6s cubic-bezier(0.075, 0.82, 0.165, 1);
|
||||
}
|
||||
|
||||
.message-enter-from {
|
||||
filter: blur(2px);
|
||||
opacity: 0;
|
||||
transform: translateY(-100%);
|
||||
}
|
||||
|
||||
.message-leave-to {
|
||||
filter: blur(6px);
|
||||
opacity: 0;
|
||||
transform: translateY(-20%);
|
||||
}
|
||||
|
||||
.message-leave-active {
|
||||
position: absolute;
|
||||
}
|
||||
</style>
|
||||
20
app/components/uni/Message/index.d.ts
vendored
Normal file
20
app/components/uni/Message/index.d.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
export type Message = {
|
||||
id: string
|
||||
content: string
|
||||
type: MessageType
|
||||
duration?: number
|
||||
}
|
||||
|
||||
export type MessageType = 'success' | 'warning' | 'error' | 'info'
|
||||
|
||||
export type MessageProviderApi = {
|
||||
destroy: (id: string) => void
|
||||
}
|
||||
|
||||
export type MessageApi = {
|
||||
info: (content: string, duration?: number) => void
|
||||
success: (content: string, duration?: number) => void
|
||||
warning: (content: string, duration?: number) => void
|
||||
error: (content: string, duration?: number) => void
|
||||
destroyAll: () => void
|
||||
}
|
||||
82
app/components/uni/Message/index.vue
Normal file
82
app/components/uni/Message/index.vue
Normal file
@@ -0,0 +1,82 @@
|
||||
<script lang="ts" setup>
|
||||
import type {
|
||||
Message,
|
||||
MessageProviderApi,
|
||||
} from '~/components/uni/Message/index'
|
||||
|
||||
const providerApi = inject<MessageProviderApi>('uni-message-provider')
|
||||
|
||||
const props = defineProps({
|
||||
message: {
|
||||
require: true,
|
||||
type: Object,
|
||||
},
|
||||
})
|
||||
|
||||
const message = ref<Message>(props.message as Message)
|
||||
|
||||
onMounted(() => {
|
||||
setTimeout(() => {
|
||||
providerApi?.destroy(message.value.id)
|
||||
}, message.value?.duration || 3000)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="message"
|
||||
:class="{
|
||||
'!text-blue-500 !border-blue-400 !bg-blue-50': message.type === 'info',
|
||||
'!text-emerald-500 !border-emerald-400 !bg-emerald-50':
|
||||
message.type === 'success',
|
||||
'!text-orange-500 !border-orange-400 !bg-orange-50':
|
||||
message.type === 'warning',
|
||||
'!text-rose-500 !border-rose-400 !bg-rose-50': message.type === 'error',
|
||||
[message.type]: message.type,
|
||||
}"
|
||||
>
|
||||
<UniIconCircleSuccess
|
||||
v-if="message.type === 'success'"
|
||||
class="text-xl"
|
||||
/>
|
||||
<UniIconCircleWarning
|
||||
v-if="message.type === 'warning'"
|
||||
class="text-xl"
|
||||
/>
|
||||
<UniIconCircleError
|
||||
v-if="message.type === 'error'"
|
||||
class="text-xl"
|
||||
/>
|
||||
<UniIconCircleInfo
|
||||
v-if="message.type === 'info'"
|
||||
class="text-xl"
|
||||
/>
|
||||
<span>
|
||||
{{ message.content }}
|
||||
</span>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.message {
|
||||
min-width: 80px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.2);
|
||||
@apply h-fit px-2 py-1.5 border bg-white border-gray-300 rounded-md text-gray-500 text-xs flex items-center gap-1.5 first-of-type:mt-2.5 mt-2.5 font-bold pointer-events-auto;
|
||||
}
|
||||
|
||||
.message.info {
|
||||
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
|
||||
}
|
||||
|
||||
.message.success {
|
||||
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
|
||||
}
|
||||
|
||||
.message.warning {
|
||||
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.2);
|
||||
}
|
||||
|
||||
.message.error {
|
||||
box-shadow: 0 4px 12px rgba(244, 63, 94, 0.2);
|
||||
}
|
||||
</style>
|
||||
6
app/components/uni/Select/index.d.ts
vendored
Normal file
6
app/components/uni/Select/index.d.ts
vendored
Normal file
@@ -0,0 +1,6 @@
|
||||
type SelectItem = {
|
||||
label: string
|
||||
value: string
|
||||
icon?: string
|
||||
disabled?: boolean
|
||||
}
|
||||
187
app/components/uni/Select/index.vue
Normal file
187
app/components/uni/Select/index.vue
Normal file
@@ -0,0 +1,187 @@
|
||||
<script lang="ts" setup>
|
||||
import { computed, type PropType } from 'vue'
|
||||
|
||||
const emit = defineEmits(['input', 'change', 'update:modelValue'])
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
required: false,
|
||||
},
|
||||
items: {
|
||||
type: Array as PropType<SelectItem[]>,
|
||||
required: true,
|
||||
},
|
||||
justify: {
|
||||
type: String as PropType<'start' | 'end'>,
|
||||
required: false,
|
||||
default: 'end',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
align: {
|
||||
type: String as PropType<'bottom' | 'top'>,
|
||||
required: false,
|
||||
default: 'bottom',
|
||||
},
|
||||
})
|
||||
|
||||
const selectWrapperRef = ref()
|
||||
const selectRef = ref()
|
||||
const optionsRef = ref()
|
||||
|
||||
const optionsAlign = computed(() => {
|
||||
switch (props.align) {
|
||||
case 'bottom':
|
||||
return 'top-full mt-2'
|
||||
case 'top':
|
||||
return 'bottom-full mb-2'
|
||||
}
|
||||
})
|
||||
const hasAnyIcon = computed(() => props.items.some((item) => item.icon))
|
||||
const selectedItem = computed(
|
||||
() =>
|
||||
props.items.find((item) => item.value === props.modelValue) as SelectItem
|
||||
)
|
||||
const optionsExpanded = ref(false)
|
||||
const selectedIconFlag = ref(true)
|
||||
|
||||
const handleSelectClick = () => {
|
||||
optionsExpanded.value = !optionsExpanded.value
|
||||
}
|
||||
const handleOptionSelect = (option: SelectItem) => {
|
||||
emit('input', option.value)
|
||||
emit('change', option.value)
|
||||
emit('update:modelValue', option.value)
|
||||
selectedIconFlag.value = false
|
||||
nextTick(() => {
|
||||
selectedIconFlag.value = true
|
||||
})
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
selectRef.value.ownerDocument.addEventListener(
|
||||
'click',
|
||||
(e: { target: any }) => {
|
||||
if (optionsExpanded && !selectRef?.value?.contains(e.target)) {
|
||||
optionsExpanded.value = false
|
||||
}
|
||||
}
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col space-y-1"
|
||||
:class="{
|
||||
'justify-start': justify === 'start',
|
||||
'justify-end': justify === 'end',
|
||||
}"
|
||||
>
|
||||
<p
|
||||
class="block w-fit text-neutral-700 dark:text-neutral-300 text-sm font-bold font-['Nunito']"
|
||||
v-if="label"
|
||||
>
|
||||
{{ label }}
|
||||
</p>
|
||||
<div
|
||||
class="relative"
|
||||
ref="selectWrapperRef"
|
||||
>
|
||||
<button
|
||||
class="relative w-full flex items-center gap-2.5 p-2 pr-6 rounded-md overflow-hidden border transition bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-800 focus:border-neutral-400 dark:focus:border-neutral-700 focus:ring-4 focus:ring-opacity-50 focus:ring-neutral-200 dark:focus:ring-neutral-800 shadow-sm"
|
||||
:class="{
|
||||
'cursor-not-allowed bg-neutral-100 dark:bg-neutral-900 text-neutral-400 dark:text-neutral-600':
|
||||
disabled,
|
||||
}"
|
||||
ref="selectRef"
|
||||
type="button"
|
||||
@click="handleSelectClick"
|
||||
:disabled="disabled"
|
||||
>
|
||||
<span
|
||||
v-if="selectedItem?.icon && !selectedIconFlag && hasAnyIcon"
|
||||
class="inline-block w-5 h-5 pointer-events-none"
|
||||
></span>
|
||||
<Icon
|
||||
v-else-if="selectedItem?.icon && selectedIconFlag && hasAnyIcon"
|
||||
:name="selectedItem?.icon"
|
||||
class="inline-block w-5 h-5 pointer-events-none"
|
||||
/>
|
||||
<Transition
|
||||
name="select-item"
|
||||
mode="out-in"
|
||||
>
|
||||
<span
|
||||
class="leading-snug whitespace-nowrap text-sm"
|
||||
:key="selectedItem?.value"
|
||||
>
|
||||
{{
|
||||
selectedItem?.label || selectedItem?.value || 'Select an option'
|
||||
}}
|
||||
</span>
|
||||
</Transition>
|
||||
<Icon
|
||||
name="tabler:dots-vertical"
|
||||
class="absolute bg-neutral-50 text-gray-500 dark:bg-neutral-700/50 dark:text-neutral-500 inset-y-0 right-0 h-full"
|
||||
/>
|
||||
</button>
|
||||
<div
|
||||
class="absolute right-0 w-full md:w-fit rounded-md border overflow-x-hidden overflow-y-auto transition shadow-lg opacity-0 bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-800 z-50 max-h-64"
|
||||
:class="{
|
||||
'opacity-100 pointer-events-auto': optionsExpanded,
|
||||
'-translate-y-4 pointer-events-none': !optionsExpanded,
|
||||
[optionsAlign]: optionsAlign,
|
||||
}"
|
||||
ref="optionsRef"
|
||||
>
|
||||
<div
|
||||
class="flex items-center gap-2.5 px-2 py-2 cursor-pointer dark:text-neutral-300 font-['Nunito'] transition whitespace-nowrap bg-white dark:bg-neutral-800 hover:bg-neutral-100 dark:hover:bg-neutral-700"
|
||||
v-for="(option, index) in items"
|
||||
:key="index"
|
||||
:class="{
|
||||
'!bg-neutral-200 dark:!bg-neutral-700 hover:!bg-neutral-200 dark:hover:!bg-neutral-700':
|
||||
option.value === selectedItem?.value,
|
||||
'!cursor-not-allowed text-neutral-300 dark:text-neutral-500 hover:bg-white dark:hover:!bg-neutral-800':
|
||||
option.disabled,
|
||||
}"
|
||||
@click="!option.disabled ? handleOptionSelect(option) : void 0"
|
||||
>
|
||||
<div
|
||||
class="inline-block w-5 h-5"
|
||||
v-if="hasAnyIcon && !option.icon"
|
||||
></div>
|
||||
<Icon
|
||||
:name="option?.icon"
|
||||
class="inline-block w-5 h-5"
|
||||
v-if="option.icon"
|
||||
/>
|
||||
<span class="leading-none whitespace-nowrap text-sm font-sans">
|
||||
{{ option.label || 'No label' }}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.select-item-enter-active,
|
||||
.select-item-leave-active {
|
||||
transition: all 0.15s ease;
|
||||
}
|
||||
|
||||
.select-item-enter-from,
|
||||
.select-item-leave-to {
|
||||
opacity: 0.5;
|
||||
filter: blur(2px);
|
||||
}
|
||||
</style>
|
||||
136
app/components/uni/TextArea/index.vue
Normal file
136
app/components/uni/TextArea/index.vue
Normal file
@@ -0,0 +1,136 @@
|
||||
import { textarea } from '@nuxt/ui';
|
||||
<script lang="ts" setup>
|
||||
const emit = defineEmits(['input', 'change', 'update:modelValue'])
|
||||
const props = defineProps({
|
||||
label: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
modelValue: {
|
||||
type: [String, Number],
|
||||
required: true,
|
||||
},
|
||||
placeholder: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: '',
|
||||
},
|
||||
justify: {
|
||||
type: String as PropType<'start' | 'end'>,
|
||||
required: false,
|
||||
default: 'end',
|
||||
},
|
||||
pattern: {
|
||||
type: [String, RegExp],
|
||||
required: false,
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
rows: {
|
||||
type: Number,
|
||||
required: false,
|
||||
default: 5,
|
||||
},
|
||||
minRows: {
|
||||
type: Number,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const textAreaRef = ref()
|
||||
const inputValue = ref(props.modelValue)
|
||||
const isError = ref(false)
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
inputValue.value = value
|
||||
if (props.pattern && value) {
|
||||
const pattern =
|
||||
typeof props.pattern === 'string'
|
||||
? new RegExp(props.pattern)
|
||||
: props.pattern
|
||||
isError.value = !pattern.test(value as string)
|
||||
pattern.lastIndex = 0
|
||||
}
|
||||
},
|
||||
{ immediate: true }
|
||||
)
|
||||
|
||||
const handleInput = (e: any) => {
|
||||
if (props.disabled) return
|
||||
const value = e.target.value
|
||||
|
||||
if (props.pattern && value) {
|
||||
const pattern =
|
||||
typeof props.pattern === 'string'
|
||||
? new RegExp(props.pattern)
|
||||
: props.pattern
|
||||
isError.value = !pattern.test(value)
|
||||
pattern.lastIndex = 0
|
||||
inputValue.value = value
|
||||
if (isError.value) return
|
||||
}
|
||||
|
||||
inputValue.value = value
|
||||
isError.value = false
|
||||
|
||||
emit('update:modelValue', e.target.value)
|
||||
emit('input', e)
|
||||
}
|
||||
|
||||
const autosize = (e: any) => {
|
||||
const el = e?.target ? e.target : e
|
||||
el.style.height = 'auto'
|
||||
el.style.height = el.scrollHeight + 'px'
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.minRows) {
|
||||
const textarea = textAreaRef.value
|
||||
textarea?.addEventListener('keydown', autosize)
|
||||
textarea?.addEventListener('input', autosize)
|
||||
textarea?.addEventListener('focus', autosize)
|
||||
autosize(textarea)
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div
|
||||
class="flex flex-col space-y-1"
|
||||
:class="{
|
||||
'justify-start': justify === 'start',
|
||||
'justify-end': justify === 'end',
|
||||
}"
|
||||
>
|
||||
<p
|
||||
class="block w-fit text-neutral-700 dark:text-neutral-300 text-sm font-bold font-['Nunito']"
|
||||
v-if="label"
|
||||
>
|
||||
{{ label }}
|
||||
</p>
|
||||
<div class="relative">
|
||||
<textarea
|
||||
class="relative w-full flex items-center gap-2.5 p-2 pr-6 rounded-md overflow-hidden overflow-y-auto border transition bg-white dark:bg-neutral-800 border-neutral-200 dark:border-neutral-800 focus:border-neutral-400 dark:focus:border-neutral-700 focus:ring-4 focus:ring-opacity-50 focus:ring-neutral-200 dark:focus:ring-neutral-800 outline-none placeholder-neutral-400 dark:placeholder-neutral-500 shadow-sm"
|
||||
:rows="minRows || rows"
|
||||
ref="textAreaRef"
|
||||
:class="{
|
||||
'!border-red-500': isError,
|
||||
'bg-neutral-100 dark:bg-neutral-900 text-neutral-400 dark:text-neutral-600':
|
||||
disabled,
|
||||
}"
|
||||
:value="inputValue"
|
||||
@input="handleInput"
|
||||
:placeholder="placeholder"
|
||||
:disabled="disabled"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
151
app/components/uni/Toggle/index.vue
Normal file
151
app/components/uni/Toggle/index.vue
Normal file
@@ -0,0 +1,151 @@
|
||||
<script setup lang="ts">
|
||||
import type { PropType } from 'vue'
|
||||
|
||||
const emit = defineEmits(['input', 'change', 'update:modelValue'])
|
||||
|
||||
const props = defineProps({
|
||||
modelValue: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
size: {
|
||||
type: String as PropType<'sm' | 'md' | 'lg'>,
|
||||
required: false,
|
||||
default: 'md',
|
||||
},
|
||||
value: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
},
|
||||
onIcon: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
offIcon: {
|
||||
type: String,
|
||||
required: false,
|
||||
},
|
||||
})
|
||||
|
||||
const checked = ref(false)
|
||||
|
||||
const buttonSizeClass = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'sm':
|
||||
return 'h-6 w-10'
|
||||
case 'md':
|
||||
return 'h-8 w-14'
|
||||
case 'lg':
|
||||
return 'h-10 w-[calc(2.5rem/0.54)]'
|
||||
}
|
||||
})
|
||||
|
||||
const buttonPaddingClass = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'sm':
|
||||
return 'p-1'
|
||||
case 'md':
|
||||
return 'p-1'
|
||||
case 'lg':
|
||||
return 'p-1.5'
|
||||
}
|
||||
})
|
||||
|
||||
const bulletSizeClass = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'sm':
|
||||
return 'h-4'
|
||||
case 'md':
|
||||
return 'h-6'
|
||||
case 'lg':
|
||||
return 'h-7'
|
||||
}
|
||||
})
|
||||
|
||||
const bulletTranslateClass = computed(() => {
|
||||
switch (props.size) {
|
||||
case 'sm':
|
||||
return 'translate-x-4'
|
||||
case 'md':
|
||||
return 'translate-x-6'
|
||||
case 'lg':
|
||||
return 'translate-x-8'
|
||||
}
|
||||
})
|
||||
|
||||
const handleCheck = () => {
|
||||
checked.value = !checked.value
|
||||
emit('update:modelValue', checked.value)
|
||||
emit('change', checked.value)
|
||||
emit('input', checked.value)
|
||||
}
|
||||
|
||||
onMounted(() => {
|
||||
if (props.modelValue) {
|
||||
checked.value = props.modelValue
|
||||
} else if (props.value) {
|
||||
checked.value = props.value
|
||||
}
|
||||
})
|
||||
|
||||
watch(
|
||||
() => props.modelValue,
|
||||
(value) => {
|
||||
checked.value = value
|
||||
}
|
||||
)
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<button
|
||||
class="relative flex items-center rounded-lg bg-neutral-100 dark:bg-neutral-800 shadow-inner transition ease-in-out group outline-none"
|
||||
:class="{
|
||||
'!bg-green-400 dark:!bg-green-400/50': checked,
|
||||
[buttonSizeClass]: buttonSizeClass,
|
||||
[buttonPaddingClass]: buttonPaddingClass,
|
||||
}"
|
||||
@click="handleCheck"
|
||||
>
|
||||
<span
|
||||
class="aspect-[1/1] translate-x-0 transition ease-in-out bg-white dark:bg-black rounded-md shadow duration-300 group-active:scale-90"
|
||||
:class="{
|
||||
'!shadow-lg': checked,
|
||||
'group-active:translate-x-3 group-active:duration-500': !checked,
|
||||
[bulletSizeClass]: bulletSizeClass,
|
||||
[bulletTranslateClass]: checked,
|
||||
}"
|
||||
>
|
||||
<span
|
||||
v-if="onIcon || offIcon"
|
||||
class="absolute inset-0 flex items-center justify-center text-neutral-400"
|
||||
>
|
||||
<Transition
|
||||
name="icon"
|
||||
mode="out-in"
|
||||
>
|
||||
<slot
|
||||
v-if="checked"
|
||||
name="on-icon"
|
||||
/>
|
||||
<slot
|
||||
v-else
|
||||
name="off-icon"
|
||||
/>
|
||||
</Transition>
|
||||
</span>
|
||||
</span>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.icon-enter-active,
|
||||
.icon-leave-active {
|
||||
transition: all 0.1s ease;
|
||||
}
|
||||
|
||||
.icon-enter-from,
|
||||
.icon-leave-to {
|
||||
opacity: 0;
|
||||
transform: scale(0.5);
|
||||
}
|
||||
</style>
|
||||
Reference in New Issue
Block a user