feat: 添加通用确认对话框组件及状态管理

This commit is contained in:
seahi committed 2026-08-15 11:46:09 +08:00
1 parent b96df15179
commit ffde675f3a
3 files changed
+180

No files matched your search

+142
View File
@@ -0,0 +1,142 @@
<script lang="ts">
import { confirmState } from '$lib/stores/confirm.svelte';
import { fade, scale } from 'svelte/transition';
import { onMount, onDestroy } from 'svelte';
// Handle escape key to cancel
function handleKeydown(e: KeyboardEvent) {
if (confirmState.isOpen && e.key === 'Escape') {
confirmState.cancel();
}
}
onMount(() => {
window.addEventListener('keydown', handleKeydown);
});
onDestroy(() => {
if (typeof window !== 'undefined') {
window.removeEventListener('keydown', handleKeydown);
}
});
</script>
{#if confirmState.isOpen}
<!-- svelte-ignore a11y_click_events_have_key_events -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<div
class="confirm-backdrop"
transition:fade={{ duration: 150 }}
onclick={(e) => {
if (e.target === e.currentTarget) confirmState.cancel();
}}
>
<div
class="confirm-dialog"
transition:scale={{ duration: 150, start: 0.95 }}
role="dialog"
aria-modal="true"
tabindex="-1"
>
<div class="confirm-content">
<p>{confirmState.message}</p>
</div>
<div class="confirm-actions">
<button class="btn btn-secondary" onclick={() => confirmState.cancel()}> 取消 </button>
<button class="btn btn-danger" onclick={() => confirmState.confirm()}> 确定 </button>
</div>
</div>
</div>
{/if}
<style>
.confirm-backdrop {
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
background: rgba(0, 0, 0, 0.4);
backdrop-filter: blur(2px);
display: flex;
align-items: center;
justify-content: center;
z-index: 9999;
}
.confirm-dialog {
background: var(--bg-color, #ffffff);
color: var(--text-color, #000000);
border: 2px solid var(--text-color, #000000);
border-radius: 8px;
width: 90%;
max-width: 400px;
box-shadow: 0 10px 25px rgba(0, 0, 0, 0.1);
overflow: hidden;
display: flex;
flex-direction: column;
}
.confirm-content {
padding: 24px;
font-size: 1.05rem;
line-height: 1.5;
}
.confirm-content p {
margin: 0;
}
.confirm-actions {
display: flex;
justify-content: flex-end;
gap: 12px;
padding: 16px 24px;
background: rgba(0, 0, 0, 0.02);
border-top: 1px solid var(--border-color, rgba(0, 0, 0, 0.08));
}
.btn {
padding: 8px 16px;
font-size: 0.9rem;
font-weight: 600;
border-radius: 4px;
cursor: pointer;
font-family: inherit;
transition: all 0.2s;
}
.btn-secondary {
background: transparent;
color: var(--text-color, #000000);
border: 1px solid var(--border-color, rgba(0, 0, 0, 0.2));
}
.btn-secondary:hover {
background: rgba(0, 0, 0, 0.05);
}
.btn-danger {
background: #dc3545;
color: white;
border: 1px solid #dc3545;
}
.btn-danger:hover {
background: #c82333;
border-color: #bd2130;
}
:global(.dark) .confirm-backdrop {
background: rgba(0, 0, 0, 0.6);
}
:global(.dark) .confirm-dialog {
background: var(--bg-color, #1a1a1a);
border-color: var(--border-color, #333333);
}
:global(.dark) .confirm-actions {
background: rgba(255, 255, 255, 0.02);
}
</style>
+36
View File
@@ -0,0 +1,36 @@
import { untrack } from 'svelte';
class ConfirmState {
isOpen = $state(false);
message = $state('');
private resolve: ((value: boolean) => void) | null = null;
async show(message: string): Promise<boolean> {
this.message = message;
this.isOpen = true;
return new Promise((res) => {
this.resolve = res;
});
}
confirm() {
untrack(() => {
if (this.resolve) this.resolve(true);
this.close();
});
}
cancel() {
untrack(() => {
if (this.resolve) this.resolve(false);
this.close();
});
}
private close() {
this.isOpen = false;
this.resolve = null;
}
}
export const confirmState = new ConfirmState();
+2
View File
@@ -3,6 +3,7 @@
import { page } from '$app/stores';
import { onMount } from 'svelte';
import Toast from '$lib/components/Toast.svelte';
import ConfirmDialog from '$lib/components/ConfirmDialog.svelte';
let { children } = $props();
@@ -102,6 +103,7 @@
</main>
<Toast />
<ConfirmDialog />
<style>
.admin-container {