feat: mining section rework, GPU filter, chart as full-width row, progress bars separate line, contrast fix
This commit is contained in:
+32
-15
@@ -143,12 +143,16 @@ type GPUInfo struct {
|
||||
MemClock float64 `json:"mem_clock_mhz"`
|
||||
Power float64 `json:"power"`
|
||||
PowerLimit float64 `json:"power_limit"`
|
||||
PowerMax float64 `json:"power_max"`
|
||||
FanSpeed float64 `json:"fan_speed"`
|
||||
PState string `json:"pstate"`
|
||||
Persistence string `json:"persistence_mode"`
|
||||
ComputeMode string `json:"compute_mode"`
|
||||
DriverVersion string `json:"driver_version"`
|
||||
CudaVersion string `json:"cuda_version"`
|
||||
UtilMemory float64 `json:"util_memory"`
|
||||
UtilEncoder float64 `json:"util_encoder"`
|
||||
UtilDecoder float64 `json:"util_decoder"`
|
||||
ClocksMaxGfx float64 `json:"clocks_max_graphics"`
|
||||
ClocksMaxMem float64 `json:"clocks_max_memory"`
|
||||
}
|
||||
|
||||
// ═══════════════════════════════════════════════
|
||||
@@ -1220,20 +1224,33 @@ func tryNvidiaSMI() []GPUInfo {
|
||||
parts[i] = strings.Trim(parts[i], "[]")
|
||||
}
|
||||
|
||||
// 0:index 1:name 2:uuid 3:bus_id 4:driver 5:vbios
|
||||
// 6:temp_gpu 7:temp_mem 8:fan 9:power 10:power_limit
|
||||
// 11:power_default 12:power_min 13:power_max
|
||||
// 14:clk_gfx 15:clk_sm 16:clk_mem 17:clk_max_gfx 18:clk_max_mem
|
||||
// 19:util_gpu 20:util_mem 21:util_enc 22:util_dec
|
||||
// 23:vram_total 24:vram_used 25:vram_free
|
||||
// 26:ecc_corr 27:ecc_uncorr 28:compute_mode 29:pstate
|
||||
gpus = append(gpus, GPUInfo{
|
||||
Name: cleanStr(parts[1]),
|
||||
VRAMTotalMB: parseFloat(parts[23]),
|
||||
VRAMUsedMB: parseFloat(parts[24]),
|
||||
Temperature: parseFloat(parts[6]),
|
||||
LoadPercent: parseFloat(parts[19]),
|
||||
CoreClock: parseFloat(parts[14]),
|
||||
MemClock: parseFloat(parts[16]),
|
||||
Power: parseFloatSafe(parts[9]),
|
||||
PowerLimit: parseFloatSafe(parts[10]),
|
||||
FanSpeed: parseFloatSafe(parts[8]),
|
||||
PState: cleanStr(parts[29]),
|
||||
ComputeMode: cleanStr(parts[28]),
|
||||
DriverVersion: cleanStr(parts[4]),
|
||||
Name: cleanStr(parts[1]),
|
||||
DriverVersion: cleanStr(parts[4]),
|
||||
Temperature: parseFloat(parts[6]),
|
||||
FanSpeed: parseFloat(parts[8]),
|
||||
Power: parseFloat(parts[9]),
|
||||
PowerLimit: parseFloat(parts[10]),
|
||||
PowerMax: parseFloat(parts[13]),
|
||||
CoreClock: parseFloat(parts[14]),
|
||||
MemClock: parseFloat(parts[16]),
|
||||
ClocksMaxGfx: parseFloat(parts[17]),
|
||||
ClocksMaxMem: parseFloat(parts[18]),
|
||||
LoadPercent: parseFloat(parts[19]),
|
||||
UtilMemory: parseFloat(parts[20]),
|
||||
UtilEncoder: parseFloat(parts[21]),
|
||||
UtilDecoder: parseFloat(parts[22]),
|
||||
VRAMTotalMB: parseFloat(parts[23]),
|
||||
VRAMUsedMB: parseFloat(parts[24]),
|
||||
ComputeMode: cleanStr(parts[28]),
|
||||
PState: cleanStr(parts[29]),
|
||||
})
|
||||
}
|
||||
return gpus
|
||||
|
||||
+65
-15
@@ -11,6 +11,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -374,6 +375,55 @@ func (h *Hub) saveYAML() {
|
||||
os.WriteFile(h.devicesAbs, append([]byte(hdr), out...), 0644)
|
||||
}
|
||||
|
||||
func filterGPURaw(raw json.RawMessage) json.RawMessage {
|
||||
var data map[string]interface{}
|
||||
if json.Unmarshal(raw, &data) != nil {
|
||||
return raw
|
||||
}
|
||||
gpusRaw, ok := data["gpus"]
|
||||
if !ok {
|
||||
return raw
|
||||
}
|
||||
gpus, ok := gpusRaw.([]interface{})
|
||||
if !ok {
|
||||
return raw
|
||||
}
|
||||
var filtered []interface{}
|
||||
for _, g := range gpus {
|
||||
gpu, ok := g.(map[string]interface{})
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
gpuName, _ := gpu["name"].(string)
|
||||
if isVirtualGPU(gpuName) {
|
||||
continue
|
||||
}
|
||||
filtered = append(filtered, g)
|
||||
}
|
||||
data["gpus"] = filtered
|
||||
out, _ := json.Marshal(data)
|
||||
return out
|
||||
}
|
||||
|
||||
func isVirtualGPU(name string) bool {
|
||||
name = strings.ToLower(name)
|
||||
blacklist := []string{
|
||||
"microsoft",
|
||||
"basic render",
|
||||
"vmware",
|
||||
"virtual",
|
||||
"hyper-v",
|
||||
"parallels",
|
||||
"virtualbox",
|
||||
}
|
||||
for _, v := range blacklist {
|
||||
if strings.Contains(name, v) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||
var p AgentPayload
|
||||
if json.Unmarshal(raw, &p) != nil || p.MachineName == "" {
|
||||
@@ -398,7 +448,7 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||
rec.GPUs = append(rec.GPUs, GPURec{Name: g.Name, VRAM: g.VRAM})
|
||||
}
|
||||
cp := *rec
|
||||
h.live[name] = raw
|
||||
h.live[name] = filterGPURaw(raw)
|
||||
var md *MiningData
|
||||
var hist []HashratePoint
|
||||
if m, ok := h.miningData[name]; ok {
|
||||
@@ -416,10 +466,23 @@ func (h *Hub) UpdateDevice(raw json.RawMessage) {
|
||||
h.saveYAML()
|
||||
h.broadcast(BrowserMsg{
|
||||
Type: "device", Machine: name,
|
||||
Device: &BrowserDevice{Info: &cp, Live: raw, Online: true, Mining: md, History: hist},
|
||||
Device: &BrowserDevice{Info: &cp, Live: filterGPURaw(raw), Online: true, Mining: md, History: hist},
|
||||
})
|
||||
}
|
||||
|
||||
func (h *Hub) DeleteDevice(name string) {
|
||||
h.mu.Lock()
|
||||
_, ok := h.records[name]
|
||||
delete(h.records, name)
|
||||
delete(h.live, name)
|
||||
h.mu.Unlock()
|
||||
if ok {
|
||||
h.saveYAML()
|
||||
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
|
||||
logInfo("delete", "%s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) MarkOffline(name string) {
|
||||
h.mu.Lock()
|
||||
_, had := h.live[name]
|
||||
@@ -449,19 +512,6 @@ func (h *Hub) MarkOffline(name string) {
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) DeleteDevice(name string) {
|
||||
h.mu.Lock()
|
||||
_, ok := h.records[name]
|
||||
delete(h.records, name)
|
||||
delete(h.live, name)
|
||||
h.mu.Unlock()
|
||||
if ok {
|
||||
h.saveYAML()
|
||||
h.broadcast(BrowserMsg{Type: "remove", Machine: name})
|
||||
logInfo("delete", "%s", name)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Hub) AddMonitor(c *websocket.Conn) {
|
||||
h.mu.Lock()
|
||||
h.monitors[c] = struct{}{}
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
<title>Hardware Monitor</title>
|
||||
<link rel="stylesheet" href="https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.8/antd.min.css">
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/vue/2.7.16/vue.min.js"></script>
|
||||
<script src="https://cdn.bootcdn.net/ajax/libs/ant-design-vue/1.7.8/antd.min.js"></script>
|
||||
<link rel="stylesheet" href="https://cdn.staticfile.net/ant-design-vue/1.7.8/antd.min.css">
|
||||
<script src="https://cdn.staticfile.net/vue/2.7.16/vue.min.js"></script>
|
||||
<script src="https://cdn.staticfile.net/ant-design-vue/1.7.8/antd.min.js"></script>
|
||||
<style>
|
||||
:root {
|
||||
--bg: #0a0d13; --card: #111520; --sec: #161b28; --border: #1e253a;
|
||||
@@ -60,7 +60,7 @@
|
||||
.pdot { position:absolute; top:50%; left:50%; width:8px; height:8px; margin:-4px; background:var(--accent); border-radius:50% }
|
||||
@keyframes pulse { 0%{transform:scale(.35);opacity:1} 100%{transform:scale(2.4);opacity:0} }
|
||||
|
||||
.device-grid { padding:12px 20px 32px; display:grid; grid-template-columns:repeat(auto-fill,minmax(600px,1fr)); gap:12px }
|
||||
.device-grid { padding:12px 20px 32px; display:grid; grid-template-columns:repeat(auto-fill,minmax(450px,1fr)); gap:12px }
|
||||
.device-grid > .device-card { min-width:0 }
|
||||
.device-card { background:var(--card); border:1px solid var(--border); border-radius:10px; transition:opacity .3s,border-color .2s,box-shadow .2s }
|
||||
.device-card:hover { border-color:#28304a; box-shadow:0 4px 24px rgba(0,0,0,.2) }
|
||||
@@ -72,8 +72,11 @@
|
||||
.dc-bd { padding:5px 0 0 }
|
||||
.dc-col { padding:0 12px 8px; flex:1 1 0; min-width:0; display:flex; flex-direction:column }
|
||||
.dc-left { border-right:1px solid var(--border) }
|
||||
.dc-gpu-full { padding:0 12px 8px }
|
||||
.dc-mobile-mining { display:none }
|
||||
|
||||
.sec { background:var(--sec); border-radius:6px; padding:8px 12px }
|
||||
.mining-sec { background:#1a2030; border:1px solid #2a3548 }
|
||||
.sec-tt { font-size:10px; font-weight:600; letter-spacing:.8px; text-transform:uppercase; color:var(--dim); margin-bottom:4px }
|
||||
.sec-tt .hl { color:var(--text) }
|
||||
.hw-row { display:flex; align-items:center; gap:6px }
|
||||
@@ -83,7 +86,8 @@
|
||||
.info-row { display:flex; justify-content:space-between; align-items:center; font-size:10px; line-height:1.8 }
|
||||
.info-row .label { color:var(--dim) } .info-row .value { font-family:var(--mono); font-weight:500 }
|
||||
|
||||
.gpu-grid { display:grid; grid-template-columns:repeat(4,1fr); gap:4px }
|
||||
.gpu-grid { display:grid; grid-template-columns:repeat(auto-fill,minmax(90px,1fr)); gap:4px }
|
||||
@media (max-width:767px) { .gpu-grid { grid-template-columns:repeat(auto-fill,minmax(80px,1fr)) } }
|
||||
.gpu-block { background:rgba(0,0,0,.2); border-radius:5px; padding:6px; cursor:pointer; transition:background .2s,transform .15s }
|
||||
.gpu-block:hover { background:rgba(0,229,160,.06); transform:translateY(-1px) }
|
||||
.gpu-active { background:rgba(0,229,160,.12); border:1px solid var(--accent); transform:translateY(-1px) }
|
||||
@@ -95,7 +99,6 @@
|
||||
.gb-pct { position:absolute; inset:0; display:flex; align-items:center; justify-content:center; font-size:8px; font-weight:700; font-family:var(--mono); color:#fff; text-shadow:0 1px 2px rgba(0,0,0,.6) }
|
||||
.gb-info { display:flex; justify-content:space-between; font-size:9px; font-family:var(--mono); margin-top:2px }
|
||||
|
||||
.gpu-popover .ant-popover { width:100%!important; left:0!important }
|
||||
.gpu-popover .ant-popover-inner { width:100%!important }
|
||||
.gpu-popover .ant-popover-inner-content { background:var(--card)!important; border:1px solid var(--border); border-radius:10px; padding:10px 12px; box-shadow:0 8px 32px rgba(0,0,0,.5); box-sizing:border-box }
|
||||
.gpu-popover .ant-popover-arrow { display:none }
|
||||
@@ -120,15 +123,15 @@
|
||||
.m-hr-lbl { width:22px; text-align:right; font-family:var(--mono); font-size:9px; color:var(--dim); flex-shrink:0 }
|
||||
.m-hr-bar { flex:1; height:6px; background:var(--dim-bar); border-radius:3px; overflow:hidden }
|
||||
.m-hr-fill { height:100%; border-radius:3px; transition:width .4s ease }
|
||||
.m-hr-val { min-width:70px; text-align:right; font-family:var(--mono); font-size:9px; font-weight:500; flex-shrink:0 }
|
||||
.m-status { display:flex; align-items:center; gap:5px; font-size:10px; margin-bottom:5px }
|
||||
.m-agent,.m-country { color:var(--dim); font-size:9px; font-family:var(--mono); margin-left:auto }
|
||||
.m-shares { display:flex; justify-content:space-between; margin-top:5px; font-family:var(--mono); font-size:9px }
|
||||
.m-hr-val { min-width:70px; text-align:right; font-family:var(--mono); font-size:10px; font-weight:500; flex-shrink:0; color:var(--text) }
|
||||
.m-status { display:flex; align-items:center; gap:5px; font-size:10px; margin-bottom:5px; color:var(--text) }
|
||||
.m-agent,.m-country { color:var(--text); font-size:9px; font-family:var(--mono); margin-left:auto }
|
||||
.m-shares { display:flex; justify-content:space-between; margin-top:5px; font-family:var(--mono); font-size:10px; color:var(--text) }
|
||||
.m-sh.valid { color:var(--accent) } .m-sh.stale { color:var(--warn) } .m-sh.invalid { color:var(--danger) }
|
||||
.m-times { display:flex; gap:10px; margin-top:5px; font-size:9px }
|
||||
.m-times span { color:var(--dim) } .m-times .v { font-family:var(--mono) }
|
||||
.chart-box { background:rgba(0,0,0,.15); border-radius:4px; padding:6px 8px; margin-top:6px; overflow:hidden }
|
||||
.chart-lbl { font-size:8px; color:var(--dim); font-family:var(--mono); margin-bottom:3px }
|
||||
.m-times { display:flex; gap:10px; margin-top:5px; font-size:9px; color:var(--text) }
|
||||
.m-times span { color:var(--text) } .m-times .v { font-family:var(--mono); color:var(--text) }
|
||||
.chart-box { background:rgba(0,0,0,.25); border-radius:4px; padding:6px 8px; margin-top:6px; overflow:hidden }
|
||||
.chart-lbl { font-size:9px; color:var(--text); font-family:var(--mono); margin-bottom:3px }
|
||||
.chart-empty { color:var(--dim); font-size:8px; text-align:center; padding:10px 0 }
|
||||
|
||||
.off-actions { display:flex; justify-content:flex-end; padding-top:0 }
|
||||
@@ -229,13 +232,26 @@
|
||||
<div class="sec"><div class="hw-row"><span class="sec-tt">硬件状态</span><span class="dot on"></span><span class="tag-on">在线</span></div></div>
|
||||
<cpu-section :cpu="getLive(name).cpu||{}" :info="devices[name].info" :is-on="true"></cpu-section>
|
||||
<mem-section :mem="getLive(name).memory||{}" :is-on="true"></mem-section>
|
||||
<gpu-section :gpus="getLive(name).gpus||[]" :expanded-gpu="expandedGpu" @toggle-gpu="toggleGpu"></gpu-section>
|
||||
</a-col>
|
||||
<a-col :xs="0" :sm="0" :md="12" class="dc-col dc-right" style="flex:1 1 0">
|
||||
<div class="sec"><div class="hw-row"><span class="sec-tt">矿池状态</span><span class="dot on"></span><span class="tag-on">在线</span></div></div>
|
||||
<div class="sec hw-status"><div class="hw-row"><span class="sec-tt">矿池状态</span><span class="dot on"></span><span class="tag-on">在线</span><span style="margin-left:auto;font-size:8px;color:var(--dim);font-family:var(--mono)">{{ miningRuntime(name) }} {{ miningCountry(name) }}</span></div></div>
|
||||
<mining-section :mining="devices[name].mining" :history="devices[name].hash_history"></mining-section>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<div class="dc-gpu-full">
|
||||
<gpu-section :gpus="getLive(name).gpus||[]" :expanded-gpu="expandedGpu" @toggle-gpu="toggleGpu"></gpu-section>
|
||||
</div>
|
||||
<div v-if="devices[name].hash_history && devices[name].hash_history.length > 1" class="dc-gpu-full">
|
||||
<div class="sec mining-sec"><div class="chart-lbl">算力走势 (30m 均值)</div><div v-html="buildChartSvg(devices[name].hash_history)"></div></div>
|
||||
</div>
|
||||
<div class="dc-mobile-mining">
|
||||
<a-popover trigger="click" placement="bottom" overlayClassName="gpu-popover">
|
||||
<template slot="content"><div class="gpu-pop-body" style="min-width:280px">
|
||||
<mining-section :mining="devices[name].mining" :history="devices[name].hash_history"></mining-section>
|
||||
</div></template>
|
||||
<a-button size="small" type="primary" ghost><a-icon type="bar-chart" /> 矿池详情</a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
@@ -299,12 +315,13 @@ Vue.component('cpu-section',{
|
||||
props:['cpu','info','isOn'],
|
||||
template:`<div class="sec">
|
||||
<div class="sec-tt">CPU · <span class="hl">{{ model }}</span></div>
|
||||
<div style="display:flex;align-items:center;gap:8px">
|
||||
<a-progress :percent="pct" :stroke-color="barColor" :stroke-width="6" size="small" :format="fmtPct" style="flex:1;min-width:0" />
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||||
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ pc }}/{{ lc }}</span>
|
||||
<span class="value" :style="{color:tempColor,fontSize:'10px',whiteSpace:'nowrap'}">{{ tmpStr }}</span>
|
||||
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ freq }}MHz</span>
|
||||
<span class="value" style="margin-left:auto;font-size:10px;font-family:var(--mono);font-weight:500" :style="{color:barColor}">{{ pct }}%</span>
|
||||
</div>
|
||||
<div style="display:block"><a-progress :percent="pct" :stroke-color="barColor" :stroke-width="5" size="small" :format="fmtPct" /></div>
|
||||
</div>`,
|
||||
computed:{
|
||||
model:function(){return this.cpu.model||(this.info&&this.info.cpu_model)||'Unknown'},
|
||||
@@ -323,10 +340,12 @@ Vue.component('cpu-section',{
|
||||
Vue.component('mem-section',{
|
||||
props:['mem','isOn'],
|
||||
template:`<div class="sec"><div class="sec-tt">MEMORY</div>
|
||||
<div v-if="mem.total_bytes" style="display:flex;align-items:center;gap:8px">
|
||||
<a-progress :percent="pct" :stroke-color="barColor" :stroke-width="6" size="small" :format="fmtPct" style="flex:1;min-width:0" />
|
||||
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ used }}/{{ total }}</span>
|
||||
<span class="value" :style="{color:barColor,fontSize:'10px',whiteSpace:'nowrap'}">{{ pct }}%</span>
|
||||
<div v-if="mem.total_bytes">
|
||||
<div style="display:flex;align-items:center;gap:8px;margin-bottom:4px">
|
||||
<span class="value" style="font-size:10px;white-space:nowrap;color:var(--dim)">{{ used }} / {{ total }}</span>
|
||||
<span class="value" :style="{color:barColor,fontSize:'10px',whiteSpace:'nowrap',marginLeft:'auto'}">{{ pct }}%</span>
|
||||
</div>
|
||||
<div style="display:block"><a-progress :percent="pct" :stroke-color="barColor" :stroke-width="5" size="small" :format="fmtPct" /></div>
|
||||
</div>
|
||||
<div v-else style="font-size:10px;color:var(--dim)">无数据</div></div>`,
|
||||
computed:{total:function(){return fmtB(this.mem.total_bytes)},used:function(){return fmtB(this.mem.used_bytes)},pct:function(){return Math.round(this.mem.percent||0)},barColor:function(){return pColor(this.pct)}},
|
||||
@@ -335,7 +354,7 @@ Vue.component('mem-section',{
|
||||
|
||||
Vue.component('gpu-section',{
|
||||
props:['gpus','expandedGpu'],
|
||||
template:`<div class="sec"><div class="sec-tt">GPU · <span class="hl">{{ gpus.length }} 张</span><span style="margin-left:auto;font-size:9px;color:var(--text);font-family:var(--mono)">{{ totalPower }} W</span></div>
|
||||
template:`<div class="sec"><div class="sec-tt">GPU · <span class="hl">{{ gpus.length }} 张</span><span style="margin-left:auto;font-size:9px;color:var(--text);font-family:var(--mono)">{{ totalPower }}W / {{ totalPowerMax }}W</span></div>
|
||||
<div class="gpu-grid"><div class="gpu-block" :class="{'gpu-active':expandedGpu===i}" v-for="(g,i) in gpus" :key="i">
|
||||
<a-popover trigger="click" placement="bottom" :visible="expandedGpu===i" @visibleChange="onVis(i,$event)" overlayClassName="gpu-popover" :getPopupContainer="getPopContainer">
|
||||
<template slot="content"><div class="gpu-pop-body">
|
||||
@@ -343,20 +362,23 @@ Vue.component('gpu-section',{
|
||||
<div class="gpu-pop-row-hd"><span class="gpu-pop-name">GPU{{ ii+1 }}</span><span class="gpu-pop-model">{{ gg.name||'N/A' }}</span></div>
|
||||
<div class="gpu-pop-grid">
|
||||
<div class="gd-item"><span class="gd-l">负载</span><span class="gd-v" :style="{color:pColor(gg.load_percent||0)}">{{ (gg.load_percent||0).toFixed(1) }}%</span></div>
|
||||
<div class="gd-item"><span class="gd-l">显存负载</span><span class="gd-v">{{ (gg.util_memory||0).toFixed(0) }}%</span></div>
|
||||
<div class="gd-item"><span class="gd-l">温度</span><span class="gd-v" :style="{color:tmpColor(gg)}">{{ tmpStr(gg) }}</span></div>
|
||||
<div class="gd-item"><span class="gd-l">功耗</span><span class="gd-v">{{ Math.round(gg.power||0) }}W / {{ Math.round(gg.power_limit||0) }}W</span></div>
|
||||
<div class="gd-item"><span class="gd-l">核心频率</span><span class="gd-v">{{ Math.round(gg.core_clock_mhz||0) }} MHz</span></div>
|
||||
<div class="gd-item"><span class="gd-l">显存频率</span><span class="gd-v">{{ Math.round(gg.mem_clock_mhz||0) }} MHz</span></div>
|
||||
<div class="gd-item"><span class="gd-l">功耗范围</span><span class="gd-v">{{ Math.round(gg.power||0) }}W / {{ Math.round(gg.power_max||0) }}W</span></div>
|
||||
<div class="gd-item"><span class="gd-l">风扇</span><span class="gd-v">{{ fanStr(gg) }}</span></div>
|
||||
<div class="gd-item"><span class="gd-l">核心频率</span><span class="gd-v">{{ Math.round(gg.core_clock_mhz||0) }} MHz</span></div>
|
||||
<div class="gd-item"><span class="gd-l">最大核心</span><span class="gd-v">{{ Math.round(gg.clocks_max_graphics||0) }} MHz</span></div>
|
||||
<div class="gd-item"><span class="gd-l">显存频率</span><span class="gd-v">{{ Math.round(gg.mem_clock_mhz||0) }} MHz</span></div>
|
||||
<div class="gd-item"><span class="gd-l">编码器</span><span class="gd-v">{{ (gg.util_encoder||0).toFixed(0) }}%</span></div>
|
||||
<div class="gd-item"><span class="gd-l">解码器</span><span class="gd-v">{{ (gg.util_decoder||0).toFixed(0) }}%</span></div>
|
||||
<div class="gd-item"><span class="gd-l">性能状态</span><span class="gd-v">{{ gg.pstate||'N/A' }}</span></div>
|
||||
<div class="gd-item"><span class="gd-l">持久模式</span><span class="gd-v">{{ gg.persistence_mode||'N/A' }}</span></div>
|
||||
<div class="gd-item"><span class="gd-l">计算模式</span><span class="gd-v">{{ gg.compute_mode||'N/A' }}</span></div>
|
||||
</div>
|
||||
<div v-if="vramTotal(gg)>0" style="margin-top:2px"><div class="info-row" style="margin-bottom:1px"><span class="label">显存</span><span class="value">{{ vramUsed(gg) }} / {{ vramTotal(gg) }} GB</span></div>
|
||||
<a-progress :percent="Math.round(vramPct(gg))" :stroke-color="pColor(vramPct(gg))" :stroke-width="4" size="small" :format="fmtPct" /></div>
|
||||
<div v-if="gg.driver_version" style="margin-top:2px;font-size:8px;color:var(--dim);display:flex;gap:8px">
|
||||
<div v-if="gg.driver_version" style="margin-top:2px;font-size:8px;color:var(--dim)">
|
||||
<span>驱动: {{ gg.driver_version }}</span>
|
||||
<span v-if="gg.cuda_version">CUDA: {{ gg.cuda_version }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div></template>
|
||||
@@ -369,7 +391,8 @@ Vue.component('gpu-section',{
|
||||
</a-popover>
|
||||
</div></div></div>`,
|
||||
computed:{
|
||||
totalPower:function(){var t=0;for(var i=0;i<this.gpus.length;i++)t+=this.gpus[i].power||0;return Math.round(t)}
|
||||
totalPower:function(){var t=0;for(var i=0;i<this.gpus.length;i++)t+=this.gpus[i].power||0;return Math.round(t)},
|
||||
totalPowerMax:function(){var t=0;for(var i=0;i<this.gpus.length;i++)t+=this.gpus[i].power_max||this.gpus[i].power_limit||0;return Math.round(t)}
|
||||
},
|
||||
methods:{
|
||||
pColor:pColor, tmpStr:function(g){var t=(g||{}).temperature||0;return t>0?t.toFixed(0)+'°':'—'},
|
||||
@@ -380,7 +403,17 @@ Vue.component('gpu-section',{
|
||||
vramStr:function(g){var t=(g||{}).vram_total||(g||{}).vram_total_mb||0,u=(g||{}).vram_used||(g||{}).vram_used_mb||0;return t>0?(u/1024).toFixed(1)+'/'+(t/1024).toFixed(0)+'G':'—'},
|
||||
fanStr:function(g){var f=(g||{}).fan_speed||0;return f>0?Math.round(f)+'%':'—'},
|
||||
fmtPct:function(v){return v+'%'},
|
||||
getPopContainer:function(){return this.$el.closest('.device-card')||document.body},
|
||||
getPopContainer:function(){
|
||||
var card=this.$el.closest('.device-card');
|
||||
if(card){
|
||||
var w=card.offsetWidth;
|
||||
this.$nextTick(function(){
|
||||
var pop=document.querySelector('.gpu-popover .ant-popover');
|
||||
if(pop) pop.style.width=w+'px';
|
||||
});
|
||||
}
|
||||
return card||document.body;
|
||||
},
|
||||
toggle:function(i){var next=this.expandedGpu===i?null:i;this.$emit('toggle-gpu',next);
|
||||
if(next!==null){var self=this;this.$nextTick(function(){setTimeout(function(){var el=document.getElementById('gpu-pop-'+next);if(el){var c=document.querySelector('.gpu-pop-body');if(c){c.scrollTop=el.offsetTop-c.clientHeight/2+el.clientHeight/2}}},50)})}},
|
||||
onVis:function(i,vis){if(!vis&&this.expandedGpu===i)this.$emit('toggle-gpu',null)}
|
||||
@@ -389,21 +422,19 @@ Vue.component('gpu-section',{
|
||||
|
||||
Vue.component('mining-section',{
|
||||
props:['mining','history'],
|
||||
template:`<div v-if="mining"><div class="sec">
|
||||
<div class="sec-tt">MINING · <span class="hl">{{ coin }}</span></div>
|
||||
<div class="m-status"><span class="dot" :class="sOn?'on':'off'"></span>
|
||||
<span :style="{color:sOn?'#00e5a0':'#ff4060',fontWeight:500,fontSize:'9px'}">{{ sOn?'在线':'离线' }}</span>
|
||||
<span class="m-agent">{{ mining.agent||'' }}</span><span class="m-country" v-if="mining.country">{{ mining.country }}</span></div>
|
||||
<div v-for="hr in hrs" :key="hr.l" class="m-hr-row"><span class="m-hr-lbl">{{ hr.l }}</span>
|
||||
<div class="m-hr-bar"><div class="m-hr-fill" :style="{width:hr.p+'%',background:pColor(hr.p)}"></div></div>
|
||||
<span class="m-hr-val" :style="{color:pColor(hr.p)}">{{ hr.v }}</span></div>
|
||||
template:`<div v-if="mining"><div class="sec mining-sec">
|
||||
<div class="sec-tt"><span class="hl">{{ coin }}</span><span style="margin-left:6px;font-size:8px;color:var(--dim)">{{ mining.agent||'' }}</span></div>
|
||||
<div style="display:flex;gap:8px;margin:6px 0">
|
||||
<div v-for="hr in hrs" :key="hr.l" style="flex:1;min-width:0;text-align:center">
|
||||
<div style="font-size:8px;color:var(--dim);margin-bottom:2px">{{ hr.l }}</div>
|
||||
<div style="font-size:12px;font-weight:500;font-family:var(--mono)" :style="{color:pColor(hr.p)}">{{ hr.v }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="m-times"><span>活跃 <span class="v">{{ fmtMsTs(mining.last_active) }}</span></span>
|
||||
<span>开始 <span class="v">{{ fmtMsTs(mining.opened_at) }}</span></span>
|
||||
<span v-if="durH">运行 <span class="v">{{ durH }} h</span></span></div>
|
||||
<span>开始 <span class="v">{{ fmtMsTs(mining.opened_at) }}</span></span></div>
|
||||
<div class="m-shares"><span class="m-sh valid">{{ mining.valid||0 }} 有效</span>
|
||||
<span class="m-sh stale">{{ mining.stale||0 }} 过期</span><span class="m-sh invalid">{{ mining.invalid||0 }} 无效</span></div>
|
||||
<div class="chart-box"><div class="chart-lbl">算力走势 (30m 均值)</div><div v-html="chartHtml"></div></div>
|
||||
</div></div><div v-else class="sec"><div class="sec-tt">MINING</div><div style="color:var(--dim);font-size:9px;text-align:center;padding:10px 0">暂无矿池数据</div></div>`,
|
||||
</div></div><div v-else class="sec mining-sec"><div class="sec-tt" style="color:var(--dim)">暂无矿池数据</div></div>`,
|
||||
computed:{
|
||||
coin:function(){return((this.mining||{}).coin||'').toUpperCase()},
|
||||
sOn:function(){return(this.mining||{}).status==='online'},
|
||||
@@ -451,6 +482,8 @@ new Vue({
|
||||
methods:{
|
||||
getLive:function(name){var e=this.devices[name];if(!e||!e.live)return{};var l=e.live;if(typeof l==='string')try{l=JSON.parse(l)}catch(ex){}return l||{}},
|
||||
isPoolOnline:function(name){var e=this.devices[name];return e&&e.mining&&e.mining.status==='online'},
|
||||
miningRuntime(name){var e=this.devices[name];if(!e||!e.mining)return'';var m=e.mining;if(m.last_active>0&&m.opened_at>0)return((m.last_active-m.opened_at)/3600000).toFixed(1)+'h';return''},
|
||||
miningCountry(name){var e=this.devices[name];return e&&e.mining?e.mining.country||'':''},
|
||||
ago:ago,
|
||||
showRefresh:function(){var self=this;this.refreshing=true;setTimeout(function(){self.refreshing=false},500)},
|
||||
countdown:function(iso){
|
||||
@@ -464,6 +497,7 @@ new Vue({
|
||||
return Math.floor(s/86400)+'天前';
|
||||
},
|
||||
toggleGpu:function(i){this.expandedGpu=i},
|
||||
buildChartSvg:buildChartSvg,
|
||||
deleteDevice:function(n){var self=this;this.$confirm({title:'确定删除 '+n+' 的设备记录?',onOk:function(){if(self.ws&&self.ws.readyState===1)self.ws.send(JSON.stringify({action:'delete',machine:n}))}})},
|
||||
handleLogin:function(e){e.preventDefault();var self=this;this.loginForm.validateFields(function(err,values){if(err)return;self.loginLoading=true;self.loginErr='';self.loginConn='';localStorage.setItem('mm_id',values.miner_id);localStorage.setItem('mm_pw',values.password);self.connectWs(values.miner_id,values.password)})},
|
||||
sendAuth:function(mid,pw){if(this.ws&&this.ws.readyState===WebSocket.OPEN)this.ws.send(JSON.stringify({type:'auth',miner_id:mid,password:pw}))},
|
||||
|
||||
Reference in New Issue
Block a user