feat:移除了弹窗,服务器添加sls

This commit is contained in:
2026-09-08 22:39:45 +08:00
commit 6a295f9a7a
4082 changed files with 1322534 additions and 0 deletions

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
import { AlertCircle, CircleCheck, Info } from 'lucide-vue-next'
import { cn } from '~/lib/utils'
const props = withDefaults(
defineProps<{ variant?: 'info' | 'success' | 'destructive'; title: string; class?: string }>(),
{ variant: 'info', class: undefined },
)
const icons = { info: Info, success: CircleCheck, destructive: AlertCircle }
const styles = {
info: 'border-border bg-muted/40',
success: 'border-emerald-500/30 bg-emerald-500/10',
destructive: 'border-red-500/30 bg-red-500/10',
}
</script>
<template>
<div
role="alert"
:class="cn('flex gap-3 rounded-lg border p-3.5 text-sm', styles[variant], props.class)"
>
<component :is="icons[variant]" class="mt-0.5 size-4 shrink-0" />
<div class="min-w-0">
<p class="font-medium">{{ title }}</p>
<div class="mt-0.5 text-muted-foreground"><slot /></div>
</div>
</div>
</template>

View File

@ -0,0 +1,33 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
const props = withDefaults(
defineProps<{
variant?: 'default' | 'secondary' | 'success' | 'warning' | 'destructive'
class?: string
}>(),
{ variant: 'secondary', class: undefined },
)
const variants = {
default: 'bg-primary text-primary-foreground',
secondary: 'bg-secondary text-secondary-foreground',
success: 'bg-emerald-500/15 text-emerald-700 dark:text-emerald-300',
warning: 'bg-amber-500/15 text-amber-800 dark:text-amber-300',
destructive: 'bg-red-500/15 text-red-700 dark:text-red-300',
}
</script>
<template>
<span
:class="
cn(
'inline-flex items-center rounded px-2 py-0.5 text-xs font-medium',
variants[variant],
props.class,
)
"
>
<slot />
</span>
</template>

View File

@ -0,0 +1,45 @@
<script setup lang="ts">
import type { ButtonHTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = withDefaults(
defineProps<{
variant?: 'default' | 'secondary' | 'ghost' | 'outline' | 'destructive'
size?: 'default' | 'sm' | 'icon'
class?: ButtonHTMLAttributes['class']
}>(),
{ variant: 'default', size: 'default', class: undefined },
)
const variants = {
default: 'bg-primary text-primary-foreground shadow-sm hover:brightness-95 active:brightness-90',
secondary: 'bg-secondary text-secondary-foreground hover:bg-secondary/75',
ghost: 'text-foreground hover:bg-surface-3 active:bg-surface-4',
outline:
'border-surface-4 bg-surface-2 text-foreground hover:border-surface-5 hover:bg-surface-3 active:bg-surface-4',
destructive: 'bg-destructive text-destructive-foreground hover:brightness-95',
}
const sizes = {
default: 'h-9 px-3.5',
sm: 'h-8 px-2.5 text-xs',
icon: 'size-9 shrink-0 p-0',
}
</script>
<template>
<button
:data-variant="variant"
:class="
cn(
'inline-flex cursor-pointer items-center justify-center gap-2 rounded-md text-sm font-medium transition-[color,background-color,border-color,box-shadow,filter] duration-150 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-auto disabled:cursor-not-allowed disabled:opacity-50',
variants[props.variant],
sizes[props.size],
props.class,
)
"
>
<slot />
</button>
</template>

View File

@ -0,0 +1,20 @@
<script setup lang="ts">
import type { HTMLAttributes } from 'vue'
import { cn } from '~/lib/utils'
const props = defineProps<{ class?: HTMLAttributes['class'] }>()
</script>
<template>
<section
:class="
cn(
'rounded-lg border border-surface-4 bg-surface-2 text-card-foreground shadow-[0_1px_2px_rgba(15,23,42,0.04)]',
props.class,
)
"
>
<slot />
</section>
</template>

View File

@ -0,0 +1,66 @@
<script setup lang="ts">
import { BarChart3, Search, Server } from 'lucide-vue-next'
import {
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
} from 'reka-ui'
const open = defineModel<boolean>('open', { required: true })
const query = ref('')
const router = useRouter()
const items = [
{ label: '数据总览', path: '/', icon: BarChart3 },
{ label: '系统状态', path: '/system', icon: Server },
]
const filtered = computed(() =>
items.filter((item) => item.label.toLowerCase().includes(query.value.toLowerCase())),
)
async function select(path: string): Promise<void> {
open.value = false
query.value = ''
await router.push(path)
}
</script>
<template>
<DialogRoot v-model:open="open">
<DialogPortal>
<DialogOverlay class="sheet-overlay fixed inset-0 z-40 bg-black/45 backdrop-blur-[1px]" />
<DialogContent
class="fixed left-1/2 top-[18vh] z-50 w-[calc(100vw-2rem)] max-w-lg -translate-x-1/2 overflow-hidden rounded-lg border border-surface-5 bg-popover shadow-2xl outline-none"
>
<DialogTitle class="sr-only">快速跳转</DialogTitle>
<DialogDescription class="sr-only">搜索控制台页面</DialogDescription>
<div class="flex items-center gap-2 border-b px-3">
<Search class="size-4 text-muted-foreground" />
<input
v-model="query"
autofocus
placeholder="输入页面名称..."
class="h-12 min-w-0 flex-1 bg-transparent text-sm outline-none placeholder:text-muted-foreground"
/>
</div>
<div class="p-1.5">
<p v-if="!filtered.length" class="px-3 py-8 text-center text-sm text-muted-foreground">
没有匹配的页面
</p>
<button
v-for="item in filtered"
:key="item.path"
type="button"
class="flex w-full cursor-pointer items-center gap-3 rounded-md px-3 py-2 text-left text-sm hover:bg-surface-3 focus:bg-surface-3 focus:outline-none"
@click="select(item.path)"
>
<component :is="item.icon" class="size-4 text-muted-foreground" />
{{ item.label }}
</button>
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>

View File

@ -0,0 +1,79 @@
<script setup lang="ts">
import { Check, ChevronDown, LogOut, Monitor, Moon, Sun } from 'lucide-vue-next'
import {
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuLabel,
DropdownMenuPortal,
DropdownMenuRoot,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from 'reka-ui'
import type { AdminSessionDto } from '~/shared/types/telemetry'
import { type ThemeMode, useTheme } from '../../composables/use-theme'
defineProps<{ session: AdminSessionDto }>()
const { mode } = useTheme()
const themes: Array<{ value: ThemeMode; label: string; icon: typeof Sun }> = [
{ value: 'light', label: '浅色', icon: Sun },
{ value: 'dark', label: '深色', icon: Moon },
{ value: 'system', label: '跟随系统', icon: Monitor },
]
</script>
<template>
<DropdownMenuRoot>
<DropdownMenuTrigger
class="inline-flex h-9 min-w-0 cursor-pointer items-center gap-2 rounded-md border border-surface-4 bg-surface-2 px-2.5 text-sm outline-none transition-colors hover:border-surface-5 hover:bg-surface-3 focus:ring-2 focus:ring-ring"
>
<span
class="flex size-6 shrink-0 items-center justify-center rounded bg-primary/15 text-xs font-semibold text-primary"
>
{{ session.identity.name.slice(0, 1).toUpperCase() }}
</span>
<span class="hidden max-w-32 truncate sm:block">{{ session.identity.name }}</span>
<ChevronDown class="size-3.5 text-muted-foreground" />
</DropdownMenuTrigger>
<DropdownMenuPortal>
<DropdownMenuContent
align="end"
:side-offset="6"
class="z-50 w-64 rounded-lg border border-surface-5 bg-popover p-1 text-popover-foreground shadow-xl outline-none"
>
<DropdownMenuLabel class="px-2 py-1.5">
<p class="truncate text-sm font-medium">{{ session.identity.name }}</p>
<p class="truncate text-xs font-normal text-muted-foreground">
{{ session.identity.email || 'Cloudflare Access 身份' }}
</p>
</DropdownMenuLabel>
<DropdownMenuSeparator class="my-1 h-px bg-border" />
<DropdownMenuLabel class="px-2 py-1 text-xs text-muted-foreground"
>界面主题</DropdownMenuLabel
>
<DropdownMenuItem
v-for="theme in themes"
:key="theme.value"
class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-sm outline-none hover:bg-surface-3 focus:bg-surface-3"
@click="mode = theme.value"
>
<component :is="theme.icon" class="size-4 text-muted-foreground" />
<span class="flex-1">{{ theme.label }}</span>
<Check v-if="mode === theme.value" class="size-4" />
</DropdownMenuItem>
<DropdownMenuSeparator class="my-1 h-px bg-border" />
<DropdownMenuItem as-child>
<a
:href="session.logoutUrl"
class="flex cursor-pointer items-center gap-2 rounded px-2 py-1.5 text-sm outline-none hover:bg-surface-3 focus:bg-surface-3"
>
<LogOut class="size-4 text-muted-foreground" />
退出登录
</a>
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenuPortal>
</DropdownMenuRoot>
</template>

View File

@ -0,0 +1,27 @@
<script setup lang="ts">
import { ChevronDown } from 'lucide-vue-next'
defineProps<{
modelValue: string
label: string
options: Array<{ value: string; label: string }>
}>()
defineEmits<{ 'update:modelValue': [value: string] }>()
</script>
<template>
<label class="relative inline-flex min-w-0 items-center">
<span class="sr-only">{{ label }}</span>
<select
:value="modelValue"
class="h-9 max-w-full cursor-pointer appearance-none rounded-md border border-surface-4 bg-surface-2 py-0 pl-3 pr-8 text-sm text-foreground outline-none transition-colors hover:border-surface-5 hover:bg-surface-3 focus:ring-2 focus:ring-ring"
@change="$emit('update:modelValue', ($event.target as HTMLSelectElement).value)"
>
<option v-for="option in options" :key="option.value" :value="option.value">
{{ option.label }}
</option>
</select>
<ChevronDown class="pointer-events-none absolute right-2 size-4 text-muted-foreground" />
</label>
</template>

View File

@ -0,0 +1,44 @@
<script setup lang="ts">
import { X } from 'lucide-vue-next'
import {
DialogClose,
DialogContent,
DialogDescription,
DialogOverlay,
DialogPortal,
DialogRoot,
DialogTitle,
} from 'reka-ui'
defineProps<{ open: boolean; title: string; description?: string }>()
defineEmits<{ 'update:open': [value: boolean] }>()
</script>
<template>
<DialogRoot :open="open" @update:open="$emit('update:open', $event)">
<DialogPortal>
<DialogOverlay class="sheet-overlay fixed inset-0 z-40 bg-black/45 backdrop-blur-[1px]" />
<DialogContent
class="sheet-panel fixed inset-y-0 right-0 z-50 flex w-full max-w-xl flex-col border-l border-surface-5 bg-surface-1 shadow-2xl focus:outline-none"
>
<header class="flex shrink-0 items-start justify-between gap-4 border-b px-5 py-4">
<div class="min-w-0">
<DialogTitle class="text-base font-semibold">{{ title }}</DialogTitle>
<DialogDescription v-if="description" class="mt-1 text-sm text-muted-foreground">
{{ description }}
</DialogDescription>
</div>
<DialogClose
class="flex size-8 shrink-0 cursor-pointer items-center justify-center rounded-md text-muted-foreground hover:bg-surface-3 hover:text-foreground focus:outline-none focus:ring-2 focus:ring-ring"
aria-label="关闭详情"
>
<X class="size-4" />
</DialogClose>
</header>
<div class="min-h-0 flex-1 overflow-y-auto px-5 py-5">
<slot />
</div>
</DialogContent>
</DialogPortal>
</DialogRoot>
</template>

View File

@ -0,0 +1,9 @@
<script setup lang="ts">
import { cn } from '~/lib/utils'
const props = defineProps<{ class?: string }>()
</script>
<template>
<div :class="cn('animate-pulse rounded-md bg-muted', props.class)" aria-hidden="true"></div>
</template>

View File

@ -0,0 +1,30 @@
<script setup lang="ts">
defineProps<{
modelValue: string
label: string
items: Array<{ value: string; label: string }>
}>()
defineEmits<{ 'update:modelValue': [value: string] }>()
</script>
<template>
<div
role="tablist"
:aria-label="label"
class="inline-flex h-9 items-center rounded-md border border-surface-4 bg-surface-3 p-1"
>
<button
v-for="item in items"
:key="item.value"
type="button"
role="tab"
:aria-selected="modelValue === item.value"
class="h-7 cursor-pointer rounded px-2.5 text-xs font-medium text-muted-foreground transition-[color,background-color,box-shadow] hover:text-foreground active:bg-surface-4"
:class="modelValue === item.value && 'bg-surface-2 text-foreground shadow-sm'"
@click="$emit('update:modelValue', item.value)"
>
{{ item.label }}
</button>
</div>
</template>

View File

@ -0,0 +1,15 @@
<script setup lang="ts">
defineProps<{ text: string }>()
</script>
<template>
<span class="group relative inline-flex">
<slot />
<span
role="tooltip"
class="pointer-events-none absolute left-1/2 top-full z-50 mt-2 hidden w-max max-w-56 -translate-x-1/2 rounded-md bg-foreground px-2 py-1 text-xs text-background shadow-md group-focus-within:block group-hover:block"
>
{{ text }}
</span>
</span>
</template>