♻️ Merge branch 'feature/server-metric'
This commit is contained in:
commit
1ba7f5daaa
6 changed files with 163 additions and 294 deletions
|
@ -1,174 +1,74 @@
|
||||||
<!--
|
<!--
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
SPDX-FileCopyrightText: syuilo and misskey-project & noridev and misskey-project
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<div class="lcfyofjk">
|
<div :class="$style.root">
|
||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
<XPie :class="$style.pie" :value="cpuUsage"/>
|
||||||
<defs>
|
<div :class="$style.div">
|
||||||
<linearGradient :id="cpuGradientId" x1="0" x2="0" y1="1" y2="0">
|
<p>CPU</p>
|
||||||
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
|
<text x="50%" y="50%" dy="0.05" text-anchor="middle" :class="$style.text">{{ (cpuUsage * 100).toFixed(1) }}<span style="font-weight: normal; font-size: .75em; margin-left: 2px;">%</span></text>
|
||||||
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
|
</div>
|
||||||
</linearGradient>
|
<XPie :class="$style.pie" :value="memUsage"/>
|
||||||
<mask :id="cpuMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
|
<div :class="$style.div">
|
||||||
<polygon
|
<p>RAM</p>
|
||||||
:points="cpuPolygonPoints"
|
<text x="50%" y="50%" dy="0.05" text-anchor="middle" :class="$style.text">{{ (memUsage * 100).toFixed(1) }}<span style="font-weight: normal; font-size: .75em; margin-left: 2px;">%</span></text>
|
||||||
fill="#fff"
|
</div>
|
||||||
fill-opacity="0.5"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
:points="cpuPolylinePoints"
|
|
||||||
fill="none"
|
|
||||||
stroke="#fff"
|
|
||||||
stroke-width="1"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
:cx="cpuHeadX"
|
|
||||||
:cy="cpuHeadY"
|
|
||||||
r="1.5"
|
|
||||||
fill="#fff"
|
|
||||||
/>
|
|
||||||
</mask>
|
|
||||||
</defs>
|
|
||||||
<rect
|
|
||||||
x="-2" y="-2"
|
|
||||||
:width="viewBoxX + 4" :height="viewBoxY + 4"
|
|
||||||
:style="`stroke: none; fill: url(#${ cpuGradientId }); mask: url(#${ cpuMaskId })`"
|
|
||||||
/>
|
|
||||||
<text x="1" y="5">CPU <tspan>{{ cpuP }}%</tspan></text>
|
|
||||||
</svg>
|
|
||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
|
||||||
<defs>
|
|
||||||
<linearGradient :id="memGradientId" x1="0" x2="0" y1="1" y2="0">
|
|
||||||
<stop offset="0%" stop-color="hsl(180, 80%, 70%)"></stop>
|
|
||||||
<stop offset="100%" stop-color="hsl(0, 80%, 70%)"></stop>
|
|
||||||
</linearGradient>
|
|
||||||
<mask :id="memMaskId" x="0" y="0" :width="viewBoxX" :height="viewBoxY">
|
|
||||||
<polygon
|
|
||||||
:points="memPolygonPoints"
|
|
||||||
fill="#fff"
|
|
||||||
fill-opacity="0.5"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
:points="memPolylinePoints"
|
|
||||||
fill="none"
|
|
||||||
stroke="#fff"
|
|
||||||
stroke-width="1"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
:cx="memHeadX"
|
|
||||||
:cy="memHeadY"
|
|
||||||
r="1.5"
|
|
||||||
fill="#fff"
|
|
||||||
/>
|
|
||||||
</mask>
|
|
||||||
</defs>
|
|
||||||
<rect
|
|
||||||
x="-2" y="-2"
|
|
||||||
:width="viewBoxX + 4" :height="viewBoxY + 4"
|
|
||||||
:style="`stroke: none; fill: url(#${ memGradientId }); mask: url(#${ memMaskId })`"
|
|
||||||
/>
|
|
||||||
<text x="1" y="5">MEM <tspan>{{ memP }}%</tspan></text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script lang="ts" setup>
|
<script lang="ts" setup>
|
||||||
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { v4 as uuid } from 'uuid';
|
import XPie from './pie-compact.vue';
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
meta: Misskey.entities.ServerInfoResponse
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const viewBoxX = ref<number>(50);
|
const cpuUsage = ref<number>(0);
|
||||||
const viewBoxY = ref<number>(30);
|
const memUsage = ref<number>(0);
|
||||||
const stats = ref<Misskey.entities.ServerStats[]>([]);
|
|
||||||
const cpuGradientId = uuid();
|
|
||||||
const cpuMaskId = uuid();
|
|
||||||
const memGradientId = uuid();
|
|
||||||
const memMaskId = uuid();
|
|
||||||
const cpuPolylinePoints = ref<string>('');
|
|
||||||
const memPolylinePoints = ref<string>('');
|
|
||||||
const cpuPolygonPoints = ref<string>('');
|
|
||||||
const memPolygonPoints = ref<string>('');
|
|
||||||
const cpuHeadX = ref<number>();
|
|
||||||
const cpuHeadY = ref<number>();
|
|
||||||
const memHeadX = ref<number>();
|
|
||||||
const memHeadY = ref<number>();
|
|
||||||
const cpuP = ref<string>('');
|
|
||||||
const memP = ref<string>('');
|
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
props.connection.on('stats', onStats);
|
props.connection.on('stats', onStats);
|
||||||
props.connection.on('statsLog', onStatsLog);
|
|
||||||
props.connection.send('requestLog', {
|
|
||||||
id: Math.random().toString().substring(2, 10),
|
|
||||||
length: 50,
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
onBeforeUnmount(() => {
|
||||||
props.connection.off('stats', onStats);
|
props.connection.off('stats', onStats);
|
||||||
props.connection.off('statsLog', onStatsLog);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
function onStats(connStats: Misskey.entities.ServerStats) {
|
function onStats(stats: Misskey.entities.ServerStats) {
|
||||||
stats.value.push(connStats);
|
cpuUsage.value = stats.cpu;
|
||||||
if (stats.value.length > 50) stats.value.shift();
|
memUsage.value = stats.mem.active / props.meta.mem.total;
|
||||||
|
|
||||||
let cpuPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - s.cpu) * viewBoxY.value]);
|
|
||||||
let memPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.mem.active / props.meta.mem.total)) * viewBoxY.value]);
|
|
||||||
cpuPolylinePoints.value = cpuPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
|
||||||
memPolylinePoints.value = memPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
|
||||||
|
|
||||||
cpuPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${cpuPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
|
|
||||||
memPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${memPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
|
|
||||||
|
|
||||||
cpuHeadX.value = cpuPolylinePointsStats.at(-1)![0];
|
|
||||||
cpuHeadY.value = cpuPolylinePointsStats.at(-1)![1];
|
|
||||||
memHeadX.value = memPolylinePointsStats.at(-1)![0];
|
|
||||||
memHeadY.value = memPolylinePointsStats.at(-1)![1];
|
|
||||||
|
|
||||||
cpuP.value = (connStats.cpu * 100).toFixed(0);
|
|
||||||
memP.value = (connStats.mem.active / props.meta.mem.total * 100).toFixed(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
|
|
||||||
for (const revStats of statsLog.toReversed()) {
|
|
||||||
onStats(revStats);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
<style lang="scss" module>
|
||||||
.lcfyofjk {
|
.root {
|
||||||
display: flex;
|
display: flex;
|
||||||
|
padding: 16px 32px;
|
||||||
|
}
|
||||||
|
|
||||||
> svg {
|
.pie {
|
||||||
display: block;
|
height: 42px;
|
||||||
padding: 10px;
|
flex-shrink: 0;
|
||||||
width: 50%;
|
margin-right: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
&:first-child {
|
.div {
|
||||||
padding-right: 5px;
|
flex: 1;
|
||||||
}
|
margin: auto;
|
||||||
|
|
||||||
&:last-child {
|
> p {
|
||||||
padding-left: 5px;
|
margin: 0 0 2px;
|
||||||
}
|
font-size: 0.8em;
|
||||||
|
|
||||||
> text {
|
|
||||||
font-size: 4.5px;
|
|
||||||
fill: currentColor;
|
|
||||||
|
|
||||||
> tspan {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-weight: bold;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
|
@ -11,10 +11,9 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
|
||||||
<div v-if="meta" data-cy-mkw-serverMetric class="mkw-serverMetric">
|
<div v-if="meta" data-cy-mkw-serverMetric class="mkw-serverMetric">
|
||||||
<XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
|
<XCpuMemory v-if="widgetProps.view === 0" :connection="connection" :meta="meta"/>
|
||||||
<XNet v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
|
<XCpu v-else-if="widgetProps.view === 1" :connection="connection" :meta="meta"/>
|
||||||
<XCpu v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
|
<XMemory v-else-if="widgetProps.view === 2" :connection="connection" :meta="meta"/>
|
||||||
<XMemory v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
|
<XDisk v-else-if="widgetProps.view === 3" :connection="connection" :meta="meta"/>
|
||||||
<XDisk v-else-if="widgetProps.view === 4" :connection="connection" :meta="meta"/>
|
|
||||||
</div>
|
</div>
|
||||||
</MkContainer>
|
</MkContainer>
|
||||||
</template>
|
</template>
|
||||||
|
@ -24,7 +23,6 @@ import { onUnmounted, ref } from 'vue';
|
||||||
import * as Misskey from 'misskey-js';
|
import * as Misskey from 'misskey-js';
|
||||||
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js';
|
import { useWidgetPropsManager, WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from '../widget.js';
|
||||||
import XCpuMemory from './cpu-mem.vue';
|
import XCpuMemory from './cpu-mem.vue';
|
||||||
import XNet from './net.vue';
|
|
||||||
import XCpu from './cpu.vue';
|
import XCpu from './cpu.vue';
|
||||||
import XMemory from './mem.vue';
|
import XMemory from './mem.vue';
|
||||||
import XDisk from './disk.vue';
|
import XDisk from './disk.vue';
|
||||||
|
@ -70,7 +68,7 @@ misskeyApiGet('server-info', {}).then(res => {
|
||||||
});
|
});
|
||||||
|
|
||||||
const toggleView = () => {
|
const toggleView = () => {
|
||||||
if (widgetProps.view === 4) {
|
if (widgetProps.view === 3) {
|
||||||
widgetProps.view = 0;
|
widgetProps.view = 0;
|
||||||
} else {
|
} else {
|
||||||
widgetProps.view++;
|
widgetProps.view++;
|
||||||
|
|
|
@ -1,147 +0,0 @@
|
||||||
<!--
|
|
||||||
SPDX-FileCopyrightText: syuilo and misskey-project
|
|
||||||
SPDX-License-Identifier: AGPL-3.0-only
|
|
||||||
-->
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="oxxrhrto">
|
|
||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
|
||||||
<polygon
|
|
||||||
:points="inPolygonPoints"
|
|
||||||
fill="#94a029"
|
|
||||||
fill-opacity="0.5"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
:points="inPolylinePoints"
|
|
||||||
fill="none"
|
|
||||||
stroke="#94a029"
|
|
||||||
stroke-width="1"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
:cx="inHeadX"
|
|
||||||
:cy="inHeadY"
|
|
||||||
r="1.5"
|
|
||||||
fill="#94a029"
|
|
||||||
/>
|
|
||||||
<text x="1" y="5">NET rx <tspan>{{ bytes(inRecent) }}</tspan></text>
|
|
||||||
</svg>
|
|
||||||
<svg :viewBox="`0 0 ${ viewBoxX } ${ viewBoxY }`">
|
|
||||||
<polygon
|
|
||||||
:points="outPolygonPoints"
|
|
||||||
fill="#ff9156"
|
|
||||||
fill-opacity="0.5"
|
|
||||||
/>
|
|
||||||
<polyline
|
|
||||||
:points="outPolylinePoints"
|
|
||||||
fill="none"
|
|
||||||
stroke="#ff9156"
|
|
||||||
stroke-width="1"
|
|
||||||
/>
|
|
||||||
<circle
|
|
||||||
:cx="outHeadX"
|
|
||||||
:cy="outHeadY"
|
|
||||||
r="1.5"
|
|
||||||
fill="#ff9156"
|
|
||||||
/>
|
|
||||||
<text x="1" y="5">NET tx <tspan>{{ bytes(outRecent) }}</tspan></text>
|
|
||||||
</svg>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<script lang="ts" setup>
|
|
||||||
import { onMounted, onBeforeUnmount, ref } from 'vue';
|
|
||||||
import * as Misskey from 'misskey-js';
|
|
||||||
import bytes from '@/filters/bytes.js';
|
|
||||||
|
|
||||||
const props = defineProps<{
|
|
||||||
connection: Misskey.ChannelConnection<Misskey.Channels['serverStats']>,
|
|
||||||
meta: Misskey.entities.ServerInfoResponse
|
|
||||||
}>();
|
|
||||||
|
|
||||||
const viewBoxX = ref<number>(50);
|
|
||||||
const viewBoxY = ref<number>(30);
|
|
||||||
const stats = ref<Misskey.entities.ServerStats[]>([]);
|
|
||||||
const inPolylinePoints = ref<string>('');
|
|
||||||
const outPolylinePoints = ref<string>('');
|
|
||||||
const inPolygonPoints = ref<string>('');
|
|
||||||
const outPolygonPoints = ref<string>('');
|
|
||||||
const inHeadX = ref<number>();
|
|
||||||
const inHeadY = ref<number>();
|
|
||||||
const outHeadX = ref<number>();
|
|
||||||
const outHeadY = ref<number>();
|
|
||||||
const inRecent = ref<number>(0);
|
|
||||||
const outRecent = ref<number>(0);
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
props.connection.on('stats', onStats);
|
|
||||||
props.connection.on('statsLog', onStatsLog);
|
|
||||||
props.connection.send('requestLog', {
|
|
||||||
id: Math.random().toString().substring(2, 10),
|
|
||||||
length: 50,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
onBeforeUnmount(() => {
|
|
||||||
props.connection.off('stats', onStats);
|
|
||||||
props.connection.off('statsLog', onStatsLog);
|
|
||||||
});
|
|
||||||
|
|
||||||
function onStats(connStats: Misskey.entities.ServerStats) {
|
|
||||||
stats.value.push(connStats);
|
|
||||||
if (stats.value.length > 50) stats.value.shift();
|
|
||||||
|
|
||||||
const inPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.rx)));
|
|
||||||
const outPeak = Math.max(1024 * 64, Math.max(...stats.value.map(s => s.net.tx)));
|
|
||||||
|
|
||||||
let inPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.rx / inPeak)) * viewBoxY.value]);
|
|
||||||
let outPolylinePointsStats = stats.value.map((s, i) => [viewBoxX.value - ((stats.value.length - 1) - i), (1 - (s.net.tx / outPeak)) * viewBoxY.value]);
|
|
||||||
inPolylinePoints.value = inPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
|
||||||
outPolylinePoints.value = outPolylinePointsStats.map(xy => `${xy[0]},${xy[1]}`).join(' ');
|
|
||||||
|
|
||||||
inPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${inPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
|
|
||||||
outPolygonPoints.value = `${viewBoxX.value - (stats.value.length - 1)},${viewBoxY.value} ${outPolylinePoints.value} ${viewBoxX.value},${viewBoxY.value}`;
|
|
||||||
|
|
||||||
inHeadX.value = inPolylinePointsStats.at(-1)![0];
|
|
||||||
inHeadY.value = inPolylinePointsStats.at(-1)![1];
|
|
||||||
outHeadX.value = outPolylinePointsStats.at(-1)![0];
|
|
||||||
outHeadY.value = outPolylinePointsStats.at(-1)![1];
|
|
||||||
|
|
||||||
inRecent.value = connStats.net.rx;
|
|
||||||
outRecent.value = connStats.net.tx;
|
|
||||||
}
|
|
||||||
|
|
||||||
function onStatsLog(statsLog: Misskey.entities.ServerStatsLog) {
|
|
||||||
for (const revStats of statsLog.toReversed()) {
|
|
||||||
onStats(revStats);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<style lang="scss" scoped>
|
|
||||||
.oxxrhrto {
|
|
||||||
display: flex;
|
|
||||||
|
|
||||||
> svg {
|
|
||||||
display: block;
|
|
||||||
padding: 10px;
|
|
||||||
width: 50%;
|
|
||||||
|
|
||||||
&:first-child {
|
|
||||||
padding-right: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
&:last-child {
|
|
||||||
padding-left: 5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
> text {
|
|
||||||
font-size: 4.5px;
|
|
||||||
fill: currentColor;
|
|
||||||
|
|
||||||
> tspan {
|
|
||||||
opacity: 0.5;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
</style>
|
|
54
packages/frontend/src/widgets/server-metric/pie-compact.vue
Normal file
54
packages/frontend/src/widgets/server-metric/pie-compact.vue
Normal file
|
@ -0,0 +1,54 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project & noridev and cherrypick-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg :class="$style.root" viewBox="0 0 1 1" preserveAspectRatio="none">
|
||||||
|
<circle
|
||||||
|
:r="r"
|
||||||
|
cx="50%" cy="50%"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="0.2"
|
||||||
|
stroke="rgba(0, 0, 0, 0.05)"
|
||||||
|
:class="$style.circle"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
:r="r"
|
||||||
|
cx="50%" cy="50%"
|
||||||
|
:stroke-dasharray="Math.PI * (r * 2)"
|
||||||
|
:stroke-dashoffset="strokeDashoffset"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="0.2"
|
||||||
|
:class="$style.circle"
|
||||||
|
:stroke="color"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from 'vue';
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const r = 0.35;
|
||||||
|
|
||||||
|
const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
|
||||||
|
const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)));
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
transform-origin: center;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: stroke-dashoffset 0.5s ease;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
</style>
|
62
packages/frontend/src/widgets/server-metric/pie-large.vue
Normal file
62
packages/frontend/src/widgets/server-metric/pie-large.vue
Normal file
|
@ -0,0 +1,62 @@
|
||||||
|
<!--
|
||||||
|
SPDX-FileCopyrightText: syuilo and misskey-project
|
||||||
|
SPDX-License-Identifier: AGPL-3.0-only
|
||||||
|
-->
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<svg :class="$style.root" viewBox="0 0 1 1" preserveAspectRatio="none">
|
||||||
|
<circle
|
||||||
|
:r="r"
|
||||||
|
cx="50%" cy="50%"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="0.1"
|
||||||
|
stroke="rgba(0, 0, 0, 0.05)"
|
||||||
|
:class="$style.circle"
|
||||||
|
/>
|
||||||
|
<circle
|
||||||
|
:r="r"
|
||||||
|
cx="50%" cy="50%"
|
||||||
|
:stroke-dasharray="Math.PI * (r * 2)"
|
||||||
|
:stroke-dashoffset="strokeDashoffset"
|
||||||
|
fill="none"
|
||||||
|
stroke-width="0.1"
|
||||||
|
:class="$style.circle"
|
||||||
|
:stroke="color"
|
||||||
|
/>
|
||||||
|
<text x="50%" y="50%" dy="0.05" text-anchor="middle" :class="$style.text">{{ (value * 100).toFixed(0) }}%</text>
|
||||||
|
</svg>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import { computed } from "vue";
|
||||||
|
|
||||||
|
const props = defineProps<{
|
||||||
|
value: number;
|
||||||
|
}>();
|
||||||
|
|
||||||
|
const r = 0.45;
|
||||||
|
|
||||||
|
const color = computed(() => `hsl(${180 - props.value * 180}, 80%, 70%)`);
|
||||||
|
const strokeDashoffset = computed(
|
||||||
|
() => (1 - props.value) * (Math.PI * (r * 2)),
|
||||||
|
);
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style lang="scss" module>
|
||||||
|
.root {
|
||||||
|
display: block;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.circle {
|
||||||
|
transform-origin: center;
|
||||||
|
transform: rotate(-90deg);
|
||||||
|
transition: stroke-dashoffset 0.5s ease;
|
||||||
|
stroke-linecap: round;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text {
|
||||||
|
font-size: 0.15px;
|
||||||
|
fill: currentColor;
|
||||||
|
}
|
||||||
|
</style>
|
|
@ -9,7 +9,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:r="r"
|
:r="r"
|
||||||
cx="50%" cy="50%"
|
cx="50%" cy="50%"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke-width="0.1"
|
stroke-width="0.15"
|
||||||
stroke="rgba(0, 0, 0, 0.05)"
|
stroke="rgba(0, 0, 0, 0.05)"
|
||||||
:class="$style.circle"
|
:class="$style.circle"
|
||||||
/>
|
/>
|
||||||
|
@ -19,7 +19,7 @@ SPDX-License-Identifier: AGPL-3.0-only
|
||||||
:stroke-dasharray="Math.PI * (r * 2)"
|
:stroke-dasharray="Math.PI * (r * 2)"
|
||||||
:stroke-dashoffset="strokeDashoffset"
|
:stroke-dashoffset="strokeDashoffset"
|
||||||
fill="none"
|
fill="none"
|
||||||
stroke-width="0.1"
|
stroke-width="0.15"
|
||||||
:class="$style.circle"
|
:class="$style.circle"
|
||||||
:stroke="color"
|
:stroke="color"
|
||||||
/>
|
/>
|
||||||
|
@ -34,7 +34,7 @@ const props = defineProps<{
|
||||||
value: number;
|
value: number;
|
||||||
}>();
|
}>();
|
||||||
|
|
||||||
const r = 0.45;
|
const r = 0.4;
|
||||||
|
|
||||||
const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
|
const color = computed(() => `hsl(${180 - (props.value * 180)}, 80%, 70%)`);
|
||||||
const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)));
|
const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)));
|
||||||
|
@ -50,10 +50,12 @@ const strokeDashoffset = computed(() => (1 - props.value) * (Math.PI * (r * 2)))
|
||||||
transform-origin: center;
|
transform-origin: center;
|
||||||
transform: rotate(-90deg);
|
transform: rotate(-90deg);
|
||||||
transition: stroke-dashoffset 0.5s ease;
|
transition: stroke-dashoffset 0.5s ease;
|
||||||
|
stroke-linecap: round;
|
||||||
}
|
}
|
||||||
|
|
||||||
.text {
|
.text {
|
||||||
font-size: 0.15px;
|
font-size: 0.15px;
|
||||||
fill: currentColor;
|
fill: currentColor;
|
||||||
|
font-weight: 700;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
Loading…
Add table
Reference in a new issue