feat:移除了弹窗,服务器添加sls
This commit is contained in:
30
apps/telemetry-dashboard/components/ui/Alert.vue
Normal file
30
apps/telemetry-dashboard/components/ui/Alert.vue
Normal 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>
|
||||
33
apps/telemetry-dashboard/components/ui/Badge.vue
Normal file
33
apps/telemetry-dashboard/components/ui/Badge.vue
Normal 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>
|
||||
45
apps/telemetry-dashboard/components/ui/Button.vue
Normal file
45
apps/telemetry-dashboard/components/ui/Button.vue
Normal 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>
|
||||
20
apps/telemetry-dashboard/components/ui/Card.vue
Normal file
20
apps/telemetry-dashboard/components/ui/Card.vue
Normal 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>
|
||||
66
apps/telemetry-dashboard/components/ui/Command.vue
Normal file
66
apps/telemetry-dashboard/components/ui/Command.vue
Normal 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>
|
||||
79
apps/telemetry-dashboard/components/ui/DropdownMenu.vue
Normal file
79
apps/telemetry-dashboard/components/ui/DropdownMenu.vue
Normal 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>
|
||||
27
apps/telemetry-dashboard/components/ui/Select.vue
Normal file
27
apps/telemetry-dashboard/components/ui/Select.vue
Normal 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>
|
||||
44
apps/telemetry-dashboard/components/ui/Sheet.vue
Normal file
44
apps/telemetry-dashboard/components/ui/Sheet.vue
Normal 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>
|
||||
9
apps/telemetry-dashboard/components/ui/Skeleton.vue
Normal file
9
apps/telemetry-dashboard/components/ui/Skeleton.vue
Normal 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>
|
||||
30
apps/telemetry-dashboard/components/ui/Tabs.vue
Normal file
30
apps/telemetry-dashboard/components/ui/Tabs.vue
Normal 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>
|
||||
15
apps/telemetry-dashboard/components/ui/Tooltip.vue
Normal file
15
apps/telemetry-dashboard/components/ui/Tooltip.vue
Normal 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>
|
||||
Reference in New Issue
Block a user