feat(devices): 实现设备管理页面与添加设备功能
- 移除设备列表页面的溢出滚动样式 - 引入设备类型服务并获取设备类型选项 - 新增添加设备模态框与表单组件 - 实现设备创建接口与表单数据验证逻辑 - 添加网络接口与IP配置的动态表单管理 - 创建可复用的模态框组件支持表单提交交互
This commit is contained in:
171
src/lib/components/Modal.svelte
Normal file
171
src/lib/components/Modal.svelte
Normal file
@@ -0,0 +1,171 @@
|
||||
<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>
|
||||
Reference in New Issue
Block a user