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

View File

@@ -51,52 +51,33 @@ async function handleLogout() {
<DropdownMenu> <DropdownMenu>
<DropdownMenuTrigger <DropdownMenuTrigger
class="group flex items-center gap-md rounded-full px-md py-1.5 pl-1.5 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 cursor-pointer outline-none
transition-all duration-250 ease-out transition-all duration-200
hover:bg-accent hover:border-border hover:-translate-y-0.5 hover:bg-accent hover:border-primary/50
data-[state=open]:bg-accent data-[state=open]:border-border" data-[state=open]:bg-accent data-[state=open]:border-primary/50"
> >
<!-- 头像容器 --> <!-- 头像 -->
<div class="relative w-9 h-9"> <Avatar class="w-8 h-8 flex-shrink-0">
<!-- 渐变环 - hover 时显示 --> <AvatarImage v-if="userStore.displayAvatar" :src="userStore.displayAvatar" alt="avatar" />
<div <AvatarFallback
class="absolute -inset-0.5 rounded-full opacity-0 group-hover:opacity-100 class="flex items-center justify-center text-primary-foreground font-bold text-sm"
transition-all duration-400 -z-10" :style="{ background: avatarGradient }"
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))" >
/> {{ userStore.displayName?.charAt(0)?.toUpperCase() || 'U' }}
<!-- 头像背景遮罩 --> </AvatarFallback>
<div class="absolute inset-0 rounded-full bg-background -z-5" /> </Avatar>
<!-- 头像 -->
<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>
<!-- 用户名 --> <!-- 用户名 -->
<span class="text-sm font-medium text-foreground truncate max-w-[100px]"> <span class="text-sm font-medium text-foreground truncate">
{{ userStore.displayName || '用户' }} {{ userStore.displayName || '用户' }}
</span> </span>
<!-- 下拉箭头 --> <!-- 下拉箭头 -->
<Icon <Icon
icon="lucide:chevron-down" icon="lucide:chevron-down"
class="w-4 h-4 text-muted-foreground shrink-0 transition-transform duration-250 class="w-4 h-4 text-muted-foreground shrink-0 transition-transform duration-200
group-hover:rotate-180 group-data-[state=open]:rotate-180" group-data-[state=open]:rotate-180"
/> />
</DropdownMenuTrigger> </DropdownMenuTrigger>

View File

@@ -4,7 +4,7 @@ import { cva } from "class-variance-authority"
export { default as Button } from "./Button.vue" export { default as Button } from "./Button.vue"
export const buttonVariants = cva( 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: { variants: {
variant: { variant: {
@@ -22,11 +22,11 @@ export const buttonVariants = cva(
}, },
size: { size: {
"default": "h-9 px-4 py-2 has-[>svg]:px-3", "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", "sm": "h-8 rounded-lg gap-1.5 px-3 has-[>svg]:px-2.5",
"lg": "h-10 rounded-md px-6 has-[>svg]:px-4", "lg": "h-10 rounded-lg px-6 has-[>svg]:px-4",
"icon": "size-9", "icon": "size-9 rounded-lg",
"icon-sm": "size-8", "icon-sm": "size-8 rounded-lg",
"icon-lg": "size-10", "icon-lg": "size-10 rounded-lg",
}, },
}, },
defaultVariants: { defaultVariants: {

View File

@@ -23,16 +23,19 @@ const forwarded = useForwardPropsEmits(props, emits)
data-slot="drawer-content" data-slot="drawer-content"
v-bind="{ ...$attrs, ...forwarded }" v-bind="{ ...$attrs, ...forwarded }"
:class="cn( :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=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=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=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', '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, 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" /> <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> </DrawerContent>
</DrawerPortal> </DrawerPortal>
</template> </template>

View File

@@ -14,6 +14,7 @@ const delegatedProps = reactiveOmit(props, "class")
<DrawerOverlay <DrawerOverlay
data-slot="drawer-overlay" data-slot="drawer-overlay"
v-bind="delegatedProps" 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> </template>

View File

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

View File

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

View File

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

View File

@@ -27,7 +27,7 @@ const forwarded = useForwardProps(delegatedProps)
> >
<slot> <slot>
<ChevronLeftIcon /> <ChevronLeftIcon />
<span class="hidden sm:block">Previous</span> <span class="hidden sm:block">上一页</span>
</slot> </slot>
</PaginationPrev> </PaginationPrev>
</template> </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 PaginationLast } from "./PaginationLast.vue"
export { default as PaginationNext } from "./PaginationNext.vue" export { default as PaginationNext } from "./PaginationNext.vue"
export { default as PaginationPrevious } from "./PaginationPrevious.vue" export { default as PaginationPrevious } from "./PaginationPrevious.vue"
export { default as TablePagination } from "./TablePagination.vue"

View File

@@ -122,6 +122,7 @@
--color-text-secondary: oklch(0.42 0.006 260); --color-text-secondary: oklch(0.42 0.006 260);
--color-text-muted: oklch(0.55 0.005 260); --color-text-muted: oklch(0.55 0.005 260);
--color-text-disabled: oklch(0.72 0.003 260); --color-text-disabled: oklch(0.72 0.003 260);
--color-text-inverse: oklch(0.99 0 0);
--color-border: oklch(0.92 0.002 260); --color-border: oklch(0.92 0.002 260);
--color-primary-hover: var(--color-primary-400); --color-primary-hover: var(--color-primary-400);
@@ -186,6 +187,18 @@
--sidebar-width: 240px; --sidebar-width: 240px;
--header-height: 56px; --header-height: 56px;
/* ========================================
Z-Index 层级系统
======================================== */
--z-dropdown: 50;
--z-sticky: 100;
--z-fixed: 150;
--z-header: 200;
--z-modal-backdrop: 300;
--z-modal: 400;
--z-popover: 500;
--z-tooltip: 600;
/* ======================================== /* ========================================
动效 动效
======================================== */ ======================================== */
@@ -277,6 +290,7 @@
--color-text-secondary: oklch(0.68 0.006 260); --color-text-secondary: oklch(0.68 0.006 260);
--color-text-muted: oklch(0.50 0.006 260); --color-text-muted: oklch(0.50 0.006 260);
--color-text-disabled: oklch(0.36 0.006 260); --color-text-disabled: oklch(0.36 0.006 260);
--color-text-inverse: oklch(0.12 0.004 260);
--color-border: oklch(0.26 0.006 260); --color-border: oklch(0.26 0.006 260);
/* 主色阶 */ /* 主色阶 */

View File

@@ -175,10 +175,10 @@ function handleUse() {
min-height: 300px; min-height: 300px;
max-height: 500px; max-height: 500px;
overflow-y: auto; overflow-y: auto;
padding: 24px; padding: var(--space-6);
border: 1px solid var(--color-border); border: 1px solid var(--border);
border-radius: 8px; border-radius: var(--radius);
background: var(--color-surface); background: var(--muted);
} }
.edit-textarea { .edit-textarea {
@@ -192,16 +192,16 @@ function handleUse() {
align-items: center; align-items: center;
width: 100%; width: 100%;
flex-wrap: wrap; flex-wrap: wrap;
gap: 8px; gap: var(--space-2);
.left-actions { .left-actions {
display: flex; display: flex;
gap: 4px; gap: var(--space-1);
} }
.right-actions { .right-actions {
display: flex; display: flex;
gap: 8px; gap: var(--space-2);
} }
} }
</style> </style>

View File

@@ -617,7 +617,7 @@ onMounted(() => loadVoiceList())
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: var(--space-4); padding: var(--space-4);
background: var(--color-bg-card); background: var(--card);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
@@ -630,7 +630,7 @@ onMounted(() => loadVoiceList())
} }
.table-wrapper { .table-wrapper {
background: var(--color-bg-card); background: var(--card);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
box-shadow: var(--shadow-sm); box-shadow: var(--shadow-sm);
overflow: hidden; overflow: hidden;
@@ -641,8 +641,8 @@ onMounted(() => loadVoiceList())
justify-content: space-between; justify-content: space-between;
align-items: center; align-items: center;
padding: var(--space-3) var(--space-4); padding: var(--space-3) var(--space-4);
border-top: 1px solid var(--color-border); border-top: 1px solid var(--border);
background: var(--color-bg-card); background: var(--card);
} }
// 上传区域 // 上传区域
@@ -652,25 +652,25 @@ onMounted(() => loadVoiceList())
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--space-8) var(--space-4); padding: var(--space-8) var(--space-4);
border: 2px dashed var(--color-border); border: 2px dashed var(--border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: var(--color-muted); background: var(--muted);
cursor: pointer; cursor: pointer;
transition: all 0.2s ease; transition: all 0.2s ease;
&:hover { &:hover {
border-color: var(--color-primary); border-color: var(--primary);
background: oklch(0.97 0.01 254.604); background: oklch(0.97 0.01 254.604);
} }
&--dragging { &--dragging {
border-color: var(--color-primary); border-color: var(--primary);
background: oklch(0.95 0.02 254.604); background: oklch(0.95 0.02 254.604);
} }
&__icon { &__icon {
font-size: 36px; font-size: 36px;
color: var(--color-primary); color: var(--primary);
margin-bottom: var(--space-3); margin-bottom: var(--space-3);
opacity: 0.8; opacity: 0.8;
} }
@@ -678,13 +678,13 @@ onMounted(() => loadVoiceList())
&__title { &__title {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 500; font-weight: 500;
color: var(--color-foreground); color: var(--foreground);
margin-bottom: var(--space-1); margin-bottom: var(--space-1);
} }
&__hint { &__hint {
font-size: var(--font-size-xs); font-size: var(--font-size-xs);
color: var(--color-muted-foreground); color: var(--muted-foreground);
} }
} }
@@ -694,9 +694,9 @@ onMounted(() => loadVoiceList())
align-items: center; align-items: center;
justify-content: center; justify-content: center;
padding: var(--space-8) var(--space-4); padding: var(--space-8) var(--space-4);
border: 2px solid var(--color-border); border: 2px solid var(--border);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
background: var(--color-muted); background: var(--muted);
} }
.upload-preview { .upload-preview {
@@ -710,7 +710,7 @@ onMounted(() => loadVoiceList())
&__icon { &__icon {
font-size: 28px; font-size: 28px;
color: var(--color-primary); color: var(--primary);
} }
&__info { &__info {
@@ -723,7 +723,7 @@ onMounted(() => loadVoiceList())
&__name { &__name {
font-size: var(--font-size-sm); font-size: var(--font-size-sm);
font-weight: 500; font-weight: 500;
color: var(--color-foreground); color: var(--foreground);
max-width: 220px; max-width: 220px;
overflow: hidden; overflow: hidden;
text-overflow: ellipsis; text-overflow: ellipsis;

View File

@@ -202,12 +202,12 @@
</section> </section>
</main> </main>
<!-- 高级设置抽屉 --> <!-- 高级设置模态框 -->
<Sheet v-model:open="showSettings" style="padding: 0 16px;"> <Dialog v-model:open="showSettings">
<SheetContent class="w-80"> <DialogContent class="max-w-md">
<SheetHeader class="pb-4 border-b border-border"> <DialogHeader>
<SheetTitle class="text-lg">高级设置</SheetTitle> <DialogTitle>高级设置</DialogTitle>
</SheetHeader> </DialogHeader>
<div class="py-6 space-y-8"> <div class="py-6 space-y-8">
<!-- 裁剪模式 - 胶囊式切换 --> <!-- 裁剪模式 - 胶囊式切换 -->
@@ -267,8 +267,14 @@
</div> </div>
</div> </div>
</div> </div>
</SheetContent>
</Sheet> <DialogFooter>
<Button variant="outline" @click="showSettings = false">
关闭
</Button>
</DialogFooter>
</DialogContent>
</Dialog>
<!-- 场景选择弹窗 --> <!-- 场景选择弹窗 -->
<SceneSelectorModal <SceneSelectorModal
@@ -313,11 +319,12 @@ import {
SelectValue SelectValue
} from '@/components/ui/select' } from '@/components/ui/select'
import { import {
Sheet, Dialog,
SheetContent, DialogContent,
SheetHeader, DialogFooter,
SheetTitle DialogHeader,
} from '@/components/ui/sheet' DialogTitle
} from '@/components/ui/dialog'
import { Label } from '@/components/ui/label' import { Label } from '@/components/ui/label'
import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group' import { RadioGroup, RadioGroupItem } from '@/components/ui/radio-group'

View File

@@ -174,43 +174,12 @@
</div> </div>
<!-- 分页 --> <!-- 分页 -->
<div v-if="paginationConfig.total > 0" class="flex items-center justify-between py-4 border-t"> <TablePagination
<span class="text-sm text-muted-foreground"> :current="paginationConfig.current"
{{ paginationConfig.total }} 条记录 :page-size="paginationConfig.pageSize"
</span> :total="paginationConfig.total"
<Pagination @change="handlePageChange"
v-slot="{ page }" />
:items-per-page="paginationConfig.pageSize"
:total="paginationConfig.total"
:sibling-count="1"
show-edges
:page="paginationConfig.current"
@update:page="handlePageChange"
>
<PaginationContent v-slot="{ items }">
<PaginationFirst @click="handlePageChange(1)" />
<PaginationPrevious @click="handlePageChange(paginationConfig.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(paginationConfig.current + 1)" />
<PaginationLast @click="handlePageChange(Math.ceil(paginationConfig.total / paginationConfig.pageSize))" />
</PaginationContent>
</Pagination>
</div>
</div> </div>
</div> </div>
@@ -249,16 +218,7 @@ import { Progress } from '@/components/ui/progress'
import { Alert } from '@/components/ui/alert' import { Alert } from '@/components/ui/alert'
import { Spinner } from '@/components/ui/spinner' import { Spinner } from '@/components/ui/spinner'
import { Checkbox } from '@/components/ui/checkbox' import { Checkbox } from '@/components/ui/checkbox'
import { import { TablePagination } from '@/components/ui/pagination'
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationFirst,
PaginationItem,
PaginationLast,
PaginationNext,
PaginationPrevious
} from '@/components/ui/pagination'
import { import {
AlertDialog, AlertDialog,
AlertDialogAction, AlertDialogAction,
@@ -396,7 +356,7 @@ onMounted(fetchList)
<style scoped lang="less"> <style scoped lang="less">
.task-page { .task-page {
padding: var(--space-4); padding: 0;
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -404,17 +364,20 @@ onMounted(fetchList)
} }
.task-page__filters { .task-page__filters {
padding: var(--space-4); flex-shrink: 0;
background: var(--color-bg-card); padding: var(--space-5);
background: var(--card);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
border: 1px solid var(--border);
} }
.task-page__content { .task-page__content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
background: var(--color-bg-card); background: var(--card);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
padding: var(--space-4); border: 1px solid var(--border);
padding: var(--space-5);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }

View File

@@ -1,15 +1,15 @@
<template> <template>
<div class="task-layout"> <div class="task-layout">
<!-- 顶部Tab栏 - 现代化设计 --> <!-- 顶部Tab栏 -->
<div class="task-layout__header"> <div class="task-layout__header">
<div class="flex items-center justify-between"> <div class="flex items-center justify-between">
<Tabs v-model:model-value="currentType" class="w-auto"> <Tabs v-model:model-value="currentType" class="w-auto">
<TabsList class="h-11 bg-muted/50 p-1 gap-1"> <TabsList class="h-auto bg-transparent p-0 gap-2">
<TabsTrigger <TabsTrigger
v-for="item in NAV_ITEMS" v-for="item in NAV_ITEMS"
:key="item.type" :key="item.type"
:value="item.type" :value="item.type"
class="h-9 px-4 gap-2 rounded-md transition-all data-[state=active]:bg-background data-[state=active]:shadow-sm data-[state=inactive]:text-muted-foreground data-[state=inactive]:hover:text-foreground" class="h-9 px-4 gap-2 rounded-lg bg-transparent transition-all data-[state=active]:bg-primary data-[state=active]:text-white data-[state=inactive]:text-muted-foreground data-[state=inactive]:hover:bg-muted focus-visible:ring-0 focus-visible:outline-none"
> >
<Icon :icon="item.icon" class="size-4" /> <Icon :icon="item.icon" class="size-4" />
<span class="font-medium">{{ item.label }}</span> <span class="font-medium">{{ item.label }}</span>
@@ -80,10 +80,9 @@ const currentComponent = computed(() => {
.task-layout__header { .task-layout__header {
flex-shrink: 0; flex-shrink: 0;
padding: var(--space-4) var(--space-6); padding: 0 var(--space-4);
background: var(--card); background: var(--color-bg-card);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
border: 1px solid var(--border);
} }
.task-layout__content { .task-layout__content {

View File

@@ -22,7 +22,7 @@
<Icon icon="lucide:search" class="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" /> <Icon icon="lucide:search" class="absolute left-3 top-1/2 -translate-y-1/2 size-4 text-muted-foreground" />
<Input <Input
v-model="filters.keyword" v-model="filters.keyword"
placeholder="搜索标题" placeholder="搜索任务名称"
class="pl-9" class="pl-9"
@keyup.enter="handleFilterChange" @keyup.enter="handleFilterChange"
/> />
@@ -63,30 +63,29 @@
<!-- 任务列表 --> <!-- 任务列表 -->
<div class="task-page__content"> <div class="task-page__content">
<!-- 批量操作工具 --> <!-- 批量操作栏 -->
<div class="batch-toolbar flex items-center gap-3 pb-3 border-b"> <div v-if="selectedRowKeys.length > 0" class="batch-actions">
<span v-if="selectedRowKeys.length > 0" class="text-sm"> <Alert class="flex items-center justify-between">
已选择 <strong class="text-primary">{{ selectedRowKeys.length }}</strong> <div class="flex items-center gap-2">
</span> <Icon icon="lucide:info" class="size-4" />
<Button <span>已选中 {{ selectedRowKeys.length }} </span>
:disabled="!hasDownloadableSelected" </div>
:loading="batchDownloading" <div class="flex items-center gap-2">
size="sm" <Button
class="text-white" :disabled="!hasDownloadableSelected"
@click="handleBatchDownloadSelected" :loading="batchDownloading"
> size="sm"
<Icon icon="lucide:download" class="mr-1 size-4" /> @click="handleBatchDownloadSelected"
批量下载 ({{ downloadableCount }}) >
</Button> <Icon icon="lucide:download" class="mr-1 size-4" />
<Button 批量下载 ({{ downloadableCount }})
variant="destructive" </Button>
size="sm" <Button variant="destructive" size="sm" @click="handleBatchDeleteSelected">
:disabled="selectedRowKeys.length === 0" <Icon icon="lucide:trash-2" class="mr-1 size-4" />
@click="handleBatchDeleteSelected" 批量删除
> </Button>
<Icon icon="lucide:trash-2" class="mr-1 size-4" /> </div>
批量删除 </Alert>
</Button>
</div> </div>
<div class="relative min-h-[200px]"> <div class="relative min-h-[200px]">
@@ -251,43 +250,12 @@
</div> </div>
<!-- 分页 --> <!-- 分页 -->
<div v-if="paginationConfig.total > 0" class="flex items-center justify-between py-4 border-t"> <TablePagination
<span class="text-sm text-muted-foreground"> :current="paginationConfig.current"
{{ paginationConfig.total }} 条记录 :page-size="paginationConfig.pageSize"
</span> :total="paginationConfig.total"
<Pagination @change="handlePageChange"
v-slot="{ page }" />
:items-per-page="paginationConfig.pageSize"
:total="paginationConfig.total"
:sibling-count="1"
show-edges
:page="paginationConfig.current"
@update:page="handlePageChange"
>
<PaginationContent v-slot="{ items }">
<PaginationFirst @click="handlePageChange(1)" />
<PaginationPrevious @click="handlePageChange(paginationConfig.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(paginationConfig.current + 1)" />
<PaginationLast @click="handlePageChange(Math.ceil(paginationConfig.total / paginationConfig.pageSize))" />
</PaginationContent>
</Pagination>
</div>
</div> </div>
</div> </div>
@@ -363,16 +331,7 @@ import {
AlertDialogHeader, AlertDialogHeader,
AlertDialogTitle AlertDialogTitle
} from '@/components/ui/alert-dialog' } from '@/components/ui/alert-dialog'
import { import { TablePagination } from '@/components/ui/pagination'
Pagination,
PaginationContent,
PaginationEllipsis,
PaginationFirst,
PaginationItem,
PaginationLast,
PaginationNext,
PaginationPrevious
} from '@/components/ui/pagination'
import { MixTaskService } from '@/api/mixTask' import { MixTaskService } from '@/api/mixTask'
import { formatDate } from '@/utils/file' import { formatDate } from '@/utils/file'
import { useTaskList } from '@/views/system/task-management/composables/useTaskList' import { useTaskList } from '@/views/system/task-management/composables/useTaskList'
@@ -620,7 +579,7 @@ onMounted(fetchList)
<style scoped lang="less"> <style scoped lang="less">
.task-page { .task-page {
padding: 0; padding: var(--space-4);
height: 100%; height: 100%;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
@@ -628,28 +587,22 @@ onMounted(fetchList)
} }
.task-page__filters { .task-page__filters {
flex-shrink: 0; padding: var(--space-4);
padding: var(--space-5); background: var(--color-bg-card);
background: var(--card);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
border: 1px solid var(--border);
} }
.task-page__content { .task-page__content {
flex: 1; flex: 1;
overflow: hidden; overflow: hidden;
background: var(--card); background: var(--color-bg-card);
border-radius: var(--radius-lg); border-radius: var(--radius-lg);
border: 1px solid var(--border); padding: var(--space-4);
padding: var(--space-5);
display: flex; display: flex;
flex-direction: column; flex-direction: column;
} }
.batch-toolbar { .batch-actions {
flex-shrink: 0;
padding-bottom: var(--space-4);
border-bottom: 1px solid var(--border);
margin-bottom: var(--space-4); margin-bottom: var(--space-4);
} }

View File

@@ -288,40 +288,49 @@ onMounted(() => {
<style scoped lang="less"> <style scoped lang="less">
.task-list-container { .task-list-container {
padding: 24px; padding: 0;
height: 100%; height: 100%;
overflow: auto; display: flex;
flex-direction: column;
gap: var(--space-4);
} }
.filter-section { .filter-section {
flex-shrink: 0;
padding: var(--space-5);
background: var(--card);
border-radius: var(--radius-lg);
border: 1px solid var(--border);
display: flex; display: flex;
gap: 12px; gap: var(--space-3);
margin-bottom: 20px; margin-bottom: 0;
} }
.table-wrapper { .table-wrapper {
background: white; flex: 1;
border-radius: 12px; background: var(--card);
border: 1px solid var(--color-gray-100); border-radius: var(--radius-lg);
border: 1px solid var(--border);
overflow: hidden; overflow: hidden;
} }
.pagination-section { .pagination-section {
flex-shrink: 0;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 16px 0; padding: var(--space-4);
margin-top: 16px; border-top: 1px solid var(--border);
} }
.prompt-content { .prompt-content {
padding: 16px; padding: var(--space-4);
background: var(--color-gray-50); background: var(--muted);
border-radius: 8px; border-radius: var(--radius);
white-space: pre-wrap; white-space: pre-wrap;
max-height: 400px; max-height: 400px;
overflow-y: auto; overflow-y: auto;
font-size: 14px; font-size: var(--font-size-base);
line-height: 1.6; line-height: 1.6;
} }
</style> </style>

View File

@@ -146,10 +146,7 @@ onMounted(async () => {
<template> <template>
<div class="profile-container"> <div class="profile-container">
<div class="page-header">
<h1 class="page-title">个人中心</h1>
<p class="page-subtitle">管理您的账户信息和资源使用情况</p>
</div>
<div class="grid grid-cols-1 lg:grid-cols-12 gap-6"> <div class="grid grid-cols-1 lg:grid-cols-12 gap-6">
<!-- 左侧用户信息卡片 --> <!-- 左侧用户信息卡片 -->
@@ -299,33 +296,16 @@ onMounted(async () => {
<style scoped> <style scoped>
.profile-container { .profile-container {
padding: 24px; padding: var(--space-6);
max-width: 1200px; max-width: 1200px;
margin: 0 auto; margin: 0 auto;
} }
.page-header { .profile-content {
margin-bottom: 32px;
}
.page-title {
font-size: 28px;
font-weight: 700;
color: var(--color-text);
margin-bottom: 8px;
}
.page-subtitle {
color: var(--color-text-secondary);
font-size: 14px;
}
/* User Card */
.user-card {
text-align: center; text-align: center;
border-radius: 16px; border-radius: 16px;
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05); box-shadow: 0 4px 20px rgba(0, 0, 0, 0.05);
background: var(--color-bg-container, #fff); background: var(--card);
height: 100%; height: 100%;
padding: 24px; padding: 24px;
} }
@@ -368,7 +348,7 @@ onMounted(async () => {
font-size: 20px; font-size: 20px;
font-weight: 600; font-weight: 600;
margin-bottom: 12px; margin-bottom: 12px;
color: var(--color-text); color: var(--foreground);
} }
.user-role-badge { .user-role-badge {
@@ -383,7 +363,7 @@ onMounted(async () => {
.divider { .divider {
height: 1px; height: 1px;
background: var(--color-border-secondary, #f0f0f0); background: var(--border);
margin: 16px 0; margin: 16px 0;
} }
@@ -395,7 +375,7 @@ onMounted(async () => {
display: flex; display: flex;
justify-content: space-between; justify-content: space-between;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid var(--color-border-secondary, #f0f0f0); border-bottom: 1px solid var(--border);
} }
.detail-item:last-child { .detail-item:last-child {
@@ -403,11 +383,11 @@ onMounted(async () => {
} }
.detail-label { .detail-label {
color: var(--color-text-secondary); color: var(--muted-foreground);
} }
.detail-value { .detail-value {
color: var(--color-text); color: var(--foreground);
font-weight: 500; font-weight: 500;
} }
@@ -415,7 +395,7 @@ onMounted(async () => {
.stat-card { .stat-card {
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
background: var(--color-bg-container, #fff); background: var(--card);
padding: 20px; padding: 20px;
transition: transform 0.2s, box-shadow 0.2s; transition: transform 0.2s, box-shadow 0.2s;
} }
@@ -452,26 +432,26 @@ onMounted(async () => {
.stat-label { .stat-label {
font-size: 14px; font-size: 14px;
color: var(--color-text-secondary); color: var(--muted-foreground);
margin-bottom: 4px; margin-bottom: 4px;
} }
.stat-value { .stat-value {
font-size: 24px; font-size: 24px;
font-weight: 700; font-weight: 700;
color: var(--color-text); color: var(--foreground);
margin-bottom: 8px; margin-bottom: 8px;
} }
.stat-unit { .stat-unit {
font-size: 14px; font-size: 14px;
font-weight: 400; font-weight: 400;
color: var(--color-text-third); color: var(--muted-foreground);
} }
.stat-desc { .stat-desc {
font-size: 12px; font-size: 12px;
color: var(--color-text-third); color: var(--muted-foreground);
} }
.stat-progress { .stat-progress {
@@ -489,7 +469,7 @@ onMounted(async () => {
justify-content: center; justify-content: center;
gap: 12px; gap: 12px;
padding: 40px 0; padding: 40px 0;
color: var(--color-text-secondary); color: var(--muted-foreground);
} }
.custom-spinner { .custom-spinner {
@@ -509,7 +489,7 @@ onMounted(async () => {
.empty-state { .empty-state {
text-align: center; text-align: center;
padding: 40px 0; padding: 40px 0;
color: var(--color-text-third); color: var(--muted-foreground);
} }
.empty-icon { .empty-icon {
@@ -522,7 +502,7 @@ onMounted(async () => {
.activity-card { .activity-card {
border-radius: 12px; border-radius: 12px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03); box-shadow: 0 2px 10px rgba(0, 0, 0, 0.03);
background: var(--color-bg-container, #fff); background: var(--card);
padding: 20px; padding: 20px;
} }
@@ -536,12 +516,12 @@ onMounted(async () => {
.activity-title { .activity-title {
font-size: 16px; font-size: 16px;
font-weight: 600; font-weight: 600;
color: var(--color-text); color: var(--foreground);
margin: 0; margin: 0;
} }
.record-count { .record-count {
color: var(--color-text-secondary); color: var(--muted-foreground);
font-size: 13px; font-size: 13px;
} }
@@ -555,7 +535,7 @@ onMounted(async () => {
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
padding: 12px 0; padding: 12px 0;
border-bottom: 1px solid var(--color-border-secondary, #f0f0f0); border-bottom: 1px solid var(--border);
} }
.record-item:last-child { .record-item:last-child {
@@ -596,13 +576,13 @@ onMounted(async () => {
.record-reason { .record-reason {
font-weight: 500; font-weight: 500;
color: var(--color-text); color: var(--foreground);
font-size: 14px; font-size: 14px;
} }
.record-time { .record-time {
font-size: 12px; font-size: 12px;
color: var(--color-text-secondary); color: var(--muted-foreground);
} }
.record-amount { .record-amount {
@@ -625,11 +605,11 @@ onMounted(async () => {
gap: 16px; gap: 16px;
margin-top: 16px; margin-top: 16px;
padding-top: 16px; padding-top: 16px;
border-top: 1px solid var(--color-border-secondary, #f0f0f0); border-top: 1px solid var(--border);
} }
.page-info { .page-info {
font-size: 14px; font-size: 14px;
color: var(--color-text-secondary); color: var(--muted-foreground);
} }
</style> </style>