feat(相似题): 做对中等、困难题后不再只推一串简单题,每道推荐写出和原题共同的标签
- 共同标签一样多时先给难度最接近的,再并列才先简单的(原来一律简单的在前, 做对困难题时五条里三条多是简单题) - 每行第二行写共同的标签(知识点在前、主题在后)和做对人数;带类型标签、 做过没对的红「−」,写法照题目列表 - 题面里的标题和「提示」「来源」一样,结果页签里的和「还能挑战」对齐 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
da360e2f68
commit
8c5399755e
6 files changed
+194
-43
No files matched your search
@@ -41,6 +41,7 @@ import {
|
||||
type ProblemRank,
|
||||
type CreateSubmissionResponse,
|
||||
type ProblemListItem,
|
||||
type SimilarProblem,
|
||||
type ProblemStats,
|
||||
type ProblemProgress,
|
||||
type CreateFlowchartResponse,
|
||||
@@ -412,7 +413,9 @@ export function submitHintFeedback(hintId: number, helpful: boolean) {
|
||||
// ==================== 相似题目推荐 ====================
|
||||
|
||||
export function getSimilarProblems(problemId: string) {
|
||||
return api.get<ProblemListItem[]>(`problems/${encodeURIComponent(problemId)}/similar`).then((response) => response.map(toProblemRow))
|
||||
return api
|
||||
.get<SimilarProblem[]>(`problems/${encodeURIComponent(problemId)}/similar`)
|
||||
.then((response) => response.map((item) => ({ ...toProblemRow(item), sharedTags: item.sharedTags })))
|
||||
}
|
||||
|
||||
// ==================== 挑战 ====================
|
||||
|
||||
@@ -333,7 +333,7 @@ const astRequirements = computed(() => Object.entries(problem.value?.astRequirem
|
||||
<Markdown html :content="problem.source" />
|
||||
</section>
|
||||
|
||||
<SimilarProblems v-if="showSimilar" title="相似题目推荐" />
|
||||
<SimilarProblems v-if="showSimilar" title="相似题目推荐" place="content" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
<script lang="ts">
|
||||
import type { ProblemRow } from 'utils/types'
|
||||
|
||||
type SimilarRow = ProblemRow & { sharedTags: string[] }
|
||||
|
||||
/**
|
||||
* 同一道题两处(题面末尾、结果页签)都挂着时只拉一次。放在模块级:写在 setup 里就成了
|
||||
* 每个实例各一份
|
||||
*/
|
||||
const cache = new Map<string, Promise<ProblemRow[]>>()
|
||||
const cache = new Map<string, Promise<SimilarRow[]>>()
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
@@ -13,17 +15,26 @@ import { storeToRefs } from 'pinia'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { getSimilarProblems } from 'oj/api'
|
||||
import { useProblemStore } from 'oj/store/problem'
|
||||
import { useTone } from 'shared/composables/tone'
|
||||
import { problemTypes } from '../utils/problemType'
|
||||
import ProblemStatus from './ProblemStatus.vue'
|
||||
import ProblemTypeTag from './ProblemTypeTag.vue'
|
||||
|
||||
/**
|
||||
* 相似题推荐。题面末尾(做对了、或者错了 3 次以上)和结果页签(不在课堂里的题做对之后)
|
||||
* 两处用:原来只在题面最底下,学生交完不会再滚回去看。什么时候出现由调用方的 v-if 决定。
|
||||
* `place` 只管标题的样子:题面里和「提示」「来源」一样带绿竖条,结果页签里和「还能挑战」对齐。
|
||||
*
|
||||
* 每行第二行写和这道题共同的标签:不点开就知道练的是什么。行的写法照题目列表
|
||||
* (灰题号、淡底难度、做对人数、类型标签、做过没对的红「−」)
|
||||
*/
|
||||
const props = defineProps<{ title: string }>()
|
||||
const props = defineProps<{ title: string; place: 'content' | 'result' }>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
const tone = useTone()
|
||||
|
||||
const { problem } = storeToRefs(useProblemStore())
|
||||
const list = ref<ProblemRow[]>([])
|
||||
const list = ref<SimilarRow[]>([])
|
||||
|
||||
watch(
|
||||
() => problem.value?._id,
|
||||
@@ -41,30 +52,36 @@ watch(
|
||||
{ immediate: true },
|
||||
)
|
||||
|
||||
// getSimilarProblems 已经过 toProblemRow,难度是中文,不是 Low/Mid/High
|
||||
function difficultyType(difficulty: string) {
|
||||
if (difficulty === '简单') return 'success'
|
||||
if (difficulty === '困难') return 'error'
|
||||
return 'warning'
|
||||
// 做对了的接口已经筛掉,剩下的只有「没做过」和「做过没对」两种;一道都没做过就不留这一列
|
||||
const showStatus = computed(() => list.value.some((row) => row.status === 'failed'))
|
||||
|
||||
const DIFFICULTY_TONE = { 简单: 'success', 中等: 'warning', 困难: 'error' } as const
|
||||
|
||||
function difficultyStyle(row: SimilarRow) {
|
||||
if (!row.difficulty) return {}
|
||||
const t = tone(DIFFICULTY_TONE[row.difficulty])
|
||||
return { color: t.color, background: t.background }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<section v-if="list.length" class="similar">
|
||||
<h3 class="title">{{ props.title }}</h3>
|
||||
<n-list bordered>
|
||||
<n-list-item v-for="sp in list" :key="sp._id">
|
||||
<n-flex align="center" justify="space-between" :wrap="false">
|
||||
<router-link :to="{ name: 'problem', params: { problemID: sp._id } }" class="link">
|
||||
<n-tag size="small" :bordered="false">{{ sp._id }}</n-tag>
|
||||
<span class="name">{{ sp.title }}</span>
|
||||
</router-link>
|
||||
<n-tag v-if="sp.difficulty" size="small" :type="difficultyType(sp.difficulty)">
|
||||
{{ sp.difficulty }}
|
||||
</n-tag>
|
||||
</n-flex>
|
||||
</n-list-item>
|
||||
</n-list>
|
||||
<section v-if="list.length" class="similar" :class="props.place">
|
||||
<h3 class="heading">{{ props.title }}</h3>
|
||||
<div class="rows">
|
||||
<router-link v-for="sp in list" :key="sp._id" :to="{ name: 'problem', params: { problemID: sp._id } }" class="row" :class="{ 'with-status': showStatus }">
|
||||
<span v-if="showStatus" class="status"><ProblemStatus :status="sp.status" /></span>
|
||||
<span class="id">{{ sp._id }}</span>
|
||||
<span class="name">
|
||||
<span class="text">{{ sp.title }}</span>
|
||||
<ProblemTypeTag v-for="kind in problemTypes(sp)" :key="kind" :kind="kind" />
|
||||
</span>
|
||||
<span v-if="sp.difficulty" class="difficulty" :style="difficultyStyle(sp)">{{ sp.difficulty }}</span>
|
||||
<span class="shared">{{ sp.sharedTags.join(' · ') }}</span>
|
||||
<span class="people"
|
||||
><b>{{ sp.solvedUsers }}</b> 人做对</span
|
||||
>
|
||||
</router-link>
|
||||
</div>
|
||||
</section>
|
||||
</template>
|
||||
|
||||
@@ -73,34 +90,135 @@ function difficultyType(difficulty: string) {
|
||||
margin-top: 20px;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
.similar.result {
|
||||
margin-top: 14px;
|
||||
}
|
||||
|
||||
.heading {
|
||||
margin: 0 0 8px;
|
||||
}
|
||||
|
||||
/* 列表项的内容区默认按内容撑宽,长标题会把难度挤出屏幕,省略号也不出来 */
|
||||
.similar :deep(.n-list-item__main) {
|
||||
min-width: 0;
|
||||
/* 和 ProblemContent 的小节标题(提示 / 来源)一样 */
|
||||
.content .heading {
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
line-height: 1.4;
|
||||
padding-left: 9px;
|
||||
border-left: 3px solid v-bind('theme.primaryColor');
|
||||
}
|
||||
|
||||
.link {
|
||||
display: inline-flex;
|
||||
/* 和结果页签里的「还能挑战」一样 */
|
||||
.result .heading {
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.rows {
|
||||
border: 1px solid v-bind('theme.dividerColor');
|
||||
border-radius: 6px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
/*
|
||||
* 两行:题号 | 题目名 + 类型标签 | 难度
|
||||
* | 共同的标签 | 做对人数
|
||||
* 做过没对的题最前面多一列红「−」
|
||||
*/
|
||||
.row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
grid-template-areas:
|
||||
'id name difficulty'
|
||||
'. shared people';
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-width: 0;
|
||||
color: inherit;
|
||||
gap: 4px 12px;
|
||||
padding: 10px 16px;
|
||||
color: v-bind('theme.textColor1');
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
.link:hover .name {
|
||||
.row.with-status {
|
||||
grid-template-columns: 16px auto minmax(0, 1fr) auto;
|
||||
grid-template-areas:
|
||||
'status id name difficulty'
|
||||
'. . shared people';
|
||||
}
|
||||
|
||||
.row + .row {
|
||||
border-top: 1px solid v-bind('theme.dividerColor');
|
||||
}
|
||||
|
||||
.row:hover {
|
||||
background: v-bind('theme.hoverColor');
|
||||
}
|
||||
|
||||
.row:hover .text {
|
||||
text-decoration: underline;
|
||||
}
|
||||
|
||||
.status {
|
||||
grid-area: status;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.id {
|
||||
grid-area: id;
|
||||
min-width: 36px;
|
||||
font-size: 14px;
|
||||
color: v-bind('theme.textColor3');
|
||||
font-variant-numeric: tabular-nums;
|
||||
}
|
||||
|
||||
.name {
|
||||
grid-area: name;
|
||||
min-width: 0;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.text {
|
||||
min-width: 0;
|
||||
font-size: 15px;
|
||||
color: v-bind('theme.primaryColor');
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.difficulty {
|
||||
grid-area: difficulty;
|
||||
justify-self: end;
|
||||
height: 22px;
|
||||
padding: 0 7px;
|
||||
border-radius: 4px;
|
||||
font-size: 13px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.shared {
|
||||
grid-area: shared;
|
||||
min-width: 0;
|
||||
font-size: 13px;
|
||||
color: v-bind('theme.textColor3');
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.people {
|
||||
grid-area: people;
|
||||
justify-self: end;
|
||||
font-size: 13px;
|
||||
color: v-bind('theme.textColor3');
|
||||
font-variant-numeric: tabular-nums;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.people b {
|
||||
font-weight: 600;
|
||||
color: v-bind('theme.textColor2');
|
||||
}
|
||||
</style>
|
||||
@@ -357,7 +357,7 @@ const columns: DataTableColumn<JudgeCaseResult>[] = [
|
||||
:language="submission.language"
|
||||
:refresh-key="submission.id"
|
||||
/>
|
||||
<SimilarProblems v-if="showSimilar" title="再练几道相似的" />
|
||||
<SimilarProblems v-if="showSimilar" title="再练几道相似的" place="result" />
|
||||
</div>
|
||||
</template>
|
||||
<div v-show="!waitingReveal">
|
||||
|
||||
Reference in new issue
Block a user