[+] Chartjs chart
This commit is contained in:
@@ -13,6 +13,7 @@
|
|||||||
"@unocss/preset-attributify": "^66.5.6",
|
"@unocss/preset-attributify": "^66.5.6",
|
||||||
"@unocss/preset-icons": "^66.5.6",
|
"@unocss/preset-icons": "^66.5.6",
|
||||||
"@unocss/reset": "^66.5.6",
|
"@unocss/reset": "^66.5.6",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"m3-svelte": "^5.14.1",
|
"m3-svelte": "^5.14.1",
|
||||||
"mongodb": "^7.0.0",
|
"mongodb": "^7.0.0",
|
||||||
"openai": "^6.9.0",
|
"openai": "^6.9.0",
|
||||||
@@ -167,6 +168,8 @@
|
|||||||
|
|
||||||
"@ktibow/material-color-utilities-nightly": ["@ktibow/material-color-utilities-nightly@0.3.11763158244000", "", {}, "sha512-t2KycnxW9kViZK3bi+AWQrWoxHNbgiSdZ4qK6TT1Ua6EPAoHrJcoFUDISbFDQK4cdxCQwkJrKeK96LP9UvMRqQ=="],
|
"@ktibow/material-color-utilities-nightly": ["@ktibow/material-color-utilities-nightly@0.3.11763158244000", "", {}, "sha512-t2KycnxW9kViZK3bi+AWQrWoxHNbgiSdZ4qK6TT1Ua6EPAoHrJcoFUDISbFDQK4cdxCQwkJrKeK96LP9UvMRqQ=="],
|
||||||
|
|
||||||
|
"@kurkle/color": ["@kurkle/color@0.3.4", "", {}, "sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w=="],
|
||||||
|
|
||||||
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.2", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg=="],
|
"@mongodb-js/saslprep": ["@mongodb-js/saslprep@1.3.2", "", { "dependencies": { "sparse-bitfield": "^3.0.3" } }, "sha512-QgA5AySqB27cGTXBFmnpifAi7HxoGUeezwo6p9dI03MuDB6Pp33zgclqVb6oVK3j6I9Vesg0+oojW2XxB59SGg=="],
|
||||||
|
|
||||||
"@neteasecloudmusicapienhanced/api": ["@neteasecloudmusicapienhanced/api@4.29.17", "", { "dependencies": { "@unblockneteasemusic/server": "^0.28.0", "axios": "^1.13.2", "crypto-js": "^4.2.0", "dotenv": "^17.2.3", "express": "^5.1.0", "express-fileupload": "^1.5.2", "md5": "^2.3.0", "music-metadata": "^11.10.0", "node-forge": "^1.3.1", "pac-proxy-agent": "^7.2.0", "qrcode": "^1.5.4", "safe-decode-uri-component": "^1.2.1", "tunnel": "^0.0.6", "xml2js": "^0.6.2", "yargs": "^18.0.0" }, "bin": { "api": "app.js" } }, "sha512-zKqmA7NoP+H3dK0b4/1K7SkxAYz69z9zwPd6+9wXcQNA42EO4AK/rDZ0ZPC9bonQjigHrvK4beFMaIl01S1iig=="],
|
"@neteasecloudmusicapienhanced/api": ["@neteasecloudmusicapienhanced/api@4.29.17", "", { "dependencies": { "@unblockneteasemusic/server": "^0.28.0", "axios": "^1.13.2", "crypto-js": "^4.2.0", "dotenv": "^17.2.3", "express": "^5.1.0", "express-fileupload": "^1.5.2", "md5": "^2.3.0", "music-metadata": "^11.10.0", "node-forge": "^1.3.1", "pac-proxy-agent": "^7.2.0", "qrcode": "^1.5.4", "safe-decode-uri-component": "^1.2.1", "tunnel": "^0.0.6", "xml2js": "^0.6.2", "yargs": "^18.0.0" }, "bin": { "api": "app.js" } }, "sha512-zKqmA7NoP+H3dK0b4/1K7SkxAYz69z9zwPd6+9wXcQNA42EO4AK/rDZ0ZPC9bonQjigHrvK4beFMaIl01S1iig=="],
|
||||||
@@ -425,6 +428,8 @@
|
|||||||
|
|
||||||
"charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="],
|
"charenc": ["charenc@0.0.2", "", {}, "sha512-yrLQ/yVUFXkzg7EDQsPieE/53+0RlaWTs+wBrvW36cyilJ2SaDWfl4Yj7MtLTXleV9uEKefbAGUPv2/iWSooRA=="],
|
||||||
|
|
||||||
|
"chart.js": ["chart.js@4.5.1", "", { "dependencies": { "@kurkle/color": "^0.3.0" } }, "sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw=="],
|
||||||
|
|
||||||
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
"chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="],
|
||||||
|
|
||||||
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
"cliui": ["cliui@9.0.1", "", { "dependencies": { "string-width": "^7.2.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-k7ndgKhwoQveBL+/1tqGJYNz097I7WOvwbmmU2AR5+magtbjPWQTS1C5vzGkBC8Ym8UWRzfKUzUUqFLypY4Q+w=="],
|
||||||
|
|||||||
@@ -41,6 +41,7 @@
|
|||||||
"@unocss/preset-attributify": "^66.5.6",
|
"@unocss/preset-attributify": "^66.5.6",
|
||||||
"@unocss/preset-icons": "^66.5.6",
|
"@unocss/preset-icons": "^66.5.6",
|
||||||
"@unocss/reset": "^66.5.6",
|
"@unocss/reset": "^66.5.6",
|
||||||
|
"chart.js": "^4.5.1",
|
||||||
"m3-svelte": "^5.14.1",
|
"m3-svelte": "^5.14.1",
|
||||||
"mongodb": "^7.0.0",
|
"mongodb": "^7.0.0",
|
||||||
"openai": "^6.9.0",
|
"openai": "^6.9.0",
|
||||||
|
|||||||
@@ -1,28 +1,102 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { PageProps } from './$types';
|
import type { PageProps } from "./$types";
|
||||||
import { goto } from '$app/navigation';
|
import { goto } from "$app/navigation";
|
||||||
import AppBar from '../../../components/appbar/AppBar.svelte';
|
import AppBar from "../../../components/appbar/AppBar.svelte";
|
||||||
import { artistAndAlbum } from '../../../shared/tools';
|
import { artistAndAlbum } from "../../../shared/tools";
|
||||||
import Button from '../../../components/Button.svelte';
|
import Button from "../../../components/Button.svelte";
|
||||||
|
|
||||||
let { data }: PageProps = $props();
|
import Chart from "chart.js/auto";
|
||||||
let { result, lrc } = data;
|
|
||||||
|
|
||||||
// Destructure result for easier access
|
let { data }: PageProps = $props();
|
||||||
let { totalTyped, startTime, endTime, totalRight, statsHistory, songId } = result;
|
let { result, lrc } = data;
|
||||||
|
|
||||||
// Calculate duration for display
|
|
||||||
let duration = endTime - startTime;
|
|
||||||
|
|
||||||
let fields = [
|
// Destructure result for easier access
|
||||||
{ label: '速度', value: Math.round(totalTyped / (Math.max(1, duration) / 60000)) },
|
let { totalTyped, startTime, endTime, totalRight, statsHistory, songId } = result;
|
||||||
{ label: '准确率', value: totalTyped === 0 ? 100 : (Math.round((totalRight / totalTyped) * 10000) / 100) },
|
|
||||||
{ label: '实时率', value: data.result.realTimeFactor.toFixed(2) + 'x' },
|
// Calculate duration for display
|
||||||
{ label: '字数', value: totalTyped }
|
let duration = endTime - startTime;
|
||||||
]
|
|
||||||
|
let fields = [
|
||||||
|
{
|
||||||
|
label: "速度",
|
||||||
|
value: Math.round(totalTyped / (Math.max(1, duration) / 60000)),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "准确率",
|
||||||
|
value:
|
||||||
|
totalTyped === 0
|
||||||
|
? 100
|
||||||
|
: Math.round((totalRight / totalTyped) * 10000) / 100,
|
||||||
|
},
|
||||||
|
{ label: "实时率", value: data.result.realTimeFactor.toFixed(2) + "x" },
|
||||||
|
{ label: "字数", value: totalTyped },
|
||||||
|
];
|
||||||
|
|
||||||
|
let chartCanvas: HTMLCanvasElement;
|
||||||
|
let chart: Chart;
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (chartCanvas && statsHistory.length > 0) {
|
||||||
|
chart = new Chart(chartCanvas, {
|
||||||
|
type: "line",
|
||||||
|
data: {
|
||||||
|
labels: statsHistory.map((_: any, i: number) => i),
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: "速度 (CPM)",
|
||||||
|
data: statsHistory.map((h: any) => h.cpm),
|
||||||
|
tension: 0.4,
|
||||||
|
pointRadius: 0,
|
||||||
|
fill: true,
|
||||||
|
borderColor: "#7b78c2",
|
||||||
|
backgroundColor: "rgba(123, 120, 194, 0.1)",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: "准确率 (%)",
|
||||||
|
data: statsHistory.map((h: any) => h.acc),
|
||||||
|
tension: 0.4,
|
||||||
|
yAxisID: "y1",
|
||||||
|
pointRadius: 0,
|
||||||
|
borderColor: "#e5a657",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false,
|
||||||
|
interaction: {
|
||||||
|
mode: "index",
|
||||||
|
intersect: false,
|
||||||
|
},
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: { enabled: true },
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: { display: false },
|
||||||
|
y: {
|
||||||
|
position: "left",
|
||||||
|
max: 300,
|
||||||
|
ticks: { display: false }
|
||||||
|
},
|
||||||
|
y1: {
|
||||||
|
position: "right",
|
||||||
|
max: 100,
|
||||||
|
grid: { display: false },
|
||||||
|
ticks: { display: false }
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
if (chart) chart.destroy();
|
||||||
|
};
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<AppBar title={data.brief.name} sub={artistAndAlbum(data.brief)}/>
|
<AppBar title={data.brief.name} sub={artistAndAlbum(data.brief)} />
|
||||||
|
|
||||||
<div class="vbox gap-16px p-content">
|
<div class="vbox gap-16px p-content">
|
||||||
<div class="hbox gap-12px items-end! h-48px">
|
<div class="hbox gap-12px items-end! h-48px">
|
||||||
@@ -31,48 +105,25 @@
|
|||||||
|
|
||||||
<div class="grid grid-cols-2 gap-16px">
|
<div class="grid grid-cols-2 gap-16px">
|
||||||
{#each fields as field}
|
{#each fields as field}
|
||||||
<div class="vbox flex-1">
|
<div class="vbox flex-1">
|
||||||
<div class="m3-font-title-medium mfg-on-surface-variant">{field.label}</div>
|
<div class="m3-font-title-medium mfg-on-surface-variant">
|
||||||
<div class="m3-font-headline-large font-medium mfg-on-surface">{field.value}</div>
|
{field.label}
|
||||||
</div>
|
</div>
|
||||||
|
<div class="m3-font-headline-large font-medium mfg-on-surface">
|
||||||
|
{field.value}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
{/each}
|
{/each}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Chart -->
|
<!-- Chart -->
|
||||||
<div class="h-120px w-full bg-surface-container rounded-12px relative overflow-hidden">
|
<div class="h-200px w-full bg-surface-container rounded-12px relative overflow-hidden">
|
||||||
<svg class="w-full h-full" viewBox="0 0 100 100" preserveAspectRatio="none">
|
<canvas bind:this={chartCanvas}></canvas>
|
||||||
<!-- Grid lines -->
|
|
||||||
{#each [0, 25, 50, 75, 100] as y}
|
|
||||||
<line x1="0" y1={y} x2="100" y2={y} stroke="rgba(0,0,0,0.05)" stroke-width="1" />
|
|
||||||
{/each}
|
|
||||||
|
|
||||||
<!-- Speed Line -->
|
|
||||||
{#if statsHistory.length > 1}
|
|
||||||
<polyline
|
|
||||||
points={statsHistory.map((h: any, i: number) => `${(i / (statsHistory.length - 1)) * 100},${100 - (h.cpm / 300) * 100}`).join(' ')}
|
|
||||||
fill="none"
|
|
||||||
stroke="#7b78c2"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-linecap="round"
|
|
||||||
stroke-linejoin="round"
|
|
||||||
/>
|
|
||||||
|
|
||||||
<!-- Accuracy Line (dashed) -->
|
|
||||||
<polyline
|
|
||||||
points={statsHistory.map((h: any, i: number) => `${(i / (statsHistory.length - 1)) * 100},${100 - (h.acc / 100) * 100}`).join(' ')}
|
|
||||||
fill="none"
|
|
||||||
stroke="#e5a657"
|
|
||||||
stroke-width="2"
|
|
||||||
stroke-dasharray="4"
|
|
||||||
stroke-opacity="0.5"
|
|
||||||
/>
|
|
||||||
{/if}
|
|
||||||
</svg>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="flex-1"></div>
|
<div class="flex-1"></div>
|
||||||
|
|
||||||
<div class="hbox justify-end pt-8px">
|
<div class="hbox justify-end pt-8px">
|
||||||
<Button onclick={() => goto(`/song/${songId}`)}>下一首</Button>
|
<Button onclick={() => goto(`/song/${songId}`)}>下一首</Button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user