优化后台提示展示与聊天室公告样式

This commit is contained in:
2026-04-21 16:43:39 +08:00
parent 281315d1cf
commit f0769a841e
13 changed files with 156 additions and 100 deletions
+154 -10
View File
@@ -11,6 +11,16 @@
</head>
<body class="bg-gray-100 flex h-screen text-gray-800">
@php
$adminFlashToasts = collect([
session('success') ? ['type' => 'success', 'message' => session('success')] : null,
session('ops_success') ? ['type' => 'success', 'message' => session('ops_success')] : null,
session('error') ? ['type' => 'error', 'message' => session('error')] : null,
])
->filter()
->values();
@endphp
<!-- 左侧侧边栏 -->
<aside class="w-64 bg-slate-900 text-white flex flex-col">
<div class="p-6 text-center border-b border-white/10">
@@ -168,16 +178,6 @@
<!-- 内容滚动区 -->
<div class="flex-1 overflow-y-auto p-6 relative">
@if (session('success'))
<div class="mb-6 bg-emerald-100 border-l-4 border-emerald-500 text-emerald-700 p-4 rounded shadow-sm">
{{ session('success') }}
</div>
@endif
@if (session('error'))
<div class="mb-6 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-sm">
{{ session('error') }}
</div>
@endif
@if ($errors->any())
<div class="mb-6 bg-red-100 border-l-4 border-red-500 text-red-700 p-4 rounded shadow-sm">
<ul class="list-disc list-inside text-sm">
@@ -192,6 +192,53 @@
</div>
</main>
<div x-data="createAdminToastStore(@js($adminFlashToasts))" x-init="boot()"
class="pointer-events-none fixed right-6 top-6 z-[100] flex w-full max-w-sm flex-col gap-3">
<template x-for="toast in toasts" :key="toast.id">
<div x-cloak x-show="visibleIds.includes(toast.id)"
x-transition:enter="transform transition duration-300 ease-out"
x-transition:enter-start="translate-y-2 opacity-0 sm:translate-x-6 sm:translate-y-0"
x-transition:enter-end="translate-y-0 opacity-100 sm:translate-x-0"
x-transition:leave="transform transition duration-200 ease-in"
x-transition:leave-start="translate-y-0 opacity-100"
x-transition:leave-end="translate-y-2 opacity-0 sm:translate-x-6 sm:translate-y-0"
class="pointer-events-auto overflow-hidden rounded-2xl border border-slate-200/80 bg-white/95 shadow-2xl ring-1 ring-slate-200/60 backdrop-blur">
<div class="flex items-start gap-3 p-4">
<div class="flex h-10 w-10 shrink-0 items-center justify-center rounded-2xl text-lg font-bold"
:class="toast.type === 'error'
? 'bg-red-50 text-red-500 ring-1 ring-red-100'
: 'bg-emerald-50 text-emerald-500 ring-1 ring-emerald-100'">
<span x-text="toast.type === 'error' ? '!' : '✓'"></span>
</div>
<div class="min-w-0 flex-1">
<div class="flex items-start justify-between gap-3">
<div>
<p class="text-sm font-bold text-slate-800"
x-text="toast.title || (toast.type === 'error' ? '操作失败' : '操作成功')"></p>
<p class="mt-1 break-words text-sm leading-6 text-slate-600" x-text="toast.message"></p>
</div>
<button type="button" @click="remove(toast.id)"
class="rounded-full p-1 text-slate-400 transition hover:bg-slate-100 hover:text-slate-600"
aria-label="关闭提示">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 20 20" fill="currentColor"
class="h-4 w-4">
<path
d="M6.28 5.22a.75.75 0 0 1 1.06 0L10 7.94l2.66-2.72a.75.75 0 0 1 1.08 1.04L11.06 9l2.68 2.74a.75.75 0 1 1-1.08 1.04L10 10.06l-2.66 2.72a.75.75 0 1 1-1.08-1.04L8.94 9 6.28 6.26a.75.75 0 0 1 0-1.04Z" />
</svg>
</button>
</div>
<div class="mt-3 h-1 overflow-hidden rounded-full bg-slate-100">
<div class="h-full rounded-full"
:class="toast.type === 'error' ? 'bg-red-400' : 'bg-emerald-400'"
:style="`animation: admin-toast-progress ${toast.duration || 4200}ms linear forwards;`">
</div>
</div>
</div>
</div>
</div>
</template>
</div>
{{-- ══════════════════════════════════════════════════════════
全局弹窗组件:window.adminDialog.alert / window.adminDialog.confirm
用法:
@@ -214,6 +261,10 @@
</div>
</div>
<style>
[x-cloak] {
display: none !important;
}
@keyframes admin-dialog-pop {
0% {
opacity: 0;
@@ -229,8 +280,101 @@
transform: scale(1);
}
}
@keyframes admin-toast-progress {
from {
transform: translateX(0);
}
to {
transform: translateX(-100%);
}
}
</style>
<script>
/**
* 后台全局 Toast 工厂。
*
* 负责统一渲染右上角成功/失败提示,并提供全局调用入口。
*
* @param initialToasts 初始提示列表
* @returns {object}
*/
function createAdminToastStore(initialToasts = []) {
return {
toasts: [],
visibleIds: [],
nextToastId: Date.now(),
/**
* 初始化 Toast 列表,并挂载全局调用方法。
*/
boot() {
initialToasts.forEach((toast) => this.push(toast));
window.adminToast = {
show: (message, type = 'success', title = null, duration = 4200) => this.push({
message,
type,
title,
duration
}),
success: (message, title = '操作成功', duration = 4200) => this.push({
message,
type: 'success',
title,
duration
}),
error: (message, title = '操作失败', duration = 4200) => this.push({
message,
type: 'error',
title,
duration
}),
};
},
/**
* 追加一条 Toast,并在指定时长后自动关闭。
*
* @param toast 待展示的提示对象
*/
push(toast) {
if (!toast?.message) {
return;
}
const normalizedToast = {
id: this.nextToastId++,
type: toast.type === 'error' ? 'error' : 'success',
title: toast.title,
message: toast.message,
duration: Number(toast.duration) > 0 ? Number(toast.duration) : 4200,
};
this.toasts.push(normalizedToast);
this.visibleIds.push(normalizedToast.id);
window.setTimeout(() => {
this.remove(normalizedToast.id);
}, normalizedToast.duration);
},
/**
* 关闭指定 Toast,并在退场动画后清理节点。
*
* @param {number} toastId
*/
remove(toastId) {
this.visibleIds = this.visibleIds.filter((visibleId) => visibleId !== toastId);
window.setTimeout(() => {
this.toasts = this.toasts.filter((toast) => toast.id !== toastId);
}, 220);
},
};
}
/**
* 后台全局弹窗组件。
*