feat(pve): 完善PVE节点功能,实现前端VM操作
- 后端:新增 /pve/hosts/:id/status 节点连接状态检测API
- 后端:新增 /pve/hosts/:id/vms 节点虚拟机列表API
- 前端:PVEHosts页面添加节点在线状态标签、节点名展示、VM列表弹窗
- 前端:MachineDetail页面添加VM状态卡片(CPU/内存/运行时长)
- 前端:启动VM按钮添加确认对话框,启停按钮根据VM状态禁用
- 前端:定时轮询VM状态,操作后自动刷新
[宪宪/K2.6🐾]
This commit is contained in:
@@ -98,6 +98,9 @@ func (h *PVEHandler) Create(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
idPtr := host.ID
|
||||
middleware.LogOperation("create", "pve_host", &idPtr, host.Name, "", host.Hostname+":"+strconv.Itoa(host.Port), c.ClientIP(), middleware.CurrentUser(c))
|
||||
|
||||
host.PasswordEnc = ""
|
||||
host.Password = ""
|
||||
c.JSON(http.StatusCreated, host)
|
||||
@@ -139,6 +142,8 @@ func (h *PVEHandler) Update(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
middleware.LogOperation("update", "pve_host", &id, host.Name, "", host.Hostname+":"+strconv.Itoa(host.Port), c.ClientIP(), middleware.CurrentUser(c))
|
||||
|
||||
host.PasswordEnc = ""
|
||||
c.JSON(http.StatusOK, host)
|
||||
}
|
||||
@@ -156,14 +161,67 @@ func (h *PVEHandler) Delete(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取主机名用于日志
|
||||
host, _ := h.service.GetByID(id)
|
||||
hostName := ""
|
||||
if host != nil {
|
||||
hostName = host.Name
|
||||
}
|
||||
|
||||
if err := h.service.Delete(id); err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
middleware.LogOperation("delete", "pve_host", &id, hostName, "", "", c.ClientIP(), middleware.CurrentUser(c))
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"message": "deleted"})
|
||||
}
|
||||
|
||||
// HostStatus 检测 PVE 节点连接状态
|
||||
func (h *PVEHandler) HostStatus(c *gin.Context) {
|
||||
if !middleware.IsAdmin(c) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
status, err := h.service.GetHostStatus(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusOK, gin.H{"status": "offline", "error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, gin.H{"status": status})
|
||||
}
|
||||
|
||||
// HostVMList 获取 PVE 节点上的虚拟机列表
|
||||
func (h *PVEHandler) HostVMList(c *gin.Context) {
|
||||
if !middleware.IsAdmin(c) {
|
||||
c.JSON(http.StatusForbidden, gin.H{"error": "admin only"})
|
||||
return
|
||||
}
|
||||
|
||||
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, gin.H{"error": "invalid id"})
|
||||
return
|
||||
}
|
||||
|
||||
vms, err := h.service.GetHostVMList(id)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, vms)
|
||||
}
|
||||
|
||||
// VMStatus 获取虚拟机状态
|
||||
func (h *PVEHandler) VMStatus(c *gin.Context) {
|
||||
if !middleware.IsAdmin(c) {
|
||||
|
||||
@@ -101,6 +101,8 @@ func main() {
|
||||
admin.POST("/pve/hosts", pveH.Create)
|
||||
admin.PUT("/pve/hosts/:id", pveH.Update)
|
||||
admin.DELETE("/pve/hosts/:id", pveH.Delete)
|
||||
admin.GET("/pve/hosts/:id/status", pveH.HostStatus)
|
||||
admin.GET("/pve/hosts/:id/vms", pveH.HostVMList)
|
||||
|
||||
// 虚拟机操作
|
||||
admin.GET("/machines/:id/vm-status", pveH.VMStatus)
|
||||
|
||||
@@ -369,6 +369,45 @@ func (s *PVEHostService) GetDecryptedPassword(host *models.PVEHost) (string, err
|
||||
return utils.Decrypt(host.PasswordEnc)
|
||||
}
|
||||
|
||||
// GetHostStatus 检测 PVE 节点连接状态
|
||||
func (s *PVEHostService) GetHostStatus(hostID int64) (string, error) {
|
||||
host, err := s.GetByID(hostID)
|
||||
if err != nil {
|
||||
return "offline", err
|
||||
}
|
||||
|
||||
password, err := s.GetDecryptedPassword(host)
|
||||
if err != nil {
|
||||
return "offline", err
|
||||
}
|
||||
|
||||
client := NewPVEClient(host, password)
|
||||
if err := client.Login(); err != nil {
|
||||
return "offline", nil
|
||||
}
|
||||
return "online", nil
|
||||
}
|
||||
|
||||
// GetHostVMList 获取 PVE 节点上的虚拟机列表
|
||||
func (s *PVEHostService) GetHostVMList(hostID int64) ([]struct {
|
||||
VMID string `json:"vmid"`
|
||||
Name string `json:"name"`
|
||||
Status string `json:"status"`
|
||||
}, error) {
|
||||
host, err := s.GetByID(hostID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
password, err := s.GetDecryptedPassword(host)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
client := NewPVEClient(host, password)
|
||||
return client.GetVMList()
|
||||
}
|
||||
|
||||
// GetVMStatus 获取虚拟机状态
|
||||
func (s *PVEHostService) GetVMStatus(hostID int64, vmid string) (*models.PVEVMStatus, error) {
|
||||
host, err := s.GetByID(hostID)
|
||||
|
||||
@@ -1 +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-lpjSKhQ-.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};
|
||||
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-ybrLS-Uj.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 +1 @@
|
||||
import{k as V,ap as $,o as w,c as k,a as o,b as t,w as d,t as f,aq as N,m as B,r as i,d as r,ar as C,h as b}from"./index-lpjSKhQ-.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};
|
||||
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-ybrLS-Uj.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-BbocaiS5.css
Normal file
1
server/static/assets/MachineDetail-BbocaiS5.css
Normal file
File diff suppressed because one or more lines are too long
1
server/static/assets/MachineDetail-CCMzEr2v.js
Normal file
1
server/static/assets/MachineDetail-CCMzEr2v.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +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-lpjSKhQ-.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};
|
||||
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-ybrLS-Uj.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-CEZ8F_J3.js
Normal file
1
server/static/assets/PVEHosts-CEZ8F_J3.js
Normal file
File diff suppressed because one or more lines are too long
1
server/static/assets/PVEHosts-D2p3oOfw.css
Normal file
1
server/static/assets/PVEHosts-D2p3oOfw.css
Normal file
@@ -0,0 +1 @@
|
||||
.toolbar[data-v-b00c62c0]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.page-title[data-v-b00c62c0]{margin:0;font-size:20px;font-weight:600}.hosts-grid[data-v-b00c62c0]{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.host-card[data-v-b00c62c0]{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px}.host-header[data-v-b00c62c0]{display:flex;align-items:center;gap:10px;margin-bottom:12px}.host-icon[data-v-b00c62c0]{font-size:20px;color:var(--el-color-primary)}.host-name[data-v-b00c62c0]{font-size:16px;font-weight:600;flex:1}.status-tag[data-v-b00c62c0]{font-size:11px;height:20px;padding:0 8px}.host-info[data-v-b00c62c0]{margin-bottom:12px}.info-row[data-v-b00c62c0]{display:flex;font-size:13px;margin-bottom:4px}.info-row .label[data-v-b00c62c0]{color:var(--text-secondary);width:50px}.info-row .value[data-v-b00c62c0]{color:var(--text)}.host-actions[data-v-b00c62c0]{display:flex;gap:8px}.vm-loading[data-v-b00c62c0]{display:flex;align-items:center;justify-content:center;gap:10px;padding:40px 0;color:var(--text-secondary)}.loading-icon[data-v-b00c62c0]{font-size:20px;animation:spin-b00c62c0 1s linear infinite}@keyframes spin-b00c62c0{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.vm-list[data-v-b00c62c0]{display:flex;flex-direction:column;gap:8px}.vm-item[data-v-b00c62c0]{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface-hover);border-radius:8px}.vm-main[data-v-b00c62c0]{display:flex;align-items:center;gap:10px}.vm-id[data-v-b00c62c0]{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-b00c62c0]{font-size:14px;font-weight:500;color:var(--text)}
|
||||
467
server/static/assets/Topology-Dk9gGVfs.js
Normal file
467
server/static/assets/Topology-Dk9gGVfs.js
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -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-lpjSKhQ-.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-ybrLS-Uj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DHYRt9JF.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
1
web/dist/assets/Login-T4gCuUld.js
vendored
Normal file
1
web/dist/assets/Login-T4gCuUld.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-ybrLS-Uj.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-BBHuDNkg.js
vendored
Normal file
1
web/dist/assets/Logs-BBHuDNkg.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-ybrLS-Uj.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-BbocaiS5.css
vendored
Normal file
1
web/dist/assets/MachineDetail-BbocaiS5.css
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/MachineDetail-CCMzEr2v.js
vendored
Normal file
1
web/dist/assets/MachineDetail-CCMzEr2v.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/MachineDetail-DmGzwGVM.js
vendored
1
web/dist/assets/MachineDetail-DmGzwGVM.js
vendored
File diff suppressed because one or more lines are too long
1
web/dist/assets/MachineList-MeWLdebZ.js
vendored
Normal file
1
web/dist/assets/MachineList-MeWLdebZ.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/MainLayout-CQLd0cJ1.js
vendored
Normal file
1
web/dist/assets/MainLayout-CQLd0cJ1.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-ybrLS-Uj.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-CEZ8F_J3.js
vendored
Normal file
1
web/dist/assets/PVEHosts-CEZ8F_J3.js
vendored
Normal file
File diff suppressed because one or more lines are too long
1
web/dist/assets/PVEHosts-D2p3oOfw.css
vendored
Normal file
1
web/dist/assets/PVEHosts-D2p3oOfw.css
vendored
Normal file
@@ -0,0 +1 @@
|
||||
.toolbar[data-v-b00c62c0]{display:flex;align-items:center;justify-content:space-between;margin-bottom:20px}.page-title[data-v-b00c62c0]{margin:0;font-size:20px;font-weight:600}.hosts-grid[data-v-b00c62c0]{display:grid;grid-template-columns:repeat(auto-fill,minmax(300px,1fr));gap:16px}.host-card[data-v-b00c62c0]{background:var(--surface);border:1px solid var(--border);border-radius:var(--radius);padding:16px}.host-header[data-v-b00c62c0]{display:flex;align-items:center;gap:10px;margin-bottom:12px}.host-icon[data-v-b00c62c0]{font-size:20px;color:var(--el-color-primary)}.host-name[data-v-b00c62c0]{font-size:16px;font-weight:600;flex:1}.status-tag[data-v-b00c62c0]{font-size:11px;height:20px;padding:0 8px}.host-info[data-v-b00c62c0]{margin-bottom:12px}.info-row[data-v-b00c62c0]{display:flex;font-size:13px;margin-bottom:4px}.info-row .label[data-v-b00c62c0]{color:var(--text-secondary);width:50px}.info-row .value[data-v-b00c62c0]{color:var(--text)}.host-actions[data-v-b00c62c0]{display:flex;gap:8px}.vm-loading[data-v-b00c62c0]{display:flex;align-items:center;justify-content:center;gap:10px;padding:40px 0;color:var(--text-secondary)}.loading-icon[data-v-b00c62c0]{font-size:20px;animation:spin-b00c62c0 1s linear infinite}@keyframes spin-b00c62c0{0%{transform:rotate(0)}to{transform:rotate(360deg)}}.vm-list[data-v-b00c62c0]{display:flex;flex-direction:column;gap:8px}.vm-item[data-v-b00c62c0]{display:flex;align-items:center;justify-content:space-between;padding:10px 12px;background:var(--surface-hover);border-radius:8px}.vm-main[data-v-b00c62c0]{display:flex;align-items:center;gap:10px}.vm-id[data-v-b00c62c0]{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-b00c62c0]{font-size:14px;font-weight:500;color:var(--text)}
|
||||
1
web/dist/assets/PVEHosts-DgnxRMDp.js
vendored
1
web/dist/assets/PVEHosts-DgnxRMDp.js
vendored
@@ -1 +0,0 @@
|
||||
import{k as N,Q as $,o as p,c as f,a as t,b as o,w as s,e as g,M,m as S,n as k,N as z,O as D,r as y,d,h as v,v as L,t as c,a8 as F,as as I,E as V,at as O,au as Q}from"./index-lpjSKhQ-.js";import{_ as T}from"./_plugin-vue_export-helper-DlAUqK2U.js";const j={class:"page"},A={class:"toolbar"},G={key:1,class:"hosts-grid"},J={class:"host-header"},K={class:"host-name"},R={class:"host-info"},W={class:"info-row"},X={class:"value"},Y={class:"info-row"},Z={class:"value"},ee={class:"host-actions"},le={__name:"PVEHosts",setup(ae){const _=y([]),i=y(!1),l=y({name:"",hostname:"",port:8006,node_name:"pve",username:"",password:"",verify_ssl:!1});N(()=>{w()});async function w(){const n=await $();_.value=n.data}function b(n){l.value=n?{...n,password:""}:{name:"",hostname:"",port:8006,node_name:"pve",username:"",password:"",verify_ssl:!1},i.value=!0}async function E(){if(!l.value.name||!l.value.hostname||!l.value.username){V.warning("请填写必填项");return}if(!l.value.id&&!l.value.password){V.warning("请填写密码");return}try{l.value.id?await O(l.value.id,l.value):await Q(l.value),V.success("保存成功"),i.value=!1,w()}catch{}}async function C(n){try{await F.confirm(`确定要删除主机 "${n.name}" 吗?`,"确认删除",{type:"warning"}),await I(n.id),V.success("删除成功"),w()}catch{}}return(n,e)=>{const r=d("el-button"),x=d("el-empty"),P=d("el-icon"),m=d("el-input"),u=d("el-form-item"),U=d("el-input-number"),H=d("el-switch"),h=d("el-form"),q=d("el-dialog");return p(),f("div",j,[t("div",A,[e[11]||(e[11]=t("h2",{class:"page-title"},"PVE 主机管理",-1)),o(r,{type:"primary",icon:g(M),onClick:e[0]||(e[0]=a=>b())},{default:s(()=>[...e[10]||(e[10]=[v("添加主机",-1)])]),_:1},8,["icon"])]),_.value.length?k("",!0):(p(),S(x,{key:0,description:"暂无 PVE 主机"})),_.value.length?(p(),f("div",G,[(p(!0),f(z,null,D(_.value,a=>(p(),f("div",{key:a.id,class:"host-card"},[t("div",J,[o(P,{class:"host-icon"},{default:s(()=>[o(g(L))]),_:1}),t("span",K,c(a.name),1)]),t("div",R,[t("div",W,[e[12]||(e[12]=t("span",{class:"label"},"地址:",-1)),t("span",X,c(a.hostname)+":"+c(a.port),1)]),t("div",Y,[e[13]||(e[13]=t("span",{class:"label"},"用户:",-1)),t("span",Z,c(a.username),1)])]),t("div",ee,[o(r,{size:"small",onClick:B=>b(a)},{default:s(()=>[...e[14]||(e[14]=[v("编辑",-1)])]),_:1},8,["onClick"]),o(r,{size:"small",type:"danger",onClick:B=>C(a)},{default:s(()=>[...e[15]||(e[15]=[v("删除",-1)])]),_:1},8,["onClick"])])]))),128))])):k("",!0),o(q,{modelValue:i.value,"onUpdate:modelValue":e[9]||(e[9]=a=>i.value=a),title:l.value.id?"编辑主机":"添加主机",width:"480px",class:"modern-dialog","destroy-on-close":""},{footer:s(()=>[o(r,{onClick:e[8]||(e[8]=a=>i.value=!1)},{default:s(()=>[...e[16]||(e[16]=[v("取消",-1)])]),_:1}),o(r,{type:"primary",onClick:E},{default:s(()=>[...e[17]||(e[17]=[v("保存",-1)])]),_:1})]),default:s(()=>[o(h,{model:l.value,"label-width":"90px"},{default:s(()=>[o(u,{label:"名称",required:""},{default:s(()=>[o(m,{modelValue:l.value.name,"onUpdate:modelValue":e[1]||(e[1]=a=>l.value.name=a),placeholder:"如 PVE-01"},null,8,["modelValue"])]),_:1}),o(u,{label:"地址",required:""},{default:s(()=>[o(m,{modelValue:l.value.hostname,"onUpdate:modelValue":e[2]||(e[2]=a=>l.value.hostname=a),placeholder:"192.168.1.100"},null,8,["modelValue"])]),_:1}),o(u,{label:"节点名"},{default:s(()=>[o(m,{modelValue:l.value.node_name,"onUpdate:modelValue":e[3]||(e[3]=a=>l.value.node_name=a),placeholder:"pve(默认)"},null,8,["modelValue"])]),_:1}),o(u,{label:"端口"},{default:s(()=>[o(U,{modelValue:l.value.port,"onUpdate:modelValue":e[4]||(e[4]=a=>l.value.port=a),min:1,max:65535,style:{width:"100%"}},null,8,["modelValue"])]),_:1}),o(u,{label:"用户名",required:""},{default:s(()=>[o(m,{modelValue:l.value.username,"onUpdate:modelValue":e[5]||(e[5]=a=>l.value.username=a),placeholder:"root@pam 或 root@vmbr0"},null,8,["modelValue"])]),_:1}),o(u,{label:"密码",required:!l.value.id},{default:s(()=>[o(m,{modelValue:l.value.password,"onUpdate:modelValue":e[6]||(e[6]=a=>l.value.password=a),type:"password","show-password":"",placeholder:l.value.id?"留空保持不变":"必填"},null,8,["modelValue","placeholder"])]),_:1},8,["required"]),o(u,{label:"SSL 验证"},{default:s(()=>[o(H,{modelValue:l.value.verify_ssl,"onUpdate:modelValue":e[7]||(e[7]=a=>l.value.verify_ssl=a)},null,8,["modelValue"])]),_:1})]),_:1},8,["model"])]),_:1},8,["modelValue","title"])])}}},te=T(le,[["__scopeId","data-v-bd0711be"]]);export{te as default};
|
||||
467
web/dist/assets/Topology-Dk9gGVfs.js
vendored
Normal file
467
web/dist/assets/Topology-Dk9gGVfs.js
vendored
Normal file
File diff suppressed because one or more lines are too long
467
web/dist/assets/Topology-DtU27Fr5.js
vendored
467
web/dist/assets/Topology-DtU27Fr5.js
vendored
File diff suppressed because one or more lines are too long
96
web/dist/assets/index-ybrLS-Uj.js
vendored
Normal file
96
web/dist/assets/index-ybrLS-Uj.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" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>局域网机器管理后台</title>
|
||||
<script type="module" crossorigin src="/assets/index-lpjSKhQ-.js"></script>
|
||||
<script type="module" crossorigin src="/assets/index-ybrLS-Uj.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-DHYRt9JF.css">
|
||||
</head>
|
||||
<body>
|
||||
|
||||
@@ -69,6 +69,8 @@ export const fetchPVEHost = (id) => api.get(`/pve/hosts/${id}`)
|
||||
export const createPVEHost = (data) => api.post('/pve/hosts', data)
|
||||
export const updatePVEHost = (id, data) => api.put(`/pve/hosts/${id}`, data)
|
||||
export const deletePVEHost = (id) => api.delete(`/pve/hosts/${id}`)
|
||||
export const fetchPVEHostStatus = (id) => api.get(`/pve/hosts/${id}/status`)
|
||||
export const fetchPVEHostVMs = (id) => api.get(`/pve/hosts/${id}/vms`)
|
||||
|
||||
// 虚拟机操作
|
||||
export const fetchVMStatus = (machineId) => api.get(`/machines/${machineId}/vm-status`)
|
||||
|
||||
@@ -21,8 +21,11 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="header-actions" v-if="isAdmin">
|
||||
<el-button v-if="machine.pve_host_id && machine.pve_vmid" type="success" :icon="VideoPlay" :loading="vmLoading" @click="startVM">启动</el-button>
|
||||
<el-button v-if="machine.pve_host_id && machine.pve_vmid" type="warning" :icon="VideoPause" :loading="vmLoading" @click="stopVM">关闭</el-button>
|
||||
<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>
|
||||
<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 text :icon="Edit" @click="openEdit(machine)">编辑</el-button>
|
||||
<el-button text type="danger" :icon="Delete" @click="delMachine">删除</el-button>
|
||||
</div>
|
||||
@@ -41,6 +44,29 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- PVE VM Status -->
|
||||
<div class="card" v-if="machine.pve_host_id && machine.pve_vmid && vmStatusInfo.status">
|
||||
<div class="card-title">PVE 虚拟机状态</div>
|
||||
<div class="vm-status-grid">
|
||||
<div class="vm-stat-item">
|
||||
<span class="vm-stat-label">运行状态</span>
|
||||
<span class="vm-stat-value" :class="vmStatusInfo.status">{{ vmStatusInfo.status === 'running' ? '运行中' : '已停止' }}</span>
|
||||
</div>
|
||||
<div class="vm-stat-item" v-if="vmStatusInfo.cpu !== undefined">
|
||||
<span class="vm-stat-label">CPU 使用率</span>
|
||||
<span class="vm-stat-value">{{ vmStatusInfo.cpu?.toFixed ? vmStatusInfo.cpu.toFixed(1) : vmStatusInfo.cpu }}%</span>
|
||||
</div>
|
||||
<div class="vm-stat-item" v-if="vmStatusInfo.memory_used !== undefined && vmStatusInfo.memory_total">
|
||||
<span class="vm-stat-label">内存使用</span>
|
||||
<span class="vm-stat-value">{{ formatBytes(vmStatusInfo.memory_used) }} / {{ formatBytes(vmStatusInfo.memory_total) }}</span>
|
||||
</div>
|
||||
<div class="vm-stat-item" v-if="vmStatusInfo.uptime">
|
||||
<span class="vm-stat-label">运行时长</span>
|
||||
<span class="vm-stat-value">{{ formatUptime(vmStatusInfo.uptime) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- System Status -->
|
||||
<div class="card" v-if="machine.cpu_info || machine.memory_info || machine.disk_info">
|
||||
<div class="card-title">系统状态</div>
|
||||
@@ -333,6 +359,7 @@ const machine = ref(null)
|
||||
const services = ref([])
|
||||
const relationships = ref([])
|
||||
const vmLoading = ref(false)
|
||||
const vmStatusInfo = ref({})
|
||||
const allMachines = ref([])
|
||||
const offlineLogs = ref([])
|
||||
const pveHosts = ref([])
|
||||
@@ -369,6 +396,7 @@ onMounted(async () => {
|
||||
const pveRes = await fetchPVEHosts()
|
||||
pveHosts.value = pveRes.data
|
||||
} catch (e) {}
|
||||
await loadVMStatus()
|
||||
}
|
||||
await checkAuth()
|
||||
const interval = uiRefreshInterval || 10000
|
||||
@@ -378,6 +406,7 @@ onMounted(async () => {
|
||||
await loadServices()
|
||||
if (isAdmin) {
|
||||
await loadRelationships()
|
||||
await loadVMStatus()
|
||||
}
|
||||
}, interval)
|
||||
}
|
||||
@@ -467,13 +496,29 @@ async function saveMachine() {
|
||||
loadMachine()
|
||||
}
|
||||
|
||||
async function startVM() {
|
||||
vmLoading.value = true
|
||||
async function loadVMStatus() {
|
||||
if (!machine.value || !machine.value.pve_host_id || !machine.value.pve_vmid) return
|
||||
try {
|
||||
const res = await fetchVMStatus(machineId)
|
||||
vmStatusInfo.value = res.data
|
||||
} catch (e) {
|
||||
vmStatusInfo.value = {}
|
||||
}
|
||||
}
|
||||
|
||||
async function startVM() {
|
||||
try {
|
||||
await ElMessageBox.confirm('确定要启动虚拟机吗?', '确认启动', { type: 'info' })
|
||||
vmLoading.value = true
|
||||
await apiStartVM(machineId)
|
||||
ElMessage.success('虚拟机已启动')
|
||||
await loadVMStatus()
|
||||
loadMachine()
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
// error handled by interceptor
|
||||
}
|
||||
}
|
||||
vmLoading.value = false
|
||||
}
|
||||
|
||||
@@ -483,8 +528,13 @@ async function stopVM() {
|
||||
vmLoading.value = true
|
||||
await apiStopVM(machineId)
|
||||
ElMessage.success('虚拟机已关闭')
|
||||
await loadVMStatus()
|
||||
loadMachine()
|
||||
} catch (e) {}
|
||||
} catch (e) {
|
||||
if (e !== 'cancel') {
|
||||
// error handled by interceptor
|
||||
}
|
||||
}
|
||||
vmLoading.value = false
|
||||
}
|
||||
|
||||
@@ -636,6 +686,26 @@ function formatDuration(seconds) {
|
||||
const hr = h % 24
|
||||
return `${d}天${hr}时${m}分`
|
||||
}
|
||||
|
||||
function formatBytes(bytes) {
|
||||
if (!bytes && bytes !== 0) return '-'
|
||||
const b = Number(bytes)
|
||||
if (b < 1024) return `${b}B`
|
||||
if (b < 1024 * 1024) return `${(b / 1024).toFixed(1)}KB`
|
||||
if (b < 1024 * 1024 * 1024) return `${(b / 1024 / 1024).toFixed(1)}MB`
|
||||
return `${(b / 1024 / 1024 / 1024).toFixed(1)}GB`
|
||||
}
|
||||
|
||||
function formatUptime(seconds) {
|
||||
if (!seconds && seconds !== 0) return '-'
|
||||
const s = Number(seconds)
|
||||
const d = Math.floor(s / 86400)
|
||||
const h = Math.floor((s % 86400) / 3600)
|
||||
const m = Math.floor((s % 3600) / 60)
|
||||
if (d > 0) return `${d}天${h}时${m}分`
|
||||
if (h > 0) return `${h}时${m}分`
|
||||
return `${m}分`
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -668,6 +738,55 @@ function formatDuration(seconds) {
|
||||
.status-badge.offline { background: rgba(239,68,68,0.10); color: #b91c1c; }
|
||||
html.dark .status-badge.online { background: rgba(52,211,153,0.15); color: #34d399; }
|
||||
html.dark .status-badge.offline { background: rgba(248,113,113,0.15); color: #f87171; }
|
||||
|
||||
.vm-status-tag {
|
||||
font-size: 12px;
|
||||
height: 24px;
|
||||
margin-right: 4px;
|
||||
}
|
||||
|
||||
.vm-status-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, 1fr);
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vm-stat-item {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
padding: 10px 12px;
|
||||
background: var(--surface-hover);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vm-stat-label {
|
||||
font-size: 11px;
|
||||
color: var(--text-muted);
|
||||
}
|
||||
|
||||
.vm-stat-value {
|
||||
font-size: 14px;
|
||||
font-weight: 600;
|
||||
color: var(--text);
|
||||
}
|
||||
|
||||
.vm-stat-value.running {
|
||||
color: #15803d;
|
||||
}
|
||||
|
||||
.vm-stat-value.stopped {
|
||||
color: #b91c1c;
|
||||
}
|
||||
|
||||
html.dark .vm-stat-value.running {
|
||||
color: #34d399;
|
||||
}
|
||||
|
||||
html.dark .vm-stat-value.stopped {
|
||||
color: #f87171;
|
||||
}
|
||||
|
||||
.host-subtitle {
|
||||
margin-top: 4px;
|
||||
font-size: 13px;
|
||||
|
||||
@@ -12,18 +12,26 @@
|
||||
<div class="host-header">
|
||||
<el-icon class="host-icon"><Monitor /></el-icon>
|
||||
<span class="host-name">{{ h.name }}</span>
|
||||
<el-tag size="small" :type="hostStatus[h.id] === 'online' ? 'success' : hostStatus[h.id] === 'offline' ? 'danger' : 'info'" effect="light" class="status-tag">
|
||||
{{ hostStatus[h.id] === 'online' ? '在线' : hostStatus[h.id] === 'offline' ? '离线' : '检测中' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
<div class="host-info">
|
||||
<div class="info-row">
|
||||
<span class="label">地址:</span>
|
||||
<span class="value">{{ h.hostname }}:{{ h.port }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">节点:</span>
|
||||
<span class="value">{{ h.node_name || 'pve' }}</span>
|
||||
</div>
|
||||
<div class="info-row">
|
||||
<span class="label">用户:</span>
|
||||
<span class="value">{{ h.username }}</span>
|
||||
</div>
|
||||
</div>
|
||||
<div class="host-actions">
|
||||
<el-button size="small" :icon="View" @click="openVMList(h)">查看虚拟机</el-button>
|
||||
<el-button size="small" @click="openEdit(h)">编辑</el-button>
|
||||
<el-button size="small" type="danger" @click="handleDelete(h)">删除</el-button>
|
||||
</div>
|
||||
@@ -60,19 +68,45 @@
|
||||
<el-button type="primary" @click="saveHost">保存</el-button>
|
||||
</template>
|
||||
</el-dialog>
|
||||
|
||||
<!-- 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="该节点上暂无虚拟机" />
|
||||
<div v-else-if="vmLoading" class="vm-loading">
|
||||
<el-icon class="loading-icon"><Loading /></el-icon>
|
||||
<span>正在获取虚拟机列表...</span>
|
||||
</div>
|
||||
<div v-else class="vm-list">
|
||||
<div v-for="vm in vmList" :key="vm.vmid" class="vm-item">
|
||||
<div class="vm-main">
|
||||
<span class="vm-id">VM {{ vm.vmid }}</span>
|
||||
<span class="vm-name">{{ vm.name }}</span>
|
||||
</div>
|
||||
<el-tag size="small" :type="vm.status === 'running' ? 'success' : 'info'" effect="light">
|
||||
{{ vm.status === 'running' ? '运行中' : '已停止' }}
|
||||
</el-tag>
|
||||
</div>
|
||||
</div>
|
||||
</el-dialog>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { ref, onMounted } from 'vue'
|
||||
import { Plus, Monitor } from '@element-plus/icons-vue'
|
||||
import { fetchPVEHosts, createPVEHost, updatePVEHost, deletePVEHost } from '@/api'
|
||||
import { Plus, Monitor, View, Loading } from '@element-plus/icons-vue'
|
||||
import { fetchPVEHosts, createPVEHost, updatePVEHost, deletePVEHost, fetchPVEHostStatus, fetchPVEHostVMs } from '@/api'
|
||||
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||
|
||||
const hosts = ref([])
|
||||
const dialogVisible = ref(false)
|
||||
const editing = ref({ name: '', hostname: '', port: 8006, node_name: 'pve', username: '', password: '', verify_ssl: false })
|
||||
|
||||
const vmDialogVisible = ref(false)
|
||||
const vmLoading = ref(false)
|
||||
const vmList = ref([])
|
||||
const selectedHost = ref(null)
|
||||
const hostStatus = ref({})
|
||||
|
||||
onMounted(() => {
|
||||
load()
|
||||
})
|
||||
@@ -80,6 +114,19 @@ onMounted(() => {
|
||||
async function load() {
|
||||
const res = await fetchPVEHosts()
|
||||
hosts.value = res.data
|
||||
// 检测每个节点的连接状态
|
||||
for (const h of hosts.value) {
|
||||
checkHostStatus(h.id)
|
||||
}
|
||||
}
|
||||
|
||||
async function checkHostStatus(id) {
|
||||
try {
|
||||
const res = await fetchPVEHostStatus(id)
|
||||
hostStatus.value[id] = res.data.status
|
||||
} catch (e) {
|
||||
hostStatus.value[id] = 'offline'
|
||||
}
|
||||
}
|
||||
|
||||
function openEdit(item) {
|
||||
@@ -124,6 +171,20 @@ async function handleDelete(host) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function openVMList(host) {
|
||||
selectedHost.value = host
|
||||
vmDialogVisible.value = true
|
||||
vmLoading.value = true
|
||||
vmList.value = []
|
||||
try {
|
||||
const res = await fetchPVEHostVMs(host.id)
|
||||
vmList.value = res.data || []
|
||||
} catch (e) {
|
||||
ElMessage.error('获取虚拟机列表失败')
|
||||
}
|
||||
vmLoading.value = false
|
||||
}
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -141,7 +202,7 @@ async function handleDelete(host) {
|
||||
|
||||
.hosts-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
|
||||
grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
@@ -167,6 +228,13 @@ async function handleDelete(host) {
|
||||
.host-name {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.status-tag {
|
||||
font-size: 11px;
|
||||
height: 20px;
|
||||
padding: 0 8px;
|
||||
}
|
||||
|
||||
.host-info {
|
||||
@@ -192,4 +260,60 @@ async function handleDelete(host) {
|
||||
display: flex;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vm-loading {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 40px 0;
|
||||
color: var(--text-secondary);
|
||||
}
|
||||
|
||||
.loading-icon {
|
||||
font-size: 20px;
|
||||
animation: spin 1s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes spin {
|
||||
from { transform: rotate(0deg); }
|
||||
to { transform: rotate(360deg); }
|
||||
}
|
||||
|
||||
.vm-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.vm-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
padding: 10px 12px;
|
||||
background: var(--surface-hover);
|
||||
border-radius: 8px;
|
||||
}
|
||||
|
||||
.vm-main {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.vm-id {
|
||||
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 {
|
||||
font-size: 14px;
|
||||
font-weight: 500;
|
||||
color: var(--text);
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user