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:
@@ -7,6 +7,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/cookiejar"
|
"net/http/cookiejar"
|
||||||
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -57,9 +58,11 @@ func (c *PVEClient) baseURL() string {
|
|||||||
// Login 获取 PVE 认证ticket
|
// Login 获取 PVE 认证ticket
|
||||||
func (c *PVEClient) Login() error {
|
func (c *PVEClient) Login() error {
|
||||||
loginURL := c.baseURL() + "/api2/json/access/ticket"
|
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 {
|
if err != nil {
|
||||||
return err
|
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)
|
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 {
|
var result struct {
|
||||||
Data struct {
|
Data struct {
|
||||||
Status string `json:"status"`
|
Status string `json:"status"`
|
||||||
Uptime int64 `json:"uptime"`
|
Uptime int64 `json:"uptime"`
|
||||||
CPU float64 `json:"cpu"`
|
CPU float64 `json:"cpu"`
|
||||||
MemoryUsed int64 `json:"mem"`
|
MemoryUsed int64 `json:"mem"`
|
||||||
MemoryTotal int64 `json:"maxmem"`
|
MemoryTotal int64 `json:"maxmem"`
|
||||||
} `json:"data"`
|
} `json:"data"`
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
|
if err := json.Unmarshal(body, &result); err != nil {
|
||||||
body, _ := io.ReadAll(resp.Body)
|
|
||||||
return nil, fmt.Errorf("decode failed: %v, body: %s", err, string(body))
|
return nil, fmt.Errorf("decode failed: %v, body: %s", err, string(body))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
1
server/static/assets/Login-DsgAjqqn.js
Normal file
1
server/static/assets/Login-DsgAjqqn.js
Normal 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
server/static/assets/Logs-B37lkquY.js
Normal file
1
server/static/assets/Logs-B37lkquY.js
Normal 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};
|
||||||
1
server/static/assets/MachineDetail-BeE91e36.css
Normal file
1
server/static/assets/MachineDetail-BeE91e36.css
Normal file
File diff suppressed because one or more lines are too long
1
server/static/assets/MachineDetail-CV5DfGMb.js
Normal file
1
server/static/assets/MachineDetail-CV5DfGMb.js
Normal file
File diff suppressed because one or more lines are too long
1
server/static/assets/MachineList-MzBh9r6c.js
Normal file
1
server/static/assets/MachineList-MzBh9r6c.js
Normal file
File diff suppressed because one or more lines are too long
1
server/static/assets/MainLayout-BpAm6i02.js
Normal file
1
server/static/assets/MainLayout-BpAm6i02.js
Normal 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
server/static/assets/PVEHosts-Bh9QYZd6.css
Normal file
1
server/static/assets/PVEHosts-Bh9QYZd6.css
Normal 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
server/static/assets/PVEHosts-DdWF4BUY.js
Normal file
1
server/static/assets/PVEHosts-DdWF4BUY.js
Normal file
File diff suppressed because one or more lines are too long
467
server/static/assets/Topology-D7ycfmKq.js
Normal file
467
server/static/assets/Topology-D7ycfmKq.js
Normal file
File diff suppressed because one or more lines are too long
100
server/static/assets/index-D__NZX4I.js
Normal file
100
server/static/assets/index-D__NZX4I.js
Normal file
File diff suppressed because one or more lines are too long
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>局域网机器管理后台</title>
|
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-DHYRt9JF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
1
web/dist/assets/Login-DsgAjqqn.js
vendored
Normal file
1
web/dist/assets/Login-DsgAjqqn.js
vendored
Normal 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
1
web/dist/assets/Logs-B37lkquY.js
vendored
Normal 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};
|
||||||
1
web/dist/assets/MachineDetail-BeE91e36.css
vendored
Normal file
1
web/dist/assets/MachineDetail-BeE91e36.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/MachineDetail-CV5DfGMb.js
vendored
Normal file
1
web/dist/assets/MachineDetail-CV5DfGMb.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/MachineList-MzBh9r6c.js
vendored
Normal file
1
web/dist/assets/MachineList-MzBh9r6c.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/MainLayout-BpAm6i02.js
vendored
Normal file
1
web/dist/assets/MainLayout-BpAm6i02.js
vendored
Normal 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
1
web/dist/assets/PVEHosts-Bh9QYZd6.css
vendored
Normal 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
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
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
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
2
web/dist/index.html
vendored
@@ -5,7 +5,7 @@
|
|||||||
<link rel="icon" href="/favicon.ico" />
|
<link rel="icon" href="/favicon.ico" />
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
<title>局域网机器管理后台</title>
|
<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">
|
<link rel="stylesheet" crossorigin href="/assets/index-DHYRt9JF.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
@@ -21,11 +21,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="header-actions" v-if="isAdmin">
|
<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">
|
<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.status === 'running' ? '🟢 VM运行中' : vmStatusInfo.status === 'stopped' ? '🔴 VM已停止' : '⏳ VM检测中' }}
|
{{ vmStatusInfo._error ? 'VM检测失败' : vmStatusInfo.status === 'running' ? 'VM运行中' : vmStatusInfo.status === 'stopped' ? 'VM已停止' : 'VM检测中' }}
|
||||||
</el-tag>
|
</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="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'">关闭</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 :icon="Edit" @click="openEdit(machine)">编辑</el-button>
|
||||||
<el-button text type="danger" :icon="Delete" @click="delMachine">删除</el-button>
|
<el-button text type="danger" :icon="Delete" @click="delMachine">删除</el-button>
|
||||||
</div>
|
</div>
|
||||||
@@ -502,7 +502,7 @@ async function loadVMStatus() {
|
|||||||
const res = await fetchVMStatus(machineId)
|
const res = await fetchVMStatus(machineId)
|
||||||
vmStatusInfo.value = res.data
|
vmStatusInfo.value = res.data
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
vmStatusInfo.value = {}
|
vmStatusInfo.value = { _error: true }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -71,7 +71,8 @@
|
|||||||
|
|
||||||
<!-- VM List Dialog -->
|
<!-- VM List Dialog -->
|
||||||
<el-dialog v-model="vmDialogVisible" :title="`${selectedHost?.name || ''} — 虚拟机列表`" width="600px" class="modern-dialog" destroy-on-close>
|
<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">
|
<div v-else-if="vmLoading" class="vm-loading">
|
||||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||||
<span>正在获取虚拟机列表...</span>
|
<span>正在获取虚拟机列表...</span>
|
||||||
@@ -104,6 +105,7 @@ const editing = ref({ name: '', hostname: '', port: 8006, node_name: 'pve', user
|
|||||||
const vmDialogVisible = ref(false)
|
const vmDialogVisible = ref(false)
|
||||||
const vmLoading = ref(false)
|
const vmLoading = ref(false)
|
||||||
const vmList = ref([])
|
const vmList = ref([])
|
||||||
|
const vmError = ref('')
|
||||||
const selectedHost = ref(null)
|
const selectedHost = ref(null)
|
||||||
const hostStatus = ref({})
|
const hostStatus = ref({})
|
||||||
|
|
||||||
@@ -114,10 +116,8 @@ onMounted(() => {
|
|||||||
async function load() {
|
async function load() {
|
||||||
const res = await fetchPVEHosts()
|
const res = await fetchPVEHosts()
|
||||||
hosts.value = res.data
|
hosts.value = res.data
|
||||||
// 检测每个节点的连接状态
|
// 并行检测每个节点的连接状态
|
||||||
for (const h of hosts.value) {
|
await Promise.all(hosts.value.map(h => checkHostStatus(h.id)))
|
||||||
checkHostStatus(h.id)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function checkHostStatus(id) {
|
async function checkHostStatus(id) {
|
||||||
@@ -177,11 +177,12 @@ async function openVMList(host) {
|
|||||||
vmDialogVisible.value = true
|
vmDialogVisible.value = true
|
||||||
vmLoading.value = true
|
vmLoading.value = true
|
||||||
vmList.value = []
|
vmList.value = []
|
||||||
|
vmError.value = ''
|
||||||
try {
|
try {
|
||||||
const res = await fetchPVEHostVMs(host.id)
|
const res = await fetchPVEHostVMs(host.id)
|
||||||
vmList.value = res.data || []
|
vmList.value = res.data || []
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
ElMessage.error('获取虚拟机列表失败')
|
vmError.value = '获取虚拟机列表失败,请检查节点连接状态'
|
||||||
}
|
}
|
||||||
vmLoading.value = false
|
vmLoading.value = false
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user