feat: code refactor
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
<template>
|
||||
<button
|
||||
:type="type"
|
||||
:disabled="disabled || loading"
|
||||
@click="$emit('click', $event)"
|
||||
class="inline-flex items-center justify-center px-4 py-2 rounded-md font-medium transition-all duration-200 focus:outline-none focus:ring-2 focus:ring-offset-2 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
:class="[
|
||||
sizeClasses,
|
||||
variantClasses,
|
||||
loading ? 'cursor-wait' : '',
|
||||
disabled ? 'pointer-events-none' : ''
|
||||
]"
|
||||
>
|
||||
<slot name="icon" v-if="$slots.icon && !loading"></slot>
|
||||
<div v-if="loading" class="animate-spin rounded-full h-4 w-4 border-2 border-current border-t-transparent mr-2"></div>
|
||||
<slot></slot>
|
||||
</button>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject } from 'vue'
|
||||
|
||||
interface Props {
|
||||
variant?: 'primary' | 'secondary' | 'danger' | 'success' | 'outline'
|
||||
size?: 'sm' | 'md' | 'lg'
|
||||
type?: 'button' | 'submit' | 'reset'
|
||||
disabled?: boolean
|
||||
loading?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
variant: 'primary',
|
||||
size: 'md',
|
||||
type: 'button',
|
||||
disabled: false,
|
||||
loading: false
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
click: [event: MouseEvent]
|
||||
}>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
const sizeClasses = computed(() => {
|
||||
const sizes = {
|
||||
sm: 'px-3 py-1.5 text-sm',
|
||||
md: 'px-4 py-2 text-sm',
|
||||
lg: 'px-6 py-3 text-base'
|
||||
}
|
||||
return sizes[props.size]
|
||||
})
|
||||
|
||||
const variantClasses = computed(() => {
|
||||
const baseClasses = 'focus:ring-2 focus:ring-offset-2'
|
||||
|
||||
if (props.variant === 'primary') {
|
||||
return `${baseClasses} bg-indigo-600 text-white hover:bg-indigo-700 focus:ring-indigo-500`
|
||||
}
|
||||
|
||||
if (props.variant === 'secondary') {
|
||||
return isDarkMode
|
||||
? `${baseClasses} bg-gray-700 text-gray-300 hover:bg-gray-600 focus:ring-gray-500 border border-gray-600`
|
||||
: `${baseClasses} bg-gray-100 text-gray-700 hover:bg-gray-200 focus:ring-gray-500 border border-gray-300`
|
||||
}
|
||||
|
||||
if (props.variant === 'danger') {
|
||||
return `${baseClasses} bg-red-600 text-white hover:bg-red-700 focus:ring-red-500`
|
||||
}
|
||||
|
||||
if (props.variant === 'success') {
|
||||
return `${baseClasses} bg-green-600 text-white hover:bg-green-700 focus:ring-green-500`
|
||||
}
|
||||
|
||||
if (props.variant === 'outline') {
|
||||
return isDarkMode
|
||||
? `${baseClasses} border border-gray-600 text-gray-300 hover:bg-gray-700 focus:ring-gray-500`
|
||||
: `${baseClasses} border border-gray-300 text-gray-700 hover:bg-gray-50 focus:ring-gray-500`
|
||||
}
|
||||
|
||||
return ''
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,130 @@
|
||||
<template>
|
||||
<Teleport to="body">
|
||||
<Transition name="modal" appear>
|
||||
<div
|
||||
v-if="show"
|
||||
class="fixed inset-0 z-50 overflow-y-auto"
|
||||
@click="handleBackdropClick"
|
||||
>
|
||||
<!-- 背景遮罩 -->
|
||||
<div class="fixed inset-0 bg-black bg-opacity-50 transition-opacity"></div>
|
||||
|
||||
<!-- 模态框容器 -->
|
||||
<div class="flex min-h-full items-center justify-center p-4">
|
||||
<div
|
||||
ref="modalRef"
|
||||
class="relative transform overflow-hidden rounded-lg shadow-xl transition-all"
|
||||
:class="[
|
||||
sizeClasses,
|
||||
isDarkMode ? 'bg-gray-800' : 'bg-white'
|
||||
]"
|
||||
@click.stop
|
||||
>
|
||||
<!-- 头部 -->
|
||||
<div
|
||||
v-if="$slots.header || title"
|
||||
class="flex items-center justify-between px-6 py-4 border-b"
|
||||
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']"
|
||||
>
|
||||
<slot name="header">
|
||||
<h3 class="text-lg font-medium" :class="[isDarkMode ? 'text-white' : 'text-gray-900']">
|
||||
{{ title }}
|
||||
</h3>
|
||||
</slot>
|
||||
<button
|
||||
v-if="closable"
|
||||
@click="$emit('close')"
|
||||
class="rounded-md p-2 transition-colors"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'text-gray-400 hover:text-gray-300 hover:bg-gray-700'
|
||||
: 'text-gray-400 hover:text-gray-500 hover:bg-gray-100'
|
||||
]"
|
||||
>
|
||||
<X class="h-5 w-5" />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<!-- 内容 -->
|
||||
<div class="px-6 py-4">
|
||||
<slot></slot>
|
||||
</div>
|
||||
|
||||
<!-- 底部 -->
|
||||
<div
|
||||
v-if="$slots.footer"
|
||||
class="flex items-center justify-end space-x-3 px-6 py-4 border-t"
|
||||
:class="[isDarkMode ? 'border-gray-700 bg-gray-900/50' : 'border-gray-200 bg-gray-50']"
|
||||
>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Transition>
|
||||
</Teleport>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { computed, inject, ref } from 'vue'
|
||||
import { X } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
show: boolean
|
||||
title?: string
|
||||
size?: 'sm' | 'md' | 'lg' | 'xl'
|
||||
closable?: boolean
|
||||
closeOnBackdrop?: boolean
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
size: 'md',
|
||||
closable: true,
|
||||
closeOnBackdrop: true
|
||||
})
|
||||
|
||||
const emit = defineEmits<{
|
||||
close: []
|
||||
}>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const modalRef = ref<HTMLElement>()
|
||||
|
||||
const sizeClasses = computed(() => {
|
||||
const sizes = {
|
||||
sm: 'max-w-md w-full',
|
||||
md: 'max-w-lg w-full',
|
||||
lg: 'max-w-2xl w-full',
|
||||
xl: 'max-w-4xl w-full'
|
||||
}
|
||||
return sizes[props.size]
|
||||
})
|
||||
|
||||
const handleBackdropClick = (event: MouseEvent) => {
|
||||
if (props.closeOnBackdrop && event.target === event.currentTarget) {
|
||||
emit('close')
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.modal-enter-active,
|
||||
.modal-leave-active {
|
||||
transition: opacity 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-active .relative,
|
||||
.modal-leave-active .relative {
|
||||
transition: all 0.3s ease;
|
||||
}
|
||||
|
||||
.modal-enter-from,
|
||||
.modal-leave-to {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.modal-enter-from .relative,
|
||||
.modal-leave-to .relative {
|
||||
transform: scale(0.95) translateY(-20px);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,115 @@
|
||||
<template>
|
||||
<div class="mt-4 flex items-center justify-between px-6 py-4 border-t"
|
||||
:class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']">
|
||||
<div class="flex items-center text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
||||
显示第 {{ (currentPage - 1) * pageSize + 1 }} 到
|
||||
{{ Math.min(currentPage * pageSize, total) }} 条,共 {{ total }} 条
|
||||
</div>
|
||||
|
||||
<div class="flex items-center space-x-2">
|
||||
<button @click="$emit('page-change', currentPage - 1)" :disabled="currentPage === 1"
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-md transition-colors duration-200" :class="[
|
||||
isDarkMode
|
||||
? currentPage === 1
|
||||
? 'bg-gray-800 text-gray-600 cursor-not-allowed'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
|
||||
: currentPage === 1
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
]">
|
||||
<ChevronLeftIcon class="w-4 h-4" />
|
||||
上一页
|
||||
</button>
|
||||
|
||||
<div class="flex items-center space-x-1">
|
||||
<template v-for="pageNum in displayedPages" :key="pageNum">
|
||||
<button v-if="pageNum !== '...'" @click="$emit('page-change', pageNum as number)"
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-md transition-colors duration-200" :class="[
|
||||
currentPage === pageNum
|
||||
? 'bg-indigo-600 text-white'
|
||||
: isDarkMode
|
||||
? 'bg-gray-800 text-gray-300 hover:bg-gray-700'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
]">
|
||||
{{ pageNum }}
|
||||
</button>
|
||||
<span v-else class="px-2" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
||||
...
|
||||
</span>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<button @click="$emit('page-change', currentPage + 1)" :disabled="currentPage >= totalPages"
|
||||
class="inline-flex items-center px-3 py-1.5 rounded-md transition-colors duration-200" :class="[
|
||||
isDarkMode
|
||||
? currentPage >= totalPages
|
||||
? 'bg-gray-800 text-gray-600 cursor-not-allowed'
|
||||
: 'bg-gray-800 text-gray-300 hover:bg-gray-700'
|
||||
: currentPage >= totalPages
|
||||
? 'bg-gray-100 text-gray-400 cursor-not-allowed'
|
||||
: 'bg-gray-100 text-gray-700 hover:bg-gray-200'
|
||||
]">
|
||||
下一页
|
||||
<ChevronRightIcon class="w-4 h-4" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, computed } from 'vue'
|
||||
import { ChevronLeftIcon, ChevronRightIcon } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
currentPage: number
|
||||
pageSize: number
|
||||
total: number
|
||||
}
|
||||
|
||||
const props = defineProps<Props>()
|
||||
|
||||
defineEmits<{
|
||||
'page-change': [page: number]
|
||||
}>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
// 计算总页数
|
||||
const totalPages = computed(() => Math.ceil(props.total / props.pageSize))
|
||||
|
||||
// 计算要显示的页码
|
||||
const displayedPages = computed(() => {
|
||||
const current = props.currentPage
|
||||
const total = totalPages.value
|
||||
const delta = 2 // 当前页码前后显示的页码数
|
||||
|
||||
const pages: (number | string)[] = []
|
||||
|
||||
// 始终显示第一页
|
||||
pages.push(1)
|
||||
|
||||
// 计算显示范围
|
||||
const left = Math.max(2, current - delta)
|
||||
const right = Math.min(total - 1, current + delta)
|
||||
|
||||
// 添加省略号和页码
|
||||
if (left > 2) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
for (let i = left; i <= right; i++) {
|
||||
pages.push(i)
|
||||
}
|
||||
|
||||
if (right < total - 1) {
|
||||
pages.push('...')
|
||||
}
|
||||
|
||||
// 始终显示最后一页
|
||||
if (total > 1) {
|
||||
pages.push(total)
|
||||
}
|
||||
|
||||
return pages
|
||||
})
|
||||
</script>
|
||||
@@ -0,0 +1,44 @@
|
||||
<template>
|
||||
<div class="rounded-lg shadow-sm overflow-hidden transition-all duration-300"
|
||||
:class="[isDarkMode ? 'bg-gray-800 bg-opacity-70' : 'bg-white']">
|
||||
<div class="px-6 py-4 border-b" :class="[isDarkMode ? 'border-gray-700' : 'border-gray-200']">
|
||||
<h3 class="text-lg font-medium" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
{{ title }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="overflow-x-auto">
|
||||
<table class="min-w-full divide-y" :class="[isDarkMode ? 'divide-gray-700' : 'divide-gray-200']">
|
||||
<thead :class="[isDarkMode ? 'bg-gray-900/50' : 'bg-gray-50']">
|
||||
<tr>
|
||||
<th v-for="header in headers" :key="header"
|
||||
class="px-6 py-3.5 text-left text-xs font-medium uppercase tracking-wider"
|
||||
:class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">
|
||||
{{ header }}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody :class="[
|
||||
isDarkMode
|
||||
? 'bg-gray-800/50 divide-y divide-gray-700'
|
||||
: 'bg-white divide-y divide-gray-200'
|
||||
]">
|
||||
<slot name="body"></slot>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<slot name="footer"></slot>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
headers: string[]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
@@ -0,0 +1,60 @@
|
||||
<template>
|
||||
<div class="space-y-2">
|
||||
<label v-if="label" class="block text-sm font-medium" :class="[isDarkMode ? 'text-gray-300' : 'text-gray-700']">
|
||||
{{ label }}
|
||||
<span v-if="required" class="text-red-500 ml-1">*</span>
|
||||
</label>
|
||||
<div class="relative">
|
||||
<input
|
||||
:type="type"
|
||||
:value="modelValue"
|
||||
@input="$emit('update:modelValue', ($event.target as HTMLInputElement).value)"
|
||||
:placeholder="placeholder"
|
||||
:required="required"
|
||||
:disabled="disabled"
|
||||
:minlength="minlength"
|
||||
:maxlength="maxlength"
|
||||
class="w-full rounded-md shadow-sm px-4 py-2.5 transition-all duration-200 ease-in-out border focus:ring-2 focus:ring-indigo-500 focus:border-indigo-500 outline-none"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'bg-gray-700 border-gray-600 text-white placeholder-gray-400 hover:border-gray-500'
|
||||
: 'border-gray-300 hover:border-gray-400 placeholder-gray-500',
|
||||
disabled ? 'opacity-50 cursor-not-allowed' : '',
|
||||
error ? 'border-red-500 focus:border-red-500 focus:ring-red-500' : ''
|
||||
]"
|
||||
/>
|
||||
<slot name="suffix"></slot>
|
||||
</div>
|
||||
<p v-if="error" class="text-sm text-red-500">{{ error }}</p>
|
||||
<p v-if="hint" class="text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-500']">{{ hint }}</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
|
||||
interface Props {
|
||||
modelValue: string
|
||||
label?: string
|
||||
type?: string
|
||||
placeholder?: string
|
||||
required?: boolean
|
||||
disabled?: boolean
|
||||
error?: string
|
||||
hint?: string
|
||||
minlength?: number
|
||||
maxlength?: number
|
||||
}
|
||||
|
||||
withDefaults(defineProps<Props>(), {
|
||||
type: 'text',
|
||||
required: false,
|
||||
disabled: false
|
||||
})
|
||||
|
||||
defineEmits<{
|
||||
'update:modelValue': [value: string]
|
||||
}>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
@@ -0,0 +1,53 @@
|
||||
<template>
|
||||
<div>
|
||||
<div class="mb-6 text-center" v-if="linkText && linkTo">
|
||||
<router-link
|
||||
:to="linkTo"
|
||||
class="text-indigo-400 hover:text-indigo-300 transition duration-300"
|
||||
>
|
||||
{{ linkText }}
|
||||
</router-link>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="px-8 py-4 bg-opacity-50 flex justify-between items-center"
|
||||
:class="[isDarkMode ? 'bg-gray-800' : 'bg-gray-100']"
|
||||
>
|
||||
<span
|
||||
class="text-sm flex items-center"
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
>
|
||||
<ShieldCheckIcon class="w-4 h-4 mr-1 text-green-400" />
|
||||
安全加密
|
||||
</span>
|
||||
<button
|
||||
@click="$emit('toggle-drawer')"
|
||||
class="text-sm hover:text-indigo-300 transition duration-300 flex items-center"
|
||||
:class="[isDarkMode ? 'text-indigo-400' : 'text-indigo-600']"
|
||||
>
|
||||
{{ drawerText }}
|
||||
<ClipboardListIcon class="w-4 h-4 ml-1" />
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { ShieldCheckIcon, ClipboardListIcon } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
linkText?: string
|
||||
linkTo?: string
|
||||
drawerText: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
'toggle-drawer': []
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
@@ -0,0 +1,57 @@
|
||||
<template>
|
||||
<div class="text-center">
|
||||
<div class="flex justify-center mb-8">
|
||||
<div
|
||||
class="rounded-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 p-1 animate-spin-slow"
|
||||
>
|
||||
<div class="rounded-full bg-gray-900 p-2">
|
||||
<BoxIcon class="w-8 h-8 text-white" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<h2
|
||||
@click="$emit('title-click')"
|
||||
class="text-3xl cursor-pointer font-extrabold text-center mb-6"
|
||||
:class="[
|
||||
isDarkMode
|
||||
? 'text-transparent bg-clip-text bg-gradient-to-r from-indigo-300 via-purple-300 to-pink-300'
|
||||
: 'text-indigo-600'
|
||||
]"
|
||||
>
|
||||
{{ title }}
|
||||
</h2>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject } from 'vue'
|
||||
import { BoxIcon } from 'lucide-vue-next'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
'title-click': []
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
defineEmits<Emits>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@keyframes spin-slow {
|
||||
from {
|
||||
transform: rotate(0deg);
|
||||
}
|
||||
to {
|
||||
transform: rotate(360deg);
|
||||
}
|
||||
}
|
||||
|
||||
.animate-spin-slow {
|
||||
animation: spin-slow 3s linear infinite;
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,101 @@
|
||||
<template>
|
||||
<form @submit.prevent="$emit('submit')">
|
||||
<div class="mb-6 relative">
|
||||
<label
|
||||
for="code"
|
||||
class="block text-sm font-medium mb-2"
|
||||
:class="[isDarkMode ? 'text-gray-300' : 'text-gray-800']"
|
||||
>取件码</label
|
||||
>
|
||||
<div class="relative">
|
||||
<input
|
||||
id="code"
|
||||
v-model="code"
|
||||
type="text"
|
||||
ref="codeInput"
|
||||
class="w-full px-4 py-3 rounded-lg placeholder-gray-400 focus:outline-none focus:ring-2 focus:ring-indigo-500 transition duration-300 pr-10"
|
||||
:class="[
|
||||
isDarkMode ? 'bg-gray-700 bg-opacity-50' : 'bg-gray-100',
|
||||
{ 'ring-2 ring-red-500': error },
|
||||
isDarkMode ? 'text-gray-300' : 'text-gray-800'
|
||||
]"
|
||||
placeholder="请输入5位取件码"
|
||||
required
|
||||
:readonly="inputStatus.readonly"
|
||||
maxlength="5"
|
||||
@focus="isInputFocused = true"
|
||||
@blur="isInputFocused = false"
|
||||
/>
|
||||
<div
|
||||
v-if="inputStatus.loading"
|
||||
class="absolute inset-y-0 right-0 flex items-center pr-3"
|
||||
>
|
||||
<span
|
||||
class="animate-spin rounded-full h-5 w-5 border-b-2 border-indigo-500"
|
||||
></span>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="absolute -bottom-0.5 left-2 h-0.5 bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 transition-all duration-300 ease-in-out"
|
||||
:class="{ 'w-97-100': isInputFocused, 'w-0': !isInputFocused }"
|
||||
></div>
|
||||
</div>
|
||||
<button
|
||||
type="submit"
|
||||
class="w-full bg-gradient-to-r from-indigo-500 via-purple-500 to-pink-500 text-white font-bold py-3 px-4 rounded-lg hover:from-indigo-600 hover:via-purple-600 hover:to-pink-600 focus:outline-none focus:ring-2 focus:ring-purple-500 focus:ring-opacity-50 transition duration-300 transform hover:scale-105 hover:shadow-lg relative overflow-hidden group"
|
||||
:disabled="inputStatus.loading"
|
||||
>
|
||||
<span class="flex items-center justify-center relative z-10">
|
||||
<span>{{ inputStatus.loading ? '处理中...' : '提取文件' }}</span>
|
||||
<ArrowRightIcon
|
||||
class="w-5 h-5 ml-2 transition-transform duration-300 transform group-hover:translate-x-1"
|
||||
/>
|
||||
</span>
|
||||
<span
|
||||
class="absolute top-0 left-0 w-full h-full bg-gradient-to-r from-pink-500 via-purple-500 to-indigo-500 opacity-0 group-hover:opacity-100 transition-opacity duration-300"
|
||||
></span>
|
||||
</button>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref, inject, watch } from 'vue'
|
||||
import { ArrowRightIcon } from 'lucide-vue-next'
|
||||
|
||||
interface InputStatus {
|
||||
readonly: boolean
|
||||
loading: boolean
|
||||
}
|
||||
|
||||
interface Props {
|
||||
inputStatus: InputStatus
|
||||
error?: boolean
|
||||
}
|
||||
|
||||
interface Emits {
|
||||
submit: []
|
||||
'update:code': [value: string]
|
||||
}
|
||||
|
||||
defineProps<Props>()
|
||||
const emit = defineEmits<Emits>()
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
const code = ref('')
|
||||
const isInputFocused = ref(false)
|
||||
const codeInput = ref<HTMLInputElement>()
|
||||
|
||||
watch(code, (newValue) => {
|
||||
emit('update:code', newValue)
|
||||
})
|
||||
|
||||
defineExpose({
|
||||
focus: () => codeInput.value?.focus()
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.w-97-100 {
|
||||
width: calc(100% - 1rem);
|
||||
}
|
||||
</style>
|
||||
@@ -0,0 +1,69 @@
|
||||
<template>
|
||||
<div class="p-6 rounded-lg shadow-md transition-colors duration-300"
|
||||
:class="[isDarkMode ? 'bg-gray-800 bg-opacity-70' : 'bg-white']">
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm" :class="[isDarkMode ? 'text-gray-400' : 'text-gray-600']">
|
||||
{{ title }}
|
||||
</p>
|
||||
<h3 class="text-2xl font-bold mt-1" :class="[isDarkMode ? 'text-white' : 'text-gray-800']">
|
||||
{{ value }}
|
||||
</h3>
|
||||
</div>
|
||||
<div class="p-3 rounded-full" :class="iconBgClass">
|
||||
<component :is="icon" class="w-6 h-6" :class="iconClass" />
|
||||
</div>
|
||||
</div>
|
||||
<p class="text-sm mt-2" :class="descriptionClass">
|
||||
<slot name="description"></slot>
|
||||
</p>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { inject, computed } from 'vue'
|
||||
import type { Component } from 'vue'
|
||||
|
||||
interface Props {
|
||||
title: string
|
||||
value: string | number
|
||||
icon: Component
|
||||
iconColor: 'indigo' | 'purple' | 'green' | 'blue'
|
||||
descriptionType?: 'success' | 'error' | 'neutral'
|
||||
}
|
||||
|
||||
const props = withDefaults(defineProps<Props>(), {
|
||||
descriptionType: 'neutral'
|
||||
})
|
||||
|
||||
const isDarkMode = inject('isDarkMode')
|
||||
|
||||
const iconBgClass = computed(() => {
|
||||
const colorMap = {
|
||||
indigo: isDarkMode ? 'bg-indigo-900' : 'bg-indigo-100',
|
||||
purple: isDarkMode ? 'bg-purple-900' : 'bg-purple-100',
|
||||
green: isDarkMode ? 'bg-green-900' : 'bg-green-100',
|
||||
blue: isDarkMode ? 'bg-blue-900' : 'bg-blue-100'
|
||||
}
|
||||
return colorMap[props.iconColor]
|
||||
})
|
||||
|
||||
const iconClass = computed(() => {
|
||||
const colorMap = {
|
||||
indigo: isDarkMode ? 'text-indigo-400' : 'text-indigo-600',
|
||||
purple: isDarkMode ? 'text-purple-400' : 'text-purple-600',
|
||||
green: isDarkMode ? 'text-green-400' : 'text-green-600',
|
||||
blue: isDarkMode ? 'text-blue-400' : 'text-blue-600'
|
||||
}
|
||||
return colorMap[props.iconColor]
|
||||
})
|
||||
|
||||
const descriptionClass = computed(() => {
|
||||
const typeMap = {
|
||||
success: isDarkMode ? 'text-green-400' : 'text-green-600',
|
||||
error: isDarkMode ? 'text-red-400' : 'text-red-600',
|
||||
neutral: isDarkMode ? 'text-gray-400' : 'text-gray-600'
|
||||
}
|
||||
return typeMap[props.descriptionType]
|
||||
})
|
||||
</script>
|
||||
Reference in New Issue
Block a user