Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
a718394284
commit
b3f5ed41ae
24 files changed
+56
-58
No files matched your search
@@ -20,7 +20,7 @@ const Markdown = defineAsyncComponent(() => import("shared/components/Markdown.v
|
||||
* 两行摘要),点哪份右边就是全文。原来是表格 + 弹框,看一份要点「查看」、关框、再点下一份。
|
||||
*
|
||||
* 「PIN」(固定让这个学生在前台看这一份)线上 697 份里一次都没用过,从每行的按钮和页头那条
|
||||
* 锁定名单,收成正文右上角一个开关「固定给他看这份」。后端是切换语义:钉一份会顺手把这个学生
|
||||
* 锁定名单,收成正文右上角一个开关「固定给学生看这份」。后端是切换语义:钉一份会顺手把这个学生
|
||||
* 别的那份取消,所以切完重拉列表
|
||||
*/
|
||||
const theme = useThemeVars()
|
||||
@@ -154,7 +154,7 @@ watchDebounced(
|
||||
</span>
|
||||
<span class="spacer"></span>
|
||||
<label class="pin">
|
||||
固定给他看这份
|
||||
固定给学生看这份
|
||||
<n-switch :value="selected.isPinned" size="small" @update:value="togglePin" />
|
||||
</label>
|
||||
</div>
|
||||
|
||||
@@ -152,7 +152,7 @@ function goStudent(username: string | null | undefined) {
|
||||
|
||||
<template v-else-if="report && !report.days.length">
|
||||
<section class="card blank">
|
||||
<b>{{ LEARNING_PERIOD_LABELS[period] }}{{ viewing ? "他" : "" }}还没交过题</b>
|
||||
<b>{{ LEARNING_PERIOD_LABELS[period] }}还没交过题</b>
|
||||
<span v-if="emptyHint" class="muted">{{ emptyHint }}</span>
|
||||
<div class="actions">
|
||||
<n-button v-for="[key, label] in longer" :key="key" @click="urlPeriod = key">
|
||||
|
||||
@@ -100,10 +100,10 @@ const folded = computed(() => props.compact && !expanded.value && !generating.va
|
||||
</div>
|
||||
</template>
|
||||
<n-spin v-else-if="waiting" size="small">
|
||||
<div class="waiting">正在看{{ username ? "他" : "你" }}的做题记录…</div>
|
||||
<div class="waiting">正在看{{ username ? "" : "你的" }}做题记录…</div>
|
||||
</n-spin>
|
||||
<div v-else class="empty">
|
||||
<p>让 AI 按这些数据,写一段{{ username ? "给他" : "给你" }}的小结和下一步建议</p>
|
||||
<p>让 AI 按这些数据,写一段{{ username ? "" : "给你的" }}小结和下一步建议</p>
|
||||
<n-button type="primary" @click="generate">
|
||||
<template #icon><Icon icon="ph:sparkle" /></template>
|
||||
AI 帮我分析
|
||||
|
||||
@@ -32,7 +32,7 @@ watch(
|
||||
)
|
||||
|
||||
const shown = computed(() => (showAll.value ? props.days : props.days.slice(0, DAY_CAP)))
|
||||
const you = computed(() => (props.username ? "他" : "你"))
|
||||
const you = computed(() => (props.username ? "" : "你"))
|
||||
const subtitle = computed(() =>
|
||||
props.className
|
||||
? `${classLabel(props.className)}课上做的题,加上${you.value}自己练的`
|
||||
|
||||
@@ -12,12 +12,12 @@ const props = defineProps<{
|
||||
item: LearningItem
|
||||
/** 课上的题才说「班上第几个做对」;自己练的题同学大多没做,排第几没意义 */
|
||||
inLesson: boolean
|
||||
/** 老师在看这个学生:称呼用「他」,点进去看他的提交 */
|
||||
/** 老师在看这个学生:不称呼(分不出男女),点进去看这个学生的提交 */
|
||||
username?: string
|
||||
}>()
|
||||
|
||||
const theme = useThemeVars()
|
||||
const you = computed(() => (props.username ? "他" : "你"))
|
||||
const you = computed(() => (props.username ? "" : "你"))
|
||||
|
||||
const problemLink = computed(() => `/problem/${props.item.displayId}`)
|
||||
const submissionLink = computed(() => ({
|
||||
@@ -67,7 +67,7 @@ const detail = computed(() => {
|
||||
<div class="tip">
|
||||
<span>{{ detail.filter(Boolean).join(" · ") }}</span>
|
||||
<router-link v-if="item.status !== 'none'" :to="submissionLink"
|
||||
>看{{ you }}的提交 ›</router-link
|
||||
>{{ username ? "看提交" : "看你的提交" }} ›</router-link
|
||||
>
|
||||
<router-link v-else-if="!username" :to="problemLink">去做 ›</router-link>
|
||||
</div>
|
||||
|
||||
@@ -680,7 +680,7 @@ function lastText(student: ClassBoardStudent) {
|
||||
size="tiny"
|
||||
@click="help(student.userId)"
|
||||
>
|
||||
帮他 · {{ helpOf(student)!.problemId }}
|
||||
去帮 · {{ helpOf(student)!.problemId }}
|
||||
</n-button>
|
||||
<span v-else-if="helpOf(student)" class="helping">
|
||||
{{ helpOf(student)!.teacherName }} 在帮
|
||||
|
||||
@@ -35,7 +35,7 @@ const waited = computed(() => {
|
||||
const next = computed(() => collabStore.nextRequest)
|
||||
|
||||
/**
|
||||
* 「看他交过什么」:只看这个学生、这道题。开新标签 —— 跳走这一页协作就断了。
|
||||
* 「看学生交过什么」:只看这个学生、这道题。开新标签 —— 跳走这一页协作就断了。
|
||||
* exactUsername:整名匹配,不然学号相近的同学(ks24a1 / ks24a10…)会混进来
|
||||
*/
|
||||
function openHisSubmissions() {
|
||||
@@ -81,7 +81,7 @@ function finishAndNext() {
|
||||
</span>
|
||||
<span v-if="!peek && waited" class="note">举手 {{ waited }}</span>
|
||||
<span class="spacer" />
|
||||
<button type="button" class="ghost" @click="openHisSubmissions">看他交过什么</button>
|
||||
<button type="button" class="ghost" @click="openHisSubmissions">看学生交过什么</button>
|
||||
<!-- 显式的「结束」:求助记录一并清掉。跳走页面发的是 leave("left"),
|
||||
那边只是退回排队 —— 见 store 里 leave 的注释 -->
|
||||
<button type="button" :class="next ? 'ghost' : 'end'" @click="finish">
|
||||
|
||||
@@ -58,9 +58,9 @@ watch([peer, () => problem.value?._id], load, { immediate: true })
|
||||
<n-button size="small" :loading="loading" @click="load">刷新</n-button>
|
||||
</n-flex>
|
||||
|
||||
<n-text v-if="failed" type="error">读不出他的提交,点「刷新」再试</n-text>
|
||||
<n-text v-if="failed" type="error">读不出提交,点「刷新」再试</n-text>
|
||||
<SubmissionResult v-else-if="latest" :submission="latest" peer />
|
||||
<n-empty v-else-if="checked" class="empty" description="他还没交过这道题" />
|
||||
<n-empty v-else-if="checked" class="empty" description="还没交过这道题" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -71,7 +71,7 @@ watch(
|
||||
},
|
||||
)
|
||||
|
||||
// 协作中的老师看的是学生的提交(ProblemSubmission 里按学生用户名查),页签写「他的提交」
|
||||
// 协作中的老师看的是学生的提交(ProblemSubmission 里按学生用户名查),页签写「学生的提交」
|
||||
const teacherCollab = useTeacherCollab()
|
||||
|
||||
/**
|
||||
@@ -127,7 +127,7 @@ function goAllSubmissions() {
|
||||
:aria-selected="lastOpened === key"
|
||||
@click="open = key"
|
||||
>
|
||||
{{ key === "submission" && teacherCollab ? "他的提交" : DRAWER_TITLE[key] }}
|
||||
{{ key === "submission" && teacherCollab ? "学生的提交" : DRAWER_TITLE[key] }}
|
||||
</button>
|
||||
</div>
|
||||
<button type="button" class="drawer-close" aria-label="关闭" @click="open = null">
|
||||
|
||||
@@ -256,7 +256,7 @@ watch(query, () => {
|
||||
<n-empty
|
||||
v-if="listed && !submissions.length"
|
||||
class="empty"
|
||||
:description="peer ? '他还没有交过这道题' : '这道题你还没有交过'"
|
||||
:description="peer ? '还没有交过这道题' : '这道题你还没有交过'"
|
||||
/>
|
||||
|
||||
<!-- 设计稿「我的提交 / 统计 / 点评:抽屉盖住左栏」:一整块带框的列表,行间一条细线 -->
|
||||
|
||||
@@ -63,10 +63,10 @@ const hint = computed(() => {
|
||||
if (props.check.passed) return ""
|
||||
if (!kind.value) {
|
||||
const name = JUDGE_STATUS[props.check.result as keyof typeof JUDGE_STATUS]?.name ?? "出错了"
|
||||
return `${props.peer ? "他" : "你"}的程序在这个例子上就「${name}」了,先让它在例子上跑通。`
|
||||
return `${props.peer ? "学生" : "你"}的程序在这个例子上就「${name}」了,先让它在例子上跑通。`
|
||||
}
|
||||
const text = explainDiff(kind.value, props.check, props.code, props.language)
|
||||
return props.peer ? text.replace(/你/g, "他") : text
|
||||
return props.peer ? text.replace(/你/g, "学生") : text
|
||||
})
|
||||
const diffLine = computed(() =>
|
||||
firstDifferentLine(props.check.expected ?? "", props.check.output ?? ""),
|
||||
@@ -161,7 +161,7 @@ function lines(text: string) {
|
||||
</div>
|
||||
</div>
|
||||
<div class="block">
|
||||
<div class="label">{{ peer ? "他" : "你" }}的输出</div>
|
||||
<div class="label">{{ peer ? "学生" : "你" }}的输出</div>
|
||||
<div v-if="check.output" class="text">
|
||||
<template v-for="(line, i) in lines(check.output)" :key="i">
|
||||
<div v-if="i === diffLine && diffParts?.output" class="line">
|
||||
|
||||
@@ -125,7 +125,7 @@ watch(
|
||||
},
|
||||
)
|
||||
const drawerLabel = (key: DrawerKey) =>
|
||||
key === "submission" && teacherCollab.value ? "他的提交" : DRAWER_TITLE[key]
|
||||
key === "submission" && teacherCollab.value ? "学生的提交" : DRAWER_TITLE[key]
|
||||
|
||||
function toggleDrawer(key: DrawerKey) {
|
||||
drawer.value = drawer.value === key ? null : key
|
||||
|
||||
@@ -113,7 +113,7 @@ function go(path: string, query: Record<string, string>) {
|
||||
<span v-if="detail.gradeAvg !== null">年级平均 {{ detail.gradeAvg }}</span>
|
||||
</div>
|
||||
<div class="big">
|
||||
<span>中间那位同学做对</span><b>{{ detail.median }} 道</b><span>一半人比他多</span>
|
||||
<span>中间那位同学做对</span><b>{{ detail.median }} 道</b><span>一半人比这多</span>
|
||||
</div>
|
||||
<div class="big">
|
||||
<span>做对过题的</span>
|
||||
@@ -205,7 +205,7 @@ function go(path: string, query: Record<string, string>) {
|
||||
@click.prevent="
|
||||
go('/submission', { username: row.user.username, exactUsername: '1' })
|
||||
"
|
||||
>看他的提交 ›</a
|
||||
>看提交 ›</a
|
||||
>
|
||||
<a href="#" @click.prevent="go('/ai-analysis', { username: row.user.username })"
|
||||
>智能分析 ›</a
|
||||
|
||||
@@ -83,8 +83,8 @@ const HEIGHTS: Record<number, number> = { 1: 56, 2: 44, 3: 34 }
|
||||
<button
|
||||
v-if="teacher"
|
||||
class="clear"
|
||||
aria-label="清空他的签名"
|
||||
title="清空他的签名"
|
||||
aria-label="清空签名"
|
||||
title="清空签名"
|
||||
@click="$emit('clearMood', row)"
|
||||
>
|
||||
×
|
||||
|
||||
@@ -171,9 +171,9 @@ function width(row: RankRow) {
|
||||
|
||||
function menuOf(row: RankRow): DropdownOption[] {
|
||||
return [
|
||||
{ label: "看他的提交", key: "submissions" },
|
||||
{ label: "看他的智能分析", key: "analysis" },
|
||||
...(row.mood ? [{ label: "清空他的签名…", key: "mood" }] : []),
|
||||
{ label: "看提交", key: "submissions" },
|
||||
{ label: "看智能分析", key: "analysis" },
|
||||
...(row.mood ? [{ label: "清空签名…", key: "mood" }] : []),
|
||||
{ label: "不计入排名…", key: "hide" },
|
||||
]
|
||||
}
|
||||
|
||||
@@ -319,7 +319,7 @@ function openSubmissions(username: string) {
|
||||
function hide(row: RankRow) {
|
||||
dialog.warning({
|
||||
title: "不计入排名",
|
||||
content: `${row.user.username} 会从所有排名里消失(本班、本年级、全服、首页周榜),他自己会看到「不计入排名」。比赛排名不受影响,随时能恢复。`,
|
||||
content: `${row.user.username} 会从所有排名里消失(本班、本年级、全服、首页周榜),本人会看到「不计入排名」。比赛排名不受影响,随时能恢复。`,
|
||||
positiveText: "不计入排名",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
@@ -347,7 +347,7 @@ function onMoodSaved(mood: string | null) {
|
||||
function confirmClearMood(row: RankRow) {
|
||||
dialog.warning({
|
||||
title: "清空个性签名",
|
||||
content: `${row.user.username} 的签名「${row.mood}」会被清空。只清这一句,他之后还能重新写。`,
|
||||
content: `${row.user.username} 的签名「${row.mood}」会被清空。只清这一句,之后还能重新写。`,
|
||||
positiveText: "清空",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: async () => {
|
||||
|
||||
@@ -197,8 +197,8 @@ const canRetry = computed(
|
||||
</a>
|
||||
<button
|
||||
class="filter-pill"
|
||||
title="只看他的提交"
|
||||
aria-label="只看他的提交"
|
||||
title="只看这个人的提交"
|
||||
aria-label="只看这个人的提交"
|
||||
@click="emit('filterUser', row.username)"
|
||||
>
|
||||
<Icon icon="ph:funnel-simple-bold" :width="12" />
|
||||
|
||||
@@ -147,8 +147,8 @@ function openStandalone() {
|
||||
</a>
|
||||
<button
|
||||
class="filter-pill"
|
||||
title="只看他的提交"
|
||||
aria-label="只看他的提交"
|
||||
title="只看这个人的提交"
|
||||
aria-label="只看这个人的提交"
|
||||
@click="emit('filterUser', row.username)"
|
||||
>
|
||||
<Icon icon="ph:funnel-simple-bold" :width="12" />
|
||||
@@ -226,7 +226,7 @@ function openStandalone() {
|
||||
v-if="shown"
|
||||
size="small"
|
||||
quaternary
|
||||
title="新页面打开(看他这道题交的每一次)"
|
||||
title="新页面打开(看这道题交的每一次)"
|
||||
aria-label="新页面打开"
|
||||
@click="openStandalone"
|
||||
>
|
||||
|
||||
@@ -106,11 +106,11 @@ onBeforeUnmount(() => clearTimeout(timer))
|
||||
const shown = computed(() => (state.value === "ok" ? detail.value : null))
|
||||
const own = computed(() => !!shown.value && shown.value.userId === userStore.user?.id)
|
||||
const teacher = computed(() => userStore.isTeacherOrAbove)
|
||||
const who = computed(() => (own.value ? "你" : "他"))
|
||||
const who = computed(() => (own.value ? "你" : ""))
|
||||
|
||||
const { explain, explainTone } = useSubmissionExplain(shown)
|
||||
|
||||
/** 答案错误、后端在例子上对比过:摆出输入 / 正确输出 / 他的输出(和题目页结果区同一块) */
|
||||
/** 答案错误、后端在例子上对比过:摆出输入 / 正确输出 / 学生的输出(和题目页结果区同一块) */
|
||||
const sampleCheck = computed(() => {
|
||||
const s = shown.value
|
||||
if (!s?.showLink || s.result !== SubmissionStatus.wrong_answer) return null
|
||||
@@ -211,7 +211,7 @@ const moreOptions = computed(() => {
|
||||
]
|
||||
: []
|
||||
// 手机上名字、班级和这个按钮排不进一行,收进「⋯」
|
||||
const all = !own.value && isMobile.value ? [{ label: "他的全部提交", key: "all" }] : []
|
||||
const all = !own.value && isMobile.value ? [{ label: "学生的全部提交", key: "all" }] : []
|
||||
if (!canRejudge.value) return [...all, ...copy]
|
||||
return [
|
||||
...all,
|
||||
@@ -281,7 +281,7 @@ function handleMore(key: string) {
|
||||
<div class="actions">
|
||||
<template v-if="teacher">
|
||||
<n-button v-if="!own && !isMobile" tag="a" :href="allSubmissionsLink">
|
||||
他的全部提交
|
||||
学生的全部提交
|
||||
</n-button>
|
||||
<n-dropdown
|
||||
v-if="moreOptions.length"
|
||||
|
||||
@@ -63,7 +63,7 @@ function hint(tag: KnowledgeLevel) {
|
||||
<div v-if="!learned.length && !untouched.length" class="meta empty">还没做对带知识点的题</div>
|
||||
|
||||
<div v-if="untouched.length" class="untouched">
|
||||
<span class="meta">班里在学、{{ teacher ? "他" : "你" }}还没碰过</span>
|
||||
<span class="meta">班里在学、{{ teacher ? "" : "你" }}还没碰过</span>
|
||||
<div class="tags">
|
||||
<router-link v-for="tag in untouched" :key="tag.name" :to="tagLink(tag.name)" class="tag">
|
||||
{{ tag.name }}
|
||||
|
||||
@@ -20,7 +20,7 @@ const props = defineProps<{
|
||||
profile: Profile
|
||||
overview: ProfileOverview
|
||||
isSelf: boolean
|
||||
/** 老师在看学生:三个数的最后一格换成「交过还没做对」,按钮换成看他的… */
|
||||
/** 老师在看学生:三个数的最后一格换成「交过还没做对」,按钮换成看学生的… */
|
||||
teacherView: boolean
|
||||
compact?: boolean
|
||||
}>()
|
||||
@@ -148,7 +148,7 @@ function onMenu(key: string) {
|
||||
} else if (key === "mood") {
|
||||
dialog.warning({
|
||||
title: "清空个性签名",
|
||||
content: `「${props.profile.mood}」会被清掉,他之后还能重新写。`,
|
||||
content: `「${props.profile.mood}」会被清掉,之后还能重新写。`,
|
||||
positiveText: "清空",
|
||||
negativeText: "取消",
|
||||
onPositiveClick: () =>
|
||||
@@ -230,7 +230,7 @@ function onMenu(key: string) {
|
||||
type="primary"
|
||||
@click="router.push({ name: 'ai', query: { username } })"
|
||||
>
|
||||
<template #icon><Icon icon="ph:sparkle" /></template>看他的智能分析
|
||||
<template #icon><Icon icon="ph:sparkle" /></template>智能分析
|
||||
</n-button>
|
||||
<n-button
|
||||
size="small"
|
||||
@@ -238,7 +238,7 @@ function onMenu(key: string) {
|
||||
router.push({ path: '/submission', query: { username, exactUsername: '1' } })
|
||||
"
|
||||
>
|
||||
<template #icon><Icon icon="ph:list-checks" /></template>看他的提交
|
||||
<template #icon><Icon icon="ph:list-checks" /></template>提交记录
|
||||
</n-button>
|
||||
<n-dropdown v-if="menu.length" :options="menu" trigger="click" @select="onMenu">
|
||||
<n-button size="small" aria-label="更多">
|
||||
@@ -283,12 +283,12 @@ function onMenu(key: string) {
|
||||
</template>
|
||||
<template v-else>
|
||||
<n-button type="primary" @click="router.push({ name: 'ai', query: { username } })">
|
||||
看他的智能分析
|
||||
智能分析
|
||||
</n-button>
|
||||
<n-button
|
||||
@click="router.push({ path: '/submission', query: { username, exactUsername: '1' } })"
|
||||
>
|
||||
看他的提交
|
||||
提交记录
|
||||
</n-button>
|
||||
</template>
|
||||
</div>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
import type { ProfileOverview } from "@oj2/contract"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
|
||||
/** 同学看他的主页时的「你和他」:两根条比做对几道,下面一句你们重合多少、你在本班第几 */
|
||||
/** 同学看别人的主页时的「和你比」:两根条比做对几道,下面一句你们重合多少、你在本班第几 */
|
||||
const props = defineProps<{
|
||||
versus: NonNullable<ProfileOverview["versus"]>
|
||||
/** 他做对几道 */
|
||||
@@ -19,8 +19,8 @@ const line = computed(() => {
|
||||
if (solved)
|
||||
parts.push(
|
||||
both === solved
|
||||
? `你做对的 ${solved} 道他都做对了`
|
||||
: `你做对的 ${solved} 道里他做对了 ${both} 道`,
|
||||
? `你做对的 ${solved} 道对方都做对了`
|
||||
: `你做对的 ${solved} 道里对方做对了 ${both} 道`,
|
||||
)
|
||||
if (classRank) parts.push(`你在本班第 ${classRank.rank} 名`)
|
||||
return parts.join(" · ")
|
||||
@@ -29,9 +29,9 @@ const line = computed(() => {
|
||||
|
||||
<template>
|
||||
<section class="card">
|
||||
<h3>你和他</h3>
|
||||
<h3>和你比</h3>
|
||||
<div class="bar-row">
|
||||
<span class="who">他</span>
|
||||
<span class="who">对方</span>
|
||||
<span class="lane"
|
||||
><span class="bar his" :style="{ width: `${(theirs / top) * 100}%` }"></span
|
||||
></span>
|
||||
|
||||
@@ -49,8 +49,6 @@ async function load() {
|
||||
}
|
||||
|
||||
watch(username, load, { immediate: true })
|
||||
|
||||
const solvedTitle = computed(() => (isSelf.value || teacherView.value ? "做对的题" : "他做对的题"))
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -76,7 +74,7 @@ const solvedTitle = computed(() => (isSelf.value || teacherView.value ? "做对
|
||||
<SolvedCard
|
||||
:solved="overview.solved"
|
||||
:unsolved="overview.unsolved"
|
||||
:title="solvedTitle"
|
||||
title="做对的题"
|
||||
:username="teacherView ? profile.user.username : undefined"
|
||||
/>
|
||||
<div class="column">
|
||||
@@ -109,7 +107,7 @@ const solvedTitle = computed(() => (isSelf.value || teacherView.value ? "做对
|
||||
<SolvedCard
|
||||
:solved="overview.solved"
|
||||
:unsolved="overview.unsolved"
|
||||
:title="solvedTitle"
|
||||
title="做对的题"
|
||||
:username="teacherView ? profile.user.username : undefined"
|
||||
compact
|
||||
/>
|
||||
|
||||
@@ -98,14 +98,14 @@ function explain(items: CollabRequestItem[], problemId: string) {
|
||||
等了 {{ waited(item.createdAt) }}
|
||||
</span>
|
||||
<n-button v-if="isDesktop" type="primary" size="small" @click="onHelp(item)">
|
||||
帮他
|
||||
去帮
|
||||
</n-button>
|
||||
<n-button
|
||||
quaternary
|
||||
circle
|
||||
size="small"
|
||||
aria-label="放下他的手"
|
||||
title="放下他的手"
|
||||
aria-label="放下举手"
|
||||
title="放下举手"
|
||||
@click="collabStore.reject(item.studentId)"
|
||||
>
|
||||
<Icon icon="mdi:close" height="16" />
|
||||
|
||||
Reference in new issue
Block a user