feat(devices): 实现设备管理页面与添加设备功能
- 移除设备列表页面的溢出滚动样式 - 引入设备类型服务并获取设备类型选项 - 新增添加设备模态框与表单组件 - 实现设备创建接口与表单数据验证逻辑 - 添加网络接口与IP配置的动态表单管理 - 创建可复用的模态框组件支持表单提交交互
This commit is contained in:
17
src/lib/api/services/deviceTypesService.ts
Normal file
17
src/lib/api/services/deviceTypesService.ts
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
import { api } from '$lib/api/httpClient.ts';
|
||||||
|
|
||||||
|
import type { Options } from '$lib/types/api.ts';
|
||||||
|
|
||||||
|
export const deviceTypesService = {
|
||||||
|
getDeviceTypesOptions: async (token:string) => {
|
||||||
|
const result = await api.get<Options[]>('/device-types/options',{
|
||||||
|
headers:{Authorization: `${token}`}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.code != 200 || !result.data){
|
||||||
|
throw new Error(result.msg);
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
}
|
||||||
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>
|
||||||
238
src/lib/components/form/AddDevice.svelte
Normal file
238
src/lib/components/form/AddDevice.svelte
Normal file
@@ -0,0 +1,238 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
import type { CreateDeviceRequest, Options } from '$lib/types/api.ts';
|
||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
|
let {
|
||||||
|
deviceTypeOptions = [],
|
||||||
|
open = $bindable(false)
|
||||||
|
} = $props<{ deviceTypeOptions: Options[] , open: boolean}>();
|
||||||
|
|
||||||
|
|
||||||
|
log.info('device type options',deviceTypeOptions);
|
||||||
|
let formData = $state<CreateDeviceRequest>({
|
||||||
|
name: '',
|
||||||
|
typeId: null,
|
||||||
|
model: '',
|
||||||
|
manufacturer: '',
|
||||||
|
purchaseDate: '',
|
||||||
|
interfaces: []
|
||||||
|
});
|
||||||
|
|
||||||
|
// 错误信息状态
|
||||||
|
let errors = $state<Record<string, string>>({});
|
||||||
|
|
||||||
|
|
||||||
|
function addInterface() {
|
||||||
|
formData.interfaces.push({
|
||||||
|
name: '',
|
||||||
|
type: 1,
|
||||||
|
addressConfigs: []
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeInterface(index: number) {
|
||||||
|
formData.interfaces.splice(index, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
function addAddressConfig(interfaceIndex: number) {
|
||||||
|
formData.interfaces[interfaceIndex].addressConfigs.push({
|
||||||
|
isPrimary: false,
|
||||||
|
isDhcp: false,
|
||||||
|
ipAddress: ''
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeAddressConfig(interfaceIndex: number, configIndex: number) {
|
||||||
|
formData.interfaces[interfaceIndex].addressConfigs.splice(configIndex, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
function validate(): boolean {
|
||||||
|
let newErrors: Record<string, string> = {};
|
||||||
|
let isValid = true;
|
||||||
|
const macRegex = /^([0-9A-Fa-f]{2}[:-]){5}([0-9A-Fa-f]{2})$/;
|
||||||
|
const ipRegex = /^((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$/;
|
||||||
|
|
||||||
|
// 基础字段验证
|
||||||
|
if (!formData.name) newErrors['name'] = '设备名称不能为空';
|
||||||
|
if (!formData.typeId) newErrors['typeId'] = '设备类型ID不能为空';
|
||||||
|
if (!formData.model) newErrors['model'] = '设备型号不能为空';
|
||||||
|
if (!formData.manufacturer) newErrors['manufacturer'] = '厂商不能为空';
|
||||||
|
|
||||||
|
// 嵌套验证
|
||||||
|
formData.interfaces.forEach((iface, i) => {
|
||||||
|
if (!iface.name) newErrors[`iface_${i}_name`] = '接口名称不能为空';
|
||||||
|
if (iface.macAddress && !macRegex.test(iface.macAddress)) {
|
||||||
|
newErrors[`iface_${i}_mac`] = 'MAC地址格式错误';
|
||||||
|
}
|
||||||
|
|
||||||
|
iface.addressConfigs.forEach((addr, j) => {
|
||||||
|
if (addr.ipAddress && !ipRegex.test(addr.ipAddress)) {
|
||||||
|
newErrors[`iface_${i}_addr_${j}_ip`] = 'IP地址格式错误';
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
if (Object.keys(newErrors).length > 0) {
|
||||||
|
errors = newErrors;
|
||||||
|
isValid = false;
|
||||||
|
} else {
|
||||||
|
errors = {};
|
||||||
|
}
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 【关键】导出方法供父组件调用 ---
|
||||||
|
|
||||||
|
export function submitAndGetPayload(): CreateDeviceRequest | null {
|
||||||
|
if (validate()) {
|
||||||
|
// 返回深拷贝的数据,防止后续修改影响
|
||||||
|
return $state.snapshot(formData);
|
||||||
|
}
|
||||||
|
|
||||||
|
return null; // 验证失败
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<div class="space-y-8">
|
||||||
|
<div class="card bg-base-100 shadow-md border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<h2 class="card-title text-primary">基础信息</h2>
|
||||||
|
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-2 gap-4">
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">设备名称 *</span></div>
|
||||||
|
<input type="text" bind:value={formData.name} class="input input-bordered w-full {errors.name ? 'input-error' : ''}" />
|
||||||
|
{#if errors.name}<div class="label"><span class="label-text-alt text-error">{errors.name}</span></div>{/if}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">厂商 *</span></div>
|
||||||
|
<input type="text" bind:value={formData.manufacturer} class="input input-bordered w-full {errors.manufacturer ? 'input-error' : ''}" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">型号 *</span></div>
|
||||||
|
<input type="text" bind:value={formData.model} class="input input-bordered w-full {errors.model ? 'input-error' : ''}" />
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">设备类型 *</span></div>
|
||||||
|
|
||||||
|
<select
|
||||||
|
bind:value={formData.typeId}
|
||||||
|
class="select select-bordered w-full {errors.typeId ? 'select-error' : ''}"
|
||||||
|
>
|
||||||
|
<option disabled selected value={null}>请选择设备类型</option>
|
||||||
|
|
||||||
|
{#each deviceTypeOptions as opt (opt.value)}
|
||||||
|
<option value={Number(opt.value)}>{opt.label}</option>
|
||||||
|
{/each}
|
||||||
|
</select>
|
||||||
|
|
||||||
|
{#if errors.typeId}
|
||||||
|
<div class="label"><span class="label-text-alt text-error">{errors.typeId}</span></div>
|
||||||
|
{/if}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control w-full">
|
||||||
|
<div class="label"><span class="label-text">采购日期</span></div>
|
||||||
|
<input type="date" bind:value={formData.purchaseDate} class="input input-bordered w-full" />
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card bg-base-100 shadow-md border border-base-200">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="flex justify-between items-center mb-4">
|
||||||
|
<h2 class="card-title text-secondary">网络接口 (Interfaces)</h2>
|
||||||
|
<button class="btn btn-sm btn-outline btn-secondary" onclick={addInterface}>+ 添加接口</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#each formData.interfaces as iface, i (i)}
|
||||||
|
<div class="collapse collapse-arrow border border-base-300 bg-base-100 mb-2">
|
||||||
|
<input type="checkbox" checked={true} />
|
||||||
|
<div class="collapse-title text-lg font-medium flex justify-between pr-12">
|
||||||
|
<span>接口 #{i + 1}: {iface.name || '(未命名)'}</span>
|
||||||
|
<button class="btn btn-xs btn-error z-10" onclick={() => removeInterface(i)}>删除</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="collapse-content space-y-4">
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-3 gap-4 mt-2">
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text mb-1">接口名称 *</span>
|
||||||
|
<input type="text" bind:value={iface.name} class="input input-bordered input-sm {errors[`iface_${i}_name`] ? 'input-error' : ''}" />
|
||||||
|
{#if errors[`iface_${i}_name`]}<span class="text-xs text-error mt-1">{errors[`iface_${i}_name`]}</span>{/if}
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text mb-1">接口类型</span>
|
||||||
|
<select bind:value={iface.type} class="select select-bordered select-sm">
|
||||||
|
<option value={1}>物理口</option>
|
||||||
|
<option value={2}>聚合口</option>
|
||||||
|
<option value={3}>虚拟口</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
<label class="form-control">
|
||||||
|
<span class="label-text mb-1">MAC 地址</span>
|
||||||
|
<input type="text" bind:value={iface.macAddress} placeholder="XX:XX:XX..." class="input input-bordered input-sm {errors[`iface_${i}_mac`] ? 'input-error' : ''}" />
|
||||||
|
{#if errors[`iface_${i}_mac`]}<span class="text-xs text-error mt-1">{errors[`iface_${i}_mac`]}</span>{/if}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="bg-base-200 p-4 rounded-lg">
|
||||||
|
<div class="flex justify-between items-center mb-2">
|
||||||
|
<h4 class="text-sm font-bold opacity-70">IP / VLAN 配置</h4>
|
||||||
|
<button class="btn btn-xs btn-neutral" onclick={() => addAddressConfig(i)}>+ 添加 IP</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{#if iface.addressConfigs.length === 0}
|
||||||
|
<div class="text-xs text-center opacity-50 py-2">暂无 IP 配置</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
{#each iface.addressConfigs as addr, j (j)}
|
||||||
|
<div class="grid grid-cols-1 md:grid-cols-12 gap-2 items-end mb-2 border-b border-base-300 pb-2 last:border-0">
|
||||||
|
<div class="md:col-span-1">
|
||||||
|
<label class="label-text text-xs">VLAN</label>
|
||||||
|
<input type="number" bind:value={addr.vlanId} class="input input-bordered input-xs w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3">
|
||||||
|
<label class="label-text text-xs">IP地址</label>
|
||||||
|
<input type="text" bind:value={addr.ipAddress} class="input input-bordered input-xs w-full {errors[`iface_${i}_addr_${j}_ip`] ? 'input-error' : ''}" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-3">
|
||||||
|
<label class="label-text text-xs">子网掩码</label>
|
||||||
|
<input type="text" bind:value={addr.subnetMask} class="input input-bordered input-xs w-full" />
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-2 flex flex-col gap-1">
|
||||||
|
<label class="label cursor-pointer justify-start gap-2 p-0 h-6">
|
||||||
|
<input type="checkbox" bind:checked={addr.isPrimary} class="checkbox checkbox-xs" />
|
||||||
|
<span class="label-text text-xs">主IP</span>
|
||||||
|
</label>
|
||||||
|
<label class="label cursor-pointer justify-start gap-2 p-0 h-6">
|
||||||
|
<input type="checkbox" bind:checked={addr.isDhcp} class="checkbox checkbox-xs" />
|
||||||
|
<span class="label-text text-xs">DHCP</span>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div class="md:col-span-1 flex justify-end">
|
||||||
|
<button class="btn btn-square btn-xs btn-ghost text-error" onclick={() => removeAddressConfig(i, j)}>✕</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="join flex justify-center" >
|
||||||
|
<button class="btn btn-error join-item" onclick={onreset}>重置</button>
|
||||||
|
<button class="btn join-item" onclick={draft}>保存草稿</button>
|
||||||
|
<button class="btn btn-primary btn-wide join-item" onclick={submit}>提交</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
@@ -37,19 +37,43 @@ export interface DeviceResponse {
|
|||||||
remark: string;
|
remark: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface CreateDeviceRequest {
|
|
||||||
name: string;
|
|
||||||
model: string;
|
|
||||||
typeId: number;
|
|
||||||
locationId: number;
|
|
||||||
snmpCommunity: string;
|
|
||||||
manufacturer: string;
|
|
||||||
purchaseDate: Date;
|
|
||||||
status: number;
|
|
||||||
remark: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface Options {
|
export interface Options {
|
||||||
label: string;
|
label: string;
|
||||||
value: string;
|
value: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export interface InterfaceAddressConfigRequest {
|
||||||
|
vlanId?: number | null;
|
||||||
|
ipAddress?: string;
|
||||||
|
subnetMask?: string;
|
||||||
|
gatewayIp?: string;
|
||||||
|
broadcastAddress?: string;
|
||||||
|
isPrimary: boolean;
|
||||||
|
isDhcp: boolean;
|
||||||
|
mtu?: number | null;
|
||||||
|
dnsServers?: string[]; // 简化处理,前端可用逗号分隔字符串转换
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface NetworkInterfaceRequest {
|
||||||
|
name: string;
|
||||||
|
type: number | null; // 1:物理口, 2:聚合口, 3:虚拟口
|
||||||
|
macAddress?: string;
|
||||||
|
portSpeed?: number | null;
|
||||||
|
duplex?: number | null;
|
||||||
|
remark?: string;
|
||||||
|
addressConfigs: InterfaceAddressConfigRequest[];
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface CreateDeviceRequest {
|
||||||
|
name: string;
|
||||||
|
typeId: number | null;
|
||||||
|
locationId?: number | null;
|
||||||
|
model: string;
|
||||||
|
manufacturer: string;
|
||||||
|
snmpCommunity?: string;
|
||||||
|
purchaseDate?: string; // 对应 Java LocalDate (YYYY-MM-DD)
|
||||||
|
remark?: string;
|
||||||
|
interfaces: NetworkInterfaceRequest[];
|
||||||
|
}
|
||||||
20
src/routes/api/devices/+server.ts
Normal file
20
src/routes/api/devices/+server.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
|
|
||||||
|
export async function POST({ request }) {
|
||||||
|
const data = await request.json();
|
||||||
|
|
||||||
|
// 实际应用中:将 data 存入数据库
|
||||||
|
|
||||||
|
log.info('client request data', data)
|
||||||
|
|
||||||
|
return new Response(
|
||||||
|
JSON.stringify({ message: 'User created successfully', user: data }),
|
||||||
|
{
|
||||||
|
status: 201, // 201 Created
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@
|
|||||||
|
|
||||||
<AppHeader />
|
<AppHeader />
|
||||||
|
|
||||||
<main class="flex-1 overflow-y-auto px-4">
|
<main class="flex-1 px-4">
|
||||||
{@render children()}
|
{@render children()}
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import type { PageServerLoad } from './$types';
|
|||||||
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
import { COOKIE_TOKEN_KEY } from '$lib/components/constants/cookiesConstants.ts';
|
||||||
import { redirect } from '@sveltejs/kit';
|
import { redirect } from '@sveltejs/kit';
|
||||||
import { deviceService } from '$lib/api/services/deviceService.ts';
|
import { deviceService } from '$lib/api/services/deviceService.ts';
|
||||||
|
import { deviceTypesService } from '$lib/api/services/deviceTypesService.ts';
|
||||||
|
|
||||||
export const load:PageServerLoad = async ({ cookies }) => {
|
export const load:PageServerLoad = async ({ cookies }) => {
|
||||||
|
|
||||||
@@ -12,11 +13,12 @@ export const load:PageServerLoad = async ({ cookies }) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const result = deviceService.getAllDevices({ page: 1, size: 10 ,token:token});
|
const result = deviceService.getAllDevices({ page: 1, size: 10 ,token:token});
|
||||||
|
const options = deviceTypesService.getDeviceTypesOptions( token);
|
||||||
return {
|
return {
|
||||||
streamed:{
|
streamed:{
|
||||||
result: {
|
result: {
|
||||||
list: result
|
list: result,
|
||||||
|
options: options
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,13 +1,17 @@
|
|||||||
<script>
|
<script>
|
||||||
import { resolve } from '$app/paths';
|
import { resolve } from '$app/paths';
|
||||||
import { log } from '$lib/log.ts';
|
|
||||||
import DevicesTable from '$lib/components/table/DevicesTable.svelte';
|
import DevicesTable from '$lib/components/table/DevicesTable.svelte';
|
||||||
|
import Modal from '$lib/components/Modal.svelte';
|
||||||
|
import AddDevice from '$lib/components/form/AddDevice.svelte';
|
||||||
|
import { log } from '$lib/log.ts';
|
||||||
|
|
||||||
const {data} = $props();
|
const {data} = $props();
|
||||||
|
let isOpen = $state(false)
|
||||||
|
|
||||||
|
let formRef = $state();
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
<div class="flex-1 h-full flex flex-col">
|
||||||
<div class="flex justify-between items-center ">
|
<div class="flex justify-between items-center ">
|
||||||
<p class="font-bold">设备管理</p>
|
<p class="font-bold">设备管理</p>
|
||||||
<div class="breadcrumbs ">
|
<div class="breadcrumbs ">
|
||||||
@@ -25,10 +29,38 @@ const {data} = $props();
|
|||||||
<p class="text-center">请稍后...</p>
|
<p class="text-center">请稍后...</p>
|
||||||
</div>
|
</div>
|
||||||
{:then list}
|
{:then list}
|
||||||
|
{#if list.total > 0 }
|
||||||
<div class="overflow-x-auto">
|
<div class="overflow-x-auto">
|
||||||
<DevicesTable devices={list} />
|
<DevicesTable
|
||||||
|
bind:open={isOpen}
|
||||||
|
devices={list} />
|
||||||
</div>
|
</div>
|
||||||
|
{:else }
|
||||||
|
<div class="flex-1 w-full flex justify-center items-center 需要占满高度">
|
||||||
|
<div class="select-none text-center">
|
||||||
|
<p class="mb-10">暂无数据</p>
|
||||||
|
<button class="btn btn-primary" onclick={()=>{isOpen = true}}>添加设备</button>
|
||||||
|
<Modal bind:open={isOpen}
|
||||||
|
title="添加设备"
|
||||||
|
width="100%"
|
||||||
|
footer={null}
|
||||||
|
>
|
||||||
|
{#await data.streamed.result.options}
|
||||||
|
<div class="">
|
||||||
|
<p class="text-center">正在加载设备列表...</p>
|
||||||
|
<p class="text-center">请稍后...</p>
|
||||||
|
</div>
|
||||||
|
{:then options}
|
||||||
|
<AddDevice deviceTypeOptions={options} />
|
||||||
|
{:catch error}
|
||||||
|
{log.error(error)}
|
||||||
|
{/await}
|
||||||
|
</Modal>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
{:catch error}
|
{:catch error}
|
||||||
<p class="text-center">{error}</p>
|
<p class="text-center">{error}</p>
|
||||||
<p class="text-center">请稍后...</p>
|
<p class="text-center">请稍后...</p>
|
||||||
{/await}
|
{/await}
|
||||||
|
</div>
|
||||||
Reference in New Issue
Block a user