Files
workspace/code/fms/fms-vue/src/components/FpsMonitor.vue
T
2026-07-24 17:34:26 +08:00

185 lines
4.1 KiB
Vue

<script setup>
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
defineOptions({ inheritAttrs: false });
const props = defineProps({
// FPS 计算采样间隔(毫秒)
sampleInterval: { type: Number, default: 500 },
// 历史柱状图保留的采样点数
historySize: { type: Number, default: 48 },
// 柱状图基准 FPS(满高对应值)
baseFps: { type: Number, default: 60 },
});
const currentFps = ref(0);
const history = ref([]);
let rafId = null;
let frames = 0;
let lastTime = 0;
let totalFrames = 0;
let totalTime = 0;
let minFps = Infinity;
function tick(now) {
frames++;
totalFrames++;
const elapsed = now - lastTime;
if (elapsed >= props.sampleInterval) {
const fps = Math.round((frames * 1000) / elapsed);
currentFps.value = fps;
if (fps < minFps) minFps = fps;
totalTime += elapsed;
const next =
history.value.length >= props.historySize ? history.value.slice(1) : history.value.slice();
next.push(fps);
history.value = next;
frames = 0;
lastTime = now;
}
rafId = requestAnimationFrame(tick);
}
onMounted(() => {
lastTime = performance.now();
rafId = requestAnimationFrame(tick);
});
onBeforeUnmount(() => {
if (rafId) cancelAnimationFrame(rafId);
});
const avgFps = computed(() => (totalTime > 0 ? Math.round((totalFrames * 1000) / totalTime) : 0));
const minFpsDisplay = computed(() => (minFps === Infinity ? "--" : minFps));
const fpsColor = computed(() => {
const f = currentFps.value;
if (f >= 50) return "#52c41a";
if (f >= 30) return "#faad14";
return "#ff4d4f";
});
function barStyle(f) {
const h = Math.max(8, Math.min(100, (f / props.baseFps) * 100));
const color = f >= 50 ? "#52c41a" : f >= 30 ? "#faad14" : "#ff4d4f";
return { height: `${h}%`, background: color };
}
function reset() {
history.value = [];
totalFrames = 0;
totalTime = 0;
minFps = Infinity;
currentFps.value = 0;
}
</script>
<template>
<div class="fps-monitor" v-bind="$attrs">
<div class="fps-monitor__head">
<span class="fps-monitor__label">FPS</span>
<span class="fps-monitor__now" :style="{ color: fpsColor }">{{ currentFps || "--" }}</span>
<button class="fps-monitor__reset" type="button" title="重置统计" @click="reset">↻</button>
</div>
<div class="fps-monitor__chart">
<div v-for="(f, i) in history" :key="i" class="fps-monitor__bar" :style="barStyle(f)"></div>
</div>
<div class="fps-monitor__foot">
<span
>avg <b>{{ avgFps || "--" }}</b></span
>
<span
>min <b>{{ minFpsDisplay }}</b></span
>
</div>
</div>
</template>
<style scoped>
.fps-monitor {
position: absolute;
top: 8px;
right: 8px;
z-index: 999;
display: flex;
flex-direction: column;
gap: 6px;
width: 108px;
padding: 8px 10px;
font-size: 12px;
line-height: 1.4;
color: rgba(0, 0, 0, 0.85);
background: rgba(255, 255, 255, 0.92);
border: 1px solid #e8e8e8;
border-radius: 6px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
backdrop-filter: blur(4px);
user-select: none;
}
.fps-monitor__head {
display: flex;
align-items: baseline;
gap: 6px;
}
.fps-monitor__label {
font-weight: 600;
color: rgba(0, 0, 0, 0.55);
}
.fps-monitor__now {
flex: 1;
font-size: 18px;
font-weight: 700;
font-variant-numeric: tabular-nums;
}
.fps-monitor__reset {
flex: none;
width: 18px;
height: 18px;
padding: 0;
font-size: 13px;
line-height: 1;
color: rgba(0, 0, 0, 0.45);
background: transparent;
border: none;
border-radius: 3px;
cursor: pointer;
transition: all 0.15s;
}
.fps-monitor__reset:hover {
color: rgba(0, 0, 0, 0.85);
background: rgba(0, 0, 0, 0.06);
}
.fps-monitor__chart {
display: flex;
align-items: flex-end;
gap: 1px;
height: 32px;
}
.fps-monitor__bar {
flex: 1;
min-width: 2px;
border-radius: 1px;
transition:
height 0.2s ease,
background 0.2s ease;
}
.fps-monitor__foot {
display: flex;
justify-content: space-between;
color: rgba(0, 0, 0, 0.55);
}
.fps-monitor__foot b {
color: rgba(0, 0, 0, 0.85);
font-variant-numeric: tabular-nums;
}
</style>