🎨chore: 使用 oxlint, oxfmt&格式化代码

This commit is contained in:
2026-02-08 21:16:25 +08:00
parent 9d35c6a9d8
commit 3a801ba016
78 changed files with 3367 additions and 1468 deletions

View File

@@ -1,2 +1,2 @@
type ButtonType = 'normal' | 'primary' | 'danger'
type ButtonSize = 'base' | 'medium' | 'small'
type ButtonSize = 'base' | 'medium' | 'small'

View File

@@ -1,36 +1,36 @@
<script lang="ts" setup>
import type {PropType} from "vue";
import type { PropType } from 'vue'
const emit = defineEmits(['click'])
const props = defineProps({
type: {
type: String as PropType<ButtonType>,
default: 'normal'
default: 'normal',
},
attrType: {
type: String as PropType<'button' | 'submit' | 'reset'>,
default: 'button'
default: 'button',
},
size: {
type: String as PropType<ButtonSize>,
default: 'base'
default: 'base',
},
block: {
type: Boolean,
default: false
default: false,
},
icon: {
type: String,
default: ''
default: '',
},
loading: {
type: Boolean,
default: false
default: false,
},
disabled: {
type: Boolean,
default: false
}
default: false,
},
})
const buttonTypeClass = computed(() => {
@@ -54,21 +54,36 @@ const handleClick = (e: any) => {
</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">
<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"/>
<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 }">
<div
class="flex items-center whitespace-nowrap leading-snug"
:class="{ 'ml-2': buttonIcon || loading }"
>
<slot />
</div>
</button>
@@ -77,7 +92,7 @@ const handleClick = (e: any) => {
<style scoped>
.icon-enter-active,
.icon-leave-active {
transition: all .3s ease;
transition: all 0.3s ease;
}
.icon-enter-from,

View File

@@ -1,19 +1,19 @@
<script setup lang="ts">
import {useMessage} from "~/composables/uni/useMessage";
import { useMessage } from '~/composables/uni/useMessage'
const props = defineProps({
hideIcon: {
type: Boolean,
default: false
default: false,
},
iconSize: {
type: String,
default: '1em'
default: '1em',
},
text: {
type: String,
required: true
}
required: true,
},
})
const message = useMessage()
@@ -22,48 +22,123 @@ 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(`复制失败`)
})
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"/>
<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">
<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">
<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"/>
<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
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
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)"/>
<rect
width="24"
height="24"
fill="currentColor"
mask="url(#lineMdCheckAll0)"
/>
</svg>
</Transition>
</div>
@@ -79,4 +154,4 @@ const fuck_copy = () => {
.icon-leave-to {
@apply opacity-0;
}
</style>
</style>

View File

@@ -26,9 +26,9 @@ const selectedFiles = ref<File[]>([])
const onIncomeFiles = (files?: FileList | null) => {
if (files && files.length > 0) {
let wantedFiles = Array.from(files).filter(file => {
let wantedFiles = Array.from(files).filter((file) => {
if (props.accept) {
const accept = props.accept.split(',').map(type => type.trim())
const accept = props.accept.split(',').map((type) => type.trim())
return accept.includes(file.type)
}
return true
@@ -46,19 +46,20 @@ const onIncomeFiles = (files?: FileList | null) => {
<template>
<div
:class="{
'bg-neutral-300 dark:bg-neutral-900 border-primary-300 dark:border-primary-800 shadow-inner': dragover,
'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"
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)
}"
@drop.prevent="
($event) => {
dragover = false
if (!$event.dataTransfer?.files) return
onIncomeFiles($event.dataTransfer?.files)
}
"
>
<input
ref="inputRef"
@@ -70,7 +71,7 @@ const onIncomeFiles = (files?: FileList | null) => {
/>
<div
:class="{
'pb-6': selectedFiles.length > 0
'pb-6': selectedFiles.length > 0,
}"
class="w-full h-full flex flex-col justify-center items-center gap-2 transition-all"
>
@@ -87,13 +88,27 @@ const onIncomeFiles = (files?: FileList | null) => {
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"/>
<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(', ')"
: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(', ') }}
{{
selectedFiles
.slice(0, 3)
.map((file) => file.name)
.join(', ')
}}
</p>
</div>
<div>
@@ -102,18 +117,18 @@ const onIncomeFiles = (files?: FileList | null) => {
size="xs"
square
variant="ghost"
@click.stop="() => {
selectedFiles = []
inputRef!.value = ''
}"
@click.stop="
() => {
selectedFiles = []
inputRef!.value = ''
}
"
>
<Icon name="i-tabler-x"/>
<Icon name="i-tabler-x" />
</UButton>
</div>
</div>
</div>
</template>
<style scoped>
</style>
<style scoped></style>

View File

@@ -1,10 +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">
<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>
<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>
</template>

View File

@@ -1,7 +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"
<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>
clipRule="evenodd"
></path>
</svg>
</template>
</template>

View File

@@ -1,10 +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">
<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>
<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>
</template>

View File

@@ -1,10 +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">
<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>
<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>
</template>

View File

@@ -1,11 +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>
<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>
</template>

View File

@@ -1,31 +1,33 @@
<script lang="ts" setup>
import type {PropType} from "vue";
import type { PropType } from 'vue'
const emit = defineEmits(['input', 'change', 'update:modelValue'])
const props = defineProps({
label: {
type: String,
required: false,
default: ''
default: '',
},
modelValue: {
type: [String, Number] as PropType<string | number | undefined>,
required: true
required: true,
},
placeholder: {
type: String,
required: false,
default: ''
default: '',
},
type: {
type: String as PropType<'text' | 'password' | 'number' | 'email' | 'tel' | 'date'>,
type: String as PropType<
'text' | 'password' | 'number' | 'email' | 'tel' | 'date'
>,
required: false,
default: 'text'
default: 'text',
},
justify: {
type: String as PropType<'start' | 'end'>,
required: false,
default: 'end'
default: 'end',
},
pattern: {
type: [String, RegExp],
@@ -34,28 +36,38 @@ const props = defineProps({
disabled: {
type: Boolean,
required: false,
default: 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 })
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
const pattern =
typeof props.pattern === 'string'
? new RegExp(props.pattern)
: props.pattern
isError.value = !pattern.test(value)
pattern.lastIndex = 0
inputValue.value = value
@@ -71,18 +83,33 @@ const handleInput = (e: any) => {
</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">
<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"/>
<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>

View File

@@ -1,24 +1,32 @@
<script lang="ts" setup>
import type {Message, MessageApi, MessageProviderApi, MessageType} from "~/components/uni/Message/index";
import type {
Message,
MessageApi,
MessageProviderApi,
MessageType,
} from '~/components/uni/Message/index'
const props = defineProps({
max: {
type: Number,
default: 5
}
default: 5,
},
})
const nuxtApp = useNuxtApp()
const messageList = ref<Message[]>([])
const createMessage = (content: string, type: MessageType, duration: number = 3000) => {
const {max} = props
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
duration,
})
if (messageList.value.length > max) {
messageList.value.shift()
@@ -27,26 +35,29 @@ const createMessage = (content: string, type: MessageType, duration: number = 30
const providerApi: MessageProviderApi = {
destroy: (id: string) => {
messageList.value.splice(messageList.value.findIndex(message => message.id === id), 1)
}
messageList.value.splice(
messageList.value.findIndex((message) => message.id === id),
1
)
},
}
const api: MessageApi = {
info: (content: string, duration: number = 3000) => {
createMessage(content, 'info', duration);
createMessage(content, 'info', duration)
},
success: (content: string, duration: number = 3000) => {
createMessage(content, 'success', duration);
createMessage(content, 'success', duration)
},
warning: (content: string, duration: number = 3000) => {
createMessage(content, 'warning', duration);
createMessage(content, 'warning', duration)
},
error: (content: string, duration: number = 3000) => {
createMessage(content, 'error', duration);
createMessage(content, 'error', duration)
},
destroyAll: function (): void {
throw new Error('Function not implemented.');
}
throw new Error('Function not implemented.')
},
}
nuxtApp.vueApp.provide('uni-message-provider', providerApi)
@@ -54,12 +65,16 @@ nuxtApp.vueApp.provide('uni-message', api)
</script>
<template>
<slot/>
<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"/>
<UniMessage
v-for="(message, k) in messageList"
:key="message.id"
:message="message"
/>
</TransitionGroup>
</div>
</div>
@@ -73,11 +88,11 @@ nuxtApp.vueApp.provide('uni-message', api)
.message-move,
.message-leave-active {
transition: all .6s ease;
transition: all 0.6s ease;
}
.message-enter-active {
transition: all .6s cubic-bezier(0.075, 0.82, 0.165, 1);
transition: all 0.6s cubic-bezier(0.075, 0.82, 0.165, 1);
}
.message-enter-from {

View File

@@ -17,4 +17,4 @@ export type MessageApi = {
warning: (content: string, duration?: number) => void
error: (content: string, duration?: number) => void
destroyAll: () => void
}
}

View File

@@ -1,14 +1,16 @@
<script lang="ts" setup>
import type {Message, MessageProviderApi} from "~/components/uni/Message/index";
import type {
Message,
MessageProviderApi,
} from '~/components/uni/Message/index'
const providerApi = inject<MessageProviderApi>('uni-message-provider')
const props = defineProps({
message: {
require: true,
type: Object
}
type: Object,
},
})
const message = ref<Message>(props.message as Message)
@@ -16,22 +18,39 @@ const message = ref<Message>(props.message as Message)
onMounted(() => {
setTimeout(() => {
providerApi?.destroy(message.value.id)
}, message.value?.duration || 3000);
}, 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" />
<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>
@@ -41,23 +60,23 @@ onMounted(() => {
<style scoped>
.message {
min-width: 80px;
box-shadow: 0 4px 12px rgba(0, 0, 0, .2);
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, .2);
box-shadow: 0 4px 12px rgba(59, 130, 246, 0.2);
}
.message.success {
box-shadow: 0 4px 12px rgba(16, 185, 129, .2);
box-shadow: 0 4px 12px rgba(16, 185, 129, 0.2);
}
.message.warning {
box-shadow: 0 4px 12px rgba(249, 115, 22, .2);
box-shadow: 0 4px 12px rgba(249, 115, 22, 0.2);
}
.message.error {
box-shadow: 0 4px 12px rgba(244, 63, 94, .2);
box-shadow: 0 4px 12px rgba(244, 63, 94, 0.2);
}
</style>

View File

@@ -3,4 +3,4 @@ type SelectItem = {
value: string
icon?: string
disabled?: boolean
}
}

View File

@@ -1,36 +1,36 @@
<script lang="ts" setup>
import {computed, type PropType} from "vue";
import { computed, type PropType } from 'vue'
const emit = defineEmits(['input', 'change', 'update:modelValue'])
const props = defineProps({
label: {
type: String,
required: false,
default: ''
default: '',
},
modelValue: {
type: [String, Number],
required: false
required: false,
},
items: {
type: Array as PropType<SelectItem[]>,
required: true
required: true,
},
justify: {
type: String as PropType<'start' | 'end'>,
required: false,
default: 'end'
default: 'end',
},
disabled: {
type: Boolean,
required: false,
default: false
default: false,
},
align: {
type: String as PropType<'bottom' | 'top'>,
required: false,
default: 'bottom'
}
default: 'bottom',
},
})
const selectWrapperRef = ref()
@@ -39,14 +39,17 @@ const optionsRef = ref()
const optionsAlign = computed(() => {
switch (props.align) {
case "bottom":
case 'bottom':
return 'top-full mt-2'
case "top":
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 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)
@@ -57,58 +60,113 @@ const handleOptionSelect = (option: SelectItem) => {
emit('input', option.value)
emit('change', option.value)
emit('update:modelValue', option.value)
selectedIconFlag.value = false;
selectedIconFlag.value = false
nextTick(() => {
selectedIconFlag.value = true;
});
selectedIconFlag.value = true
})
}
onMounted(() => {
selectRef.value.ownerDocument.addEventListener('click', (e: { target: any; }) => {
if (optionsExpanded && !selectRef?.value?.contains(e.target)) {
optionsExpanded.value = false
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">
<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">{{
<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>
}}
</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" />
<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
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>
@@ -118,12 +176,12 @@ onMounted(() => {
<style scoped>
.select-item-enter-active,
.select-item-leave-active {
transition: all .15s ease;
transition: all 0.15s ease;
}
.select-item-enter-from,
.select-item-leave-to {
opacity: .5;
opacity: 0.5;
filter: blur(2px);
}
</style>

View File

@@ -1,4 +1,3 @@
import { textarea } from '@nuxt/ui';
<script lang="ts" setup>
const emit = defineEmits(['input', 'change', 'update:modelValue'])
@@ -6,21 +5,21 @@ const props = defineProps({
label: {
type: String,
required: false,
default: ''
default: '',
},
modelValue: {
type: [String, Number],
required: true
required: true,
},
placeholder: {
type: String,
required: false,
default: ''
default: '',
},
justify: {
type: String as PropType<'start' | 'end'>,
required: false,
default: 'end'
default: 'end',
},
pattern: {
type: [String, RegExp],
@@ -29,12 +28,12 @@ const props = defineProps({
disabled: {
type: Boolean,
required: false,
default: false
default: false,
},
rows: {
type: Number,
required: false,
default: 5
default: 5,
},
minRows: {
type: Number,
@@ -46,21 +45,31 @@ 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 })
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
const pattern =
typeof props.pattern === 'string'
? new RegExp(props.pattern)
: props.pattern
isError.value = !pattern.test(value)
pattern.lastIndex = 0
inputValue.value = value
@@ -92,19 +101,34 @@ onMounted(() => {
</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">
<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" />
<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>

View File

@@ -1,21 +1,21 @@
<script setup lang="ts">
import type {PropType} from "vue";
import type { PropType } from 'vue'
const emit = defineEmits(['input', 'change', 'update:modelValue'])
const props = defineProps({
modelValue: {
type: Boolean,
required: false
required: false,
},
size: {
type: String as PropType<'sm' | 'md' | 'lg'>,
required: false,
default: 'md'
default: 'md',
},
value: {
type: Boolean,
required: false
required: false,
},
onIcon: {
type: String,
@@ -24,7 +24,7 @@ const props = defineProps({
offIcon: {
type: String,
required: false,
}
},
})
const checked = ref(false)
@@ -88,31 +88,49 @@ onMounted(() => {
}
})
watch(() => props.modelValue, (value) => {
checked.value = 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="{
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">
[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"/>
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>
@@ -130,4 +148,4 @@ watch(() => props.modelValue, (value) => {
opacity: 0;
transform: scale(0.5);
}
</style>
</style>