初始提交: 配置管理面板 (Vite 8 + Svelte 5 + Bulma)
This commit is contained in:
13
index.html
Normal file
13
index.html
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||||
|
<title>配置管理</title>
|
||||||
|
<link rel="icon" href="data:image/svg+xml,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><text y='.9em' font-size='90'>⚙</text></svg>" />
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
110
mock-server.js
Normal file
110
mock-server.js
Normal file
@@ -0,0 +1,110 @@
|
|||||||
|
// Mock backend server (port 3001) - matches /config API from api.rest
|
||||||
|
import { createServer } from 'node:http';
|
||||||
|
import { fileURLToPath } from 'node:url';
|
||||||
|
import path from 'node:path';
|
||||||
|
|
||||||
|
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||||
|
|
||||||
|
// In-memory mock data store
|
||||||
|
const store = {
|
||||||
|
lan: { dhcp: false, ip: '192.168.6.51', mask: '255.255.255.0', gateway: '192.168.6.1', dns: '8.8.8.8' },
|
||||||
|
main_relay: { lockDelayMs: 5000, unlockDelayMs: 2000 },
|
||||||
|
elevator: { enabled: true, timeoutMs: 8000, portHintList: [], portCount: 0 },
|
||||||
|
device: { type: 0, sn: 1, name: 'test', unit: 1, room: 101 },
|
||||||
|
};
|
||||||
|
|
||||||
|
const server = createServer((req, res) => {
|
||||||
|
const url = new URL(req.url, `http://${req.headers.host}`);
|
||||||
|
const pathname = url.pathname; // e.g. /config/lan
|
||||||
|
const apiKey = req.headers['x-api-key'] || '';
|
||||||
|
|
||||||
|
// CORS
|
||||||
|
res.setHeader('Access-Control-Allow-Origin', '*');
|
||||||
|
res.setHeader('Access-Control-Allow-Methods', 'GET,PUT,OPTIONS');
|
||||||
|
res.setHeader('Access-Control-Allow-Headers', 'Content-Type, Authorization, X-API-Key');
|
||||||
|
if (req.method === 'OPTIONS') { res.writeHead(204); return res.end(); }
|
||||||
|
|
||||||
|
// Auth check
|
||||||
|
const auth = req.headers['authorization'] || '';
|
||||||
|
if (auth && !auth.startsWith('Basic ') && !auth.startsWith('Bearer ')) {
|
||||||
|
res.writeHead(401, { 'Content-Type': 'application/json' });
|
||||||
|
return res.end(JSON.stringify({ err: 'Authorization header must start with Basic or Bearer' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!pathname.startsWith('/config/')) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
return res.end(JSON.stringify({ err: 'Not found' }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const resource = pathname.replace(/^\/config\//, '');
|
||||||
|
const routeKey = ['lan', 'main_relay', 'elevator', 'device'];
|
||||||
|
|
||||||
|
if (!routeKey.includes(resource)) {
|
||||||
|
res.writeHead(404, { 'Content-Type': 'application/json' });
|
||||||
|
return res.end(JSON.stringify({ err: `Unknown resource: ${resource}` }));
|
||||||
|
}
|
||||||
|
|
||||||
|
const timeout = setTimeout(() => {
|
||||||
|
if (!res.headersSent) {
|
||||||
|
res.writeHead(504, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ err: 'Gateway timeout' }));
|
||||||
|
}
|
||||||
|
}, 3000);
|
||||||
|
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
// Simulate latency: 100-400ms
|
||||||
|
const delay = Math.random() * 300 + 100;
|
||||||
|
setTimeout(() => {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(store[resource]));
|
||||||
|
}, delay);
|
||||||
|
} else if (req.method === 'PUT') {
|
||||||
|
let body = '';
|
||||||
|
req.on('data', chunk => { body += chunk; });
|
||||||
|
req.on('end', () => {
|
||||||
|
const delay = Math.random() * 300 + 100;
|
||||||
|
setTimeout(() => {
|
||||||
|
try {
|
||||||
|
const data = JSON.parse(body);
|
||||||
|
// Validate based on resource type
|
||||||
|
if (resource === 'lan') {
|
||||||
|
if (!data.ip || !data.mask || !data.gateway) {
|
||||||
|
throw new Error('request error: missing required fields');
|
||||||
|
}
|
||||||
|
store[resource] = { ...store[resource], ...data };
|
||||||
|
} else if (resource === 'main_relay') {
|
||||||
|
if (typeof data.lockDelayMs !== 'number' || typeof data.unlockDelayMs !== 'number') {
|
||||||
|
throw new Error('request error: invalid delay values');
|
||||||
|
}
|
||||||
|
store[resource] = { ...store[resource], ...data };
|
||||||
|
} else if (resource === 'elevator') {
|
||||||
|
if (!data.enabled) {
|
||||||
|
throw new Error('request error: disabled not allowed');
|
||||||
|
}
|
||||||
|
store[resource] = { ...store[resource], ...data, portHintList: [], portCount: Object.keys(data).filter(k => k.startsWith('port')).length };
|
||||||
|
} else if (resource === 'device') {
|
||||||
|
if (!data.name) {
|
||||||
|
throw new Error('request error: name required');
|
||||||
|
}
|
||||||
|
store[resource] = { ...store[resource], ...data, sn: Date.now() % 10000 };
|
||||||
|
}
|
||||||
|
res.writeHead(200, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify(store[resource]));
|
||||||
|
} catch (e) {
|
||||||
|
res.writeHead(400, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ err: e.message }));
|
||||||
|
}
|
||||||
|
}, delay);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
res.writeHead(405, { 'Content-Type': 'application/json' });
|
||||||
|
res.end(JSON.stringify({ err: `Method ${req.method} not allowed` }));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const PORT = 3001;
|
||||||
|
server.listen(PORT, () => {
|
||||||
|
console.log(`Mock server running at http://localhost:${PORT}/config/{lan,main_relay,elevator,device}`);
|
||||||
|
});
|
||||||
1445
package-lock.json
generated
Normal file
1445
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
21
package.json
Normal file
21
package.json
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"name": "config-panel",
|
||||||
|
"private": true,
|
||||||
|
"version": "1.0.0",
|
||||||
|
"type": "module",
|
||||||
|
"scripts": {
|
||||||
|
"dev": "concurrently -n mock,vite \"node mock-server.js\" \"vite\"",
|
||||||
|
"mock": "node mock-server.js",
|
||||||
|
"build": "vite build",
|
||||||
|
"preview": "vite preview"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"bulma": "^1.0.4",
|
||||||
|
"svelte": "^5.56.0"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@sveltejs/vite-plugin-svelte": "7.1.2",
|
||||||
|
"concurrently": "^9.2.0",
|
||||||
|
"vite": "8.1.0"
|
||||||
|
}
|
||||||
|
}
|
||||||
55
src/App.svelte
Normal file
55
src/App.svelte
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
<script>
|
||||||
|
import { onMount } from 'svelte';
|
||||||
|
import LanPage from './pages/LanPage.svelte';
|
||||||
|
import MainRelayPage from './pages/MainRelayPage.svelte';
|
||||||
|
import ElevatorPage from './pages/ElevatorPage.svelte';
|
||||||
|
import DevicePage from './pages/DevicePage.svelte';
|
||||||
|
|
||||||
|
let currentPage = $state('lan');
|
||||||
|
let mobileMenuOpen = $state(false);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<nav class="navbar is-light" role="navigation">
|
||||||
|
<div class="container">
|
||||||
|
<div class="navbar-brand">
|
||||||
|
<a class="navbar-item has-text-weight-bold" href="#" onclick="event.preventDefault()">⚙️ 配置管理面板</a>
|
||||||
|
<a role="button" class="navbar-burger" onclick={() => mobileMenuOpen = !mobileMenuOpen} aria-label="menu" data-target="mainNav">
|
||||||
|
<span /><span /><span />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div id="mainNav" class="navbar-menu" class:is-active={$mobileMenuOpen}>
|
||||||
|
<div class="navbar-start">
|
||||||
|
{#each ['lan', 'main_relay', 'elevator', 'device'] as page}
|
||||||
|
<a class="navbar-item" href="#" onclick="{() => { $currentPage = page; $mobileMenuOpen = false; }}"
|
||||||
|
class:list={[ 'is-active', $currentPage === page ]}>
|
||||||
|
{#if page === 'lan'}🌐 LAN
|
||||||
|
{:else if page === 'main_relay'}🔌 主继电器
|
||||||
|
{:else if page === 'elevator'}🛗 电梯
|
||||||
|
{:else}📟 设备{/if}
|
||||||
|
</a>
|
||||||
|
{/each}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
|
||||||
|
<main class="section">
|
||||||
|
<div class="container">
|
||||||
|
{@switch $currentPage}
|
||||||
|
{@case 'lan'} <LanPage /> {@/case}
|
||||||
|
{@case 'main_relay'}<MainRelayPage />{@/case}
|
||||||
|
{@case 'elevator'} <ElevatorPage /> {@/case}
|
||||||
|
{@case 'device'} <DevicePage /> {@/case}
|
||||||
|
{/switch}
|
||||||
|
</div>
|
||||||
|
</main>
|
||||||
|
|
||||||
|
<footer class="footer">
|
||||||
|
<div class="content has-text-centered"><p style="color:#999;font-size:0.8rem;">© 2025 Config Panel — Mock Backend Active</p></div>
|
||||||
|
</footer>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.is-active { color: #00d1b2 !important; font-weight: bold !important; }
|
||||||
|
.navbar-burger span { display: block; width: 35px; height: 3px; background: #3273dc; margin: auto; }
|
||||||
|
main section { padding-top: 30px; }
|
||||||
|
</style>
|
||||||
33
src/global.css
Normal file
33
src/global.css
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
/* Global responsive base styles — Bulma + custom overrides */
|
||||||
|
@import 'bulma/css/bulma.min.css';
|
||||||
|
|
||||||
|
html {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 768px) {
|
||||||
|
html {
|
||||||
|
font-size: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (min-width: 1024px) {
|
||||||
|
html {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
min-height: 100vh;
|
||||||
|
background-color: #fdfdfd;
|
||||||
|
}
|
||||||
|
|
||||||
|
#app {
|
||||||
|
flex: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
footer.footer {
|
||||||
|
margin-top: auto;
|
||||||
|
}
|
||||||
18
src/lib/api.js
Normal file
18
src/lib/api.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const BASE = '/config';
|
||||||
|
|
||||||
|
async function request(method, resource, body) {
|
||||||
|
const opts = { method, headers: { 'Content-Type': 'application/json' } };
|
||||||
|
if (body && method === 'PUT') opts.body = JSON.stringify(body);
|
||||||
|
const res = await fetch(`${BASE}/${resource}`, opts);
|
||||||
|
if (!res.ok) { const t = await res.text(); throw new Error(t || `HTTP ${res.status}`); }
|
||||||
|
return res.json();
|
||||||
|
}
|
||||||
|
|
||||||
|
export const getLan = () => request('GET', 'lan');
|
||||||
|
export const putLan = d => request('PUT', 'lan', d);
|
||||||
|
export const getMainRelay = () => request('GET', 'main_relay');
|
||||||
|
export const putMainRelay = d => request('PUT', 'main_relay', d);
|
||||||
|
export const getElevator = () => request('GET', 'elevator');
|
||||||
|
export const putElevator = d => request('PUT', 'elevator', d);
|
||||||
|
export const getDevice = () => request('GET', 'device');
|
||||||
|
export const putDevice = d => request('PUT', 'device', d);
|
||||||
5
src/main.js
Normal file
5
src/main.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { mount } from 'svelte';
|
||||||
|
import App from './App.svelte';
|
||||||
|
import './global.css';
|
||||||
|
|
||||||
|
mount(App, { target: document.getElementById('app') });
|
||||||
23
src/pages/DevicePage.svelte
Normal file
23
src/pages/DevicePage.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { getDevice, putDevice } from '../lib/api.js';
|
||||||
|
|
||||||
|
let config = $state({});
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state(null);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const data = await getDevice();
|
||||||
|
config = { ...data };
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSave(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
putDevice(config).then(() => window.location.reload());
|
||||||
|
}
|
||||||
23
src/pages/ElevatorPage.svelte
Normal file
23
src/pages/ElevatorPage.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { getElevator, putElevator } from '../lib/api.js';
|
||||||
|
|
||||||
|
let config = $state({});
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state(null);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const data = await getElevator();
|
||||||
|
config = { ...data };
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSave(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
putElevator(config).then(() => window.location.reload());
|
||||||
|
}
|
||||||
23
src/pages/LanPage.svelte
Normal file
23
src/pages/LanPage.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { getLan, putLan } from '../lib/api.js';
|
||||||
|
import styles from './LanPage.module.svelte';
|
||||||
|
|
||||||
|
let config = $state({});
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state(null);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const data = await getLan();
|
||||||
|
config = { ...data };
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSave() {
|
||||||
|
putLan(config).then(() => window.location.reload());
|
||||||
|
}
|
||||||
23
src/pages/MainRelayPage.svelte
Normal file
23
src/pages/MainRelayPage.svelte
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { onMount } from 'svelte';
|
||||||
|
import { getMainRelay, putMainRelay } from '../lib/api.js';
|
||||||
|
|
||||||
|
let config = $state({});
|
||||||
|
let loading = $state(false);
|
||||||
|
let error = $state(null);
|
||||||
|
|
||||||
|
onMount(async () => {
|
||||||
|
loading = true;
|
||||||
|
try {
|
||||||
|
const data = await getMainRelay();
|
||||||
|
config = { ...data };
|
||||||
|
} catch (e) {
|
||||||
|
error = e.message;
|
||||||
|
} finally {
|
||||||
|
loading = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function handleSave(event: Event) {
|
||||||
|
event.preventDefault();
|
||||||
|
putMainRelay(config).then(() => window.location.reload());
|
||||||
|
}
|
||||||
5
svelte.config.js
Normal file
5
svelte.config.js
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default {
|
||||||
|
preprocess: vitePreprocess(),
|
||||||
|
};
|
||||||
14
vite.config.js
Normal file
14
vite.config.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
import { defineConfig } from 'vite';
|
||||||
|
import { svelte } from '@sveltejs/vite-plugin-svelte';
|
||||||
|
|
||||||
|
export default defineConfig({
|
||||||
|
plugins: [svelte()],
|
||||||
|
server: {
|
||||||
|
proxy: {
|
||||||
|
'/config': {
|
||||||
|
target: 'http://localhost:3001',
|
||||||
|
changeOrigin: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user