- 移除设备列表页面的溢出滚动样式 - 引入设备类型服务并获取设备类型选项 - 新增添加设备模态框与表单组件 - 实现设备创建接口与表单数据验证逻辑 - 添加网络接口与IP配置的动态表单管理 - 创建可复用的模态框组件支持表单提交交互
171 lines
3.7 KiB
Svelte
171 lines
3.7 KiB
Svelte
<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> |