Deploy / deploy (push) Canceled after 0s
布置时多一个「做什么」:写代码 / 先画再写 / 画流程图(只能选打开了画流程图的题)。 - 先画再写:一道题图画到 A/S、代码也交对才算做完;学生画一道写一道、全画完再写都行, 打开题目自动停在该做的那一步(图没画对停画布,画对了换到布置的语言) - 也能分开布置:先一节画流程图,看板上「用这几道题写代码」再开一节;学生那边把今天 几节课的要求合起来,还没画完的照样看得到要画 - 看板格子是一条进度:图 1/6 → 空心「图 ✓」→ 代码 ✗ → ✓,先写代码没画图的写「缺图」; 卡在画图上的点开讲那张图(要改的框标红 + AI 的哪里错/怎么改),按走对几组归堆 - 写代码的课不再拿画对的流程图顶替做完;数据统计里画流程图的课数流程图提交 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
199 lines
5.7 KiB
Vue
199 lines
5.7 KiB
Vue
<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>
|