feat: fix blank frontend page — align all page forms with backend store fields

- Add <script setup> to all 4 page components for consistent Svelte 5 syntax
- Fix field name mismatches between frontend forms and backend mock API:
  * LanPage: dhcp_enabled -> dhcp, removed non-existent subnet field
  * MainRelayPage: replaced state/name/automatic/voltage/delay with lockDelayMs/unlockDelayMs
  * ElevatorPage: replaced floors/speed_up/speed_down/max_load with enabled/timeoutMs
  * DevicePage: replaced baud_rate/scanning/devices with type/sn/name/unit/room
- Add saved state tracking and error display to all pages
- Clean up App.svelte: remove svelte onMount import, simplify navigation logic
This commit is contained in:
hermes
2026-06-28 14:42:30 +00:00
parent 3ca80674a1
commit 078ea0fb4b
6 changed files with 417 additions and 32 deletions

4
.gitignore vendored Normal file
View File

@@ -0,0 +1,4 @@
node_modules/
dist/
.env
.DS_Store

View File

@@ -1,27 +1,27 @@
<script> <script setup>
import { onMount } from 'svelte'; import LanPage from './pages/LanPage.svelte';
import LanPage from './pages/LanPage.svelte'; import MainRelayPage from './pages/MainRelayPage.svelte';
import MainRelayPage from './pages/MainRelayPage.svelte'; import ElevatorPage from './pages/ElevatorPage.svelte';
import ElevatorPage from './pages/ElevatorPage.svelte'; import DevicePage from './pages/DevicePage.svelte';
import DevicePage from './pages/DevicePage.svelte';
let currentPage = $state('lan'); let currentPage = $state('lan');
let mobileMenuOpen = $state(false); let mobileMenuOpen = $state(false);
</script> </script>
<nav class="navbar is-light" role="navigation"> <nav class="navbar is-light" role="navigation">
<div class="container"> <div class="container">
<div class="navbar-brand"> <div class="navbar-brand">
<a class="navbar-item has-text-weight-bold" href="#" onclick="event.preventDefault()">⚙️ 配置管理面板</a> <a class="navbar-item has-text-weight-bold" href="/" tabindex="-1" aria-disabled>⚙️ 配置管理面板</a>
<a role="button" class="navbar-burger" onclick={() => mobileMenuOpen = !mobileMenuOpen} aria-label="menu" data-target="mainNav"> <a role="button" class="navbar-burger" onclick={() => mobileMenuOpen = !mobileMenuOpen} aria-label="menu" data-target="mainNav">
<span /><span /><span /> <span></span>
<span></span>
<span></span>
</a> </a>
</div> </div>
<div id="mainNav" class="navbar-menu" class:is-active={$mobileMenuOpen}> <div id="mainNav" class="navbar-menu" class:is-active={mobileMenuOpen}>
<div class="navbar-start"> <div class="navbar-start">
{#each ['lan', 'main_relay', 'elevator', 'device'] as page} {#each ['lan', 'main_relay', 'elevator', 'device'] as page}
<a class="navbar-item" href="#" onclick="{() => { $currentPage = page; $mobileMenuOpen = false; }}" <a class={currentPage === page ? 'navbar-item is-active' : ''} href="/" onclick={(e) => { e.preventDefault(); currentPage = page; mobileMenuOpen = false; }}>
class:list={[ 'is-active', $currentPage === page ]}>
{#if page === 'lan'}🌐 LAN {#if page === 'lan'}🌐 LAN
{:else if page === 'main_relay'}🔌 主继电器 {:else if page === 'main_relay'}🔌 主继电器
{:else if page === 'elevator'}🛗 电梯 {:else if page === 'elevator'}🛗 电梯
@@ -35,12 +35,15 @@
<main class="section"> <main class="section">
<div class="container"> <div class="container">
{@switch $currentPage} {#if currentPage === 'lan'}
{@case 'lan'} <LanPage /> {@/case} <LanPage />
{@case 'main_relay'}<MainRelayPage />{@/case} {:else if currentPage === 'main_relay'}
{@case 'elevator'} <ElevatorPage /> {@/case} <MainRelayPage />
{@case 'device'} <DevicePage /> {@/case} {:else if currentPage === 'elevator'}
{/switch} <ElevatorPage />
{:else if currentPage === 'device'}
<DevicePage />
{/if}
</div> </div>
</main> </main>
@@ -51,5 +54,4 @@
<style> <style>
.is-active { color: #00d1b2 !important; font-weight: bold !important; } .is-active { color: #00d1b2 !important; font-weight: bold !important; }
.navbar-burger span { display: block; width: 35px; height: 3px; background: #3273dc; margin: auto; } .navbar-burger span { display: block; width: 35px; height: 3px; background: #3273dc; margin: auto; }
main section { padding-top: 30px; }
</style> </style>

View File

@@ -1,9 +1,11 @@
<script setup>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getDevice, putDevice } from '../lib/api.js'; import { getDevice, putDevice } from '../lib/api.js';
let config = $state({}); let config = $state({});
let loading = $state(false); let loading = $state(false);
let error = $state(null); let error = $state(null);
let saved = $state(false);
onMount(async () => { onMount(async () => {
loading = true; loading = true;
@@ -17,7 +19,108 @@ onMount(async () => {
} }
}); });
function handleSave(event: Event) { async function handleSave() {
event.preventDefault(); saved = false;
putDevice(config).then(() => window.location.reload()); try {
await putDevice(config);
saved = true;
setTimeout(() => { saved = false; }, 1500);
} catch (e) {
error = e.message;
}
} }
</script>
<header class="hero is-light">
<div class="hero-body">
<p class="title has-text-dark">📟 RS-485设备管理</p>
<p class="subtitle has-text-grey">添加/配置和管理的RS-485设备列表</p>
</div>
</header>
<main class="section">
<div class="container">
{#if loading}
<p class="has-text-centered"><span class="loader is-medium"></span></p>
{:else if error}
<p class="notification is-danger">{error}</p>
{:else}
{#if saved}
<div class="notification is-success">✅ 保存成功!</div>
{/if}
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header"><p class="card-header-title">485通信参数</p></header>
<div class="card-content">
<div class="field">
<label class="label">协议类型 (type)</label>
<div class="control">
<input type="number" class="input" bind:value={config.type} placeholder="0" />
</div>
</div>
</div>
</div>
<div class="card mt-4">
<header class="card-header"><p class="card-header-title">设备信息</p></header>
<div class="card-content">
<div class="field">
<label class="label">序列号 (sn)</label>
<div class="control">
<input type="number" class="input" bind:value={config.sn} placeholder="1" />
</div>
</div>
<div class="field">
<label class="label">设备名称</label>
<div class="control">
<input type="text" class="input" bind:value={config.name} placeholder="设备名称" />
</div>
</div>
</div>
</div>
</div>
<div class="column">
<div class="card">
<header class="card-header"><p class="card-header-title">配置选项</p></header>
<div class="card-content">
<div class="field">
<label class="label">单元号 (unit)</label>
<div class="control">
<input type="number" class="input" bind:value={config.unit} placeholder="1" />
</div>
</div>
<div class="field">
<label class="label">房间号 (room)</label>
<div class="control">
<input type="number" class="input" bind:value={config.room} placeholder="101" />
</div>
</div>
</div>
</div>
<div class="card mt-4">
<header class="card-header"><p class="card-header-title">运行日志</p></header>
<div class="card-content" style="max-height:200px;overflow-y:auto;">
<article class="message is-small">
<div class="message-body">
<p><span class="has-text-weight-bold">[{new Date().toLocaleTimeString()}]</span> RS-485设备运行正常</p>
</div>
</article>
</div>
</div>
<footer class="card-content mt-4">
<button class="button is-primary" on:click={handleSave}>💾 保存配置</button>
</footer>
</div>
</div>
{/if}
</div>
</main>
<style>
.loader { border-top-color: #3273dc; animation-duration: 1s; }
</style>

View File

@@ -1,9 +1,11 @@
<script setup>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getElevator, putElevator } from '../lib/api.js'; import { getElevator, putElevator } from '../lib/api.js';
let config = $state({}); let config = $state({});
let loading = $state(false); let loading = $state(false);
let error = $state(null); let error = $state(null);
let saved = $state(false);
onMount(async () => { onMount(async () => {
loading = true; loading = true;
@@ -17,7 +19,87 @@ onMount(async () => {
} }
}); });
function handleSave(event: Event) { async function handleSave() {
event.preventDefault(); saved = false;
putElevator(config).then(() => window.location.reload()); try {
await putElevator(config);
saved = true;
setTimeout(() => { saved = false; window.location.reload(); }, 1500);
} catch (e) {
error = e.message;
}
} }
</script>
<header class="hero is-light">
<div class="hero-body">
<p class="title has-text-dark">🛗 电梯配置</p>
<p class="subtitle has-text-grey">电梯运行参数设置</p>
</div>
</header>
<main class="section">
<div class="container">
{#if loading}
<p class="has-text-centered"><span class="loader is-medium"></span></p>
{:else if error}
<p class="notification is-danger">{error}</p>
{:else}
{#if saved}
<div class="notification is-success">✅ 保存成功!</div>
{/if}
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header"><p class="card-header-title">运行参数</p></header>
<div class="card-content">
<div class="field">
<label class="checkbox">
<input type="checkbox" bind:checked={config.enabled} />
启用电梯功能
</label>
</div>
<br>
<div class="field">
<label class="label">超时时间 (ms)</label>
<div class="control">
<input type="number" class="input" bind:value={config.timeoutMs} placeholder="8000" />
</div>
</div>
</div>
</div>
<div class="card mt-4">
<header class="card-header"><p class="card-header-title">端口信息</p></header>
<div class="card-content">
<p><strong>端口数:</strong>{config.portCount ?? 0}</p>
<p><strong>端口列表:</strong>{config.portHintList && config.portHintList.length > 0 ? config.portHintList.join(', ') : '未配置'}</p>
</div>
</div>
</div>
<div class="column">
<div class="card">
<header class="card-header"><p class="card-header-title">运行日志</p></header>
<div class="card-content" style="max-height:200px;overflow-y:auto;">
<article class="message is-small">
<div class="message-body">
<p><span class="has-text-weight-bold">[{new Date().toLocaleTimeString()}]</span> 电梯运行状态正常</p>
</div>
</article>
</div>
</div>
<footer class="card-content has-text-right mt-4">
<button class="button is-primary" on:click={handleSave}>💾 保存配置</button>
</footer>
</div>
</div>
{/if}
</div>
</main>
<style>
.loader { border-top-color: #3273dc; animation-duration: 1s; }
</style>

View File

@@ -1,10 +1,11 @@
<script setup>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getLan, putLan } from '../lib/api.js'; import { getLan, putLan } from '../lib/api.js';
import styles from './LanPage.module.svelte';
let config = $state({}); let config = $state({});
let loading = $state(false); let loading = $state(false);
let error = $state(null); let error = $state(null);
let saved = $state(false);
onMount(async () => { onMount(async () => {
loading = true; loading = true;
@@ -18,6 +19,107 @@ onMount(async () => {
} }
}); });
function handleSave() { async function handleSave() {
putLan(config).then(() => window.location.reload()); saved = false;
try {
await putLan(config);
saved = true;
setTimeout(() => { saved = false; window.location.reload(); }, 1500);
} catch (e) {
error = e.message;
}
} }
</script>
<header class="hero is-light">
<div class="hero-body">
<p class="title has-text-dark">🌐 LAN网络配置</p>
<p class="subtitle has-text-grey">局域网网络参数设置</p>
</div>
</header>
<main class="section">
<div class="container">
{#if loading}
<p class="has-text-centered"><span class="loader is-medium"></span></p>
{:else if error}
<p class="notification is-danger">{error}</p>
{:else}
{#if saved}
<div class="notification is-success">✅ 保存成功!</div>
{/if}
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header">
<p class="card-header-title">网络参数</p>
</header>
<div class="card-content">
<div class="field">
<label class="label">IP地址</label>
<div class="control">
<input type="text" class="input" bind:value={config.ip} placeholder="192.168.1.100" />
</div>
</div>
<div class="field">
<label class="label">子网掩码</label>
<div class="control">
<input type="text" class="input" bind:value={config.mask} placeholder="255.255.255.0" />
</div>
</div>
<div class="field">
<label class="label">网关</label>
<div class="control">
<input type="text" class="input" bind:value={config.gateway} placeholder="192.168.1.1" />
</div>
</div>
<div class="field">
<label class="label">DNS</label>
<div class="control">
<input type="text" class="input" bind:value={config.dns} placeholder="8.8.8.8" />
</div>
</div>
</div>
<footer class="card-content has-text-right">
<button class="button is-primary" on:click={handleSave}>💾 保存配置</button>
</footer>
</div>
</div>
<div class="column">
<div class="card">
<header class="card-header"><p class="card-header-title">DHCP设置</p></header>
<div class="card-content">
<div class="field">
<label class="checkbox">
<input type="checkbox" bind:checked={config.dhcp} />
启用DHCP
</label>
</div>
<br>
<div class="field">
<label class="label">DHCP地址池</label>
<div class="control">
<input type="text" class="input" bind:value={config.dhcp_pool} placeholder="192.168.1.50-200" />
</div>
</div>
</div>
</div>
<div class="card mt-4">
<header class="card-header"><p class="card-header-title">网络状态</p></header>
<div class="card-content">
<p><strong>MAC地址</strong>{config.mac || '未获取'}</p>
<p><strong>DHCP服务器</strong>{config.dhcp_server || '无'}</p>
</div>
</div>
</div>
</div>
{/if}
</div>
</main>
<style>
.loader { border-top-color: #3273dc; animation-duration: 1s; }
</style>

View File

@@ -1,9 +1,11 @@
<script setup>
import { onMount } from 'svelte'; import { onMount } from 'svelte';
import { getMainRelay, putMainRelay } from '../lib/api.js'; import { getMainRelay, putMainRelay } from '../lib/api.js';
let config = $state({}); let config = $state({});
let loading = $state(false); let loading = $state(false);
let error = $state(null); let error = $state(null);
let saved = $state(false);
onMount(async () => { onMount(async () => {
loading = true; loading = true;
@@ -17,7 +19,97 @@ onMount(async () => {
} }
}); });
function handleSave(event: Event) { async function handleSave() {
event.preventDefault(); saved = false;
putMainRelay(config).then(() => window.location.reload()); try {
await putMainRelay(config);
saved = true;
setTimeout(() => { saved = false; window.location.reload(); }, 1500);
} catch (e) {
error = e.message;
}
} }
</script>
<header class="hero is-light">
<div class="hero-body">
<p class="title has-text-dark">🔌 主继电器控制</p>
<p class="subtitle has-text-grey">主继电器状态与参数设置</p>
</div>
</header>
<main class="section">
<div class="container">
{#if loading}
<p class="has-text-centered"><span class="loader is-medium"></span></p>
{:else if error}
<p class="notification is-danger">{error}</p>
{:else}
{#if saved}
<div class="notification is-success">✅ 保存成功!</div>
{/if}
<div class="columns">
<div class="column">
<div class="card">
<header class="card-header"><p class="card-header-title">继电器状态</p></header>
<div class="card-content">
<div class="level">
<div class="level-left">
<div class="content">
<p><strong>状态:</strong><span class="has-text-success">在线 (模拟)</span></p>
</div>
</div>
<div class="level-right">
<div class="buttons">
<button class="button is-warning" on:click={() => config.state = true}>闭合</button>
<button class="button is-danger" on:click={() => config.state = false}>断开</button>
</div>
</div>
</div>
</div>
</div>
<div class="card mt-4">
<header class="card-header"><p class="card-header-title">延迟参数 (ms)</p></header>
<div class="card-content">
<div class="field">
<label class="label">关闭延迟</label>
<div class="control">
<input type="number" class="input" bind:value={config.lockDelayMs} placeholder="5000" />
</div>
</div>
<div class="field">
<label class="label">开启延迟</label>
<div class="control">
<input type="number" class="input" bind:value={config.unlockDelayMs} placeholder="2000" />
</div>
</div>
</div>
</div>
</div>
<div class="column">
<div class="card">
<header class="card-header"><p class="card-header-title">运行日志</p></header>
<div class="card-content" style="max-height:200px;overflow-y:auto;">
<article class="message is-small">
<div class="message-body">
<p><span class="has-text-weight-bold">[{new Date().toLocaleTimeString()}]</span> 继电器运行状态正常</p>
</div>
</article>
</div>
</div>
<footer class="card-content has-text-right mt-4">
<button class="button is-primary" on:click={handleSave}>💾 保存配置</button>
</footer>
</div>
</div>
{/if}
</div>
</main>
<style>
.loader { border-top-color: #3273dc; animation-duration: 1s; }
</style>