Files
OJ2/apps/web/src/oj/problem/components/ProblemChips.vue
T
xuyueandClaude Opus 5.5 38a331c2b7
Deploy / deploy (push) Canceled after 0s
feat(布置作业): 能布置「画流程图」「先画再写」,看板分清卡在画图还是写代码
布置时多一个「做什么」:写代码 / 先画再写 / 画流程图(只能选打开了画流程图的题)。
- 先画再写:一道题图画到 A/S、代码也交对才算做完;学生画一道写一道、全画完再写都行,
  打开题目自动停在该做的那一步(图没画对停画布,画对了换到布置的语言)
- 也能分开布置:先一节画流程图,看板上「用这几道题写代码」再开一节;学生那边把今天
  几节课的要求合起来,还没画完的照样看得到要画
- 看板格子是一条进度:图 1/6 → 空心「图 ✓」→ 代码 ✗ → ✓,先写代码没画图的写「缺图」;
  卡在画图上的点开讲那张图(要改的框标红 + AI 的哪里错/怎么改),按走对几组归堆
- 写代码的课不再拿画对的流程图顶替做完;数据统计里画流程图的课数流程图提交

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 21:50:48 +08:00

199 lines
5.7 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { useThemeVars } from 'naive-ui'
import type { RouteLocationRaw } from 'vue-router'
/**
* 上下文条里的一排题号(课堂、比赛共用)。只放题号,题目名在鼠标停上去时给。
*
* 状态用图标区分,不只靠颜色:✓ 做完(实心绿)/ 做过没对(课堂是橙色 !,比赛是红色 ✕)/
* 灰底 没做;当前题描边,做完了的当前题实心绿再套一圈。课堂是圆角胶囊、比赛是方角
* (设计稿「上下文条与协作状态」)。一节课一般 5 道以内,一行放得下;多了就横向滚动,
* 当前题保持在视野里。
*/
export interface ChipItem {
displayId: string
title: string
status: 'done' | 'tried' | 'none'
to: RouteLocationRaw
/** 悬停时代替「做过,还没对」那句(课堂里要画又要写的题,说差哪一步) */
note?: string
}
const props = defineProps<{
items: ChipItem[]
current: string
variant: 'lesson' | 'contest'
}>()
const theme = useThemeVars()
/** 「做完」的实心绿上用什么字色:暗色主题的成功色是浅薄荷绿,白字几乎看不见 */
const isDark = useDark()
const doneText = computed(() => (isDark.value ? 'rgba(0, 0, 0, 0.85)' : '#fff'))
const STATUS_TEXT = computed(
() =>
({
done: '做完了',
tried: props.variant === 'contest' ? '交过,还没对' : '做过,还没对',
none: '还没做',
}) as const,
)
const isCurrent = (item: ChipItem) => item.displayId.toLowerCase() === props.current.toLowerCase()
const scroller = useTemplateRef<HTMLElement>('scroller')
// 换题之后把当前那一颗滚进来。`nearest`:本来就看得见就不动,免得一换题整排跳一下
watch(
() => [props.current, props.items.length],
async () => {
await nextTick()
scroller.value?.querySelector<HTMLElement>('.chip.current')?.scrollIntoView({ block: 'nearest', inline: 'nearest' })
},
{ immediate: true },
)
</script>
<template>
<!-- 类名带前缀:父组件(ContextBar)的 .lesson / .contest 会作用到子组件根元素上 -->
<div ref="scroller" class="chips" :class="`chips-${variant}`">
<n-tooltip v-for="item in items" :key="item.displayId" :delay="300">
<template #trigger>
<router-link
:to="item.to"
class="chip"
:class="[item.status, { current: isCurrent(item) }]"
:aria-label="`${item.displayId} ${item.title}(${item.note ?? STATUS_TEXT[item.status]})`"
:aria-current="isCurrent(item) ? 'page' : undefined"
>
<!-- 内嵌 SVG,不走 Iconify:图标源拉不到时状态就只剩颜色了 -->
<svg
v-if="item.status === 'done'"
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
aria-hidden="true"
>
<path d="M5 12l5 5 9-10" />
</svg>
<svg
v-else-if="item.status === 'tried' && variant === 'contest'"
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M6 6l12 12M18 6L6 18" />
</svg>
<svg
v-else-if="item.status === 'tried'"
width="11"
height="11"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="3"
stroke-linecap="round"
aria-hidden="true"
>
<path d="M12 7v6" />
<path d="M12 17h.01" />
</svg>
<span>{{ item.displayId }}</span>
</router-link>
</template>
{{ item.displayId }} {{ item.title }} · {{ item.note ?? STATUS_TEXT[item.status] }}
</n-tooltip>
</div>
</template>
<style scoped>
.chips {
display: flex;
align-items: center;
gap: 6px;
flex: 0 1 auto;
min-width: 0;
overflow-x: auto;
scrollbar-width: none;
/* 当前题外面那一圈别被裁掉 */
padding: 4px 3px;
}
.chips::-webkit-scrollbar {
display: none;
}
.chip {
flex: none;
display: inline-flex;
align-items: center;
gap: 3px;
height: 26px;
box-sizing: border-box;
padding: 0 8px;
border-radius: 13px;
border: 1.5px solid transparent;
font-size: 13px;
font-variant-numeric: tabular-nums;
text-decoration: none;
color: v-bind('theme.textColor2');
background-color: rgba(128, 128, 128, 0.14);
}
.chips-contest .chip {
border-radius: 4px;
}
.chip.done {
color: v-bind(doneText);
background-color: v-bind('theme.successColor');
}
.chip.tried {
color: v-bind('theme.warningColorPressed');
background-color: rgba(240, 160, 32, 0.12);
border: 1px solid rgba(240, 160, 32, 0.45);
}
.chips-contest .chip.tried {
color: v-bind('theme.errorColorPressed');
background-color: rgba(208, 48, 80, 0.12);
border-color: transparent;
}
.chip.current:not(.done) {
color: v-bind('theme.primaryColorPressed');
background-color: v-bind('theme.cardColor');
border: 1.5px solid v-bind('theme.primaryColor');
font-weight: 700;
}
.chips-contest .chip.current:not(.done) {
color: v-bind('theme.warningColorPressed');
border-color: v-bind('theme.warningColor');
}
/* 当前题做完了:实心绿,外面再套一圈(中间隔一道底色) */
.chip.done.current {
font-weight: 700;
border-color: v-bind('theme.primaryColorPressed');
box-shadow:
0 0 0 2px v-bind('theme.cardColor'),
0 0 0 3.5px v-bind('theme.primaryColorPressed');
}
.chip:hover {
filter: brightness(0.95);
}
</style>