Files
chaos_it/src/lib/components/Modal.svelte
Chaos f973284140 feat(devices): 实现设备管理页面与添加设备功能
- 移除设备列表页面的溢出滚动样式
- 引入设备类型服务并获取设备类型选项
- 新增添加设备模态框与表单组件
- 实现设备创建接口与表单数据验证逻辑
- 添加网络接口与IP配置的动态表单管理
- 创建可复用的模态框组件支持表单提交交互
2025-12-01 07:08:20 +08:00

171 lines
3.7 KiB
Svelte
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<script lang="ts">
interface Props {
open?: boolean;
title?: string;
width?: string | number;
centered?: boolean;
confirmLoading?: boolean;
footer?: import('svelte').Snippet | null | undefined;
okText?: string;
cancelText?: string;
maskClosable?: boolean;
destroyOnHidden?: boolean;
children?: import('svelte').Snippet;
titleSlot?: import('svelte').Snippet;
footerSlot?: import('svelte').Snippet ;
onOk?: () => Promise<void> | void;
onCancel?: () => void;
}
let {
open = $bindable(false),
title = '',
width = 520,
footer = undefined,
centered = true,
confirmLoading = false,
okText = '确定',
cancelText = '取消',
maskClosable = true,
destroyOnHidden = false,
children,
titleSlot,
footerSlot,
onOk,
onCancel
}: Props = $props();
let dialog: HTMLDialogElement;
let internalLoading = $state(false);
// 1. 唯一的 DOM 操作入口:$effect
// 所有的开关逻辑都通过改变 open 变量来触发这里
$effect(() => {
if (!dialog) return;
if (open && !dialog.open) {
dialog.showModal();
}
else if (!open && dialog.open) {
dialog.close();
}
});
// 2. 处理原生关闭(仅用于处理 ESC 键等浏览器原生行为)
function handleNativeClose() {
// 只有当状态认为它是“开”,但 DOM 变成了“关”时,才需要同步
if (open) {
open = false;
onCancel?.();
}
}
// 3. 按钮点击:只修改状态
function handleCancel() {
if (internalLoading) return;
// 先触发回调,再关闭
onCancel?.();
open = false;
}
async function handleOk() {
if (internalLoading) return;
if (onOk) {
const result = onOk();
if (result instanceof Promise) {
internalLoading = true;
try {
await result;
open = false; // 成功后,修改状态来关闭
} catch (e) {
console.error('Modal ok error', e);
} finally {
internalLoading = false;
}
} else {
open = false; // 修改状态来关闭
}
} else {
open = false; // 修改状态来关闭
}
}
function handleBackdropClick(e: MouseEvent) {
if (maskClosable && e.target === dialog) {
// 同样,只修改状态
onCancel?.();
open = false;
}
}
let widthStyle = $derived(typeof width === 'number' ? `max-width: ${width}px` : `max-width: ${width}`);
</script>
<dialog
bind:this={dialog}
class="modal"
class:modal-bottom={!centered}
class:modal-middle={centered}
onclose={handleNativeClose}
onclick={handleBackdropClick}
>
<div class="modal-box" style={widthStyle}>
<!-- 移除 stopPropagation改用新的 handleCancel -->
<button
class="btn btn-sm btn-circle btn-ghost absolute right-2 top-2"
onclick={handleCancel}
>✕</button>
{#if title || titleSlot}
<h3 class="font-bold text-lg mb-4">
{#if titleSlot}
{@render titleSlot()}
{:else}
{title}
{/if}
</h3>
{/if}
<div class="py-4">
{#if destroyOnHidden && !open}
<!-- Destroyed -->
{:else if children}
{@render children()}
{/if}
</div>
<div class="modal-action">
{#if footer === undefined}
{#if footerSlot}
{@render footerSlot()}
{:else}
<button class="btn" disabled={internalLoading || confirmLoading}
onclick={handleCancel}
>
{cancelText}
</button>
<button
class="btn btn-primary"
class:loading={internalLoading || confirmLoading}
disabled={internalLoading || confirmLoading}
onclick={handleOk}
>
{#if internalLoading || confirmLoading}
<span class="loading loading-spinner"></span>
{/if}
{okText}
</button>
{/if}
{:else if footer === null}
<!-- No footer -->
{/if}
</div>
</div>
<form method="dialog" class="modal-backdrop">
<button>close</button>
</form>
</dialog>