Initial commit

This commit is contained in:
fawney19
2025-12-10 20:52:44 +08:00
commit f784106826
485 changed files with 110993 additions and 0 deletions

View File

@@ -0,0 +1,185 @@
<template>
<div
role="alert"
class="flex items-start gap-4 px-6 py-3 rounded-lg border shadow-sm max-w-md"
:class="variantClasses"
>
<!-- 图标带圆形进度环 -->
<div class="relative shrink-0 w-8 h-8">
<!-- 进度环背景 -->
<svg
v-if="toast.duration && toast.duration > 0"
class="absolute inset-0 w-8 h-8 -rotate-90"
>
<circle
cx="16"
cy="16"
r="14"
fill="none"
stroke="currentColor"
stroke-width="2"
class="opacity-15"
/>
<circle
cx="16"
cy="16"
r="14"
fill="none"
stroke="currentColor"
stroke-width="2"
:stroke-dasharray="circumference"
:stroke-dashoffset="strokeDashoffset"
stroke-linecap="round"
class="transition-[stroke-dashoffset] duration-75"
:class="progressColorClass"
/>
</svg>
<!-- 图标 -->
<div class="absolute inset-0 flex items-center justify-center" :class="iconClasses">
<component :is="icon" class="w-4 h-4" />
</div>
</div>
<!-- 内容 -->
<div class="flex-1 min-w-0">
<p v-if="toast.title" class="text-sm font-medium" :class="titleClasses">
{{ toast.title }}
</p>
<p v-if="toast.message" class="text-sm" :class="messageClasses">
{{ toast.message }}
</p>
</div>
<!-- 关闭按钮 -->
<button
@click="$emit('remove')"
class="shrink-0 p-1 rounded transition-colors opacity-40 hover:opacity-100"
:class="closeClasses"
type="button"
aria-label="关闭"
>
<X class="w-3.5 h-3.5" />
</button>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, onUnmounted } from 'vue'
import { CheckCircle2, XCircle, AlertTriangle, Info, X } from 'lucide-vue-next'
interface Toast {
id: string
title?: string
message?: string
variant?: 'success' | 'error' | 'warning' | 'info'
duration?: number
}
const props = defineProps<{
toast: Toast
}>()
const emit = defineEmits<{
remove: []
}>()
const progress = ref(100)
let startTime = 0
let rafId: number | null = null
let timeoutId: ReturnType<typeof setTimeout> | null = null
// 圆形进度环参数
const circumference = 2 * Math.PI * 14 // r=14
const strokeDashoffset = computed(() => {
return circumference * (1 - progress.value / 100)
})
const updateProgress = () => {
if (!props.toast.duration || props.toast.duration <= 0) return
const elapsed = Date.now() - startTime
const remaining = Math.max(0, 100 - (elapsed / props.toast.duration) * 100)
progress.value = remaining
if (remaining <= 0) {
emit('remove')
} else {
rafId = requestAnimationFrame(updateProgress)
}
}
onMounted(() => {
if (props.toast.duration && props.toast.duration > 0) {
startTime = Date.now()
rafId = requestAnimationFrame(updateProgress)
// 保底 timeout确保即使在后台也能移除
timeoutId = setTimeout(() => {
emit('remove')
}, props.toast.duration + 100)
}
})
onUnmounted(() => {
if (rafId) cancelAnimationFrame(rafId)
if (timeoutId) clearTimeout(timeoutId)
})
const icons = {
success: CheckCircle2,
error: XCircle,
warning: AlertTriangle,
info: Info
}
const icon = computed(() => icons[props.toast.variant || 'info'])
const variantClasses = computed(() => {
const variant = props.toast.variant || 'info'
const classes: Record<string, string> = {
success: 'border-[#5F8D4E]/30 bg-white dark:bg-[var(--slate-dark)]',
error: 'border-[var(--error)]/30 bg-white dark:bg-[var(--slate-dark)]',
warning: 'border-[var(--book-cloth)]/30 bg-white dark:bg-[var(--slate-dark)]',
info: 'border-[var(--slate-medium)]/20 bg-white dark:bg-[var(--slate-dark)]'
}
return classes[variant]
})
const iconClasses = computed(() => {
const variant = props.toast.variant || 'info'
const classes: Record<string, string> = {
success: 'text-[#5F8D4E]',
error: 'text-[var(--error)]',
warning: 'text-[var(--book-cloth)]',
info: 'text-[var(--slate-medium)] dark:text-[var(--cloud-medium)]'
}
return classes[variant]
})
const progressColorClass = computed(() => {
const variant = props.toast.variant || 'info'
const classes: Record<string, string> = {
success: 'stroke-[#5F8D4E]',
error: 'stroke-[var(--error)]',
warning: 'stroke-[var(--book-cloth)]',
info: 'stroke-[var(--slate-medium)]'
}
return classes[variant]
})
const titleClasses = computed(() => {
return 'text-[var(--color-text)]'
})
const messageClasses = computed(() => {
return 'text-[var(--slate-medium)] dark:text-[var(--cloud-medium)]'
})
const closeClasses = computed(() => {
return 'text-[var(--slate-medium)] hover:bg-[var(--color-border-soft)] dark:text-[var(--cloud-medium)]'
})
</script>
<style scoped>
</style>