feat(devices): 实现设备管理页面与添加设备功能

- 移除设备列表页面的溢出滚动样式
- 引入设备类型服务并获取设备类型选项
- 新增添加设备模态框与表单组件
- 实现设备创建接口与表单数据验证逻辑
- 添加网络接口与IP配置的动态表单管理
- 创建可复用的模态框组件支持表单提交交互
This commit is contained in:
Chaos
2025-12-01 07:08:20 +08:00
parent e2374571d7
commit f973284140
8 changed files with 541 additions and 37 deletions

View 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>