This commit is contained in:
2026-03-17 00:46:51 +08:00
parent 4a5fdd3961
commit f0ecab4350
20 changed files with 283 additions and 287 deletions

View File

@@ -15,11 +15,11 @@ const shouldShowUser = computed(() => {
<template>
<header
class="fixed top-0 left-0 right-0 z-[100]
class="fixed top-0 left-0 right-0
flex items-center px-[30px]
bg-sidebar border-b border-sidebar-border
text-sidebar-foreground"
:style="{ height: 'var(--header-height)' }"
bg-white dark:bg-slate-900 border-b border-slate-200 dark:border-slate-800 shadow-sm
text-slate-900 dark:text-slate-50"
:style="{ height: 'var(--header-height)', zIndex: 'var(--z-header)' }"
>
<div class="flex items-center gap-md flex-1">
<BrandLogo :size="40" />

View File

@@ -51,52 +51,33 @@ async function handleLogout() {
<DropdownMenu>
<DropdownMenuTrigger
class="group flex items-center gap-md rounded-full px-md py-1.5 pl-1.5
bg-muted border border-border
bg-card border border-border
cursor-pointer outline-none
transition-all duration-250 ease-out
hover:bg-accent hover:border-border hover:-translate-y-0.5
data-[state=open]:bg-accent data-[state=open]:border-border"
transition-all duration-200
hover:bg-accent hover:border-primary/50
data-[state=open]:bg-accent data-[state=open]:border-primary/50"
>
<!-- 头像容器 -->
<div class="relative w-9 h-9">
<!-- 渐变环 - hover 时显示 -->
<div
class="absolute -inset-0.5 rounded-full opacity-0 group-hover:opacity-100
transition-all duration-400 -z-10"
style="background: conic-gradient(from 0deg, rgba(59, 130, 246, 0.8), rgba(99, 102, 241, 0.8), rgba(139, 92, 246, 0.8), rgba(59, 130, 246, 0.8))"
/>
<!-- 头像背景遮罩 -->
<div class="absolute inset-0 rounded-full bg-background -z-5" />
<!-- 头像 -->
<Avatar class="w-9 h-9 relative z-10">
<AvatarImage v-if="userStore.displayAvatar" :src="userStore.displayAvatar" alt="avatar" />
<AvatarFallback
class="flex items-center justify-center text-primary-foreground font-bold text-[15px]"
:style="{ background: avatarGradient }"
>
{{ userStore.displayName?.charAt(0)?.toUpperCase() || 'U' }}
</AvatarFallback>
</Avatar>
<!-- 在线状态点 -->
<div
class="absolute bottom-0 right-0 w-2.5 h-2.5 rounded-full bg-success
border-2 border-background z-20
shadow-[0_0_0_2px_oklch(from_var(--success)_l_c_h_/_0.3)]"
/>
</div>
<!-- 头像 -->
<Avatar class="w-8 h-8 flex-shrink-0">
<AvatarImage v-if="userStore.displayAvatar" :src="userStore.displayAvatar" alt="avatar" />
<AvatarFallback
class="flex items-center justify-center text-primary-foreground font-bold text-sm"
:style="{ background: avatarGradient }"
>
{{ userStore.displayName?.charAt(0)?.toUpperCase() || 'U' }}
</AvatarFallback>
</Avatar>
<!-- 用户名 -->
<span class="text-sm font-medium text-foreground truncate max-w-[100px]">
<span class="text-sm font-medium text-foreground truncate">
{{ userStore.displayName || '用户' }}
</span>
<!-- 下拉箭头 -->
<Icon
icon="lucide:chevron-down"
class="w-4 h-4 text-muted-foreground shrink-0 transition-transform duration-250
group-hover:rotate-180 group-data-[state=open]:rotate-180"
class="w-4 h-4 text-muted-foreground shrink-0 transition-transform duration-200
group-data-[state=open]:rotate-180"
/>
</DropdownMenuTrigger>

View File

@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority"
export { default as Button } from "./Button.vue"
export const buttonVariants = cva(
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-md text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
"inline-flex items-center justify-center gap-2 whitespace-nowrap rounded-lg text-sm font-medium transition-all disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4 shrink-0 [&_svg]:shrink-0 outline-none focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:ring-[3px]",
{
variants: {
variant: {
@@ -22,11 +22,11 @@ export const buttonVariants = cva(
},
size: {
"default": "h-9 px-4 py-2 has-[>svg]:px-3",
"sm": "h-8 rounded-md gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4",
"icon": "size-9",
"icon-sm": "size-8",
"icon-lg": "size-10",
"sm": "h-8 rounded-lg gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-lg px-6 has-[>svg]:px-4",
"icon": "size-9 rounded-lg",
"icon-sm": "size-8 rounded-lg",
"icon-lg": "size-10 rounded-lg",
},
},
defaultVariants: {

View File

@@ -23,16 +23,19 @@ const forwarded = useForwardPropsEmits(props, emits)
data-slot="drawer-content"
v-bind="{ ...$attrs, ...forwarded }"
:class="cn(
'group/drawer-content bg-background fixed z-50 flex h-auto flex-col',
'group/drawer-content bg-background fixed flex h-auto flex-col',
'data-[vaul-drawer-direction=top]:inset-x-0 data-[vaul-drawer-direction=top]:top-0 data-[vaul-drawer-direction=top]:mb-24 data-[vaul-drawer-direction=top]:max-h-[80vh] data-[vaul-drawer-direction=top]:rounded-b-lg',
'data-[vaul-drawer-direction=bottom]:inset-x-0 data-[vaul-drawer-direction=bottom]:bottom-0 data-[vaul-drawer-direction=bottom]:mt-24 data-[vaul-drawer-direction=bottom]:max-h-[80vh] data-[vaul-drawer-direction=bottom]:rounded-t-lg',
'data-[vaul-drawer-direction=right]:inset-y-0 data-[vaul-drawer-direction=right]:right-0 data-[vaul-drawer-direction=right]:w-3/4 data-[vaul-drawer-direction=right]:sm:max-w-sm',
'data-[vaul-drawer-direction=left]:inset-y-0 data-[vaul-drawer-direction=left]:left-0 data-[vaul-drawer-direction=left]:w-3/4 data-[vaul-drawer-direction=left]:sm:max-w-sm',
props.class,
)"
:style="{ zIndex: 'var(--z-modal)' }"
>
<div class="bg-muted mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full group-data-[vaul-drawer-direction=bottom]/drawer-content:block" />
<slot />
<div class="p-4 overflow-auto">
<slot />
</div>
</DrawerContent>
</DrawerPortal>
</template>

View File

@@ -14,6 +14,7 @@ const delegatedProps = reactiveOmit(props, "class")
<DrawerOverlay
data-slot="drawer-overlay"
v-bind="delegatedProps"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 z-50 bg-black/80', props.class)"
:class="cn('data-[state=open]:animate-in data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:fade-in-0 fixed inset-0 bg-black/80', props.class)"
:style="{ zIndex: 'var(--z-modal-backdrop)' }"
/>
</template>

View File

@@ -27,7 +27,7 @@ const forwarded = useForwardProps(delegatedProps)
>
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">First</span>
<span class="hidden sm:block">首页</span>
</slot>
</PaginationFirst>
</template>

View File

@@ -26,7 +26,7 @@ const forwarded = useForwardProps(delegatedProps)
v-bind="forwarded"
>
<slot>
<span class="hidden sm:block">Last</span>
<span class="hidden sm:block">末页</span>
<ChevronRightIcon />
</slot>
</PaginationLast>

View File

@@ -26,7 +26,7 @@ const forwarded = useForwardProps(delegatedProps)
v-bind="forwarded"
>
<slot>
<span class="hidden sm:block">Next</span>
<span class="hidden sm:block">下一页</span>
<ChevronRightIcon />
</slot>
</PaginationNext>

View File

@@ -27,7 +27,7 @@ const forwarded = useForwardProps(delegatedProps)
>
<slot>
<ChevronLeftIcon />
<span class="hidden sm:block">Previous</span>
<span class="hidden sm:block">上一页</span>
</slot>
</PaginationPrev>
</template>

View File

@@ -0,0 +1,85 @@
<script setup lang="ts">
import { computed } from 'vue'
import { Button } from '@/components/ui/button'
import {
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationFirst,
PaginationItem,
PaginationLast,
PaginationNext,
PaginationPrevious
} from '@/components/ui/pagination'
interface Props {
/** 当前页码 */
current: number
/** 每页条数 */
pageSize: number
/** 总条数 */
total: number
/** 显示的页码按钮数量 */
siblingCount?: number
/** 是否显示首页/末页按钮 */
showEdges?: boolean
}
const props = withDefaults(defineProps<Props>(), {
siblingCount: 1,
showEdges: true
})
const emit = defineEmits<{
'update:current': [page: number]
'change': [page: number]
}>()
const totalPages = computed(() => Math.ceil(props.total / props.pageSize))
const handlePageChange = (page: number) => {
if (page < 1 || page > totalPages.value || page === props.current) return
emit('update:current', page)
emit('change', page)
}
</script>
<template>
<div v-if="total > 0" class="flex flex-wrap items-center justify-between gap-4 py-4 border-t">
<span class="text-sm text-muted-foreground shrink-0">
{{ total }} 条记录
</span>
<Pagination
v-slot="{ page }"
:items-per-page="pageSize"
:total="total"
:sibling-count="siblingCount"
:show-edges="showEdges"
:page="current"
@update:page="handlePageChange"
>
<PaginationContent v-slot="{ items }">
<PaginationFirst @click="handlePageChange(1)" />
<PaginationPrevious @click="handlePageChange(current - 1)" />
<template v-for="(item, index) in items" :key="index">
<PaginationItem
v-if="item.type === 'page'"
:value="item.value"
as-child
>
<Button
:variant="page === item.value ? 'default' : 'outline'"
size="icon-sm"
class="h-8 w-8"
>
{{ item.value }}
</Button>
</PaginationItem>
<PaginationEllipsis v-else :index="index" />
</template>
<PaginationNext @click="handlePageChange(current + 1)" />
<PaginationLast @click="handlePageChange(totalPages)" />
</PaginationContent>
</Pagination>
</div>
</template>

View File

@@ -6,3 +6,4 @@ export { default as PaginationItem } from "./PaginationItem.vue"
export { default as PaginationLast } from "./PaginationLast.vue"
export { default as PaginationNext } from "./PaginationNext.vue"
export { default as PaginationPrevious } from "./PaginationPrevious.vue"
export { default as TablePagination } from "./TablePagination.vue"