Files
console-dev/app/components/instance/InstanceMonitor.vue
T
cneicy 42dfead9cd
前端验证 / verify (push) Failing after 15m3s
feat(ui): 优化控制台响应式布局与视觉体验
- 统一页面标题、圆角、字体和响应式断点,调整暗色主题及图表配色
- 新增表格列标签插件,在窄屏下将表格转换为卡片布局,优化实例、订单、工单等列表展示
- 支持移动端点击订单行进入详情,并完善订单凭据的打印布局
- 在财务中心展示本月及累计消费,补充充值关闭时的提示
- 优化工单详情布局,将附件上传入口整合至富文本编辑器工具栏
- 调整实例操作按钮、SSH 密钥操作及安全菜单图标
- 为多个页面显式导入 definePageMeta,完善页面元数据声明
2026-11-07 00:09:57 +08:00

764 lines
26 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script>
/** 多模块监控图表:统一 CPU/内存/带宽/磁盘系列,保留各模块时间单位与指标口径差异。 */
import "~/assets/styles/InstanceMonitor.B0X15iQt.css";
import { toDisplayString } from "vue";
import { renderList } from "vue";
import { onMounted } from "vue";
import { onBeforeUnmount } from "vue";
import { watch } from "vue";
import { defineAsyncComponent } from "vue";
import { createBlock } from "vue";
import { ref } from "vue";
import { withCtx } from "vue";
import { createCommentVNode } from "vue";
import { computed } from "vue";
import { createElementBlock } from "vue";
import { Fragment } from "vue";
import { createElementVNode } from "vue";
import { createTextVNode } from "vue";
import { unref } from "vue";
import { defineComponent } from "vue";
import { createVNode } from "vue";
import { openBlock } from "vue";
import { loadModule } from "~/utils/load-module.js";
import { useApi } from "~/lib/auth.js";
import button from "primevue/button";
import { t as _plugin_vue_export_helper_t } from "~/lib/_plugin-vue_export-helper.js";
import { t as useTheme_t } from "~/lib/useTheme.js";
import message from "primevue/message";
import ListSegments from "~/components/ListSegments.vue";
import skeleton from "primevue/skeleton";
import { n as finance_resource_chart_n } from "~/lib/finance-resource-chart.js";
import { t as finance_resource_chart_t } from "~/lib/finance-resource-chart.js";
var O = defineAsyncComponent(() =>
loadModule(
() => import("~/components/ChartBox.vue").then((e) => e.default || e),
[],
import.meta.url,
),
),
k = {
class: `mon`,
},
A = {
class: `range-row`,
},
j = {
class: `hint`,
},
M = {
key: 1,
class: `charts`,
},
N = {
class: `chart-title`,
},
P = {
class: `chart-viewport`,
},
F = {
key: 2,
class: `charts`,
},
I = {
class: `chart-title`,
},
L = {
key: 0,
class: `chart-unit`,
},
R = {
class: `chart-viewport`,
},
z = {
key: 0,
class: `chart-error`,
},
B = {
key: 2,
class: `chart-empty`,
},
V = {
class: `refresh-row`,
},
H = `#f59e0b`,
U = Object.assign(
_plugin_vue_export_helper_t(
defineComponent({
__name: `InstanceMonitor`,
props: {
hostId: {},
module: {},
},
setup(a) {
let g = a,
y = useApi(),
S = useTheme_t(),
U = computed(() => (S.isDark.value ? `#60a5fa` : `#2563eb`)),
W = ref([]),
G = ref(!1),
K = ref(``),
q = 0,
J = !1,
Y = ``,
X = [
{
label: `1小时`,
value: 3600,
},
{
label: `24小时`,
value: 86400,
},
{
label: `7天`,
value: 604800,
},
{
label: `1个月`,
value: 2592e3,
},
{
label: `1年`,
value: 31536e3,
},
],
Z = ref(86400),
Q = (e) => {
let t = new Date(e * 1e3);
return `${String(t.getHours()).padStart(2, `0`)}:${String(t.getMinutes()).padStart(2, `0`)}`;
};
async function te(e, t, n = `mf_cloud`) {
// 云模块各指标并行读取并分别展示失败;物理智简只请求实际支持的带宽图。
let r = [
{
type: `cpu`,
title: `CPU 使用率`,
max: 100,
pick: [
{
name: `CPU`,
fn: (e) => e.value,
color: U.value,
unit: `%`,
},
],
},
{
type: `memory`,
title: `内存使用率`,
max: 100,
pick: [
{
name: `内存`,
fn: (e) =>
e.total
? (Number(e.used ?? 0) / Number(e.total)) * 100
: void 0,
color: U.value,
unit: `%`,
},
],
},
{
type: `bw`,
title: `带宽(入 / 出)`,
pick: [
{
name: `入`,
fn: (e) => e.in_bw,
color: U.value,
},
{
name: `出`,
fn: (e) => e.out_bw,
color: H,
},
],
scale: `bits`,
},
{
type: `disk_io`,
title: `硬盘 IO(读 / 写)`,
pick: [
{
name: `读`,
fn: (e) => e.read_bytes,
color: U.value,
unit: `B/s`,
},
{
name: `写`,
fn: (e) => e.write_bytes,
color: H,
unit: `B/s`,
},
],
},
];
function i(e) {
let t = e > 0 ? Math.max(0, Math.floor(Math.log2(e) / 10)) : 0,
n = [`bps`, `Kbps`, `Mbps`, `Gbps`, `Tbps`];
return {
divisor: 1024 ** t,
unit: n[Math.min(t, n.length - 1)],
};
}
let a = n === `jy_dcim` ? r.filter((e) => e.type === `bw`) : r,
o = await Promise.allSettled(
a.map((r) =>
y.get(`/${n}/${e}/chart`, {
type: r.type,
start_time: t,
}),
),
);
return a.map((e, t) => {
let n = o[t];
if (n.status === `rejected`)
return {
key: e.type,
title: e.title,
labels: [],
series: [],
error:
n.reason instanceof Error
? n.reason.message
: `监控数据加载失败,请重试`,
};
let r = n.value.list ?? [],
a = e.pick.map((e) =>
r.map((t) => {
let n = e.fn(t);
return n === void 0 || Number.isNaN(n) ? null : Number(n);
}),
),
s =
e.scale === `bits`
? i(Math.max(0, ...a.flat().filter((e) => e !== null)))
: null;
return {
key: e.type,
title: e.title,
max: e.max,
labels: r.map((e) => Q(e.time)),
series: e.pick.map((e, t) => ({
name: e.name,
unit: s ? s.unit : e.unit,
color: e.color,
data: a[t].map((e) =>
e === null
? null
: Number((e / (s?.divisor ?? 1)).toFixed(2)),
),
})),
};
});
}
async function ne(e, t, n) {
// 财务接口使用毫秒起止时间,CPU/内存交给专用转换器,其他系列保留服务端单位。
let r = [
{
type: `cpu`,
title: `CPU 使用率`,
max: 100,
},
{
type: `memory`,
title: `内存使用率`,
max: 100,
},
{
type: `flow`,
title: `带宽(进 / 出)`,
},
{
type: `disk`,
title: `硬盘 IO(读 / 写)`,
},
],
i = await Promise.allSettled(
r.map((r) =>
y.get(`/remf_finance/${e}/chart`, {
type: r.type,
start: t,
end: n,
}),
),
);
return r.map((e, t) => {
let n = i[t];
if (n.status === `rejected`)
return {
key: e.type,
title: e.title,
labels: [],
series: [],
error:
n.reason instanceof Error
? n.reason.message
: `监控数据加载失败,请重试`,
};
let r = n.value ?? {};
if (e.type === `cpu` || e.type === `memory`) {
let t = finance_resource_chart_n(r, e.type);
return {
key: e.type,
title: e.title,
max: 100,
labels: t.labels,
series: [
{
name: e.type === `cpu` ? `CPU` : `内存`,
unit: `%`,
color: U.value,
data: t.data,
},
],
};
}
let a = (r.list?.[0] ?? []).map((e) =>
String(e.time ?? ``).slice(11, 16),
),
o = (r.list ?? []).map((e, t) => ({
name: String(r.label?.[t] ?? `系列 ${t + 1}`).replace(
/\(.*\)$/,
``,
),
unit: r.unit,
color: t === 0 ? U.value : H,
data: e.map((e) => finance_resource_chart_t(e.value)),
}));
return {
key: e.type,
title: e.title,
max: e.max,
labels: a,
series: o,
};
});
}
async function re(e) {
// 快快云监控时间范围由接口决定,带宽值按其既有口径从千分单位转换为 Mbps。
let t = await y.get(`/kky_ecs/monitor/${e}`),
n = (t.sample_info ?? []).map((e) =>
String(e.timestamp ?? ``).slice(11, 16),
),
r = (e, t, r, i, a, o = 1) => ({
key: e,
title: t,
labels: n,
series: (r ?? []).map((e, t) => ({
name: e.title,
unit: a,
color: i[t % i.length],
data: (e.value ?? []).map((e) =>
typeof e == `number` ? Number((e / o).toFixed(2)) : null,
),
})),
});
return [
r(`cpu`, `CPU 使用率`, t.cpu, [U.value], `%`),
r(`memory`, `内存`, t.memory, [U.value, H], `KB`),
r(`bandwidth`, `带宽`, t.bandwidth, [U.value, H], `Mbps`, 1e3),
r(`disk`, `硬盘 IO`, t.disk, [U.value, H], `KB/s`),
];
}
async function $() {
// 请求绑定模块、实例和所选时间窗;相同窗口刷新可留旧图,不同窗口先清空避免误读。
if (J) return;
let e = ++q,
t = g.hostId,
n = g.module,
r = Z.value,
i = `${n}:${t}:${n === `kky_ecs` ? `` : r}`,
a = () =>
!J &&
e === q &&
t === g.hostId &&
n === g.module &&
(n === `kky_ecs` || r === Z.value);
((G.value = !0), (K.value = ``), Y !== i && (W.value = []));
try {
let e = Date.now(),
o =
n === `mf_cloud` ||
n === `remf_cloud` ||
n === `jy_cloud` ||
n === `jy_dcim`
? await te(t, Math.floor(e / 1e3) - r, n)
: n === `remf_finance`
? await ne(t, e - r * 1e3, e)
: await re(t);
a() && ((W.value = o), (Y = i));
} catch (e) {
if (!a()) return;
((K.value = e instanceof Error ? e.message : `加载监控数据失败`),
(W.value = []));
} finally {
a() && (G.value = !1);
}
}
function ie(e) {
if (g.module === `kky_ecs` || e === void 0) return;
let t = Number(e);
if (X.some((e) => e.value === t)) return ((Z.value = t), $());
}
let ae = (e) => e.series.some((e) => e.data.some((e) => e !== null));
function oe(e) {
// 缺测点保持 null,不插值连接;百分比轴上限和单位来自归一化指标定义。
let t = S.isDark.value ? `#a1a1aa` : `#94a3b8`,
n = S.isDark.value ? `rgba(255,255,255,.08)` : `rgba(0,0,0,.06)`;
return {
grid: {
left: 6,
right: 10,
top: 26,
bottom: 4,
outerBoundsMode: `same`,
outerBoundsContain: `axisLabel`,
},
tooltip: {
trigger: `axis`,
},
legend:
e.series.length > 1
? {
top: 0,
right: 0,
itemWidth: 10,
itemHeight: 2,
textStyle: {
fontSize: 11,
},
}
: void 0,
xAxis: {
type: `category`,
data: e.labels,
boundaryGap: !1,
axisTick: {
show: !1,
},
axisLine: {
lineStyle: {
color: n,
},
},
axisLabel: {
color: t,
fontSize: 11,
},
},
yAxis: {
type: `value`,
max: e.max,
axisLine: {
show: !1,
},
axisTick: {
show: !1,
},
axisLabel: {
color: t,
fontSize: 11,
formatter: e.series.every((e) => e.unit === `%`)
? (e) => `${e}%`
: void 0,
},
splitLine: {
lineStyle: {
color: n,
},
},
},
series: e.series.map((t) => ({
name: t.name,
type: `line`,
smooth: !0,
showSymbol: !1,
connectNulls: !1,
areaStyle:
e.series.length === 1
? {
opacity: 0.12,
}
: void 0,
lineStyle: {
width: 1.6,
color: t.color,
},
itemStyle: {
color: t.color,
},
data: t.data,
tooltip: {
valueFormatter: (e) =>
e == null
? `暂无数据`
: t.unit === `%`
? `${e}%`
: t.unit
? `${e} ${t.unit}`
: String(e),
},
})),
};
}
return (
onMounted($),
watch(() => [g.hostId, g.module], $),
onBeforeUnmount(() => {
((J = !0), (q += 1));
}),
(n, r) => {
let i = ListSegments,
s = button,
u = message,
g = skeleton,
y = O;
return (
openBlock(),
createElementBlock(`div`, k, [
createElementVNode(`div`, A, [
a.module === `kky_ecs`
? createCommentVNode(``, !0)
: (openBlock(),
createBlock(
i,
{
key: 0,
"aria-label": `监控时间范围`,
"model-value": unref(Z),
options: X,
"onUpdate:modelValue": ie,
},
null,
8,
[`model-value`],
)),
createElementVNode(`span`, j, [
a.module === `kky_ecs`
? (openBlock(),
createElementBlock(
Fragment,
{
key: 1,
},
[
createTextVNode(
`数据由平台采集,可查询的时间范围固定。`,
),
],
64,
))
: (openBlock(),
createElementBlock(
Fragment,
{
key: 0,
},
[
createTextVNode(
`数据由平台采集,实例未运行时可能为空。`,
),
],
64,
)),
]),
]),
unref(K)
? (openBlock(),
createBlock(
u,
{
key: 0,
severity: `error`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(toDisplayString(unref(K)) + ` `, 1),
createVNode(
s,
{
label: `重试`,
size: `small`,
severity: `secondary`,
text: ``,
loading: unref(G),
onClick: $,
},
null,
8,
[`loading`],
),
]),
_: 1,
},
))
: createCommentVNode(``, !0),
unref(G) && !unref(W).length
? (openBlock(),
createElementBlock(`div`, M, [
(openBlock(),
createElementBlock(
Fragment,
null,
renderList(4, (e) =>
createElementVNode(
`div`,
{
key: e,
class: `chart-card`,
},
[
createElementVNode(`div`, N, [
createVNode(g, {
width: `35%`,
height: `24px`,
}),
]),
createElementVNode(`div`, P, [
createVNode(g, {
height: `280px`,
}),
]),
],
),
),
64,
)),
]))
: (openBlock(),
createElementBlock(`div`, F, [
(openBlock(!0),
createElementBlock(
Fragment,
null,
renderList(
unref(W),
(t) => (
openBlock(),
createElementBlock(
`div`,
{
key: t.key,
class: `chart-card`,
},
[
createElementVNode(`div`, I, [
createTextVNode(
toDisplayString(t.title),
1,
),
t.series[0]?.unit
? (openBlock(),
createElementBlock(
`span`,
L,
`(` +
toDisplayString(t.series[0].unit) +
`)`,
1,
))
: createCommentVNode(``, !0),
]),
createElementVNode(`div`, R, [
t.error
? (openBlock(),
createElementBlock(`div`, z, [
createVNode(
u,
{
severity: `error`,
closable: !1,
},
{
default: withCtx(() => [
createTextVNode(
toDisplayString(t.error) +
` `,
1,
),
createVNode(
s,
{
label: `重试`,
size: `small`,
severity: `secondary`,
text: ``,
loading: unref(G),
onClick: $,
},
null,
8,
[`loading`],
),
]),
_: 2,
},
1024,
),
]))
: ae(t)
? (openBlock(),
createBlock(
y,
{
key: 1,
option: oe(t),
height: 280,
loading: unref(G),
},
null,
8,
[`option`, `loading`],
))
: (openBlock(),
createElementBlock(
`p`,
B,
`暂无监控数据`,
)),
]),
],
)
),
),
128,
)),
])),
createElementVNode(`div`, V, [
createVNode(
s,
{
label: `刷新`,
icon: `pi pi-refresh`,
size: `small`,
severity: `secondary`,
text: ``,
loading: unref(G),
onClick: $,
},
null,
8,
[`loading`],
),
]),
])
);
}
);
},
}),
[[`__scopeId`, `data-v-6a900feb`]],
),
{
__name: `InstanceMonitor`,
},
);
export {};
export default U;
</script>