Files
workspace/code/fms/fms-vue-old2/src/components/FpsMonitor.vue
T
2026-08-13 22:26:56 +08:00

206 lines
4.8 KiB
Vue

<script setup lang="ts">
import { computed, onBeforeUnmount, onMounted, ref } from "vue";
import { RotateCcw } from "@lucide/vue";
import { Button } from "@/components/ui/button";
defineOptions({ inheritAttrs: false });
const props = withDefaults(
defineProps<{
sampleInterval?: number;
historySize?: number;
baseFps?: number;
}>(),
{
sampleInterval: 500,
historySize: 48,
baseFps: 60,
},
);
const currentFps = ref(0);
const averageFps = ref(0);
const minimumFps = ref<number | null>(null);
const history = ref<number[]>([]);
const normalizedSampleInterval = computed(() => Math.max(100, props.sampleInterval));
const normalizedHistorySize = computed(() => Math.max(1, Math.floor(props.historySize)));
const normalizedBaseFps = computed(() => Math.max(1, props.baseFps));
let animationFrameId = 0;
let sampleFrames = 0;
let sampleStartedAt = 0;
let totalFrames = 0;
let totalTime = 0;
function resetSampleWindow() {
sampleFrames = 0;
sampleStartedAt = performance.now();
}
function tick(now: number) {
sampleFrames += 1;
totalFrames += 1;
const elapsed = now - sampleStartedAt;
if (elapsed >= normalizedSampleInterval.value) {
const fps = Math.round((sampleFrames * 1000) / elapsed);
currentFps.value = fps;
minimumFps.value = minimumFps.value == null ? fps : Math.min(minimumFps.value, fps);
totalTime += elapsed;
averageFps.value = Math.round((totalFrames * 1000) / totalTime);
history.value = [...history.value, fps].slice(-normalizedHistorySize.value);
sampleFrames = 0;
sampleStartedAt = now;
}
animationFrameId = requestAnimationFrame(tick);
}
function reset() {
currentFps.value = 0;
averageFps.value = 0;
minimumFps.value = null;
history.value = [];
totalFrames = 0;
totalTime = 0;
resetSampleWindow();
}
function handleVisibilityChange() {
if (!document.hidden) resetSampleWindow();
}
onMounted(() => {
resetSampleWindow();
document.addEventListener("visibilitychange", handleVisibilityChange);
animationFrameId = requestAnimationFrame(tick);
});
onBeforeUnmount(() => {
cancelAnimationFrame(animationFrameId);
document.removeEventListener("visibilitychange", handleVisibilityChange);
});
function getFpsColor(fps: number) {
if (fps >= normalizedBaseFps.value * (5 / 6)) return "var(--chart-2)";
if (fps >= normalizedBaseFps.value / 2) return "var(--chart-4)";
return "var(--destructive)";
}
const currentFpsColor = computed(() => getFpsColor(currentFps.value));
function getBarStyle(fps: number) {
const height = Math.max(8, Math.min(100, (fps / normalizedBaseFps.value) * 100));
return {
height: `${height}%`,
backgroundColor: getFpsColor(fps),
};
}
</script>
<template>
<aside class="fps-monitor" aria-label="FPS 性能监控" v-bind="$attrs">
<div class="fps-monitor__header">
<span class="fps-monitor__label">FPS</span>
<strong class="fps-monitor__current" :style="{ color: currentFpsColor }">
{{ currentFps || "--" }}
</strong>
<Button
variant="ghost"
size="icon-xs"
type="button"
title="重置统计"
aria-label="重置 FPS 统计"
@click="reset"
>
<RotateCcw />
</Button>
</div>
<div class="fps-monitor__chart" aria-hidden="true">
<span
v-for="(fps, index) in history"
:key="index"
class="fps-monitor__bar"
:style="getBarStyle(fps)"
/>
</div>
<div class="fps-monitor__summary">
<span
>avg <b>{{ averageFps || "--" }}</b></span
>
<span
>min <b>{{ minimumFps ?? "--" }}</b></span
>
</div>
</aside>
</template>
<style scoped lang="scss">
.fps-monitor {
position: fixed;
top: 8px;
right: 8px;
z-index: 1000;
display: flex;
flex-direction: column;
gap: 6px;
width: 132px;
padding: 8px 10px;
color: var(--foreground);
font-size: 12px;
line-height: 1.4;
letter-spacing: 0;
user-select: none;
background: var(--background);
border: 1px solid var(--border);
border-radius: var(--radius-md);
box-shadow: 0 2px 8px color-mix(in srgb, var(--foreground) 10%, transparent);
}
.fps-monitor__header {
display: flex;
align-items: center;
gap: 6px;
min-height: 24px;
}
.fps-monitor__label {
color: var(--muted-foreground);
font-weight: 600;
}
.fps-monitor__current {
flex: 1;
font-size: 18px;
font-variant-numeric: tabular-nums;
line-height: 1;
}
.fps-monitor__chart {
display: flex;
align-items: flex-end;
gap: 1px;
height: 32px;
overflow: hidden;
}
.fps-monitor__bar {
flex: 1;
min-width: 0;
border-radius: 1px;
}
.fps-monitor__summary {
display: flex;
justify-content: space-between;
color: var(--muted-foreground);
}
.fps-monitor__summary b {
color: var(--foreground);
font-variant-numeric: tabular-nums;
}
</style>