add frontend

This commit is contained in:
voltsrage
2026-06-19 17:42:09 +08:00
parent 49973271e5
commit 5f69ce1a95
53 changed files with 6591 additions and 0 deletions
@@ -0,0 +1,25 @@
<script setup>
import { computed } from 'vue'
import { twMerge } from 'tailwind-merge'
const props = defineProps({
variant: { type: String, default: 'info', validator: v => ['critical', 'warning', 'info', 'success'].includes(v) },
size: { type: String, default: 'sm' },
})
const classes = computed(() => {
const base = 'inline-flex items-center font-medium rounded-full'
const sizes = { xs: 'px-1.5 py-0.5 text-xs', sm: 'px-2 py-0.5 text-xs', md: 'px-2.5 py-1 text-sm' }
const variants = {
critical: 'bg-severity-critical/10 text-severity-critical dark:bg-severity-critical-dark/20 dark:text-red-300',
warning: 'bg-severity-warning/10 text-severity-warning dark:bg-severity-warning-dark/20 dark:text-amber-300',
info: 'bg-severity-info/10 text-severity-info dark:bg-severity-info-dark/20 dark:text-blue-300',
success: 'bg-green-100 text-green-800 dark:bg-green-900/20 dark:text-green-300',
}
return twMerge(base, sizes[props.size], variants[props.variant])
})
</script>
<template>
<span :class="classes"><slot /></span>
</template>
@@ -0,0 +1,55 @@
<script setup>
import { computed, useAttrs } from 'vue'
import { twMerge } from 'tailwind-merge'
defineOptions({ inheritAttrs: false })
const props = defineProps({
variant: {
type: String,
default: 'primary',
validator: v => ['primary', 'secondary', 'danger', 'ghost'].includes(v),
},
size: {
type: String,
default: 'md',
validator: v => ['sm', 'md', 'lg'].includes(v),
},
disabled: { type: Boolean, default: false },
type: { type: String, default: 'button' },
})
const attrs = useAttrs()
const classes = computed(() => {
const base =
'inline-flex items-center justify-center font-medium rounded-lg transition duration-200 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-offset-2 disabled:opacity-50 disabled:pointer-events-none'
const sizes = {
sm: 'px-3 py-1.5 text-xs',
md: 'px-4 py-2 text-sm',
lg: 'px-5 py-2.5 text-base',
}
const variants = {
primary:
'bg-blue-600 text-white hover:bg-blue-700 focus-visible:ring-blue-500 dark:bg-blue-500 dark:hover:bg-blue-600',
secondary:
'bg-gray-100 text-gray-900 hover:bg-gray-200 focus-visible:ring-gray-400 dark:bg-gray-700 dark:text-gray-100 dark:hover:bg-gray-600',
danger:
'bg-red-600 text-white hover:bg-red-700 focus-visible:ring-red-500 dark:bg-red-500 dark:hover:bg-red-600',
ghost:
'bg-transparent text-gray-700 hover:bg-gray-100 focus-visible:ring-gray-400 dark:text-gray-300 dark:hover:bg-gray-800',
}
return twMerge(base, sizes[props.size], variants[props.variant], attrs.class)
})
</script>
<template>
<button
:type="type"
:disabled="disabled"
:class="classes"
v-bind="{ ...attrs, class: undefined }"
>
<slot />
</button>
</template>
@@ -0,0 +1,32 @@
<script setup>
import { computed } from 'vue'
import { twMerge } from 'tailwind-merge'
const props = defineProps({
padding: {
type: String,
default: 'md',
validator: v => ['none', 'sm', 'md', 'lg'].includes(v),
},
})
const classes = computed(() => {
const base =
'rounded-lg border border-gray-200 bg-white shadow-sm dark:border-gray-700 dark:bg-gray-900'
const paddings = {
none: '',
sm: 'p-3',
md: 'p-4',
lg: 'p-6',
}
return twMerge(base, paddings[props.padding])
})
</script>
<template>
<div :class="classes">
<slot name="header" />
<slot />
<slot name="footer" />
</div>
</template>
@@ -0,0 +1,32 @@
<script setup>
defineProps({
message: { type: String, default: 'No data' },
})
</script>
<template>
<div class="flex flex-col items-center justify-center py-12 text-center">
<div class="mb-4 text-gray-400 dark:text-gray-500">
<slot name="icon">
<svg
class="mx-auto h-12 w-12"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
aria-hidden="true"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="1.5"
d="M20 13V6a2 2 0 00-2-2H6a2 2 0 00-2 2v7m16 0v5a2 2 0 01-2 2H6a2 2 0 01-2-2v-5m16 0h-2.586a1 1 0 00-.707.293l-2.414 2.414a1 1 0 01-.707.293h-3.172a1 1 0 01-.707-.293l-2.414-2.414A1 1 0 006.586 13H4"
/>
</svg>
</slot>
</div>
<p class="text-sm text-gray-500 dark:text-gray-400">{{ message }}</p>
<div v-if="$slots.action" class="mt-4">
<slot name="action" />
</div>
</div>
</template>
@@ -0,0 +1,27 @@
<script setup>
import { onMounted, onBeforeUnmount, ref } from 'vue'
defineProps({ open: Boolean, title: String })
const emit = defineEmits(['close'])
const modalRef = ref(null)
function onKeydown(e) {
if (e.key === 'Escape') emit('close')
}
onMounted(() => document.addEventListener('keydown', onKeydown))
onBeforeUnmount(() => document.removeEventListener('keydown', onKeydown))
</script>
<template>
<Teleport to="body">
<div v-if="open" class="fixed inset-0 z-50 flex items-center justify-center">
<div class="absolute inset-0 bg-black/50 backdrop-blur-sm" @click="$emit('close')" />
<div ref="modalRef" role="dialog" aria-modal="true"
class="relative z-10 w-full max-w-md rounded-lg bg-white p-6 shadow-lg dark:bg-gray-800">
<h2 v-if="title" class="mb-4 text-lg font-semibold dark:text-white">{{ title }}</h2>
<slot />
</div>
</div>
</Teleport>
</template>
@@ -0,0 +1,15 @@
<script setup>
defineProps({
rows: { type: Number, default: 3 },
})
</script>
<template>
<div class="animate-pulse space-y-3" role="status" aria-label="Loading">
<div
v-for="n in rows"
:key="n"
class="h-12 rounded-lg bg-gray-200 dark:bg-gray-700"
/>
</div>
</template>