feat: 添加全局通知组件并替换原有的表单反馈提示
This commit is contained in:
1 parent
467341ad11
commit
d76932ae34
5 files changed
+173
-53
No files matched your search
@@ -0,0 +1,114 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import { toastState } from '$lib/stores/toast.svelte';
|
||||||
|
import { fade, fly } from 'svelte/transition';
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="toast-container">
|
||||||
|
{#each toastState.messages as msg (msg.id)}
|
||||||
|
<!-- Use fly in and fade out for a sleek animation -->
|
||||||
|
<div
|
||||||
|
class="toast-msg type-{msg.type}"
|
||||||
|
in:fly={{ y: -20, duration: 300 }}
|
||||||
|
out:fade={{ duration: 200 }}
|
||||||
|
>
|
||||||
|
{#if msg.type === 'success'}
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="toast-icon">
|
||||||
|
<polyline points="20 6 9 17 4 12"></polyline>
|
||||||
|
</svg>
|
||||||
|
{:else if msg.type === 'error'}
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="toast-icon">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="15" y1="9" x2="9" y2="15"></line>
|
||||||
|
<line x1="9" y1="9" x2="15" y2="15"></line>
|
||||||
|
</svg>
|
||||||
|
{:else}
|
||||||
|
<svg viewBox="0 0 24 24" width="18" height="18" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round" class="toast-icon">
|
||||||
|
<circle cx="12" cy="12" r="10"></circle>
|
||||||
|
<line x1="12" y1="16" x2="12" y2="12"></line>
|
||||||
|
<line x1="12" y1="8" x2="12.01" y2="8"></line>
|
||||||
|
</svg>
|
||||||
|
{/if}
|
||||||
|
<span>{msg.text}</span>
|
||||||
|
|
||||||
|
<button class="toast-close" onclick={() => toastState.remove(msg.id)} aria-label="Close">
|
||||||
|
<svg viewBox="0 0 24 24" width="14" height="14" stroke="currentColor" stroke-width="2" fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<line x1="18" y1="6" x2="6" y2="18"></line>
|
||||||
|
<line x1="6" y1="6" x2="18" y2="18"></line>
|
||||||
|
</svg>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.toast-container {
|
||||||
|
position: fixed;
|
||||||
|
top: 25px;
|
||||||
|
right: 25px;
|
||||||
|
z-index: 9999;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
pointer-events: none; /* Let clicks pass through empty space */
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-msg {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
padding: 14px 20px;
|
||||||
|
background-color: var(--bg-color, #fff);
|
||||||
|
color: var(--text-color, #222);
|
||||||
|
border: 2px solid var(--text-color, #222);
|
||||||
|
box-shadow: 4px 4px 0 rgba(0, 0, 0, 0.15);
|
||||||
|
font-weight: 600;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
min-width: 250px;
|
||||||
|
max-width: 400px;
|
||||||
|
pointer-events: auto; /* Re-enable clicks on the toast itself */
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-msg.type-success {
|
||||||
|
border-color: #419b38;
|
||||||
|
color: #419b38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-msg.type-success .toast-close {
|
||||||
|
color: #419b38;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-msg.type-error {
|
||||||
|
border-color: #d9534f;
|
||||||
|
color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-msg.type-error .toast-close {
|
||||||
|
color: #d9534f;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-icon {
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-msg span {
|
||||||
|
flex-grow: 1;
|
||||||
|
word-break: break-word;
|
||||||
|
line-height: 1.4;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close {
|
||||||
|
background: none;
|
||||||
|
border: none;
|
||||||
|
padding: 4px;
|
||||||
|
cursor: pointer;
|
||||||
|
color: var(--text-muted, #888);
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
transition: transform 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.toast-close:hover {
|
||||||
|
transform: scale(1.1);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
export type ToastType = 'success' | 'error' | 'info';
|
||||||
|
|
||||||
|
export interface ToastMessage {
|
||||||
|
id: number;
|
||||||
|
text: string;
|
||||||
|
type: ToastType;
|
||||||
|
}
|
||||||
|
|
||||||
|
class ToastState {
|
||||||
|
messages = $state<ToastMessage[]>([]);
|
||||||
|
private nextId = 0;
|
||||||
|
|
||||||
|
add(text: string, type: ToastType = 'info', duration: number = 3000) {
|
||||||
|
const id = this.nextId++;
|
||||||
|
this.messages.push({ id, text, type });
|
||||||
|
|
||||||
|
if (duration > 0) {
|
||||||
|
setTimeout(() => {
|
||||||
|
this.remove(id);
|
||||||
|
}, duration);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
remove(id: number) {
|
||||||
|
this.messages = this.messages.filter((msg) => msg.id !== id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const toastState = new ToastState();
|
||||||
|
|
||||||
|
// Helper export for simpler usage
|
||||||
|
export function addToast(text: string, type: ToastType = 'info', duration: number = 3000) {
|
||||||
|
toastState.add(text, type, duration);
|
||||||
|
}
|
||||||
@@ -1,6 +1,8 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { page } from '$app/stores';
|
import { page } from '$app/stores';
|
||||||
import { onMount } from 'svelte';
|
import { onMount } from 'svelte';
|
||||||
|
import Toast from '$lib/components/Toast.svelte';
|
||||||
|
|
||||||
let { children } = $props();
|
let { children } = $props();
|
||||||
|
|
||||||
let scrolled = $state(false);
|
let scrolled = $state(false);
|
||||||
@@ -58,6 +60,7 @@
|
|||||||
<span class="user-email">{$page.data.user?.email || 'Admin'}</span>
|
<span class="user-email">{$page.data.user?.email || 'Admin'}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<Toast />
|
||||||
</header>
|
</header>
|
||||||
|
|
||||||
<main id="pjax-container" style="padding-top: 58px; min-height: calc(100vh - 120px);">
|
<main id="pjax-container" style="padding-top: 58px; min-height: calc(100vh - 120px);">
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
import type { ActionData, PageData } from './$types';
|
import type { ActionData, PageData } from './$types';
|
||||||
|
import { addToast } from '$lib/stores/toast.svelte';
|
||||||
|
|
||||||
let { data, form } = $props<{ data: PageData; form: ActionData }>();
|
let { data, form } = $props<{ data: PageData; form: ActionData }>();
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (form?.success) {
|
||||||
|
addToast('操作成功!', 'success');
|
||||||
|
} else if (form?.error) {
|
||||||
|
addToast(form.error, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let isUploading = $state(false);
|
let isUploading = $state(false);
|
||||||
let fileInput: HTMLInputElement;
|
let fileInput: HTMLInputElement;
|
||||||
|
|
||||||
@@ -22,10 +31,10 @@
|
|||||||
const markdown = isImage ? `` : `[${file.filename}](${url})`;
|
const markdown = isImage ? `` : `[${file.filename}](${url})`;
|
||||||
|
|
||||||
navigator.clipboard.writeText(markdown).then(() => {
|
navigator.clipboard.writeText(markdown).then(() => {
|
||||||
alert('Markdown 链接已复制到剪贴板!');
|
addToast('Markdown 链接已复制到剪贴板!', 'success');
|
||||||
}).catch(err => {
|
}).catch(err => {
|
||||||
console.error('Failed to copy: ', err);
|
console.error('Failed to copy: ', err);
|
||||||
alert('复制失败,请手动复制。');
|
addToast('复制失败,请手动复制。', 'error');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -46,13 +55,7 @@
|
|||||||
<h2>文件管理</h2>
|
<h2>文件管理</h2>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if form?.error}
|
|
||||||
<div class="alert error">{form.error}</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if form?.success}
|
|
||||||
<div class="alert success">操作成功!</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="upload-zone">
|
<div class="upload-zone">
|
||||||
<form method="POST" action="?/upload" enctype="multipart/form-data" use:enhance={() => {
|
<form method="POST" action="?/upload" enctype="multipart/form-data" use:enhance={() => {
|
||||||
@@ -152,22 +155,7 @@
|
|||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
}
|
}
|
||||||
|
|
||||||
.alert {
|
|
||||||
padding: 12px 15px;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
border: 2px solid var(--text-color);
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert.error {
|
|
||||||
background-color: #ffcccc;
|
|
||||||
color: #cc0000;
|
|
||||||
}
|
|
||||||
|
|
||||||
.alert.success {
|
|
||||||
background-color: #d4edda;
|
|
||||||
color: #155724;
|
|
||||||
}
|
|
||||||
|
|
||||||
.upload-zone {
|
.upload-zone {
|
||||||
margin-bottom: 30px;
|
margin-bottom: 30px;
|
||||||
@@ -298,7 +286,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.delete-btn:hover {
|
.delete-btn:hover {
|
||||||
background: #ff4444;
|
background: #d9534f;
|
||||||
color: white;
|
color: white;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,18 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageData, ActionData } from './$types';
|
import type { PageData, ActionData } from './$types';
|
||||||
import { enhance } from '$app/forms';
|
import { enhance } from '$app/forms';
|
||||||
|
import { addToast } from '$lib/stores/toast.svelte';
|
||||||
|
|
||||||
let { data, form }: { data: PageData; form: ActionData } = $props();
|
let { data, form }: { data: PageData; form: ActionData } = $props();
|
||||||
let activeTab = $state('basic');
|
let activeTab = $state('basic');
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (form?.success) {
|
||||||
|
addToast('设置保存成功!', 'success');
|
||||||
|
} else if (form?.error) {
|
||||||
|
addToast(form.error, 'error');
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="admin-page-header">
|
<div class="admin-page-header">
|
||||||
@@ -16,17 +25,6 @@
|
|||||||
<button class="tab-btn {activeTab === 'system' ? 'active' : ''}" onclick={() => activeTab = 'system'}>系统与安全</button>
|
<button class="tab-btn {activeTab === 'system' ? 'active' : ''}" onclick={() => activeTab = 'system'}>系统与安全</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{#if form?.success}
|
|
||||||
<div class="success-alert">
|
|
||||||
设置保存成功!
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
{#if form?.error}
|
|
||||||
<div class="error-alert">
|
|
||||||
{form.error}
|
|
||||||
</div>
|
|
||||||
{/if}
|
|
||||||
|
|
||||||
<div class="settings-container">
|
<div class="settings-container">
|
||||||
{#if activeTab === 'basic'}
|
{#if activeTab === 'basic'}
|
||||||
@@ -166,23 +164,6 @@
|
|||||||
border-bottom-color: var(--accent-color, #419b38);
|
border-bottom-color: var(--accent-color, #419b38);
|
||||||
}
|
}
|
||||||
|
|
||||||
.success-alert {
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: transparent;
|
|
||||||
border-left: 4px solid #419b38;
|
|
||||||
color: #419b38;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.error-alert {
|
|
||||||
padding: 12px 16px;
|
|
||||||
background-color: transparent;
|
|
||||||
border-left: 4px solid #d9534f;
|
|
||||||
color: #d9534f;
|
|
||||||
margin-bottom: 30px;
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
.settings-container {
|
.settings-container {
|
||||||
display: grid;
|
display: grid;
|
||||||
|
|||||||
Reference in new issue
Block a user