fix(pve): 修复review反馈 — body二次读取、URL编码、前端并行检测、错误态、emoji风格

后端修复(缅因猫 review):
- server/services/pve.go: Login 使用 url.Values 编码 form data,修复密码含特殊字符导致认证失败
- server/services/pve.go: GetVMStatus 先 io.ReadAll 再 json.Unmarshal,修复 decode 错误时 body 为空的问题

前端修复(暹罗猫 UI review + 布偶猫实现):
- PVEHosts.vue: 节点状态检测改为 Promise.all 并行,避免 N 个节点串行 RTT
- PVEHosts.vue: VM 弹窗增加错误态提示(el-alert),失败时不再只 Toast 报错
- MachineDetail.vue: VM 状态标签去掉 emoji,改用纯文字 + el-tag 语义色
- MachineDetail.vue: VM 状态 API 失败时标记 _error 态,显示「VM检测失败」而非「检测中」

[宪宪/K2.6🐾]
This commit is contained in:
openclaw
2026-04-21 02:17:09 +08:00
parent e0f3a34b48
commit dbeaeda083
25 changed files with 1180 additions and 22 deletions

View File

@@ -7,6 +7,7 @@ import (
"io"
"net/http"
"net/http/cookiejar"
"net/url"
"strings"
"time"
@@ -57,9 +58,11 @@ func (c *PVEClient) baseURL() string {
// Login 获取 PVE 认证ticket
func (c *PVEClient) Login() error {
loginURL := c.baseURL() + "/api2/json/access/ticket"
data := fmt.Sprintf("username=%s&password=%s", c.Username, c.Password)
formData := url.Values{}
formData.Set("username", c.Username)
formData.Set("password", c.Password)
req, err := http.NewRequest("POST", loginURL, strings.NewReader(data))
req, err := http.NewRequest("POST", loginURL, strings.NewReader(formData.Encode()))
if err != nil {
return err
}
@@ -127,18 +130,22 @@ func (c *PVEClient) GetVMStatus(vmid string) (*models.PVEVMStatus, error) {
return nil, fmt.Errorf("get vm status failed, status: %d", resp.StatusCode)
}
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("read body failed: %v", err)
}
var result struct {
Data struct {
Status string `json:"status"`
Uptime int64 `json:"uptime"`
CPU float64 `json:"cpu"`
MemoryUsed int64 `json:"mem"`
MemoryTotal int64 `json:"maxmem"`
Status string `json:"status"`
Uptime int64 `json:"uptime"`
CPU float64 `json:"cpu"`
MemoryUsed int64 `json:"mem"`
MemoryTotal int64 `json:"maxmem"`
} `json:"data"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
body, _ := io.ReadAll(resp.Body)
if err := json.Unmarshal(body, &result); err != nil {
return nil, fmt.Errorf("decode failed: %v, body: %s", err, string(body))
}

View File

@@ -0,0 +1 @@
import{o as b,c as x,a as o,b as s,w as l,r as g,d as u,u as k,e as v,f as N,g as w,l as z,h as y,E as d,i as C,s as V,j as M}from"./index-D__NZX4I.js";import{_ as A}from"./_plugin-vue_export-helper-DlAUqK2U.js";const B={class:"login-page"},E={class:"login-card"},K={class:"input-wrap"},U={class:"input-wrap"},$={class:"login-guest"},j={__name:"Login",setup(I){const c=k(),a=g({username:"",password:""}),n=g(!1);async function r(){if(!a.value.username||!a.value.password){d.warning("请输入用户名和密码");return}n.value=!0;try{const t=await C(a.value);V(t.data),d.success("登录成功"),c.push("/")}catch{}n.value=!1}async function h(){n.value=!0;try{const t=await M();V(t.data),d.success("已进入访客模式"),c.push("/")}catch{}n.value=!1}return(t,e)=>{const p=u("el-icon"),_=u("el-input"),m=u("el-form-item"),f=u("el-button"),L=u("el-form");return b(),x("div",B,[o("div",E,[e[4]||(e[4]=o("div",{class:"login-header"},[o("div",{class:"logo"},"LM"),o("h1",null,"LAN Manager"),o("p",null,"轻量级局域网资产管理平台")],-1)),s(L,{model:a.value,class:"login-form"},{default:l(()=>[s(m,null,{default:l(()=>[o("div",K,[s(p,{class:"input-icon"},{default:l(()=>[s(v(N))]),_:1}),s(_,{modelValue:a.value.username,"onUpdate:modelValue":e[0]||(e[0]=i=>a.value.username=i),placeholder:"用户名",size:"large",onKeydown:w(r,["enter"])},null,8,["modelValue"])])]),_:1}),s(m,null,{default:l(()=>[o("div",U,[s(p,{class:"input-icon"},{default:l(()=>[s(v(z))]),_:1}),s(_,{modelValue:a.value.password,"onUpdate:modelValue":e[1]||(e[1]=i=>a.value.password=i),type:"password","show-password":"",placeholder:"密码",size:"large",onKeydown:w(r,["enter"])},null,8,["modelValue"])])]),_:1}),s(f,{type:"primary",size:"large",class:"login-btn",loading:n.value,onClick:r},{default:l(()=>[...e[2]||(e[2]=[y("登录",-1)])]),_:1},8,["loading"]),o("div",$,[s(f,{text:"",size:"small",onClick:h},{default:l(()=>[...e[3]||(e[3]=[y("访客模式进入",-1)])]),_:1})])]),_:1},8,["model"])]),e[5]||(e[5]=o("div",{class:"login-footer"}," © LAN Manager ",-1))])}}},q=A(j,[["__scopeId","data-v-92ccba4b"]]);export{q as default};

View File

@@ -0,0 +1 @@
import{k as V,aq as $,o as w,c as k,a as o,b as t,w as d,t as f,ar as N,m as B,r as i,d as r,as as C,h as b}from"./index-D__NZX4I.js";import{_ as L}from"./_plugin-vue_export-helper-DlAUqK2U.js";const M={class:"page"},T={class:"page-header"},U={class:"header-actions"},I={class:"card"},q={class:"table-header"},E={class:"pagination-bar"},F={__name:"Logs",setup(H){const h=i([]),p=i(0),c=i(1),u=i(20),_=i(""),g=i(!1);V(()=>v());async function v(){g.value=!0;const l=await $({page:c.value,page_size:u.value,search:_.value});h.value=l.data.list,p.value=l.data.total,g.value=!1}function y(l){if(!l)return"-";const e=new Date(l);if(isNaN(e.getTime()))return l;const s=m=>String(m).padStart(2,"0");return`${e.getFullYear()}-${s(e.getMonth()+1)}-${s(e.getDate())} ${s(e.getHours())}:${s(e.getMinutes())}:${s(e.getSeconds())}`}return(l,e)=>{const s=r("el-input"),m=r("el-button"),n=r("el-table-column"),z=r("el-tag"),x=r("el-table"),D=r("el-pagination"),S=C("loading");return w(),k("div",M,[o("div",T,[e[4]||(e[4]=o("div",null,[o("div",{class:"page-title"},"操作日志"),o("div",{class:"page-subtitle"},"查看最近的系统操作记录")],-1)),o("div",U,[t(s,{modelValue:_.value,"onUpdate:modelValue":e[0]||(e[0]=a=>_.value=a),placeholder:"搜索操作内容",clearable:"",style:{width:"240px"}},null,8,["modelValue"]),t(m,{type:"primary",onClick:v},{default:d(()=>[...e[3]||(e[3]=[b("查询",-1)])]),_:1})])]),o("div",I,[o("div",q,"共 "+f(p.value)+" 条记录",1),N((w(),B(x,{data:h.value,stripe:"",style:{width:"100%"},class:"modern-table"},{default:d(()=>[t(n,{prop:"id",label:"ID",width:"70"}),t(n,{prop:"action",label:"操作",width:"140"}),t(n,{prop:"target_type",label:"对象",width:"100"},{default:d(({row:a})=>[t(z,{size:"small",effect:"plain",round:""},{default:d(()=>[b(f(a.target_type),1)]),_:2},1024)]),_:1}),t(n,{prop:"target_name",label:"对象名称","min-width":"160"}),t(n,{prop:"details",label:"详情","min-width":"240","show-overflow-tooltip":""}),t(n,{prop:"created_at",label:"时间",width:"170"},{default:d(({row:a})=>[b(f(y(a.created_at)),1)]),_:1})]),_:1},8,["data"])),[[S,g.value]]),o("div",E,[t(D,{"current-page":c.value,"onUpdate:currentPage":e[1]||(e[1]=a=>c.value=a),"page-size":u.value,"onUpdate:pageSize":e[2]||(e[2]=a=>u.value=a),"page-sizes":[10,20,50,100],layout:"total, sizes, prev, pager, next",total:p.value,onChange:v},null,8,["current-page","page-size","total"])])])])}}},j=L(F,[["__scopeId","data-v-b1b09e4f"]]);export{j as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{k as h,o as i,c as m,a as s,b as e,w as t,m as d,n as r,t as v,p as C,r as x,q as w,d as _,u as E,e as l,v as L,x as M,y as A,z as B,A as D,B as N,C as V,f as z,D as I,F as S,E as q,G as F,H as G}from"./index-D__NZX4I.js";import{_ as H}from"./_plugin-vue_export-helper-DlAUqK2U.js";const P={class:"layout"},R={key:0,class:"sidebar"},T={class:"nav"},$={class:"sidebar-footer"},j={class:"user-info"},J={class:"user-name"},K={class:"main-inner"},O={__name:"MainLayout",setup(Q){const f=E(),u=w(()=>F().is_admin),o=x(!1);h(()=>{o.value=document.documentElement.classList.contains("dark")});function p(){o.value=!o.value,document.documentElement.classList.toggle("dark",o.value),localStorage.setItem("theme",o.value?"dark":"light")}async function g(){try{await S()}catch{}G(),q.success("已退出"),f.push("/login")}return(k,a)=>{const n=_("el-icon"),c=_("router-link"),y=_("el-button"),b=_("router-view");return i(),m("div",P,[u.value?(i(),m("aside",R,[a[5]||(a[5]=s("div",{class:"brand"},[s("div",{class:"brand-logo"},"LM"),s("div",{class:"brand-text"},"LAN Manager")],-1)),s("nav",T,[e(c,{to:"/machines",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(L))]),_:1}),a[0]||(a[0]=s("span",null,"机器列表",-1))]),_:1}),u.value?(i(),d(c,{key:0,to:"/topology",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(M))]),_:1}),a[1]||(a[1]=s("span",null,"拓扑图",-1))]),_:1})):r("",!0),u.value?(i(),d(c,{key:1,to:"/logs",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(A))]),_:1}),a[2]||(a[2]=s("span",null,"操作日志",-1))]),_:1})):r("",!0),u.value?(i(),d(c,{key:2,to:"/pve-hosts",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(B))]),_:1}),a[3]||(a[3]=s("span",null,"PVE 主机",-1))]),_:1})):r("",!0)]),s("div",{class:"theme-toggle",onClick:p},[e(n,{class:"theme-icon"},{default:t(()=>[(i(),d(D(o.value?l(N):l(V))))]),_:1}),s("span",null,v(o.value?"浅色模式":"深色模式"),1)]),s("div",$,[s("div",j,[e(n,{class:"user-icon"},{default:t(()=>[e(l(z))]),_:1}),s("span",J,v(u.value?"管理员":"访客"),1)]),e(y,{text:"",class:"logout-btn",onClick:g},{default:t(()=>[e(n,null,{default:t(()=>[e(l(I))]),_:1}),a[4]||(a[4]=s("span",null,"退出",-1))]),_:1})])])):r("",!0),s("main",{class:C(["main",{"no-sidebar":!u.value}])},[s("div",K,[e(b)])],2)])}}},X=H(O,[["__scopeId","data-v-dac2493b"]]);export{X as default};

View File

@@ -0,0 +1 @@
.toolbar[data-v-fede7675]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.page-title[data-v-fede7675]{margin:0;font-size:20px;font-weight:600}.hosts-grid[data-v-fede7675]{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.host-card[data-v-fede7675]{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px}.host-header[data-v-fede7675]{display:flex;align-items:center;gap:10px;margin-bottom:12px}.host-icon[data-v-fede7675]{font-size:20px;color:var(--el-color-primary)}.host-name[data-v-fede7675]{font-size:16px;font-weight:600;flex:1}.status-tag[data-v-fede7675]{font-size:11px;height:20px;padding:0 8px}.host-info[data-v-fede7675]{margin-bottom:12px}.info-row[data-v-fede7675]{display:flex;font-size:13px;margin-bottom:4px}.info-row .label[data-v-fede7675]{color:var(--text-secondary);width:50px}.info-row .value[data-v-fede7675]{color:var(--text)}.host-actions[data-v-fede7675]{display:flex;gap:8px}.vm-loading[data-v-fede7675]{display:flex;align-items:center;justify-content:center;gap:10px;padding:40px 0;color:var(--text-secondary)}.loading-icon[data-v-fede7675]{font-size:20px;animation:spin-fede7675 1s linear infinite}@keyframes spin-fede7675{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.vm-list[data-v-fede7675]{display:flex;flex-direction:column;gap:8px}.vm-item[data-v-fede7675]{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface-hover);border-radius:8px}.vm-main[data-v-fede7675]{display:flex;align-items:center;gap:10px}.vm-id[data-v-fede7675]{font-size:12px;font-weight:700;color:var(--el-color-primary);background:var(--surface);padding:2px 8px;border-radius:4px;border:1px solid var(--border)}.vm-name[data-v-fede7675]{font-size:14px;font-weight:500;color:var(--text)}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>局域网机器管理后台</title>
<script type="module" crossorigin src="/assets/index-ybrLS-Uj.js"></script>
<script type="module" crossorigin src="/assets/index-D__NZX4I.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DHYRt9JF.css">
</head>
<body>

1
web/dist/assets/Login-DsgAjqqn.js vendored Normal file
View File

@@ -0,0 +1 @@
import{o as b,c as x,a as o,b as s,w as l,r as g,d as u,u as k,e as v,f as N,g as w,l as z,h as y,E as d,i as C,s as V,j as M}from"./index-D__NZX4I.js";import{_ as A}from"./_plugin-vue_export-helper-DlAUqK2U.js";const B={class:"login-page"},E={class:"login-card"},K={class:"input-wrap"},U={class:"input-wrap"},$={class:"login-guest"},j={__name:"Login",setup(I){const c=k(),a=g({username:"",password:""}),n=g(!1);async function r(){if(!a.value.username||!a.value.password){d.warning("请输入用户名和密码");return}n.value=!0;try{const t=await C(a.value);V(t.data),d.success("登录成功"),c.push("/")}catch{}n.value=!1}async function h(){n.value=!0;try{const t=await M();V(t.data),d.success("已进入访客模式"),c.push("/")}catch{}n.value=!1}return(t,e)=>{const p=u("el-icon"),_=u("el-input"),m=u("el-form-item"),f=u("el-button"),L=u("el-form");return b(),x("div",B,[o("div",E,[e[4]||(e[4]=o("div",{class:"login-header"},[o("div",{class:"logo"},"LM"),o("h1",null,"LAN Manager"),o("p",null,"轻量级局域网资产管理平台")],-1)),s(L,{model:a.value,class:"login-form"},{default:l(()=>[s(m,null,{default:l(()=>[o("div",K,[s(p,{class:"input-icon"},{default:l(()=>[s(v(N))]),_:1}),s(_,{modelValue:a.value.username,"onUpdate:modelValue":e[0]||(e[0]=i=>a.value.username=i),placeholder:"用户名",size:"large",onKeydown:w(r,["enter"])},null,8,["modelValue"])])]),_:1}),s(m,null,{default:l(()=>[o("div",U,[s(p,{class:"input-icon"},{default:l(()=>[s(v(z))]),_:1}),s(_,{modelValue:a.value.password,"onUpdate:modelValue":e[1]||(e[1]=i=>a.value.password=i),type:"password","show-password":"",placeholder:"密码",size:"large",onKeydown:w(r,["enter"])},null,8,["modelValue"])])]),_:1}),s(f,{type:"primary",size:"large",class:"login-btn",loading:n.value,onClick:r},{default:l(()=>[...e[2]||(e[2]=[y("登录",-1)])]),_:1},8,["loading"]),o("div",$,[s(f,{text:"",size:"small",onClick:h},{default:l(()=>[...e[3]||(e[3]=[y("访客模式进入",-1)])]),_:1})])]),_:1},8,["model"])]),e[5]||(e[5]=o("div",{class:"login-footer"}," © LAN Manager ",-1))])}}},q=A(j,[["__scopeId","data-v-92ccba4b"]]);export{q as default};

1
web/dist/assets/Logs-B37lkquY.js vendored Normal file
View File

@@ -0,0 +1 @@
import{k as V,aq as $,o as w,c as k,a as o,b as t,w as d,t as f,ar as N,m as B,r as i,d as r,as as C,h as b}from"./index-D__NZX4I.js";import{_ as L}from"./_plugin-vue_export-helper-DlAUqK2U.js";const M={class:"page"},T={class:"page-header"},U={class:"header-actions"},I={class:"card"},q={class:"table-header"},E={class:"pagination-bar"},F={__name:"Logs",setup(H){const h=i([]),p=i(0),c=i(1),u=i(20),_=i(""),g=i(!1);V(()=>v());async function v(){g.value=!0;const l=await $({page:c.value,page_size:u.value,search:_.value});h.value=l.data.list,p.value=l.data.total,g.value=!1}function y(l){if(!l)return"-";const e=new Date(l);if(isNaN(e.getTime()))return l;const s=m=>String(m).padStart(2,"0");return`${e.getFullYear()}-${s(e.getMonth()+1)}-${s(e.getDate())} ${s(e.getHours())}:${s(e.getMinutes())}:${s(e.getSeconds())}`}return(l,e)=>{const s=r("el-input"),m=r("el-button"),n=r("el-table-column"),z=r("el-tag"),x=r("el-table"),D=r("el-pagination"),S=C("loading");return w(),k("div",M,[o("div",T,[e[4]||(e[4]=o("div",null,[o("div",{class:"page-title"},"操作日志"),o("div",{class:"page-subtitle"},"查看最近的系统操作记录")],-1)),o("div",U,[t(s,{modelValue:_.value,"onUpdate:modelValue":e[0]||(e[0]=a=>_.value=a),placeholder:"搜索操作内容",clearable:"",style:{width:"240px"}},null,8,["modelValue"]),t(m,{type:"primary",onClick:v},{default:d(()=>[...e[3]||(e[3]=[b("查询",-1)])]),_:1})])]),o("div",I,[o("div",q,"共 "+f(p.value)+" 条记录",1),N((w(),B(x,{data:h.value,stripe:"",style:{width:"100%"},class:"modern-table"},{default:d(()=>[t(n,{prop:"id",label:"ID",width:"70"}),t(n,{prop:"action",label:"操作",width:"140"}),t(n,{prop:"target_type",label:"对象",width:"100"},{default:d(({row:a})=>[t(z,{size:"small",effect:"plain",round:""},{default:d(()=>[b(f(a.target_type),1)]),_:2},1024)]),_:1}),t(n,{prop:"target_name",label:"对象名称","min-width":"160"}),t(n,{prop:"details",label:"详情","min-width":"240","show-overflow-tooltip":""}),t(n,{prop:"created_at",label:"时间",width:"170"},{default:d(({row:a})=>[b(f(y(a.created_at)),1)]),_:1})]),_:1},8,["data"])),[[S,g.value]]),o("div",E,[t(D,{"current-page":c.value,"onUpdate:currentPage":e[1]||(e[1]=a=>c.value=a),"page-size":u.value,"onUpdate:pageSize":e[2]||(e[2]=a=>u.value=a),"page-sizes":[10,20,50,100],layout:"total, sizes, prev, pager, next",total:p.value,onChange:v},null,8,["current-page","page-size","total"])])])])}}},j=L(F,[["__scopeId","data-v-b1b09e4f"]]);export{j as default};

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -0,0 +1 @@
import{k as h,o as i,c as m,a as s,b as e,w as t,m as d,n as r,t as v,p as C,r as x,q as w,d as _,u as E,e as l,v as L,x as M,y as A,z as B,A as D,B as N,C as V,f as z,D as I,F as S,E as q,G as F,H as G}from"./index-D__NZX4I.js";import{_ as H}from"./_plugin-vue_export-helper-DlAUqK2U.js";const P={class:"layout"},R={key:0,class:"sidebar"},T={class:"nav"},$={class:"sidebar-footer"},j={class:"user-info"},J={class:"user-name"},K={class:"main-inner"},O={__name:"MainLayout",setup(Q){const f=E(),u=w(()=>F().is_admin),o=x(!1);h(()=>{o.value=document.documentElement.classList.contains("dark")});function p(){o.value=!o.value,document.documentElement.classList.toggle("dark",o.value),localStorage.setItem("theme",o.value?"dark":"light")}async function g(){try{await S()}catch{}G(),q.success("已退出"),f.push("/login")}return(k,a)=>{const n=_("el-icon"),c=_("router-link"),y=_("el-button"),b=_("router-view");return i(),m("div",P,[u.value?(i(),m("aside",R,[a[5]||(a[5]=s("div",{class:"brand"},[s("div",{class:"brand-logo"},"LM"),s("div",{class:"brand-text"},"LAN Manager")],-1)),s("nav",T,[e(c,{to:"/machines",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(L))]),_:1}),a[0]||(a[0]=s("span",null,"机器列表",-1))]),_:1}),u.value?(i(),d(c,{key:0,to:"/topology",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(M))]),_:1}),a[1]||(a[1]=s("span",null,"拓扑图",-1))]),_:1})):r("",!0),u.value?(i(),d(c,{key:1,to:"/logs",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(A))]),_:1}),a[2]||(a[2]=s("span",null,"操作日志",-1))]),_:1})):r("",!0),u.value?(i(),d(c,{key:2,to:"/pve-hosts",class:"nav-item","active-class":"active"},{default:t(()=>[e(n,null,{default:t(()=>[e(l(B))]),_:1}),a[3]||(a[3]=s("span",null,"PVE 主机",-1))]),_:1})):r("",!0)]),s("div",{class:"theme-toggle",onClick:p},[e(n,{class:"theme-icon"},{default:t(()=>[(i(),d(D(o.value?l(N):l(V))))]),_:1}),s("span",null,v(o.value?"浅色模式":"深色模式"),1)]),s("div",$,[s("div",j,[e(n,{class:"user-icon"},{default:t(()=>[e(l(z))]),_:1}),s("span",J,v(u.value?"管理员":"访客"),1)]),e(y,{text:"",class:"logout-btn",onClick:g},{default:t(()=>[e(n,null,{default:t(()=>[e(l(I))]),_:1}),a[4]||(a[4]=s("span",null,"退出",-1))]),_:1})])])):r("",!0),s("main",{class:C(["main",{"no-sidebar":!u.value}])},[s("div",K,[e(b)])],2)])}}},X=H(O,[["__scopeId","data-v-dac2493b"]]);export{X as default};

1
web/dist/assets/PVEHosts-Bh9QYZd6.css vendored Normal file
View File

@@ -0,0 +1 @@
.toolbar[data-v-fede7675]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.page-title[data-v-fede7675]{margin:0;font-size:20px;font-weight:600}.hosts-grid[data-v-fede7675]{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.host-card[data-v-fede7675]{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px}.host-header[data-v-fede7675]{display:flex;align-items:center;gap:10px;margin-bottom:12px}.host-icon[data-v-fede7675]{font-size:20px;color:var(--el-color-primary)}.host-name[data-v-fede7675]{font-size:16px;font-weight:600;flex:1}.status-tag[data-v-fede7675]{font-size:11px;height:20px;padding:0 8px}.host-info[data-v-fede7675]{margin-bottom:12px}.info-row[data-v-fede7675]{display:flex;font-size:13px;margin-bottom:4px}.info-row .label[data-v-fede7675]{color:var(--text-secondary);width:50px}.info-row .value[data-v-fede7675]{color:var(--text)}.host-actions[data-v-fede7675]{display:flex;gap:8px}.vm-loading[data-v-fede7675]{display:flex;align-items:center;justify-content:center;gap:10px;padding:40px 0;color:var(--text-secondary)}.loading-icon[data-v-fede7675]{font-size:20px;animation:spin-fede7675 1s linear infinite}@keyframes spin-fede7675{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.vm-list[data-v-fede7675]{display:flex;flex-direction:column;gap:8px}.vm-item[data-v-fede7675]{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface-hover);border-radius:8px}.vm-main[data-v-fede7675]{display:flex;align-items:center;gap:10px}.vm-id[data-v-fede7675]{font-size:12px;font-weight:700;color:var(--el-color-primary);background:var(--surface);padding:2px 8px;border-radius:4px;border:1px solid var(--border)}.vm-name[data-v-fede7675]{font-size:14px;font-weight:500;color:var(--text)}

1
web/dist/assets/PVEHosts-DdWF4BUY.js vendored Normal file

File diff suppressed because one or more lines are too long

467
web/dist/assets/Topology-D7ycfmKq.js vendored Normal file

File diff suppressed because one or more lines are too long

100
web/dist/assets/index-D__NZX4I.js vendored Normal file

File diff suppressed because one or more lines are too long

2
web/dist/index.html vendored
View File

@@ -5,7 +5,7 @@
<link rel="icon" href="/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>局域网机器管理后台</title>
<script type="module" crossorigin src="/assets/index-ybrLS-Uj.js"></script>
<script type="module" crossorigin src="/assets/index-D__NZX4I.js"></script>
<link rel="stylesheet" crossorigin href="/assets/index-DHYRt9JF.css">
</head>
<body>

View File

@@ -21,11 +21,11 @@
</div>
</div>
<div class="header-actions" v-if="isAdmin">
<el-tag v-if="machine.pve_host_id && machine.pve_vmid" size="small" :type="vmStatusInfo.status === 'running' ? 'success' : 'info'" effect="light" class="vm-status-tag">
{{ vmStatusInfo.status === 'running' ? '🟢 VM运行中' : vmStatusInfo.status === 'stopped' ? '🔴 VM已停止' : ' VM检测中' }}
<el-tag v-if="machine.pve_host_id && machine.pve_vmid" size="small" :type="vmStatusInfo._error ? 'info' : vmStatusInfo.status === 'running' ? 'success' : vmStatusInfo.status === 'stopped' ? 'danger' : 'info'" effect="light" class="vm-status-tag">
{{ vmStatusInfo._error ? 'VM检测失败' : vmStatusInfo.status === 'running' ? 'VM运行中' : vmStatusInfo.status === 'stopped' ? 'VM已停止' : 'VM检测中' }}
</el-tag>
<el-button v-if="machine.pve_host_id && machine.pve_vmid" type="success" :icon="VideoPlay" :loading="vmLoading" @click="startVM" :disabled="vmStatusInfo.status === 'running'">启动</el-button>
<el-button v-if="machine.pve_host_id && machine.pve_vmid" type="warning" :icon="VideoPause" :loading="vmLoading" @click="stopVM" :disabled="vmStatusInfo.status === 'stopped'">关闭</el-button>
<el-button v-if="machine.pve_host_id && machine.pve_vmid" type="success" :icon="VideoPlay" :loading="vmLoading" @click="startVM" :disabled="vmStatusInfo.status === 'running' || vmStatusInfo._error">启动</el-button>
<el-button v-if="machine.pve_host_id && machine.pve_vmid" type="warning" :icon="VideoPause" :loading="vmLoading" @click="stopVM" :disabled="vmStatusInfo.status === 'stopped' || vmStatusInfo._error">关闭</el-button>
<el-button text :icon="Edit" @click="openEdit(machine)">编辑</el-button>
<el-button text type="danger" :icon="Delete" @click="delMachine">删除</el-button>
</div>
@@ -502,7 +502,7 @@ async function loadVMStatus() {
const res = await fetchVMStatus(machineId)
vmStatusInfo.value = res.data
} catch (e) {
vmStatusInfo.value = {}
vmStatusInfo.value = { _error: true }
}
}

View File

@@ -71,7 +71,8 @@
<!-- VM List Dialog -->
<el-dialog v-model="vmDialogVisible" :title="`${selectedHost?.name || ''} — 虚拟机列表`" width="600px" class="modern-dialog" destroy-on-close>
<el-empty v-if="!vmLoading && !vmList.length" description="该节点上暂无虚拟机" />
<el-alert v-if="vmError" :title="vmError" type="error" :closable="false" show-icon />
<el-empty v-else-if="!vmLoading && !vmList.length" description="该节点上暂无虚拟机" />
<div v-else-if="vmLoading" class="vm-loading">
<el-icon class="loading-icon"><Loading /></el-icon>
<span>正在获取虚拟机列表...</span>
@@ -104,6 +105,7 @@ const editing = ref({ name: '', hostname: '', port: 8006, node_name: 'pve', user
const vmDialogVisible = ref(false)
const vmLoading = ref(false)
const vmList = ref([])
const vmError = ref('')
const selectedHost = ref(null)
const hostStatus = ref({})
@@ -114,10 +116,8 @@ onMounted(() => {
async function load() {
const res = await fetchPVEHosts()
hosts.value = res.data
// 检测每个节点的连接状态
for (const h of hosts.value) {
checkHostStatus(h.id)
}
// 并行检测每个节点的连接状态
await Promise.all(hosts.value.map(h => checkHostStatus(h.id)))
}
async function checkHostStatus(id) {
@@ -177,11 +177,12 @@ async function openVMList(host) {
vmDialogVisible.value = true
vmLoading.value = true
vmList.value = []
vmError.value = ''
try {
const res = await fetchPVEHostVMs(host.id)
vmList.value = res.data || []
} catch (e) {
ElMessage.error('获取虚拟机列表失败')
vmError.value = '获取虚拟机列表失败,请检查节点连接状态'
}
vmLoading.value = false
}