From a7b23b760a69e8e52dad6c6e255e142f2111592c Mon Sep 17 00:00:00 2001 From: kory Date: Mon, 28 Sep 2026 08:49:09 +0800 Subject: [PATCH] style(web): use minimal custom svg icons for theme switch button --- internal/web/static/app.css | 7 ++++ internal/web/static/app.js | 58 +++++++++++++++++++++++++++++++++- internal/web/static/index.html | 2 +- web/static/app.css | 7 ++++ web/static/app.js | 58 +++++++++++++++++++++++++++++++++- web/static/index.html | 2 +- 6 files changed, 130 insertions(+), 4 deletions(-) diff --git a/internal/web/static/app.css b/internal/web/static/app.css index 05e7ac3..6982e4c 100644 --- a/internal/web/static/app.css +++ b/internal/web/static/app.css @@ -9,3 +9,10 @@ header strong{font-size:18px}header span{margin-left:12px;color:var(--text-muted @media(max-width:680px){header{align-items:flex-start;padding-top:12px;padding-bottom:12px}header span{display:none}.brand-wrap{padding-top:2px}.brand-logo{width:32px;height:32px}.header-actions{flex-direction:column;align-items:stretch;width:112px}.header-actions button{width:100%;padding:8px 9px}.devices{grid-template-columns:1fr}.cardhead{flex-direction:column;align-items:stretch;gap:8px}.cardhead h2{margin-bottom:0}.cardhead button,#device{width:100%;min-width:0}.actions button{flex:1 1 calc(50% - 4px)}} @media(max-width:450px){main{padding:9px}.card{padding:13px;border-radius:12px}.actions{display:grid;grid-template-columns:1fr 1fr}.actions button{width:100%;min-width:0}.login form{padding:22px}.header-actions{width:104px}} @media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important}} + +/* 主题切换极简矢量图标按钮 */ +.btn-icon{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;padding:0;border-radius:9px;background:var(--bg-sub);border:1px solid var(--border-strong);color:var(--text-main);cursor:pointer;transition:background .2s,border-color .2s} +.btn-icon:hover{filter:brightness(1.1)} +.theme-svg{display:none;width:18px;height:18px} +:root[data-theme="dark"] .svg-sun{display:block;color:#facc15} +:root[data-theme="light"] .svg-moon{display:block;color:#475569} diff --git a/internal/web/static/app.js b/internal/web/static/app.js index 5d6df6f..4bc9750 100644 --- a/internal/web/static/app.js +++ b/internal/web/static/app.js @@ -1 +1,57 @@ -const $=s=>document.querySelector(s);(function(){const b=$("#themeToggle");if(!b)return;function sync(){const t=document.documentElement.dataset.theme||"dark";b.textContent=t==="light"?"深色模式":"浅色模式"}b.onclick=()=>{const t=document.documentElement.dataset.theme==="light"?"dark":"light";document.documentElement.dataset.theme=t;localStorage.setItem("vpc_theme",t);sync()};sync()})();async function api(path,opt={}){let r=await fetch("/panel/api"+path,opt);if(r.status===401){location.href="/panel/login";throw Error("unauthorized")}if(!r.ok)throw Error(await r.text());return r.json()}function esc(v){return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]))}async function loadDevices(){let ds=await api("/devices");$("#devices").innerHTML=ds.map(d=>'
'+esc(d.name)+'
'+esc(d.status)+' · '+(d.is_local?"local":"remote")+' · '+esc(d.arch||"")+'/'+esc(d.os||"")+'
').join("");$("#device").innerHTML=ds.map(d=>'').join("")}async function loadAudits(){let a=await api("/audits?limit=100");$("#audits").innerHTML=a.map(x=>''+new Date(x.timestamp*1000).toLocaleString()+''+esc(x.device)+''+esc(x.action)+''+esc(x.command)+''+x.exit_code+''+x.duration_ms+'ms'+esc(x.caller_ip)+'').join("")}$("#refresh").onclick=loadDevices;$("#auditRefresh").onclick=loadAudits;$("#run").onclick=async()=>{let o=$("#output");o.textContent="执行中…";try{let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})});o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms`}catch(e){o.textContent=e.message}};$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json");$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。");$("#rotateKey").onclick=async()=>{if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return;try{let x=await api("/key/rotate",{method:"POST"});$("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key}catch(e){$("#newKey").textContent=e.message}};$("#logout").onclick=async()=>{await fetch("/panel/logout",{method:"POST"});location.href="/panel/login"};loadDevices().then(loadAudits).catch(console.error); \ No newline at end of file +const $=s=>document.querySelector(s); +(function(){ + const b=$("#themeToggle"); + if(!b)return; + b.onclick=()=>{ + const current=document.documentElement.dataset.theme||"dark"; + const next=current==="light"?"dark":"light"; + document.documentElement.dataset.theme=next; + localStorage.setItem("vpc_theme",next); + }; +})(); +async function api(path,opt={}){ + let r=await fetch("/panel/api"+path,opt); + if(r.status===401){location.href="/panel/login";throw Error("unauthorized")} + if(!r.ok)throw Error(await r.text()); + return r.json() +} +function esc(v){ + return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c])) +} +async function loadDevices(){ + let ds=await api("/devices"); + $("#devices").innerHTML=ds.map(d=>'
'+esc(d.name)+'
'+esc(d.status)+' · '+(d.is_local?"local":"remote")+' · '+esc(d.arch||"")+'/'+esc(d.os||"")+'
').join(""); + $("#device").innerHTML=ds.map(d=>'').join("") +} +async function loadAudits(){ + let a=await api("/audits?limit=100"); + $("#audits").innerHTML=a.map(x=>''+new Date(x.timestamp*1000).toLocaleString()+''+esc(x.device)+''+esc(x.action)+''+esc(x.command)+''+x.exit_code+''+x.duration_ms+'ms'+esc(x.caller_ip)+'').join("") +} +$("#refresh").onclick=loadDevices; +$("#auditRefresh").onclick=loadAudits; +$("#run").onclick=async()=>{ + let o=$("#output"); + o.textContent="执行中…"; + try{ + let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})}); + o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms` + }catch(e){ + o.textContent=e.message + } +}; +$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json"); +$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。"); +$("#rotateKey").onclick=async()=>{ + if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return; + try{ + let x=await api("/key/rotate",{method:"POST"}); + $("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key + }catch(e){ + $("#newKey").textContent=e.message + } +}; +$("#logout").onclick=async()=>{ + await fetch("/panel/logout",{method:"POST"}); + location.href="/panel/login" +}; +loadDevices().then(loadAudits).catch(console.error); diff --git a/internal/web/static/index.html b/internal/web/static/index.html index c47b7dc..632123e 100644 --- a/internal/web/static/index.html +++ b/internal/web/static/index.html @@ -1 +1 @@ -VPS-Commander
VPS-CommanderPrivate Infrastructure Console

设备看板

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json

快速控制台

审计日志

时间设备动作命令/路径退出码耗时来源
+VPS-Commander
VPS-CommanderPrivate Infrastructure Console

设备看板

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json

快速控制台

审计日志

时间设备动作命令/路径退出码耗时来源
diff --git a/web/static/app.css b/web/static/app.css index 05e7ac3..6982e4c 100644 --- a/web/static/app.css +++ b/web/static/app.css @@ -9,3 +9,10 @@ header strong{font-size:18px}header span{margin-left:12px;color:var(--text-muted @media(max-width:680px){header{align-items:flex-start;padding-top:12px;padding-bottom:12px}header span{display:none}.brand-wrap{padding-top:2px}.brand-logo{width:32px;height:32px}.header-actions{flex-direction:column;align-items:stretch;width:112px}.header-actions button{width:100%;padding:8px 9px}.devices{grid-template-columns:1fr}.cardhead{flex-direction:column;align-items:stretch;gap:8px}.cardhead h2{margin-bottom:0}.cardhead button,#device{width:100%;min-width:0}.actions button{flex:1 1 calc(50% - 4px)}} @media(max-width:450px){main{padding:9px}.card{padding:13px;border-radius:12px}.actions{display:grid;grid-template-columns:1fr 1fr}.actions button{width:100%;min-width:0}.login form{padding:22px}.header-actions{width:104px}} @media(prefers-reduced-motion:reduce){*,*::before,*::after{transition:none!important}} + +/* 主题切换极简矢量图标按钮 */ +.btn-icon{display:inline-flex;align-items:center;justify-content:center;width:38px;height:38px;padding:0;border-radius:9px;background:var(--bg-sub);border:1px solid var(--border-strong);color:var(--text-main);cursor:pointer;transition:background .2s,border-color .2s} +.btn-icon:hover{filter:brightness(1.1)} +.theme-svg{display:none;width:18px;height:18px} +:root[data-theme="dark"] .svg-sun{display:block;color:#facc15} +:root[data-theme="light"] .svg-moon{display:block;color:#475569} diff --git a/web/static/app.js b/web/static/app.js index 5d6df6f..4bc9750 100644 --- a/web/static/app.js +++ b/web/static/app.js @@ -1 +1,57 @@ -const $=s=>document.querySelector(s);(function(){const b=$("#themeToggle");if(!b)return;function sync(){const t=document.documentElement.dataset.theme||"dark";b.textContent=t==="light"?"深色模式":"浅色模式"}b.onclick=()=>{const t=document.documentElement.dataset.theme==="light"?"dark":"light";document.documentElement.dataset.theme=t;localStorage.setItem("vpc_theme",t);sync()};sync()})();async function api(path,opt={}){let r=await fetch("/panel/api"+path,opt);if(r.status===401){location.href="/panel/login";throw Error("unauthorized")}if(!r.ok)throw Error(await r.text());return r.json()}function esc(v){return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c]))}async function loadDevices(){let ds=await api("/devices");$("#devices").innerHTML=ds.map(d=>'
'+esc(d.name)+'
'+esc(d.status)+' · '+(d.is_local?"local":"remote")+' · '+esc(d.arch||"")+'/'+esc(d.os||"")+'
').join("");$("#device").innerHTML=ds.map(d=>'').join("")}async function loadAudits(){let a=await api("/audits?limit=100");$("#audits").innerHTML=a.map(x=>''+new Date(x.timestamp*1000).toLocaleString()+''+esc(x.device)+''+esc(x.action)+''+esc(x.command)+''+x.exit_code+''+x.duration_ms+'ms'+esc(x.caller_ip)+'').join("")}$("#refresh").onclick=loadDevices;$("#auditRefresh").onclick=loadAudits;$("#run").onclick=async()=>{let o=$("#output");o.textContent="执行中…";try{let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})});o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms`}catch(e){o.textContent=e.message}};$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json");$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。");$("#rotateKey").onclick=async()=>{if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return;try{let x=await api("/key/rotate",{method:"POST"});$("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key}catch(e){$("#newKey").textContent=e.message}};$("#logout").onclick=async()=>{await fetch("/panel/logout",{method:"POST"});location.href="/panel/login"};loadDevices().then(loadAudits).catch(console.error); \ No newline at end of file +const $=s=>document.querySelector(s); +(function(){ + const b=$("#themeToggle"); + if(!b)return; + b.onclick=()=>{ + const current=document.documentElement.dataset.theme||"dark"; + const next=current==="light"?"dark":"light"; + document.documentElement.dataset.theme=next; + localStorage.setItem("vpc_theme",next); + }; +})(); +async function api(path,opt={}){ + let r=await fetch("/panel/api"+path,opt); + if(r.status===401){location.href="/panel/login";throw Error("unauthorized")} + if(!r.ok)throw Error(await r.text()); + return r.json() +} +function esc(v){ + return String(v??"").replace(/[&<>"]/g,c=>({"&":"&","<":"<",">":">",'"':"""}[c])) +} +async function loadDevices(){ + let ds=await api("/devices"); + $("#devices").innerHTML=ds.map(d=>'
'+esc(d.name)+'
'+esc(d.status)+' · '+(d.is_local?"local":"remote")+' · '+esc(d.arch||"")+'/'+esc(d.os||"")+'
').join(""); + $("#device").innerHTML=ds.map(d=>'').join("") +} +async function loadAudits(){ + let a=await api("/audits?limit=100"); + $("#audits").innerHTML=a.map(x=>''+new Date(x.timestamp*1000).toLocaleString()+''+esc(x.device)+''+esc(x.action)+''+esc(x.command)+''+x.exit_code+''+x.duration_ms+'ms'+esc(x.caller_ip)+'').join("") +} +$("#refresh").onclick=loadDevices; +$("#auditRefresh").onclick=loadAudits; +$("#run").onclick=async()=>{ + let o=$("#output"); + o.textContent="执行中…"; + try{ + let x=await api("/exec",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({device:$("#device").value,command:$("#command").value,timeout:30})}); + o.textContent=`exit_code: ${x.exit_code}\n\nstdout:\n${x.stdout}\n\nstderr:\n${x.stderr}\nduration: ${x.duration_ms}ms` + }catch(e){ + o.textContent=e.message + } +}; +$("#copySchema").onclick=()=>navigator.clipboard.writeText(location.origin+"/openapi.json"); +$("#copyPrompt").onclick=()=>navigator.clipboard.writeText("你是我的私有 VPS 运维助手。执行操作前确认目标设备;优先使用 list_devices 确认节点状态;高风险操作先向用户确认。"); +$("#rotateKey").onclick=async()=>{ + if(!confirm("轮换后旧 API Key 会立即失效,确认继续?"))return; + try{ + let x=await api("/key/rotate",{method:"POST"}); + $("#newKey").textContent="新 API Key(仅本次显示):\n"+x.api_key + }catch(e){ + $("#newKey").textContent=e.message + } +}; +$("#logout").onclick=async()=>{ + await fetch("/panel/logout",{method:"POST"}); + location.href="/panel/login" +}; +loadDevices().then(loadAudits).catch(console.error); diff --git a/web/static/index.html b/web/static/index.html index c47b7dc..632123e 100644 --- a/web/static/index.html +++ b/web/static/index.html @@ -1 +1 @@ -VPS-Commander
VPS-CommanderPrivate Infrastructure Console

设备看板

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json

快速控制台

审计日志

时间设备动作命令/路径退出码耗时来源
+VPS-Commander
VPS-CommanderPrivate Infrastructure Console

设备看板

Actions 向导

当前 Hub 的 OpenAPI Schema:

/openapi.json

快速控制台

审计日志

时间设备动作命令/路径退出码耗时来源