变更项:\n1. 在 markdown_to_image 增加 get_md2img_health_snapshot 健康快照能力,输出 runtime 线程、事件循环、浏览器连接、启动来源与 PID 状态。\n2. 新增系统接口 GET /api/system/md2img_health,支持后台查询转图运行时健康信息。\n3. 新增系统接口 POST /api/system/md2img_warmup,支持后台手动触发转图预热并返回最新状态。\n4. 在资源监控页面接入转图健康状态条,展示运行时在线状态、浏览器连接状态及关键摘要信息。\n5. 在资源监控页面增加转图预热与状态刷新按钮,便于线上快速自愈与排障。\n6. 补充中文注释与错误提示,保持后端与前端可观测性一致。
214 lines
10 KiB
HTML
214 lines
10 KiB
HTML
{% extends "base.html" %}
|
||
|
||
{% block title %}资源监控 - 机器人管理后台{% endblock %}
|
||
|
||
{% block content %}
|
||
<div class="page-shell system-page">
|
||
<div class="page-hero">
|
||
<div class="page-hero-copy">
|
||
<div class="page-eyebrow">System Workspace</div>
|
||
<h1>资源监控</h1>
|
||
<p>直接在后台查看系统资源变化与运行状态,保持监控入口简洁清晰。</p>
|
||
<div class="md2img-health-inline" v-loading="md2imgLoading">
|
||
<span class="health-title">转图运行时</span>
|
||
<el-tag size="mini" :type="runtimeTagType">{% raw %}{{ runtimeTagText }}{% endraw %}</el-tag>
|
||
<el-tag size="mini" :type="browserTagType">{% raw %}{{ browserTagText }}{% endraw %}</el-tag>
|
||
<span class="health-brief">{% raw %}{{ md2imgBrief }}{% endraw %}</span>
|
||
<span class="health-time" v-if="md2imgHealth && md2imgHealth.timestamp">
|
||
{% raw %}{{ md2imgHealth.timestamp }}{% endraw %}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
<div class="page-hero-actions">
|
||
<el-button type="success" plain :loading="md2imgWarming" @click="warmupMd2Img">
|
||
<i class="el-icon-magic-stick"></i> 预热转图
|
||
</el-button>
|
||
<el-button type="info" plain :loading="md2imgLoading" @click="loadMd2ImgHealth">
|
||
<i class="el-icon-refresh"></i> 刷新转图状态
|
||
</el-button>
|
||
<el-button type="primary" plain @click="reloadIframe"><i class="el-icon-refresh"></i> 刷新面板</el-button>
|
||
<el-button type="primary" @click="openInNewTab"><i class="el-icon-top-right"></i> 新窗口打开</el-button>
|
||
<el-button type="danger" @click="confirmRestart"><i class="el-icon-refresh-left"></i> 重启服务</el-button>
|
||
</div>
|
||
</div>
|
||
|
||
<el-card class="iframe-shell-card" shadow="hover">
|
||
<div slot="header" class="workspace-header">
|
||
<div>
|
||
<h3>监控面板</h3>
|
||
<p>直接在控制台内查看系统资源变化与运行状态。</p>
|
||
</div>
|
||
<div class="iframe-url">{{ src_url }}</div>
|
||
</div>
|
||
<div class="iframe-shell">
|
||
<iframe ref="monitorFrame" src="{{ src_url }}" frameborder="0"></iframe>
|
||
</div>
|
||
</el-card>
|
||
</div>
|
||
{% endblock %}
|
||
|
||
{% block scripts %}
|
||
<script>
|
||
new Vue({
|
||
el: '#app',
|
||
mixins: [baseApp],
|
||
data() {
|
||
return {
|
||
currentView: '14',
|
||
frameUrl: '{{ src_url }}',
|
||
restarting: false,
|
||
md2imgLoading: false,
|
||
md2imgWarming: false,
|
||
md2imgHealth: null
|
||
}
|
||
},
|
||
computed: {
|
||
runtimeTagText() {
|
||
const runtime = this.md2imgHealth && this.md2imgHealth.runtime ? this.md2imgHealth.runtime : {};
|
||
return runtime.loop_running ? '运行时在线' : '运行时未就绪';
|
||
},
|
||
runtimeTagType() {
|
||
const runtime = this.md2imgHealth && this.md2imgHealth.runtime ? this.md2imgHealth.runtime : {};
|
||
return runtime.loop_running ? 'success' : 'warning';
|
||
},
|
||
browserTagText() {
|
||
const browser = this.md2imgHealth && this.md2imgHealth.browser ? this.md2imgHealth.browser : {};
|
||
return browser.connected ? '浏览器已连接' : '浏览器未连接';
|
||
},
|
||
browserTagType() {
|
||
const browser = this.md2imgHealth && this.md2imgHealth.browser ? this.md2imgHealth.browser : {};
|
||
return browser.connected ? 'success' : 'info';
|
||
},
|
||
md2imgBrief() {
|
||
if (!this.md2imgHealth) return '尚未获取状态';
|
||
const runtime = this.md2imgHealth.runtime || {};
|
||
const browser = this.md2imgHealth.browser || {};
|
||
const loopText = runtime.loop_id ? `loop=${runtime.loop_id}` : 'loop=-';
|
||
const pidText = browser.pid ? `pid=${browser.pid}` : 'pid=-';
|
||
const sourceText = browser.launch_source ? `source=${browser.launch_source}` : 'source=-';
|
||
return `${loopText} · ${pidText} · ${sourceText}`;
|
||
}
|
||
},
|
||
mounted() {
|
||
this.currentView = '14';
|
||
this.loadMd2ImgHealth();
|
||
},
|
||
methods: {
|
||
reloadIframe() {
|
||
if (this.$refs.monitorFrame) {
|
||
this.$refs.monitorFrame.src = this.frameUrl;
|
||
}
|
||
},
|
||
openInNewTab() {
|
||
window.open(this.frameUrl, '_blank');
|
||
},
|
||
confirmRestart() {
|
||
this.$confirm('确认执行 ./restart.sh 重启服务吗?这会中断当前服务几秒钟。', '重启确认', {
|
||
confirmButtonText: '确认重启',
|
||
cancelButtonText: '取消',
|
||
type: 'warning'
|
||
}).then(() => {
|
||
this.triggerRestart();
|
||
}).catch(() => {});
|
||
},
|
||
async triggerRestart() {
|
||
if (this.restarting) return;
|
||
this.restarting = true;
|
||
try {
|
||
const response = await axios.post('/api/restart_service');
|
||
if (response.data.success) {
|
||
this.$message.success(response.data.message || '已触发重启');
|
||
} else {
|
||
this.$message.error(response.data.message || '重启失败');
|
||
}
|
||
} catch (error) {
|
||
this.$message.error(error.response?.data?.message || '触发重启失败');
|
||
} finally {
|
||
this.restarting = false;
|
||
}
|
||
},
|
||
async loadMd2ImgHealth() {
|
||
if (this.md2imgLoading) return;
|
||
this.md2imgLoading = true;
|
||
try {
|
||
// 默认不强制拉起 runtime,避免纯查看状态时引入副作用。
|
||
const response = await axios.get('/api/system/md2img_health');
|
||
if (response.data && response.data.success) {
|
||
this.md2imgHealth = response.data.data || null;
|
||
} else {
|
||
this.$message.error(response.data?.message || '获取转图状态失败');
|
||
}
|
||
} catch (error) {
|
||
this.$message.error(error.response?.data?.message || '获取转图状态失败');
|
||
} finally {
|
||
this.md2imgLoading = false;
|
||
}
|
||
},
|
||
async warmupMd2Img() {
|
||
if (this.md2imgWarming) return;
|
||
this.md2imgWarming = true;
|
||
try {
|
||
const response = await axios.post('/api/system/md2img_warmup', { timeout_seconds: 60 });
|
||
if (response.data && response.data.success) {
|
||
this.$message.success(response.data.message || '转图预热成功');
|
||
this.md2imgHealth = response.data.data || this.md2imgHealth;
|
||
} else {
|
||
this.$message.error(response.data?.message || '转图预热失败');
|
||
}
|
||
} catch (error) {
|
||
this.$message.error(error.response?.data?.message || '转图预热失败');
|
||
} finally {
|
||
this.md2imgWarming = false;
|
||
this.loadMd2ImgHealth();
|
||
}
|
||
}
|
||
}
|
||
});
|
||
</script>
|
||
|
||
<style>
|
||
.page-shell { display: flex; flex-direction: column; gap: 16px; }
|
||
.page-hero {
|
||
display: flex; align-items: flex-end; justify-content: space-between; gap: 18px; padding: 24px 26px; border-radius: 24px;
|
||
background: linear-gradient(135deg, rgba(79,70,229,0.10), rgba(59,130,246,0.08), rgba(255,255,255,0.9));
|
||
border: 1px solid rgba(148, 163, 184, 0.16); box-shadow: 0 18px 40px rgba(15, 23, 42, 0.06);
|
||
}
|
||
.page-hero-actions { display: flex; align-items: center; gap: 12px; }
|
||
.page-eyebrow { font-size: 12px; text-transform: uppercase; letter-spacing: .08em; color: #6366f1; font-weight: 700; margin-bottom: 8px; }
|
||
.page-hero-copy h1 { font-size: 30px; line-height: 1.1; margin-bottom: 10px; color: #0f172a; }
|
||
.page-hero-copy p { color: #64748b; font-size: 14px; }
|
||
.md2img-health-inline {
|
||
margin-top: 12px;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
flex-wrap: wrap;
|
||
padding: 8px 10px;
|
||
border-radius: 12px;
|
||
background: rgba(255,255,255,0.72);
|
||
border: 1px solid rgba(148, 163, 184, 0.18);
|
||
}
|
||
.health-title { font-size: 12px; font-weight: 700; color: #334155; }
|
||
.health-brief { font-size: 12px; color: #475569; }
|
||
.health-time { font-size: 12px; color: #94a3b8; }
|
||
.workspace-header { display: flex; align-items: center; justify-content: space-between; gap: 16px; }
|
||
.workspace-header h3 { font-size: 18px; margin-bottom: 4px; }
|
||
.workspace-header p { font-size: 13px; color: #64748b; }
|
||
.iframe-url {
|
||
max-width: 40%; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 12px;
|
||
color: #94a3b8; padding: 8px 12px; border-radius: 999px; background: rgba(248,250,252,0.9); border: 1px solid rgba(148,163,184,0.12);
|
||
}
|
||
.iframe-shell-card { height: calc(100vh - 230px); }
|
||
.iframe-shell-card .el-card__body { height: calc(100% - 73px); }
|
||
.iframe-shell { height: 100%; border-radius: 18px; overflow: hidden; border: 1px solid rgba(148,163,184,0.12); background: rgba(248,250,252,0.82); }
|
||
.iframe-shell iframe { width: 100%; height: 100%; border: none; display: block; background: #fff; }
|
||
@media (max-width: 960px) {
|
||
.page-hero { flex-direction: column; align-items: flex-start; }
|
||
.workspace-header { flex-direction: column; align-items: flex-start; }
|
||
.page-hero-actions { flex-wrap: wrap; }
|
||
.iframe-url { max-width: 100%; }
|
||
.md2img-health-inline { width: 100%; }
|
||
}
|
||
</style>
|
||
{% endblock %}
|