原来 8 张图挤在两列大网格里,左列还套了一层两列小网格 —— 四张小图各自只有半页的 一半宽,"难度掌握情况"的标题被挤断成两行、周期图的 x 轴标签斜着叠在一起、一年热力图 53 列塞进半宽几乎看不清。改成单列为主,宽的图给全宽,窄的图两两并排。 砍掉的重复:进步曲线 / 提交效率 / 周期综合读的是同一个 durationData,半年视图统共 6 个桶四个字段,摊成三张卡六条曲线还都是双轴。合并成一张:柱是完成题目数和总提交数, 线是 AC 率,等级进 tooltip(S/A/B/C 四档离散值连成折线读不出东西,逐题等级表格里有)。 同期解题排名分布也砍了:五片扇形对十几道题做统计本来就是噪声,而排名逐题列在 SolvedTable 里,饼图没有增加任何信息。 换形式: - 标签雷达图 → 横向条形。雷达对比较大小是最差的形式之一,原来还把值归一化成"占最多 标签的百分比",第一名恒为 100%,等于只画了个排序。现在画真实题数。 - 难度掌握情况 → 难度分布。去掉叠在里面的 S/A/B/C 维度,3×4 十二个格子对一个两个月 做十来道题的学生大部分恒为 0。 热力图改成一格一周(53 格,周一起算,最后一格是本周)。按天切的话一年 365 格里三百多 格是空的,中职学生一年也就二三十天有提交,整张图看着像没用过。颜色阈值跟着按周重定。 新增三个维度: - 错在哪里:判完的失败提交按状态码分组。编译错误占大头说明语法不熟,答案错误占大头 说明是逻辑问题,两种情况老师该给的建议完全不同。 - 几次做对:到首次通过为止提交了几次,分一次过 / 2-3 / 4-6 / 7次以上。原来只有一个 "平均提交次数",看不出分布。 - 流程图得分:detailsData.flowcharts 早就在下发,但全页一张图都没有,只在解题表格的 第二个 tab 里列着。 顺带: - 时间活跃度从"只统计 AC 时间"改成"统计全部提交",星期和时段由后端按东八区聚合。 只看 AC 的话十来个点撒进 7×4 的格子几乎全是空的,和热力图的时区口径也对不上。 - 两两并排用弹性容器而不是固定两列网格:知识点分布在没有标签时整张卡不渲染, 固定两列会空掉一半。 - AI 卡片原来有三个条件挂载点(solved>10 在右列、≤10 在全宽行、=0 藏在 Overview 里面),单列之后收成最后一张无条件的卡。 - DurationChart 右轴的 S/A/B/C 一个刻度都没显示过:轴范围 -0.5~3.5,生成的刻度值是 -0.5/0.5/1.5/2.5/3.5,拿去索引 gradeOrder 全是 undefined。 契约新增 activity / errors / rankScope / solved[].attempts / durationData[].acceptedCount。 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01LZuPwqDmLEiK9zgQ9z9sVn
94 lines
2.1 KiB
Vue
94 lines
2.1 KiB
Vue
<template>
|
|
<n-card size="small">
|
|
<template #header>
|
|
<div class="cool-title">
|
|
<span class="title-text">AI 帮你分析</span>
|
|
</div>
|
|
</template>
|
|
<n-spin :show="aiStore.loading.ai" :delay="50">
|
|
<n-flex align="center" justify="center" class="container">
|
|
<n-button
|
|
v-if="!aiStore.mdContent && !aiStore.loading.ai"
|
|
type="primary"
|
|
size="large"
|
|
:loading="aiStore.loading.fetching"
|
|
@click="handleAnalyze"
|
|
>
|
|
<template #icon>
|
|
<Icon icon="ph:sparkle" />
|
|
</template>
|
|
开始分析
|
|
</n-button>
|
|
<MdPreview v-else :model-value="aiStore.mdContent" />
|
|
</n-flex>
|
|
</n-spin>
|
|
</n-card>
|
|
</template>
|
|
<script setup lang="ts">
|
|
import { useAIStore } from "oj/store/ai"
|
|
import { MdPreview } from "md-editor-v3"
|
|
import "md-editor-v3/lib/preview.css"
|
|
import { Icon } from "@iconify/vue"
|
|
|
|
const aiStore = useAIStore()
|
|
|
|
async function handleAnalyze() {
|
|
if (aiStore.loading.fetching || aiStore.loading.ai) {
|
|
return
|
|
}
|
|
if (aiStore.pinnedReport) {
|
|
await aiStore.simulatePinnedStream()
|
|
} else {
|
|
await aiStore.fetchAIAnalysis()
|
|
}
|
|
}
|
|
|
|
onMounted(async () => {
|
|
if (!aiStore.targetUsername) {
|
|
await aiStore.fetchPinnedReport()
|
|
}
|
|
})
|
|
</script>
|
|
<style scoped>
|
|
.cool-title {
|
|
position: relative;
|
|
padding: 8px 0;
|
|
}
|
|
|
|
.title-text {
|
|
font-size: 16px;
|
|
font-weight: 700;
|
|
background: linear-gradient(45deg, #667eea, #764ba2, #f093fb);
|
|
background-size: 200% 200%;
|
|
-webkit-background-clip: text;
|
|
background-clip: text;
|
|
-webkit-text-fill-color: transparent;
|
|
letter-spacing: 0.8px;
|
|
position: relative;
|
|
z-index: 2;
|
|
animation: gradient-flow 3s ease infinite;
|
|
}
|
|
|
|
@keyframes gradient-flow {
|
|
0% {
|
|
background-position: 0% 50%;
|
|
}
|
|
50% {
|
|
background-position: 100% 50%;
|
|
}
|
|
100% {
|
|
background-position: 0% 50%;
|
|
}
|
|
}
|
|
|
|
/* 只按钮那一屏的时候别留一大片空白;出内容后高度由内容自己撑,
|
|
这里只是给加载中的 spin 一点地方 */
|
|
.container {
|
|
min-height: 120px;
|
|
}
|
|
|
|
:deep(.md-editor-preview h1) {
|
|
margin-top: 0;
|
|
}
|
|
</style>
|