feat: 添加通用确认对话框组件及状态管理
This commit is contained in:
1 parent
b96df15179
commit
ffde675f3a
3 files changed
+180
No files matched your search
@@ -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>
|
||||
@@ -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();
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user