fix(文案): 不再用「他」称呼学生,男生女生看着都别扭;老师看主页的按钮改成「智能分析」「提交记录」
Deploy / deploy (push) Canceled after 0s

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
xuyueandClaude Opus 5.5 committed 2026-10-22 10:41:24 +08:00
1 parent a718394284
commit b3f5ed41ae
24 files changed
+56 -58

No files matched your search

+2 -2
View File
@@ -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>
+1 -1
View File
@@ -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">
+2 -2
View File
@@ -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 帮我分析
+1 -1
View File
@@ -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>
+1 -1
View File
@@ -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">
+1 -1
View File
@@ -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" },
]
}
+2 -2
View File
@@ -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>
+2 -4
View File
@@ -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" />