- 添加设备创建接口,支持认证检查和重定向 - 更新设备服务以使用新的请求体类型 - 修改表单组件以支持设备创建请求和响应处理 - 扩展HTTP客户端和类型定义以支持对象类型的请求体 - 添加调试日志记录API响应信息 - 更新按钮点击事件处理函数以触发设备创建流程
265 lines
9.3 KiB
Svelte
265 lines
9.3 KiB
Svelte
<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()) {
|
|
const snapshot = $state.snapshot(formData);
|
|
|
|
fetch('/api/devices', {
|
|
method: 'POST',
|
|
headers: {},
|
|
body: JSON.stringify(snapshot)
|
|
}).then(res => res.json())
|
|
.then(res => {
|
|
if (res.ok) {
|
|
log.info('设备创建成功', res);
|
|
open = false;
|
|
} else {
|
|
log.error('设备创建失败', res);
|
|
}
|
|
})
|
|
.catch(err => {
|
|
log.error('设备创建失败', err);
|
|
});
|
|
return snapshot;
|
|
}
|
|
|
|
return null; // 验证失败
|
|
|
|
|
|
}
|
|
|
|
const handleSubmit = () => {
|
|
const payload = submitAndGetPayload();
|
|
if (payload) {
|
|
log.info('设备创建成功', payload);
|
|
open = false;
|
|
}
|
|
};
|
|
</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={handleSubmit}>提交</button>
|
|
</div>
|
|
</div> |