fmt
This commit is contained in:
@@ -1,145 +1,144 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="难度掌握情况" size="small" v-if="show">
|
<n-card title="难度掌握情况" size="small" v-if="show">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">
|
||||||
了解不同难度题目的完成等级分布
|
了解不同难度题目的完成等级分布
|
||||||
</n-text>
|
</n-text>
|
||||||
</template>
|
</template>
|
||||||
<div style="height: 300px">
|
<div style="height: 300px">
|
||||||
<Bar :data="data" :options="options" />
|
<Bar :data="data" :options="options" />
|
||||||
</div>
|
</div>
|
||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { Bar } from "vue-chartjs"
|
import { Bar } from "vue-chartjs"
|
||||||
import {
|
import {
|
||||||
Chart as ChartJS,
|
Chart as ChartJS,
|
||||||
CategoryScale,
|
CategoryScale,
|
||||||
LinearScale,
|
LinearScale,
|
||||||
BarElement,
|
BarElement,
|
||||||
Title,
|
Title,
|
||||||
Tooltip,
|
Tooltip,
|
||||||
Legend,
|
Legend,
|
||||||
} from "chart.js"
|
} from "chart.js"
|
||||||
import { useAIStore } from "oj/store/ai"
|
import { useAIStore } from "oj/store/ai"
|
||||||
import type { Grade } from "utils/types"
|
import type { Grade } from "utils/types"
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||||
|
|
||||||
const aiStore = useAIStore()
|
const aiStore = useAIStore()
|
||||||
|
|
||||||
// 难度和等级的顺序(后端返回的是中文)
|
// 难度和等级的顺序(后端返回的是中文)
|
||||||
const difficultyOrder = ["简单", "中等", "困难"]
|
const difficultyOrder = ["简单", "中等", "困难"]
|
||||||
const gradeOrder: Grade[] = ["S", "A", "B", "C"]
|
const gradeOrder: Grade[] = ["S", "A", "B", "C"]
|
||||||
|
|
||||||
// 统计每个难度-等级组合的题目数量
|
// 统计每个难度-等级组合的题目数量
|
||||||
const matrix = computed(() => {
|
const matrix = computed(() => {
|
||||||
const result: { [difficulty: string]: { [grade: string]: number } } = {}
|
const result: { [difficulty: string]: { [grade: string]: number } } = {}
|
||||||
|
|
||||||
// 初始化矩阵
|
// 初始化矩阵
|
||||||
difficultyOrder.forEach((diff) => {
|
difficultyOrder.forEach((diff) => {
|
||||||
result[diff] = {}
|
result[diff] = {}
|
||||||
gradeOrder.forEach((grade) => {
|
gradeOrder.forEach((grade) => {
|
||||||
result[diff][grade] = 0
|
result[diff][grade] = 0
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
// 统计数据
|
// 统计数据
|
||||||
aiStore.detailsData.solved.forEach((item) => {
|
aiStore.detailsData.solved.forEach((item) => {
|
||||||
const diff = item.difficulty
|
const diff = item.difficulty
|
||||||
const grade = item.grade
|
const grade = item.grade
|
||||||
if (diff && grade && result[diff]) {
|
if (diff && grade && result[diff]) {
|
||||||
result[diff][grade]++
|
result[diff][grade]++
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return result
|
return result
|
||||||
})
|
})
|
||||||
|
|
||||||
const show = computed(() => {
|
const show = computed(() => {
|
||||||
return aiStore.detailsData.solved.length > 0
|
return aiStore.detailsData.solved.length > 0
|
||||||
})
|
})
|
||||||
|
|
||||||
// 为每个等级准备数据集
|
// 为每个等级准备数据集
|
||||||
const data = computed(() => {
|
const data = computed(() => {
|
||||||
// 为每个等级生成一个 dataset
|
// 为每个等级生成一个 dataset
|
||||||
const datasets = gradeOrder.map((grade) => {
|
const datasets = gradeOrder.map((grade) => {
|
||||||
return {
|
return {
|
||||||
label: `等级 ${grade}`,
|
label: `等级 ${grade}`,
|
||||||
data: difficultyOrder.map((diff) => matrix.value[diff][grade]),
|
data: difficultyOrder.map((diff) => matrix.value[diff][grade]),
|
||||||
backgroundColor: getGradeColor(grade),
|
backgroundColor: getGradeColor(grade),
|
||||||
borderColor: getGradeColor(grade),
|
borderColor: getGradeColor(grade),
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: difficultyOrder,
|
labels: difficultyOrder,
|
||||||
datasets,
|
datasets,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 根据等级返回对应的颜色
|
// 根据等级返回对应的颜色
|
||||||
function getGradeColor(grade: Grade): string {
|
function getGradeColor(grade: Grade): string {
|
||||||
const colors: { [key in Grade]: string } = {
|
const colors: { [key in Grade]: string } = {
|
||||||
S: "#FF6384",
|
S: "#FF6384",
|
||||||
A: "#FFCE56",
|
A: "#FFCE56",
|
||||||
B: "#36A2EB",
|
B: "#36A2EB",
|
||||||
C: "#95F204",
|
C: "#95F204",
|
||||||
}
|
}
|
||||||
return colors[grade]
|
return colors[grade]
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: {
|
interaction: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
mode: "index" as const,
|
mode: "index" as const,
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
ticks: {
|
ticks: {
|
||||||
stepSize: 1,
|
stepSize: 1,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: "题目数量",
|
text: "题目数量",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: true,
|
display: true,
|
||||||
position: "bottom" as const,
|
position: "bottom" as const,
|
||||||
labels: {
|
labels: {
|
||||||
boxWidth: 12,
|
boxWidth: 12,
|
||||||
padding: 8,
|
padding: 8,
|
||||||
font: {
|
font: {
|
||||||
size: 11,
|
size: 11,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
footer: (items: any[]) => {
|
footer: (items: any[]) => {
|
||||||
const total = items.reduce((sum, item) => sum + item.parsed.y, 0)
|
const total = items.reduce((sum, item) => sum + item.parsed.y, 0)
|
||||||
return `该难度总计: ${total} 题`
|
return `该难度总计: ${total} 题`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card :title="title" size="small">
|
<n-card :title="title" size="small">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px"> 全面评估学习情况 </n-text>
|
||||||
全面评估学习情况
|
|
||||||
</n-text>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<Chart type="bar" :data="data" :options="options" />
|
<Chart type="bar" :data="data" :options="options" />
|
||||||
@@ -173,11 +171,20 @@ const options = computed<ChartOptions<"bar" | "line">>(() => {
|
|||||||
return `${dsLabel}: ${ctx.formattedValue}`
|
return `${dsLabel}: ${ctx.formattedValue}`
|
||||||
},
|
},
|
||||||
footer: (items: TooltipItem<"bar">[]) => {
|
footer: (items: TooltipItem<"bar">[]) => {
|
||||||
const barItems = items.filter(item => (item.dataset as any).yAxisID === "y")
|
const barItems = items.filter(
|
||||||
|
(item) => (item.dataset as any).yAxisID === "y",
|
||||||
|
)
|
||||||
if (barItems.length >= 2) {
|
if (barItems.length >= 2) {
|
||||||
const problemCount = barItems.find(item => item.dataset.label === "完成题目数")?.parsed.y || 0
|
const problemCount =
|
||||||
const submissionCount = barItems.find(item => item.dataset.label === "总提交次数")?.parsed.y || 0
|
barItems.find((item) => item.dataset.label === "完成题目数")
|
||||||
const efficiency = submissionCount > 0 ? ((problemCount / submissionCount) * 100).toFixed(1) : "0"
|
?.parsed.y || 0
|
||||||
|
const submissionCount =
|
||||||
|
barItems.find((item) => item.dataset.label === "总提交次数")
|
||||||
|
?.parsed.y || 0
|
||||||
|
const efficiency =
|
||||||
|
submissionCount > 0
|
||||||
|
? ((problemCount / submissionCount) * 100).toFixed(1)
|
||||||
|
: "0"
|
||||||
return `AC率: ${efficiency}%`
|
return `AC率: ${efficiency}%`
|
||||||
}
|
}
|
||||||
return ""
|
return ""
|
||||||
|
|||||||
@@ -1,236 +1,234 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card :title="title" size="small" v-if="show">
|
<n-card :title="title" size="small" v-if="show">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">反映刷题质量提升</n-text>
|
||||||
反映刷题质量提升
|
</template>
|
||||||
</n-text>
|
<div class="chart">
|
||||||
</template>
|
<Chart type="line" :data="data" :options="options" />
|
||||||
<div class="chart">
|
</div>
|
||||||
<Chart type="line" :data="data" :options="options" />
|
</n-card>
|
||||||
</div>
|
</template>
|
||||||
</n-card>
|
<script setup lang="ts">
|
||||||
</template>
|
import type { ChartData, ChartOptions, TooltipItem } from "chart.js"
|
||||||
<script setup lang="ts">
|
import { Chart } from "vue-chartjs"
|
||||||
import type { ChartData, ChartOptions, TooltipItem } from "chart.js"
|
import {
|
||||||
import { Chart } from "vue-chartjs"
|
Chart as ChartJS,
|
||||||
import {
|
CategoryScale,
|
||||||
Chart as ChartJS,
|
LinearScale,
|
||||||
CategoryScale,
|
PointElement,
|
||||||
LinearScale,
|
LineElement,
|
||||||
PointElement,
|
Title,
|
||||||
LineElement,
|
Tooltip,
|
||||||
Title,
|
Legend,
|
||||||
Tooltip,
|
Filler,
|
||||||
Legend,
|
} from "chart.js"
|
||||||
Filler,
|
import { useAIStore } from "oj/store/ai"
|
||||||
} from "chart.js"
|
import { parseTime } from "utils/functions"
|
||||||
import { useAIStore } from "oj/store/ai"
|
|
||||||
import { parseTime } from "utils/functions"
|
// 注册折线图所需的 Chart.js 组件
|
||||||
|
ChartJS.register(
|
||||||
// 注册折线图所需的 Chart.js 组件
|
CategoryScale,
|
||||||
ChartJS.register(
|
LinearScale,
|
||||||
CategoryScale,
|
PointElement,
|
||||||
LinearScale,
|
LineElement,
|
||||||
PointElement,
|
Title,
|
||||||
LineElement,
|
Tooltip,
|
||||||
Title,
|
Legend,
|
||||||
Tooltip,
|
Filler,
|
||||||
Legend,
|
)
|
||||||
Filler,
|
|
||||||
)
|
const aiStore = useAIStore()
|
||||||
|
|
||||||
const aiStore = useAIStore()
|
const title = computed(() => {
|
||||||
|
if (aiStore.duration === "months:2") {
|
||||||
const title = computed(() => {
|
return "过去两个月的每周提交效率"
|
||||||
if (aiStore.duration === "months:2") {
|
} else if (aiStore.duration === "months:6") {
|
||||||
return "过去两个月的每周提交效率"
|
return "过去半年的每月提交效率"
|
||||||
} else if (aiStore.duration === "months:6") {
|
} else if (aiStore.duration === "years:1") {
|
||||||
return "过去半年的每月提交效率"
|
return "过去一年的每月提交效率"
|
||||||
} else if (aiStore.duration === "years:1") {
|
} else {
|
||||||
return "过去一年的每月提交效率"
|
return "过去四周的提交效率"
|
||||||
} else {
|
}
|
||||||
return "过去四周的提交效率"
|
})
|
||||||
}
|
|
||||||
})
|
// 判断是否有数据
|
||||||
|
const show = computed(() => {
|
||||||
// 判断是否有数据
|
return aiStore.durationData.length > 0
|
||||||
const show = computed(() => {
|
})
|
||||||
return aiStore.durationData.length > 0
|
|
||||||
})
|
// 计算提交效率数据
|
||||||
|
const efficiencyData = computed(() => {
|
||||||
// 计算提交效率数据
|
return aiStore.durationData.map((duration) => {
|
||||||
const efficiencyData = computed(() => {
|
const problemCount = duration.problem_count || 0
|
||||||
return aiStore.durationData.map((duration) => {
|
const submissionCount = duration.submission_count || 0
|
||||||
const problemCount = duration.problem_count || 0
|
|
||||||
const submissionCount = duration.submission_count || 0
|
// 计算效率:提交次数/完成题目数
|
||||||
|
// 值越接近1,说明一次AC率越高
|
||||||
// 计算效率:提交次数/完成题目数
|
const efficiency = problemCount > 0 ? submissionCount / problemCount : 0
|
||||||
// 值越接近1,说明一次AC率越高
|
|
||||||
const efficiency = problemCount > 0 ? submissionCount / problemCount : 0
|
// 计算一次AC率(百分比)
|
||||||
|
const onePassRate =
|
||||||
// 计算一次AC率(百分比)
|
problemCount > 0 ? (problemCount / submissionCount) * 100 : 0
|
||||||
const onePassRate = problemCount > 0 ? (problemCount / submissionCount) * 100 : 0
|
|
||||||
|
return {
|
||||||
return {
|
label: [
|
||||||
label: [
|
parseTime(duration.start, "M月D日"),
|
||||||
parseTime(duration.start, "M月D日"),
|
parseTime(duration.end, "M月D日"),
|
||||||
parseTime(duration.end, "M月D日"),
|
].join("~"),
|
||||||
].join("~"),
|
efficiency: efficiency,
|
||||||
efficiency: efficiency,
|
onePassRate: onePassRate,
|
||||||
onePassRate: onePassRate,
|
problemCount: problemCount,
|
||||||
problemCount: problemCount,
|
submissionCount: submissionCount,
|
||||||
submissionCount: submissionCount,
|
}
|
||||||
}
|
})
|
||||||
})
|
})
|
||||||
})
|
|
||||||
|
// 图表数据
|
||||||
// 图表数据
|
const data = computed<ChartData<"line">>(() => {
|
||||||
const data = computed<ChartData<"line">>(() => {
|
const efficiency = efficiencyData.value
|
||||||
const efficiency = efficiencyData.value
|
|
||||||
|
return {
|
||||||
return {
|
labels: efficiency.map((e) => e.label),
|
||||||
labels: efficiency.map((e) => e.label),
|
datasets: [
|
||||||
datasets: [
|
{
|
||||||
{
|
label: "平均提交次数",
|
||||||
label: "平均提交次数",
|
data: efficiency.map((e) => e.efficiency),
|
||||||
data: efficiency.map((e) => e.efficiency),
|
borderColor: "rgb(99, 102, 241)",
|
||||||
borderColor: "rgb(99, 102, 241)",
|
backgroundColor: "rgba(99, 102, 241, 0.1)",
|
||||||
backgroundColor: "rgba(99, 102, 241, 0.1)",
|
tension: 0.4,
|
||||||
tension: 0.4,
|
fill: true,
|
||||||
fill: true,
|
pointRadius: 5,
|
||||||
pointRadius: 5,
|
pointHoverRadius: 7,
|
||||||
pointHoverRadius: 7,
|
borderWidth: 2.5,
|
||||||
borderWidth: 2.5,
|
pointBackgroundColor: "rgb(99, 102, 241)",
|
||||||
pointBackgroundColor: "rgb(99, 102, 241)",
|
pointBorderColor: "#fff",
|
||||||
pointBorderColor: "#fff",
|
pointBorderWidth: 2,
|
||||||
pointBorderWidth: 2,
|
yAxisID: "y",
|
||||||
yAxisID: "y",
|
},
|
||||||
},
|
{
|
||||||
{
|
label: "一次AC率",
|
||||||
label: "一次AC率",
|
data: efficiency.map((e) => e.onePassRate),
|
||||||
data: efficiency.map((e) => e.onePassRate),
|
borderColor: "rgb(34, 197, 94)",
|
||||||
borderColor: "rgb(34, 197, 94)",
|
backgroundColor: "rgba(34, 197, 94, 0.1)",
|
||||||
backgroundColor: "rgba(34, 197, 94, 0.1)",
|
tension: 0.4,
|
||||||
tension: 0.4,
|
fill: true,
|
||||||
fill: true,
|
pointRadius: 5,
|
||||||
pointRadius: 5,
|
pointHoverRadius: 7,
|
||||||
pointHoverRadius: 7,
|
borderWidth: 2.5,
|
||||||
borderWidth: 2.5,
|
pointBackgroundColor: "rgb(34, 197, 94)",
|
||||||
pointBackgroundColor: "rgb(34, 197, 94)",
|
pointBorderColor: "#fff",
|
||||||
pointBorderColor: "#fff",
|
pointBorderWidth: 2,
|
||||||
pointBorderWidth: 2,
|
yAxisID: "y1",
|
||||||
yAxisID: "y1",
|
},
|
||||||
},
|
],
|
||||||
],
|
}
|
||||||
}
|
})
|
||||||
})
|
|
||||||
|
// 图表配置
|
||||||
// 图表配置
|
const options = computed<ChartOptions<"line">>(() => {
|
||||||
const options = computed<ChartOptions<"line">>(() => {
|
return {
|
||||||
return {
|
responsive: true,
|
||||||
responsive: true,
|
maintainAspectRatio: false,
|
||||||
maintainAspectRatio: false,
|
interaction: {
|
||||||
interaction: {
|
mode: "index",
|
||||||
mode: "index",
|
intersect: false,
|
||||||
intersect: false,
|
},
|
||||||
},
|
scales: {
|
||||||
scales: {
|
x: {
|
||||||
x: {
|
ticks: {
|
||||||
ticks: {
|
maxRotation: 0,
|
||||||
maxRotation: 0,
|
minRotation: 0,
|
||||||
minRotation: 0,
|
autoSkip: true,
|
||||||
autoSkip: true,
|
},
|
||||||
},
|
},
|
||||||
},
|
y: {
|
||||||
y: {
|
type: "linear",
|
||||||
type: "linear",
|
position: "left",
|
||||||
position: "left",
|
title: {
|
||||||
title: {
|
display: true,
|
||||||
display: true,
|
text: "平均提交次数(次/题)",
|
||||||
text: "平均提交次数(次/题)",
|
font: {
|
||||||
font: {
|
size: 13,
|
||||||
size: 13,
|
},
|
||||||
},
|
},
|
||||||
},
|
beginAtZero: true,
|
||||||
beginAtZero: true,
|
ticks: {
|
||||||
ticks: {
|
callback: function (value: string | number) {
|
||||||
callback: function (value: string | number) {
|
return Number(value).toFixed(1)
|
||||||
return Number(value).toFixed(1)
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
y1: {
|
||||||
y1: {
|
type: "linear",
|
||||||
type: "linear",
|
position: "right",
|
||||||
position: "right",
|
min: 0,
|
||||||
min: 0,
|
max: 100,
|
||||||
max: 100,
|
title: {
|
||||||
title: {
|
display: true,
|
||||||
display: true,
|
text: "一次AC率(%)",
|
||||||
text: "一次AC率(%)",
|
font: {
|
||||||
font: {
|
size: 13,
|
||||||
size: 13,
|
},
|
||||||
},
|
},
|
||||||
},
|
ticks: {
|
||||||
ticks: {
|
callback: function (value: string | number) {
|
||||||
callback: function (value: string | number) {
|
return Number(value).toFixed(0) + "%"
|
||||||
return Number(value).toFixed(0) + "%"
|
},
|
||||||
},
|
},
|
||||||
},
|
grid: {
|
||||||
grid: {
|
drawOnChartArea: false,
|
||||||
drawOnChartArea: false,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
plugins: {
|
||||||
plugins: {
|
title: {
|
||||||
title: {
|
display: false,
|
||||||
display: false,
|
},
|
||||||
},
|
tooltip: {
|
||||||
tooltip: {
|
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
||||||
backgroundColor: "rgba(0, 0, 0, 0.8)",
|
padding: 12,
|
||||||
padding: 12,
|
callbacks: {
|
||||||
callbacks: {
|
label: function (ctx: TooltipItem<"line">) {
|
||||||
label: function (ctx: TooltipItem<"line">) {
|
const index = ctx.dataIndex
|
||||||
const index = ctx.dataIndex
|
const item = efficiencyData.value[index]
|
||||||
const item = efficiencyData.value[index]
|
const dsLabel = ctx.dataset.label || ""
|
||||||
const dsLabel = ctx.dataset.label || ""
|
|
||||||
|
if (ctx.datasetIndex === 0) {
|
||||||
if (ctx.datasetIndex === 0) {
|
// 平均提交次数
|
||||||
// 平均提交次数
|
return [
|
||||||
return [
|
`${dsLabel}: ${item.efficiency.toFixed(2)} 次/题`,
|
||||||
`${dsLabel}: ${item.efficiency.toFixed(2)} 次/题`,
|
`完成题目: ${item.problemCount} 题`,
|
||||||
`完成题目: ${item.problemCount} 题`,
|
`总提交: ${item.submissionCount} 次`,
|
||||||
`总提交: ${item.submissionCount} 次`,
|
]
|
||||||
]
|
} else {
|
||||||
} else {
|
// 一次AC率
|
||||||
// 一次AC率
|
return [
|
||||||
return [
|
`${dsLabel}: ${item.onePassRate.toFixed(1)}%`,
|
||||||
`${dsLabel}: ${item.onePassRate.toFixed(1)}%`,
|
`提示: 值越高表示刷题质量越好`,
|
||||||
`提示: 值越高表示刷题质量越好`,
|
]
|
||||||
]
|
}
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
legend: {
|
||||||
legend: {
|
display: true,
|
||||||
display: true,
|
position: "bottom" as const,
|
||||||
position: "bottom" as const,
|
labels: {
|
||||||
labels: {
|
boxWidth: 12,
|
||||||
boxWidth: 12,
|
boxHeight: 12,
|
||||||
boxHeight: 12,
|
padding: 8,
|
||||||
padding: 8,
|
font: {
|
||||||
font: {
|
size: 12,
|
||||||
size: 12,
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
})
|
||||||
})
|
</script>
|
||||||
</script>
|
<style scoped>
|
||||||
<style scoped>
|
.chart {
|
||||||
.chart {
|
height: 300px;
|
||||||
height: 300px;
|
width: 100%;
|
||||||
width: 100%;
|
}
|
||||||
}
|
</style>
|
||||||
</style>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="过去一年的提交热力图" size="small">
|
<n-card title="过去一年的提交热力图" size="small">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">激励持续学习</n-text>
|
||||||
激励持续学习
|
|
||||||
</n-text>
|
|
||||||
</template>
|
</template>
|
||||||
<n-spin :show="aiStore.loading.heatmap">
|
<n-spin :show="aiStore.loading.heatmap">
|
||||||
<div class="heatmap-container" ref="containerRef">
|
<div class="heatmap-container" ref="containerRef">
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card :title="title" size="small" v-if="show">
|
<n-card :title="title" size="small" v-if="show">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">追踪学习成长轨迹</n-text>
|
||||||
追踪学习成长轨迹
|
|
||||||
</n-text>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<Chart type="line" :data="data" :options="options" />
|
<Chart type="line" :data="data" :options="options" />
|
||||||
@@ -81,13 +79,14 @@ const progressData = computed(() => {
|
|||||||
|
|
||||||
// 计算本期等级的权重值
|
// 计算本期等级的权重值
|
||||||
const currentGradeValue = gradeOrder.indexOf(duration.grade || "C")
|
const currentGradeValue = gradeOrder.indexOf(duration.grade || "C")
|
||||||
|
|
||||||
// 累加加权等级
|
// 累加加权等级
|
||||||
totalWeightedGrade += currentGradeValue * problemCount
|
totalWeightedGrade += currentGradeValue * problemCount
|
||||||
totalProblems += problemCount
|
totalProblems += problemCount
|
||||||
|
|
||||||
// 计算累计平均等级
|
// 计算累计平均等级
|
||||||
const avgGradeValue = totalProblems > 0 ? totalWeightedGrade / totalProblems : 0
|
const avgGradeValue =
|
||||||
|
totalProblems > 0 ? totalWeightedGrade / totalProblems : 0
|
||||||
|
|
||||||
return {
|
return {
|
||||||
label: [
|
label: [
|
||||||
|
|||||||
@@ -1,134 +1,132 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="解题排名分布" size="small" v-if="show">
|
<n-card title="解题排名分布" size="small" v-if="show">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">了解解题速度和竞争力</n-text>
|
||||||
了解解题速度和竞争力
|
</template>
|
||||||
</n-text>
|
<div style="height: 300px">
|
||||||
</template>
|
<Pie :data="data" :options="options" />
|
||||||
<div style="height: 300px">
|
</div>
|
||||||
<Pie :data="data" :options="options" />
|
</n-card>
|
||||||
</div>
|
</template>
|
||||||
</n-card>
|
|
||||||
</template>
|
<script setup lang="ts">
|
||||||
|
import { Pie } from "vue-chartjs"
|
||||||
<script setup lang="ts">
|
import { Chart as ChartJS, ArcElement, Title, Tooltip, Legend } from "chart.js"
|
||||||
import { Pie } from "vue-chartjs"
|
import { useAIStore } from "oj/store/ai"
|
||||||
import {
|
|
||||||
Chart as ChartJS,
|
ChartJS.register(ArcElement, Title, Tooltip, Legend)
|
||||||
ArcElement,
|
|
||||||
Title,
|
const aiStore = useAIStore()
|
||||||
Tooltip,
|
|
||||||
Legend,
|
// 排名区间定义
|
||||||
} from "chart.js"
|
const RANK_RANGES = [
|
||||||
import { useAIStore } from "oj/store/ai"
|
{ label: "前10%", min: 0, max: 10, color: "#FF6384" },
|
||||||
|
{ label: "10-30%", min: 10, max: 30, color: "#FFCE56" },
|
||||||
ChartJS.register(ArcElement, Title, Tooltip, Legend)
|
{ label: "30-50%", min: 30, max: 50, color: "#36A2EB" },
|
||||||
|
{ label: "50-70%", min: 50, max: 70, color: "#4BC0C0" },
|
||||||
const aiStore = useAIStore()
|
{ label: "70%以后", min: 70, max: 100, color: "#9966FF" },
|
||||||
|
]
|
||||||
// 排名区间定义
|
|
||||||
const RANK_RANGES = [
|
// 计算每道题的排名百分位并分类
|
||||||
{ label: "前10%", min: 0, max: 10, color: "#FF6384" },
|
const rankDistribution = computed(() => {
|
||||||
{ label: "10-30%", min: 10, max: 30, color: "#FFCE56" },
|
const distribution = RANK_RANGES.map((range) => ({
|
||||||
{ label: "30-50%", min: 30, max: 50, color: "#36A2EB" },
|
...range,
|
||||||
{ label: "50-70%", min: 50, max: 70, color: "#4BC0C0" },
|
count: 0,
|
||||||
{ label: "70%以后", min: 70, max: 100, color: "#9966FF" },
|
problems: [] as string[],
|
||||||
]
|
}))
|
||||||
|
|
||||||
// 计算每道题的排名百分位并分类
|
aiStore.detailsData.solved.forEach((item) => {
|
||||||
const rankDistribution = computed(() => {
|
const rank = item.rank
|
||||||
const distribution = RANK_RANGES.map((range) => ({
|
const acCount = item.ac_count
|
||||||
...range,
|
|
||||||
count: 0,
|
if (rank && acCount && acCount > 0) {
|
||||||
problems: [] as string[],
|
// 计算百分位:(rank / acCount) * 100
|
||||||
}))
|
// 例如:第5名/共100人 = 5%
|
||||||
|
const percentile = (rank / acCount) * 100
|
||||||
aiStore.detailsData.solved.forEach((item) => {
|
|
||||||
const rank = item.rank
|
// 找到对应的区间
|
||||||
const acCount = item.ac_count
|
const rangeIndex = RANK_RANGES.findIndex(
|
||||||
|
(r) => percentile >= r.min && percentile < r.max,
|
||||||
if (rank && acCount && acCount > 0) {
|
)
|
||||||
// 计算百分位:(rank / acCount) * 100
|
|
||||||
// 例如:第5名/共100人 = 5%
|
if (rangeIndex !== -1) {
|
||||||
const percentile = (rank / acCount) * 100
|
distribution[rangeIndex].count++
|
||||||
|
distribution[rangeIndex].problems.push(
|
||||||
// 找到对应的区间
|
`${item.problem.display_id}: ${item.problem.title}`,
|
||||||
const rangeIndex = RANK_RANGES.findIndex(
|
)
|
||||||
(r) => percentile >= r.min && percentile < r.max,
|
}
|
||||||
)
|
}
|
||||||
|
})
|
||||||
if (rangeIndex !== -1) {
|
|
||||||
distribution[rangeIndex].count++
|
return distribution
|
||||||
distribution[rangeIndex].problems.push(
|
})
|
||||||
`${item.problem.display_id}: ${item.problem.title}`,
|
|
||||||
)
|
const show = computed(() => {
|
||||||
}
|
return aiStore.detailsData.solved.length > 0
|
||||||
}
|
})
|
||||||
})
|
|
||||||
|
const data = computed(() => {
|
||||||
return distribution
|
return {
|
||||||
})
|
labels: RANK_RANGES.map((r) => r.label),
|
||||||
|
datasets: [
|
||||||
const show = computed(() => {
|
{
|
||||||
return aiStore.detailsData.solved.length > 0
|
label: "题目数量",
|
||||||
})
|
data: rankDistribution.value.map((r) => r.count),
|
||||||
|
backgroundColor: RANK_RANGES.map((r) => r.color),
|
||||||
const data = computed(() => {
|
borderColor: RANK_RANGES.map((r) => r.color),
|
||||||
return {
|
borderWidth: 1,
|
||||||
labels: RANK_RANGES.map((r) => r.label),
|
},
|
||||||
datasets: [
|
],
|
||||||
{
|
}
|
||||||
label: "题目数量",
|
})
|
||||||
data: rankDistribution.value.map((r) => r.count),
|
|
||||||
backgroundColor: RANK_RANGES.map((r) => r.color),
|
const options = {
|
||||||
borderColor: RANK_RANGES.map((r) => r.color),
|
responsive: true,
|
||||||
borderWidth: 1,
|
maintainAspectRatio: false,
|
||||||
},
|
plugins: {
|
||||||
],
|
legend: {
|
||||||
}
|
display: true,
|
||||||
})
|
position: "bottom" as const,
|
||||||
|
labels: {
|
||||||
const options = {
|
boxWidth: 12,
|
||||||
responsive: true,
|
boxHeight: 12,
|
||||||
maintainAspectRatio: false,
|
padding: 8,
|
||||||
plugins: {
|
font: {
|
||||||
legend: {
|
size: 12,
|
||||||
display: true,
|
},
|
||||||
position: "bottom" as const,
|
},
|
||||||
labels: {
|
},
|
||||||
boxWidth: 12,
|
title: {
|
||||||
boxHeight: 12,
|
display: false,
|
||||||
padding: 8,
|
},
|
||||||
font: {
|
tooltip: {
|
||||||
size: 12,
|
callbacks: {
|
||||||
},
|
label: (context: any) => {
|
||||||
},
|
const count = context.parsed
|
||||||
},
|
const total = rankDistribution.value.reduce(
|
||||||
title: {
|
(sum, r) => sum + r.count,
|
||||||
display: false,
|
0,
|
||||||
},
|
)
|
||||||
tooltip: {
|
const percentage =
|
||||||
callbacks: {
|
total > 0 ? ((count / total) * 100).toFixed(1) : "0.0"
|
||||||
label: (context: any) => {
|
const label = context.label || ""
|
||||||
const count = context.parsed
|
return `${label}: ${count} 道题 (${percentage}%)`
|
||||||
const total = rankDistribution.value.reduce((sum, r) => sum + r.count, 0)
|
},
|
||||||
const percentage = total > 0 ? ((count / total) * 100).toFixed(1) : "0.0"
|
afterLabel: (context: any) => {
|
||||||
const label = context.label || ""
|
const index = context.dataIndex
|
||||||
return `${label}: ${count} 道题 (${percentage}%)`
|
const problems = rankDistribution.value[index].problems
|
||||||
},
|
if (problems.length > 0 && problems.length <= 5) {
|
||||||
afterLabel: (context: any) => {
|
return problems
|
||||||
const index = context.dataIndex
|
} else if (problems.length > 5) {
|
||||||
const problems = rankDistribution.value[index].problems
|
return [
|
||||||
if (problems.length > 0 && problems.length <= 5) {
|
...problems.slice(0, 3),
|
||||||
return problems
|
`... 还有 ${problems.length - 3} 道题`,
|
||||||
} else if (problems.length > 5) {
|
]
|
||||||
return [...problems.slice(0, 3), `... 还有 ${problems.length - 3} 道题`]
|
}
|
||||||
}
|
return ""
|
||||||
return ""
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
}
|
||||||
}
|
</script>
|
||||||
</script>
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,178 +1,177 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="连续做题统计" size="small">
|
<n-card title="连续做题统计" size="small">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">激励持续学习</n-text>
|
||||||
激励持续学习
|
</template>
|
||||||
</n-text>
|
<n-spin :show="aiStore.loading.heatmap">
|
||||||
</template>
|
<n-grid :cols="2" :x-gap="12" :y-gap="12">
|
||||||
<n-spin :show="aiStore.loading.heatmap">
|
<n-gi>
|
||||||
<n-grid :cols="2" :x-gap="12" :y-gap="12">
|
<n-statistic label="当前连续" :value="currentStreak">
|
||||||
<n-gi>
|
<template #suffix>
|
||||||
<n-statistic label="当前连续" :value="currentStreak">
|
<span style="font-size: 14px">天</span>
|
||||||
<template #suffix>
|
<span
|
||||||
<span style="font-size: 14px">天</span>
|
v-if="currentStreak > 0"
|
||||||
<span v-if="currentStreak > 0" style="font-size: 20px; margin-left: 4px">
|
style="font-size: 20px; margin-left: 4px"
|
||||||
🔥
|
>
|
||||||
</span>
|
🔥
|
||||||
</template>
|
</span>
|
||||||
</n-statistic>
|
</template>
|
||||||
</n-gi>
|
</n-statistic>
|
||||||
<n-gi>
|
</n-gi>
|
||||||
<n-statistic label="最长连续" :value="maxStreak">
|
<n-gi>
|
||||||
<template #suffix>
|
<n-statistic label="最长连续" :value="maxStreak">
|
||||||
<span style="font-size: 14px">天</span>
|
<template #suffix>
|
||||||
<span v-if="maxStreak >= 7" style="font-size: 20px; margin-left: 4px">
|
<span style="font-size: 14px">天</span>
|
||||||
⭐
|
<span
|
||||||
</span>
|
v-if="maxStreak >= 7"
|
||||||
</template>
|
style="font-size: 20px; margin-left: 4px"
|
||||||
</n-statistic>
|
>
|
||||||
</n-gi>
|
⭐
|
||||||
<n-gi>
|
</span>
|
||||||
<n-statistic label="本周做题" :value="weekCount">
|
</template>
|
||||||
<template #suffix>
|
</n-statistic>
|
||||||
<span style="font-size: 14px">天</span>
|
</n-gi>
|
||||||
</template>
|
<n-gi>
|
||||||
</n-statistic>
|
<n-statistic label="本周做题" :value="weekCount">
|
||||||
</n-gi>
|
<template #suffix>
|
||||||
<n-gi>
|
<span style="font-size: 14px">天</span>
|
||||||
<n-statistic label="本月做题" :value="monthCount">
|
</template>
|
||||||
<template #suffix>
|
</n-statistic>
|
||||||
<span style="font-size: 14px">天</span>
|
</n-gi>
|
||||||
</template>
|
<n-gi>
|
||||||
</n-statistic>
|
<n-statistic label="本月做题" :value="monthCount">
|
||||||
</n-gi>
|
<template #suffix>
|
||||||
</n-grid>
|
<span style="font-size: 14px">天</span>
|
||||||
<n-divider style="margin: 12px 0" />
|
</template>
|
||||||
<n-flex vertical size="small">
|
</n-statistic>
|
||||||
<n-text depth="2" style="font-size: 12px">
|
</n-gi>
|
||||||
<span v-if="currentStreak === 0">
|
</n-grid>
|
||||||
开始做题,建立学习连续记录!
|
<n-divider style="margin: 12px 0" />
|
||||||
</span>
|
<n-flex vertical size="small">
|
||||||
<span v-else-if="currentStreak < 3">
|
<n-text depth="2" style="font-size: 12px">
|
||||||
继续保持,争取连续3天!
|
<span v-if="currentStreak === 0"> 开始做题,建立学习连续记录! </span>
|
||||||
</span>
|
<span v-else-if="currentStreak < 3"> 继续保持,争取连续3天! </span>
|
||||||
<span v-else-if="currentStreak < 7">
|
<span v-else-if="currentStreak < 7">
|
||||||
很棒!继续保持一周连续记录!
|
很棒!继续保持一周连续记录!
|
||||||
</span>
|
</span>
|
||||||
<span v-else-if="currentStreak < 30">
|
<span v-else-if="currentStreak < 30">
|
||||||
太棒了!坚持满30天将获得「持之以恒」成就!
|
太棒了!坚持满30天将获得「持之以恒」成就!
|
||||||
</span>
|
</span>
|
||||||
<span v-else>
|
<span v-else>
|
||||||
🎉 恭喜你!你已经连续学习 {{ currentStreak }} 天,真的非常厉害!
|
🎉 恭喜你!你已经连续学习 {{ currentStreak }} 天,真的非常厉害!
|
||||||
</span>
|
</span>
|
||||||
</n-text>
|
</n-text>
|
||||||
</n-flex>
|
</n-flex>
|
||||||
</n-spin>
|
</n-spin>
|
||||||
</n-card>
|
</n-card>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { useAIStore } from "oj/store/ai"
|
import { useAIStore } from "oj/store/ai"
|
||||||
|
|
||||||
const aiStore = useAIStore()
|
const aiStore = useAIStore()
|
||||||
|
|
||||||
// 计算连续天数
|
// 计算连续天数
|
||||||
const streakData = computed(() => {
|
const streakData = computed(() => {
|
||||||
const heatmap = aiStore.heatmapData
|
const heatmap = aiStore.heatmapData
|
||||||
if (!heatmap || heatmap.length === 0) {
|
if (!heatmap || heatmap.length === 0) {
|
||||||
return {
|
return {
|
||||||
currentStreak: 0,
|
currentStreak: 0,
|
||||||
maxStreak: 0,
|
maxStreak: 0,
|
||||||
weekCount: 0,
|
weekCount: 0,
|
||||||
monthCount: 0,
|
monthCount: 0,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 按时间戳排序
|
// 按时间戳排序
|
||||||
const sortedData = [...heatmap].sort((a, b) => a.timestamp - b.timestamp)
|
const sortedData = [...heatmap].sort((a, b) => a.timestamp - b.timestamp)
|
||||||
|
|
||||||
let currentStreak = 0
|
let currentStreak = 0
|
||||||
let maxStreak = 0
|
let maxStreak = 0
|
||||||
let tempStreak = 0
|
let tempStreak = 0
|
||||||
let lastDate: Date | null = null
|
let lastDate: Date | null = null
|
||||||
|
|
||||||
const now = new Date()
|
const now = new Date()
|
||||||
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
const today = new Date(now.getFullYear(), now.getMonth(), now.getDate())
|
||||||
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
|
const weekAgo = new Date(today.getTime() - 7 * 24 * 60 * 60 * 1000)
|
||||||
const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
|
const monthAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000)
|
||||||
|
|
||||||
let weekCount = 0
|
let weekCount = 0
|
||||||
let monthCount = 0
|
let monthCount = 0
|
||||||
|
|
||||||
// 检查今天是否有做题
|
// 检查今天是否有做题
|
||||||
const todayData = sortedData.find((item) => {
|
const todayData = sortedData.find((item) => {
|
||||||
const itemDate = new Date(item.timestamp)
|
const itemDate = new Date(item.timestamp)
|
||||||
return (
|
return (
|
||||||
itemDate.getFullYear() === today.getFullYear() &&
|
itemDate.getFullYear() === today.getFullYear() &&
|
||||||
itemDate.getMonth() === today.getMonth() &&
|
itemDate.getMonth() === today.getMonth() &&
|
||||||
itemDate.getDate() === today.getDate()
|
itemDate.getDate() === today.getDate()
|
||||||
)
|
)
|
||||||
})
|
})
|
||||||
const hasToday = todayData && todayData.value > 0
|
const hasToday = todayData && todayData.value > 0
|
||||||
|
|
||||||
// 遍历数据计算连续天数
|
// 遍历数据计算连续天数
|
||||||
for (const item of sortedData) {
|
for (const item of sortedData) {
|
||||||
if (item.value > 0) {
|
if (item.value > 0) {
|
||||||
const currentDate = new Date(item.timestamp)
|
const currentDate = new Date(item.timestamp)
|
||||||
|
|
||||||
// 统计本周和本月
|
// 统计本周和本月
|
||||||
if (currentDate >= weekAgo) {
|
if (currentDate >= weekAgo) {
|
||||||
weekCount++
|
weekCount++
|
||||||
}
|
}
|
||||||
if (currentDate >= monthAgo) {
|
if (currentDate >= monthAgo) {
|
||||||
monthCount++
|
monthCount++
|
||||||
}
|
}
|
||||||
|
|
||||||
if (lastDate === null) {
|
if (lastDate === null) {
|
||||||
tempStreak = 1
|
tempStreak = 1
|
||||||
} else {
|
} else {
|
||||||
const dayDiff = Math.floor(
|
const dayDiff = Math.floor(
|
||||||
(currentDate.getTime() - lastDate.getTime()) / (24 * 60 * 60 * 1000),
|
(currentDate.getTime() - lastDate.getTime()) / (24 * 60 * 60 * 1000),
|
||||||
)
|
)
|
||||||
if (dayDiff === 1) {
|
if (dayDiff === 1) {
|
||||||
tempStreak++
|
tempStreak++
|
||||||
} else {
|
} else {
|
||||||
maxStreak = Math.max(maxStreak, tempStreak)
|
maxStreak = Math.max(maxStreak, tempStreak)
|
||||||
tempStreak = 1
|
tempStreak = 1
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lastDate = currentDate
|
lastDate = currentDate
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
maxStreak = Math.max(maxStreak, tempStreak)
|
maxStreak = Math.max(maxStreak, tempStreak)
|
||||||
|
|
||||||
// 计算当前连续天数(必须包含今天或昨天)
|
// 计算当前连续天数(必须包含今天或昨天)
|
||||||
if (lastDate) {
|
if (lastDate) {
|
||||||
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
|
const yesterday = new Date(today.getTime() - 24 * 60 * 60 * 1000)
|
||||||
const lastDateOnly = new Date(
|
const lastDateOnly = new Date(
|
||||||
lastDate.getFullYear(),
|
lastDate.getFullYear(),
|
||||||
lastDate.getMonth(),
|
lastDate.getMonth(),
|
||||||
lastDate.getDate(),
|
lastDate.getDate(),
|
||||||
)
|
)
|
||||||
|
|
||||||
if (
|
if (
|
||||||
lastDateOnly.getTime() === today.getTime() ||
|
lastDateOnly.getTime() === today.getTime() ||
|
||||||
lastDateOnly.getTime() === yesterday.getTime()
|
lastDateOnly.getTime() === yesterday.getTime()
|
||||||
) {
|
) {
|
||||||
currentStreak = tempStreak
|
currentStreak = tempStreak
|
||||||
} else {
|
} else {
|
||||||
currentStreak = 0
|
currentStreak = 0
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
currentStreak,
|
currentStreak,
|
||||||
maxStreak,
|
maxStreak,
|
||||||
weekCount,
|
weekCount,
|
||||||
monthCount,
|
monthCount,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const currentStreak = computed(() => streakData.value.currentStreak)
|
const currentStreak = computed(() => streakData.value.currentStreak)
|
||||||
const maxStreak = computed(() => streakData.value.maxStreak)
|
const maxStreak = computed(() => streakData.value.maxStreak)
|
||||||
const weekCount = computed(() => streakData.value.weekCount)
|
const weekCount = computed(() => streakData.value.weekCount)
|
||||||
const monthCount = computed(() => streakData.value.monthCount)
|
const monthCount = computed(() => streakData.value.monthCount)
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card :title="title" size="small" v-if="show">
|
<n-card :title="title" size="small" v-if="show">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">可视化知识点覆盖面</n-text>
|
||||||
可视化知识点覆盖面
|
|
||||||
</n-text>
|
|
||||||
</template>
|
</template>
|
||||||
<div class="chart">
|
<div class="chart">
|
||||||
<Radar :data="data" :options="options" />
|
<Radar :data="data" :options="options" />
|
||||||
|
|||||||
@@ -1,153 +1,152 @@
|
|||||||
<template>
|
<template>
|
||||||
<n-card title="时间活跃度分析" size="small" v-if="show">
|
<n-card title="时间活跃度分析" size="small" v-if="show">
|
||||||
<template #header-extra>
|
<template #header-extra>
|
||||||
<n-text depth="3" style="font-size: 12px">
|
<n-text depth="3" style="font-size: 12px">发现最佳学习时段</n-text>
|
||||||
发现最佳学习时段
|
</template>
|
||||||
</n-text>
|
<div style="height: 300px">
|
||||||
</template>
|
<Bar :data="data" :options="options" />
|
||||||
<div style="height: 300px">
|
</div>
|
||||||
<Bar :data="data" :options="options" />
|
</n-card>
|
||||||
</div>
|
</template>
|
||||||
</n-card>
|
|
||||||
</template>
|
<script setup lang="ts">
|
||||||
|
import { Bar } from "vue-chartjs"
|
||||||
<script setup lang="ts">
|
import {
|
||||||
import { Bar } from "vue-chartjs"
|
Chart as ChartJS,
|
||||||
import {
|
CategoryScale,
|
||||||
Chart as ChartJS,
|
LinearScale,
|
||||||
CategoryScale,
|
BarElement,
|
||||||
LinearScale,
|
Title,
|
||||||
BarElement,
|
Tooltip,
|
||||||
Title,
|
Legend,
|
||||||
Tooltip,
|
} from "chart.js"
|
||||||
Legend,
|
import { useAIStore } from "oj/store/ai"
|
||||||
} from "chart.js"
|
|
||||||
import { useAIStore } from "oj/store/ai"
|
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
||||||
|
|
||||||
ChartJS.register(CategoryScale, LinearScale, BarElement, Title, Tooltip, Legend)
|
const aiStore = useAIStore()
|
||||||
|
|
||||||
const aiStore = useAIStore()
|
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
|
||||||
|
const TIME_PERIODS = [
|
||||||
const WEEKDAYS = ["周日", "周一", "周二", "周三", "周四", "周五", "周六"]
|
{ label: "凌晨(0-6)", start: 0, end: 6 },
|
||||||
const TIME_PERIODS = [
|
{ label: "上午(6-12)", start: 6, end: 12 },
|
||||||
{ label: "凌晨(0-6)", start: 0, end: 6 },
|
{ label: "下午(12-18)", start: 12, end: 18 },
|
||||||
{ label: "上午(6-12)", start: 6, end: 12 },
|
{ label: "晚上(18-24)", start: 18, end: 24 },
|
||||||
{ label: "下午(12-18)", start: 12, end: 18 },
|
]
|
||||||
{ label: "晚上(18-24)", start: 18, end: 24 },
|
|
||||||
]
|
// 统计每个星期几和时间段的做题数量
|
||||||
|
const activityMatrix = computed(() => {
|
||||||
// 统计每个星期几和时间段的做题数量
|
const matrix: { [weekday: number]: { [period: number]: number } } = {}
|
||||||
const activityMatrix = computed(() => {
|
|
||||||
const matrix: { [weekday: number]: { [period: number]: number } } = {}
|
// 初始化矩阵
|
||||||
|
for (let i = 0; i < 7; i++) {
|
||||||
// 初始化矩阵
|
matrix[i] = {}
|
||||||
for (let i = 0; i < 7; i++) {
|
for (let j = 0; j < TIME_PERIODS.length; j++) {
|
||||||
matrix[i] = {}
|
matrix[i][j] = 0
|
||||||
for (let j = 0; j < TIME_PERIODS.length; j++) {
|
}
|
||||||
matrix[i][j] = 0
|
}
|
||||||
}
|
|
||||||
}
|
// 统计数据
|
||||||
|
aiStore.detailsData.solved.forEach((item) => {
|
||||||
// 统计数据
|
const date = new Date(item.ac_time)
|
||||||
aiStore.detailsData.solved.forEach((item) => {
|
const weekday = date.getDay() // 0-6,0是周日
|
||||||
const date = new Date(item.ac_time)
|
const hour = date.getHours() // 0-23
|
||||||
const weekday = date.getDay() // 0-6,0是周日
|
|
||||||
const hour = date.getHours() // 0-23
|
// 找到对应的时间段
|
||||||
|
const periodIndex = TIME_PERIODS.findIndex(
|
||||||
// 找到对应的时间段
|
(p) => hour >= p.start && hour < p.end,
|
||||||
const periodIndex = TIME_PERIODS.findIndex(
|
)
|
||||||
(p) => hour >= p.start && hour < p.end,
|
if (periodIndex !== -1) {
|
||||||
)
|
matrix[weekday][periodIndex]++
|
||||||
if (periodIndex !== -1) {
|
}
|
||||||
matrix[weekday][periodIndex]++
|
})
|
||||||
}
|
|
||||||
})
|
return matrix
|
||||||
|
})
|
||||||
return matrix
|
|
||||||
})
|
const show = computed(() => {
|
||||||
|
return aiStore.detailsData.solved.length > 0
|
||||||
const show = computed(() => {
|
})
|
||||||
return aiStore.detailsData.solved.length > 0
|
|
||||||
})
|
// 为每个时间段准备数据集
|
||||||
|
const data = computed(() => {
|
||||||
// 为每个时间段准备数据集
|
const datasets = TIME_PERIODS.map((period, periodIndex) => {
|
||||||
const data = computed(() => {
|
return {
|
||||||
const datasets = TIME_PERIODS.map((period, periodIndex) => {
|
label: period.label,
|
||||||
return {
|
data: WEEKDAYS.map(
|
||||||
label: period.label,
|
(_, weekday) => activityMatrix.value[weekday][periodIndex],
|
||||||
data: WEEKDAYS.map((_, weekday) => activityMatrix.value[weekday][periodIndex]),
|
),
|
||||||
backgroundColor: getTimePeriodColor(periodIndex),
|
backgroundColor: getTimePeriodColor(periodIndex),
|
||||||
borderColor: getTimePeriodColor(periodIndex),
|
borderColor: getTimePeriodColor(periodIndex),
|
||||||
borderWidth: 1,
|
borderWidth: 1,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
return {
|
return {
|
||||||
labels: WEEKDAYS,
|
labels: WEEKDAYS,
|
||||||
datasets,
|
datasets,
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 根据时间段返回对应的颜色
|
// 根据时间段返回对应的颜色
|
||||||
function getTimePeriodColor(periodIndex: number): string {
|
function getTimePeriodColor(periodIndex: number): string {
|
||||||
const colors = [
|
const colors = [
|
||||||
"#9D9D9D", // 凌晨 - 灰色
|
"#9D9D9D", // 凌晨 - 灰色
|
||||||
"#FFD700", // 上午 - 金色
|
"#FFD700", // 上午 - 金色
|
||||||
"#4ECDC4", // 下午 - 青色
|
"#4ECDC4", // 下午 - 青色
|
||||||
"#5B5F97", // 晚上 - 深蓝紫
|
"#5B5F97", // 晚上 - 深蓝紫
|
||||||
]
|
]
|
||||||
return colors[periodIndex] || "#999"
|
return colors[periodIndex] || "#999"
|
||||||
}
|
}
|
||||||
|
|
||||||
const options = {
|
const options = {
|
||||||
responsive: true,
|
responsive: true,
|
||||||
maintainAspectRatio: false,
|
maintainAspectRatio: false,
|
||||||
interaction: {
|
interaction: {
|
||||||
intersect: false,
|
intersect: false,
|
||||||
mode: "index" as const,
|
mode: "index" as const,
|
||||||
},
|
},
|
||||||
scales: {
|
scales: {
|
||||||
x: {
|
x: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
grid: {
|
grid: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
y: {
|
y: {
|
||||||
stacked: true,
|
stacked: true,
|
||||||
ticks: {
|
ticks: {
|
||||||
stepSize: 1,
|
stepSize: 1,
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: true,
|
display: true,
|
||||||
text: "完成题目数",
|
text: "完成题目数",
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
plugins: {
|
plugins: {
|
||||||
legend: {
|
legend: {
|
||||||
display: true,
|
display: true,
|
||||||
position: "bottom" as const,
|
position: "bottom" as const,
|
||||||
labels: {
|
labels: {
|
||||||
boxWidth: 12,
|
boxWidth: 12,
|
||||||
padding: 8,
|
padding: 8,
|
||||||
font: {
|
font: {
|
||||||
size: 11,
|
size: 11,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
title: {
|
title: {
|
||||||
display: false,
|
display: false,
|
||||||
},
|
},
|
||||||
tooltip: {
|
tooltip: {
|
||||||
callbacks: {
|
callbacks: {
|
||||||
footer: (items: any[]) => {
|
footer: (items: any[]) => {
|
||||||
const total = items.reduce((sum, item) => sum + item.parsed.y, 0)
|
const total = items.reduce((sum, item) => sum + item.parsed.y, 0)
|
||||||
return `当天总计: ${total} 题`
|
return `当天总计: ${total} 题`
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user