refactor(frontend): 优化 UI 基础组件

- 更新 avatar-image, badge, checkbox, input, switch 等组件
- 优化 dialog, pagination, select-item, tabs 等组件
- 调整 table-card, refresh-button 组件
This commit is contained in:
fawney19
2025-12-12 16:15:07 +08:00
parent e902595d58
commit 44e7117d4a
12 changed files with 74 additions and 25 deletions

View File

@@ -19,5 +19,9 @@ const imageClass = computed(() =>
</script> </script>
<template> <template>
<AvatarImagePrimitive :class="imageClass" :src="src || ''" :alt="alt" /> <AvatarImagePrimitive
:class="imageClass"
:src="src || ''"
:alt="alt"
/>
</template> </template>

View File

@@ -3,6 +3,10 @@ import { cva } from 'class-variance-authority'
import { cn } from '@/lib/utils' import { cn } from '@/lib/utils'
import { computed } from 'vue' import { computed } from 'vue'
const props = withDefaults(defineProps<Props>(), {
variant: 'default',
})
const badgeVariants = cva( const badgeVariants = cva(
'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2', 'inline-flex items-center rounded-full border px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2',
{ {
@@ -34,10 +38,6 @@ interface Props {
class?: string class?: string
} }
const props = withDefaults(defineProps<Props>(), {
variant: 'default',
})
const badgeClass = computed(() => const badgeClass = computed(() =>
cn(badgeVariants({ variant: props.variant }), props.class) cn(badgeVariants({ variant: props.variant }), props.class)
) )

View File

@@ -5,7 +5,7 @@
:checked="isChecked" :checked="isChecked"
v-bind="$attrs" v-bind="$attrs"
@change="handleChange" @change="handleChange"
/> >
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -1,6 +1,10 @@
<template> <template>
<Teleport to="body"> <Teleport to="body">
<div v-if="isOpen" class="fixed inset-0 overflow-y-auto" :style="{ zIndex: containerZIndex }"> <div
v-if="isOpen"
class="fixed inset-0 overflow-y-auto"
:style="{ zIndex: containerZIndex }"
>
<!-- 背景遮罩 --> <!-- 背景遮罩 -->
<Transition <Transition
enter-active-class="duration-200 ease-out" enter-active-class="duration-200 ease-out"
@@ -30,21 +34,38 @@
> >
<div <div
v-if="isOpen" v-if="isOpen"
@click.stop
class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border" class="relative transform rounded-lg bg-background text-left shadow-2xl transition-all sm:my-8 sm:w-full border border-border"
:style="{ zIndex: contentZIndex }" :style="{ zIndex: contentZIndex }"
:class="maxWidthClass" :class="maxWidthClass"
@click.stop
> >
<!-- Header 区域优先使用 slot否则使用 title prop --> <!-- Header 区域优先使用 slot否则使用 title prop -->
<slot name="header"> <slot name="header">
<div v-if="title" class="border-b border-border px-6 py-4"> <div
v-if="title"
class="border-b border-border px-6 py-4"
>
<div class="flex items-center gap-3"> <div class="flex items-center gap-3">
<div v-if="icon" class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0" :class="iconClass"> <div
<component :is="icon" class="h-5 w-5 text-primary" /> v-if="icon"
class="flex h-9 w-9 items-center justify-center rounded-lg bg-primary/10 flex-shrink-0"
:class="iconClass"
>
<component
:is="icon"
class="h-5 w-5 text-primary"
/>
</div> </div>
<div class="flex-1 min-w-0"> <div class="flex-1 min-w-0">
<h3 class="text-lg font-semibold text-foreground leading-tight">{{ title }}</h3> <h3 class="text-lg font-semibold text-foreground leading-tight">
<p v-if="description" class="text-xs text-muted-foreground">{{ description }}</p> {{ title }}
</h3>
<p
v-if="description"
class="text-xs text-muted-foreground"
>
{{ description }}
</p>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -5,7 +5,7 @@
:autocomplete="autocompleteAttr" :autocomplete="autocompleteAttr"
v-bind="$attrs" v-bind="$attrs"
@input="handleInput" @input="handleInput"
/> >
</template> </template>
<script setup lang="ts"> <script setup lang="ts">

View File

@@ -48,7 +48,10 @@
</Button> </Button>
<!-- 页码按钮智能省略 --> <!-- 页码按钮智能省略 -->
<template v-for="page in pageNumbers" :key="page"> <template
v-for="page in pageNumbers"
:key="page"
>
<Button <Button
v-if="typeof page === 'number'" v-if="typeof page === 'number'"
:variant="page === current ? 'default' : 'outline'" :variant="page === current ? 'default' : 'outline'"
@@ -59,7 +62,10 @@
> >
{{ page }} {{ page }}
</Button> </Button>
<span v-else class="px-2 text-muted-foreground select-none">{{ page }}</span> <span
v-else
class="px-2 text-muted-foreground select-none"
>{{ page }}</span>
</template> </template>
<Button <Button

View File

@@ -7,7 +7,10 @@
:title="title" :title="title"
@click="handleClick" @click="handleClick"
> >
<RefreshCcw class="w-3.5 h-3.5" :class="loading ? 'animate-spin' : ''" /> <RefreshCcw
class="w-3.5 h-3.5"
:class="loading ? 'animate-spin' : ''"
/>
</Button> </Button>
</template> </template>

View File

@@ -21,7 +21,11 @@ const itemClass = computed(() =>
</script> </script>
<template> <template>
<SelectItemPrimitive :class="itemClass" :value="value" :disabled="disabled"> <SelectItemPrimitive
:class="itemClass"
:value="value"
:disabled="disabled"
>
<span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center"> <span class="absolute left-2 flex h-3.5 w-3.5 items-center justify-center">
<SelectItemIndicator> <SelectItemIndicator>
<Check class="h-4 w-4" /> <Check class="h-4 w-4" />

View File

@@ -3,15 +3,15 @@
type="button" type="button"
role="switch" role="switch"
:aria-checked="modelValue" :aria-checked="modelValue"
class="relative inline-flex h-6 w-11 items-center rounded-full transition-colors"
:class="[ :class="[
'relative inline-flex h-6 w-11 items-center rounded-full transition-colors',
modelValue ? 'bg-primary' : 'bg-muted' modelValue ? 'bg-primary' : 'bg-muted'
]" ]"
@click="$emit('update:modelValue', !modelValue)" @click="$emit('update:modelValue', !modelValue)"
> >
<span <span
class="inline-block h-4 w-4 transform rounded-full bg-white transition-transform"
:class="[ :class="[
'inline-block h-4 w-4 transform rounded-full bg-white transition-transform',
modelValue ? 'translate-x-6' : 'translate-x-1' modelValue ? 'translate-x-6' : 'translate-x-1'
]" ]"
/> />

View File

@@ -1,14 +1,22 @@
<template> <template>
<Card class="overflow-hidden"> <Card class="overflow-hidden">
<!-- 标题和操作栏 --> <!-- 标题和操作栏 -->
<div v-if="$slots.header || title" class="px-6 py-3.5 border-b border-border/60"> <div
v-if="$slots.header || title"
class="px-6 py-3.5 border-b border-border/60"
>
<slot name="header"> <slot name="header">
<div class="flex items-center justify-between gap-4"> <div class="flex items-center justify-between gap-4">
<!-- 左侧标题 --> <!-- 左侧标题 -->
<h3 class="text-base font-semibold">{{ title }}</h3> <h3 class="text-base font-semibold">
{{ title }}
</h3>
<!-- 右侧操作区 --> <!-- 右侧操作区 -->
<div v-if="$slots.actions" class="flex items-center gap-2"> <div
v-if="$slots.actions"
class="flex items-center gap-2"
>
<slot name="actions" /> <slot name="actions" />
</div> </div>
</div> </div>

View File

@@ -1,5 +1,8 @@
<template> <template>
<div :class="listClass" ref="listRef"> <div
ref="listRef"
:class="listClass"
>
<!-- 滑动指示器 - 放在按钮前面 --> <!-- 滑动指示器 - 放在按钮前面 -->
<div <div
class="tabs-indicator" class="tabs-indicator"

View File

@@ -3,8 +3,8 @@
:class="triggerClass" :class="triggerClass"
:data-state="isActive ? 'active' : 'inactive'" :data-state="isActive ? 'active' : 'inactive'"
:data-value="props.value" :data-value="props.value"
@click="handleClick"
type="button" type="button"
@click="handleClick"
> >
<slot /> <slot />
</button> </button>