style: apply animal island theme to led panel

This commit is contained in:
2026-05-19 19:35:49 +08:00
parent 120a7f5624
commit 5628ab0ffc
2 changed files with 77 additions and 38 deletions
+76 -37
View File
@@ -27,52 +27,91 @@ CONTROLLABLE_COLORS = tuple(c.strip().lower() for c in os.environ.get("LED_COLOR
NAME_RE = re.compile(r"^[A-Za-z0-9_.:+@=-]+$")
CSS = r"""
:root { color-scheme: dark; --bg:#070b16; --card:#101827; --muted:#8ea0c0; --text:#eef5ff; --line:#25324a; --accent:#5eead4; --danger:#fb7185; --warn:#fbbf24; }
:root {
color-scheme: light;
--island-sky:#8ed8f8;
--island-sea:#65c7d0;
--island-mint:#82d5bb;
--island-leaf:#8ac68a;
--island-sand:#f7e6a5;
--island-paper:#fff8df;
--island-paper-2:#fff1bd;
--island-ink:#725d42;
--island-muted:#9a835a;
--island-line:#d9b970;
--island-teal:#2bb3a3;
--island-blue:#889df0;
--island-red:#fc736d;
--island-orange:#e59266;
--island-shadow:rgba(114,93,66,.23);
--text:var(--island-ink);
--muted:var(--island-muted);
--accent:var(--island-teal);
--danger:var(--island-red);
--warn:#f7cd67;
}
* { box-sizing: border-box; }
body { margin:0; min-height:100vh; font-family: -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif; background: radial-gradient(circle at top left,rgba(59,130,246,.26),transparent 28rem), radial-gradient(circle at top right,rgba(20,184,166,.18),transparent 24rem), linear-gradient(135deg,#070b16 0%,#0b1020 58%,#111827 100%); color:var(--text); }
.container { max-width: 1180px; margin: 0 auto; padding: 22px; }
body {
margin:0;
min-height:100vh;
font-family: 'Nunito','Zen Maru Gothic','Noto Sans SC', -apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,Arial,sans-serif;
color:var(--text);
background:
radial-gradient(circle at 12% 14%, rgba(255,255,255,.72) 0 0.62rem, transparent .66rem),
radial-gradient(circle at 84% 12%, rgba(255,255,255,.70) 0 0.48rem, transparent .52rem),
radial-gradient(circle at 22% 82%, rgba(130,213,187,.35), transparent 17rem),
linear-gradient(180deg, #b7ecff 0%, #d8f7ff 36%, #fff6d3 37%, #f7e6a5 100%);
}
body::before { content:''; position:fixed; left:0; right:0; bottom:0; height:118px; pointer-events:none; background: radial-gradient(65px 32px at 10% 75%, rgba(82,178,112,.30), transparent 70%), radial-gradient(80px 38px at 88% 85%, rgba(43,179,163,.28), transparent 72%), linear-gradient(180deg, transparent, rgba(101,199,208,.23)); }
.container { max-width: 1180px; margin: 0 auto; padding: 24px; position:relative; z-index:1; }
.header { display:grid; grid-template-columns: minmax(0,1fr) auto; gap:16px; align-items:start; margin-bottom:18px; }
.hero { border:1px solid rgba(148,163,184,.18); background:rgba(15,23,42,.62); backdrop-filter: blur(16px); border-radius:24px; padding:18px 20px; box-shadow:0 18px 48px rgba(0,0,0,.28); }
h1 { margin:0 0 8px; font-size:30px; letter-spacing:.2px; }
.sub { color:var(--muted); font-size:14px; line-height:1.6; }
.hero { position:relative; overflow:hidden; border:3px solid rgba(114,93,66,.22); background:linear-gradient(180deg,var(--island-paper),var(--island-paper-2)); border-radius:34px 34px 38px 38px; padding:22px 24px; box-shadow:0 14px 0 rgba(114,93,66,.10), 0 24px 55px var(--island-shadow); }
.hero::after { content:'🏝'; position:absolute; right:22px; top:12px; font-size:54px; opacity:.20; transform:rotate(8deg); }
h1 { margin:0 0 8px; font-size:32px; letter-spacing:.4px; color:#6b5137; text-shadow:0 2px 0 rgba(255,255,255,.65); }
.sub { color:var(--muted); font-size:14px; line-height:1.7; max-width:760px; }
.sub code { background:#fff4c5; border:1px dashed #d9b970; border-radius:10px; padding:2px 6px; color:#6b5137; }
.toolbar { display:flex; gap:10px; flex-wrap:wrap; justify-content:flex-end; }
.summary { display:flex; gap:10px; flex-wrap:wrap; margin-top:14px; }
.pill { border:1px solid rgba(148,163,184,.18); background:rgba(15,23,42,.72); color:var(--muted); padding:7px 10px; border-radius:999px; font-size:13px; }
.pill b { color:var(--text); }
.grid { display:grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap:16px; }
.card { position:relative; overflow:hidden; background: linear-gradient(180deg,rgba(18,26,47,.96),rgba(10,15,28,.96)); border:1px solid var(--line); border-radius:24px; padding:18px; box-shadow: 0 14px 42px rgba(0,0,0,.28); }
.card::before { content:''; position:absolute; inset:-40% -30% auto auto; width:180px; height:180px; border-radius:50%; background:radial-gradient(circle,var(--led-color),transparent 68%); opacity:.16; pointer-events:none; }
.card.on { border-color: color-mix(in srgb, var(--led-color) 45%, var(--line)); }
.card.off { opacity:.82; }
.card-head { display:flex; align-items:center; justify-content:space-between; gap:12px; }
.name { font-weight:800; font-size:19px; display:flex; align-items:center; gap:10px; word-break:break-all; }
.dot { width:14px; height:14px; border-radius:50%; background:var(--led-color); color:var(--led-color); box-shadow:0 0 18px currentColor; }
.bulb-wrap { display:grid; place-items:center; margin:18px 0 12px; }
.bulb { width:118px; height:118px; border-radius:999px; display:grid; place-items:center; color:#020617; font-size:28px; font-weight:900; background:radial-gradient(circle at 34% 30%,#fff 0 4%,var(--led-color) 10%,rgba(71,85,105,.24) 72%); box-shadow:0 0 calc(12px + var(--level)*42px) color-mix(in srgb, var(--led-color) 72%, transparent), inset 0 0 28px rgba(255,255,255,.12); filter:saturate(calc(.55 + var(--level))); }
.card.off .bulb { background:#1e293b; color:#64748b; box-shadow:inset 0 0 24px rgba(0,0,0,.35); }
.meta { margin-top:8px; color:var(--muted); font-size:13px; line-height:1.55; }
.summary { display:flex; gap:10px; flex-wrap:wrap; margin-top:15px; }
.pill { border:2px solid rgba(114,93,66,.13); background:#fffefa; color:var(--muted); padding:8px 12px; border-radius:999px; font-size:13px; box-shadow:0 4px 0 rgba(114,93,66,.08); }
.pill b { color:#6b5137; }
.grid { display:grid; grid-template-columns: repeat(3,minmax(0,1fr)); gap:18px; }
.card { position:relative; overflow:hidden; background:linear-gradient(180deg,#fffefa,#fff3cf); border:3px solid rgba(114,93,66,.18); border-radius:30px; padding:18px; box-shadow:0 10px 0 rgba(114,93,66,.12), 0 20px 36px rgba(114,93,66,.18); transition:.18s ease; }
.card::before { content:''; position:absolute; inset:-52px -42px auto auto; width:170px; height:170px; border-radius:45% 55% 52% 48%; background:radial-gradient(circle,var(--led-color),transparent 68%); opacity:.18; pointer-events:none; }
.card::after { content:''; position:absolute; left:18px; right:18px; top:8px; height:10px; border-radius:999px; background:repeating-linear-gradient(90deg, rgba(255,255,255,.58) 0 10px, transparent 10px 19px); opacity:.55; pointer-events:none; }
.card.on { border-color: color-mix(in srgb, var(--led-color) 45%, #d9b970); transform:translateY(-2px); }
.card.off { opacity:.88; filter:saturate(.78); }
.card-head { display:flex; align-items:center; justify-content:space-between; gap:12px; margin-top:4px; }
.name { font-weight:900; font-size:20px; display:flex; align-items:center; gap:10px; word-break:break-all; color:#6b5137; }
.dot { width:16px; height:16px; border-radius:50%; background:var(--led-color); color:var(--led-color); box-shadow:0 0 16px currentColor, 0 2px 0 rgba(114,93,66,.18); border:2px solid rgba(255,255,255,.76); }
.bulb-wrap { display:grid; place-items:center; margin:20px 0 13px; }
.bulb { width:126px; height:126px; border-radius:48% 52% 47% 53%; display:grid; place-items:center; color:#5a452e; font-size:29px; font-weight:1000; background:radial-gradient(circle at 34% 28%,#fff 0 5%,color-mix(in srgb,var(--led-color) 84%,#fff6c9) 11%,var(--led-color) 58%,rgba(255,246,211,.56) 74%); box-shadow:0 0 calc(12px + var(--level)*44px) color-mix(in srgb, var(--led-color) 70%, transparent), inset 0 -12px 20px rgba(114,93,66,.10), 0 9px 0 rgba(114,93,66,.12); filter:saturate(calc(.65 + var(--level))); border:3px solid rgba(255,255,255,.65); }
.card.off .bulb { background:#eadfbe; color:#aa9671; box-shadow:inset 0 -10px 18px rgba(114,93,66,.12), 0 8px 0 rgba(114,93,66,.10); }
.meta { margin-top:10px; color:var(--muted); font-size:13px; line-height:1.58; background:rgba(255,255,255,.45); border:1px dashed rgba(114,93,66,.18); border-radius:16px; padding:10px; }
.row { display:flex; gap:8px; flex-wrap:wrap; align-items:center; margin-top:12px; }
.stack { display:grid; gap:10px; margin-top:14px; }
button, select, input[type=number], input[type=text] { border:1px solid var(--line); background:#0f172a; color:var(--text); border-radius:12px; padding:10px 12px; font-size:14px; }
button { cursor:pointer; transition:.15s ease; }
button:hover { border-color:var(--accent); transform: translateY(-1px); }
button.primary { background:#0f766e; border-color:#14b8a6; }
button.off { background:#3f1d2a; border-color:#7f1d1d; }
button.warn { background:#422006; border-color:#92400e; }
.actions { display:grid; grid-template-columns:1fr 1fr; gap:8px; }
button, select, input[type=number], input[type=text] { border:2px solid rgba(114,93,66,.18); background:#fffefa; color:var(--text); border-radius:999px; padding:10px 13px; font-size:14px; font-family:inherit; box-shadow:0 4px 0 rgba(114,93,66,.10); }
select { max-width: 100%; }
button { cursor:pointer; transition:.15s ease; font-weight:900; }
button:hover { border-color:#2bb3a3; transform: translateY(-2px); box-shadow:0 6px 0 rgba(114,93,66,.12); }
button:active { transform: translateY(2px); box-shadow:0 2px 0 rgba(114,93,66,.14); }
button.primary { background:linear-gradient(180deg,#91dfc8,#55c2aa); border-color:#2bb3a3; color:#fffefa; text-shadow:0 1px 0 rgba(0,0,0,.16); }
button.off { background:linear-gradient(180deg,#ffaaa6,#fc736d); border-color:#d95b56; color:#fffefa; text-shadow:0 1px 0 rgba(0,0,0,.18); }
button.warn { background:linear-gradient(180deg,#ffe082,#f7cd67); border-color:#d9a844; color:#725d42; }
.actions { display:grid; grid-template-columns:1fr 1fr; gap:9px; }
.actions button { width:100%; }
.sub-actions button { padding:8px 10px; font-size:13px; }
.sub-actions button { padding:8px 10px; font-size:13px; background:#fff8df; }
input[type=range] { width:100%; accent-color: var(--led-color); }
.range-line { display:grid; grid-template-columns: 1fr 66px; gap:10px; align-items:center; margin-top:12px; }
.badge { color:#07111f; background:var(--accent); padding:3px 8px; border-radius:999px; font-size:12px; font-weight:800; }
.badge.saved { background:#fbbf24; }
.err { background:#450a0a; border:1px solid #7f1d1d; color:#fecaca; padding:12px; border-radius:14px; margin-bottom:16px; }
.toast { position:fixed; right:16px; bottom:16px; max-width:520px; padding:12px 14px; border-radius:14px; background:#052e2b; border:1px solid #14b8a6; box-shadow:0 18px 40px rgba(0,0,0,.35); display:none; z-index:20; }
.range-line { display:grid; grid-template-columns: 1fr 68px; gap:10px; align-items:center; margin-top:12px; }
.range-line input[type=number] { border-radius:16px; padding:9px 8px; text-align:center; }
.badge { color:#fffefa; background:#2bb3a3; padding:4px 9px; border-radius:999px; font-size:12px; font-weight:900; box-shadow:0 3px 0 rgba(114,93,66,.12); }
.badge.saved { background:#f7cd67; color:#725d42; }
.err { background:#fff1bd; border:2px dashed #fc736d; color:#92513d; padding:14px; border-radius:20px; margin-bottom:16px; }
.toast { position:fixed; right:16px; bottom:16px; max-width:520px; padding:13px 16px; border-radius:20px; background:#fffefa; color:#725d42; border:3px solid #82d5bb; box-shadow:0 14px 0 rgba(114,93,66,.10), 0 20px 40px rgba(114,93,66,.22); display:none; z-index:20; font-weight:800; }
.small { font-size:12px; color:var(--muted); }
.path { word-break:break-all; }
a { color:var(--accent); }
a { color:#2b8f86; }
@media (max-width: 900px) { .grid{grid-template-columns:1fr;} .header{grid-template-columns:1fr;} .toolbar{justify-content:flex-start;} }
@media (max-width: 640px) { .container{padding:14px;} h1{font-size:25px;} .hero{padding:16px;} .bulb{width:96px;height:96px;} .actions{grid-template-columns:1fr;} }
@media (max-width: 640px) { .container{padding:14px;} h1{font-size:26px;} .hero{padding:17px;} .bulb{width:102px;height:102px;} .actions{grid-template-columns:1fr;} button,select,input[type=number],input[type=text]{width:100%;} }
"""
JS = r"""
@@ -395,7 +434,7 @@ class Handler(BaseHTTPRequestHandler):
def do_GET(self):
parsed = urllib.parse.urlparse(self.path)
if parsed.path == "/":
body = f"""<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>LED Manager</title><style>{CSS}</style></head><body><div class='container'><div class='header'><div class='hero'><h1>RGB LED 控制台</h1><div class='sub'>只显示实际可控的红 / 绿 / 蓝灯;配置保存到 <code>{html.escape(str(CONFIG_PATH))}</code>,页面每 5 秒自动刷新。</div><div class='summary'><span class='pill'>可控灯 <b id='count'>-</b></span><span class='pill'>已亮 <b id='oncount'>-</b></span><span class='pill'>保存 <b id='savedcount'>-</b></span><span class='pill'>目录 <b>{html.escape(str(LED_ROOT))}</b></span></div></div><div class='toolbar'><button class='off' onclick='turnOffAll()'>关闭全部</button><button onclick='applySaved()'>应用保存</button><button onclick='load()'>刷新</button></div></div><div id='root' class='grid'></div></div><div id='toast' class='toast'></div><script>{JS}</script></body></html>"""
body = f"""<!doctype html><html><head><meta charset='utf-8'><meta name='viewport' content='width=device-width,initial-scale=1'><title>LED Island Console</title><style>{CSS}</style></head><body><div class='container'><div class='header'><div class='hero'><h1>🏝 RGB LED 岛屿控制台</h1><div class='sub'>Animal-Island 风格面板:只显示实际可控的红 / 绿 / 蓝灯;配置保存到 <code>{html.escape(str(CONFIG_PATH))}</code>,页面每 5 秒自动刷新。</div><div class='summary'><span class='pill'>可控灯 <b id='count'>-</b></span><span class='pill'>已亮 <b id='oncount'>-</b></span><span class='pill'>开机保存 <b id='savedcount'>-</b></span><span class='pill'>LED 目录 <b>{html.escape(str(LED_ROOT))}</b></span></div></div><div class='toolbar'><button class='off' onclick='turnOffAll()'>关闭全部</button><button class='warn' onclick='applySaved()'>应用保存</button><button onclick='load()'>刷新</button></div></div><div id='root' class='grid'></div></div><div id='toast' class='toast'></div><script>{JS}</script></body></html>"""
data = body.encode("utf-8")
self.send_response(200)
self.send_header("Content-Type", "text/html; charset=utf-8")
+1 -1
View File
@@ -55,7 +55,7 @@ def main():
try:
import urllib.request as _urlreq
html = _urlreq.urlopen(f'http://127.0.0.1:{port}/', timeout=5).read().decode()
assert 'RGB LED 控制台' in html and '关闭全部' in html and 'oncount' in html
assert 'RGB LED 岛屿控制台' in html and 'Animal-Island 风格面板' in html and '关闭全部' in html and 'oncount' in html
leds = req(port, '/api/leds')
names = [led['name'] for led in leds['leds']]
assert leds['ok'] and names == ['blue:wan', 'green:wlan', 'red:power']