- 学生名字一律只显示名字(老号 ks232张三 显示成 张三),顶栏、首页问候、悬停提示、请假名单、比赛成绩表都一样 - 全服 / 年级排名、全部提交、几个班同时举手时,名字后面淡写班级;提交列表的名字列窄,只写班号(陈长 258) - AI 班级分析点名「要多关心的同学」时不再带 ks 前缀 - 建号查重补漏:同班同名也比真名(加过数字的张三2、编号制老号);名字自己带数字(王芳1)也认得出被占; 导入时后端再拦一次同班同名,建号页要等查重结果对上当前名单才能点 - 登录名单换成新接口 /classes/:c/members(名字按界面规则剥,换过班的老号不再露出全名), 旧接口原样留给上线时还开着旧登录页的机房电脑 - 后台手写老格式 ks232张三 新建用户、没选班时按前缀补班级;PUT /me/profile 只带真名时不再 500 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
221d264701
commit
2bda05a978
32 files changed
+229
-111
No files matched your search
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import type { ContestClassView, ContestScoreCell, ContestScoreRow } from '@oj2/contract'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { updateACMHelperChecked } from 'admin/api'
|
||||
@@ -225,12 +226,13 @@ onKeyStroke(['ArrowLeft', 'ArrowRight', 'ArrowUp', 'ArrowDown'], (event) => {
|
||||
|
||||
function exportGrades() {
|
||||
const title = contestStore.contest?.title ?? '比赛'
|
||||
const head = ['用户名', '班级', '做对', '名次', '用时', ...problems.value.map((p) => `第${p._id}题 ${p.title}`)]
|
||||
const head = ['名字', '班级', '做对', '名次', '用时', ...problems.value.map((p) => `第${p._id}题 ${p.title}`)]
|
||||
const lines = [head]
|
||||
const byUsername = [...rows.value].sort((a, b) => a.username.localeCompare(b.username, 'zh-CN'))
|
||||
for (const row of byUsername) {
|
||||
// 按名字的拼音排:老号 ks 开头、新号是中文名,按用户名排两种号会各排各的
|
||||
const byName = [...rows.value].sort((a, b) => displayName(a.username).localeCompare(displayName(b.username), 'zh-CN'))
|
||||
for (const row of byName) {
|
||||
lines.push([
|
||||
row.username,
|
||||
displayName(row.username),
|
||||
row.className ? classLabel(row.className) : '',
|
||||
String(row.solved),
|
||||
String(row.rank ?? ''),
|
||||
@@ -242,7 +244,14 @@ function exportGrades() {
|
||||
])
|
||||
}
|
||||
for (const user of absent.value) {
|
||||
lines.push([user.username, user.className ? classLabel(user.className) : '', user.leave ? '请假' : '没考', '', '', ...problems.value.map(() => '')])
|
||||
lines.push([
|
||||
displayName(user.username),
|
||||
user.className ? classLabel(user.className) : '',
|
||||
user.leave ? '请假' : '没考',
|
||||
'',
|
||||
'',
|
||||
...problems.value.map(() => ''),
|
||||
])
|
||||
}
|
||||
const csv = lines.map((line) => line.map((value) => `"${value.replaceAll('"', '""')}"`).join(',')).join('\r\n')
|
||||
// 带 BOM,Excel 双击打开中文才不乱码
|
||||
@@ -430,7 +439,7 @@ const gradeColumns = computed(() => `24px 124px 40px 36px repeat(${problems.valu
|
||||
no: cellOf(row, p.id) && !cellOf(row, p.id)!.isAc,
|
||||
sel: selected?.userId === row.userId && selected.problemId === p.id,
|
||||
}"
|
||||
:title="`${row.username} · 第 ${p._id} 题`"
|
||||
:title="`${displayName(row.username)} · 第 ${p._id} 题`"
|
||||
@click="pick(row, p.id)"
|
||||
>
|
||||
<template v-if="cellOf(row, p.id)?.isAc">✓</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import type { ContestScoreRow } from '@oj2/contract'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { useContestStore } from 'oj/store/contest'
|
||||
@@ -131,7 +132,7 @@ const warning = computed(() => tone('warning'))
|
||||
<span class="count-num">{{ item.solved }} 人做对</span>
|
||||
</span>
|
||||
<span class="first">
|
||||
<template v-if="item.first">最先:{{ item.first.username }} · {{ firstMinute(item.first.acTime) }}</template>
|
||||
<template v-if="item.first">最先:{{ displayName(item.first.username) }} · {{ firstMinute(item.first.acTime) }}</template>
|
||||
<template v-else>还没人做对</template>
|
||||
</span>
|
||||
</template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import { continueStep, doneCount, tutorialPath, splitTitle, type TutorialType } from 'oj/learn/utils'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { getAnnouncementList, getKnowledgeMap, getContestList, getLastVisit, getLearnProgress, getSubmissions, getTutorials, getWeeklyRank } from 'oj/api'
|
||||
@@ -276,8 +277,8 @@ function contestTime(contest: Contest) {
|
||||
<div class="home">
|
||||
<!-- 中间原来还有一个「题号或题目名 找题」框,和顶栏右上角那个重复,拿掉了 -->
|
||||
<section class="hero">
|
||||
<h1 v-if="firstVisit" class="hello">欢迎来到判题狗,{{ userStore.user?.username }}</h1>
|
||||
<h1 v-else class="hello">{{ greeting }},{{ userStore.user?.username }}</h1>
|
||||
<h1 v-if="firstVisit" class="hello">欢迎来到判题狗,{{ displayName(userStore.user?.username ?? '') }}</h1>
|
||||
<h1 v-else class="hello">{{ greeting }},{{ displayName(userStore.user?.username ?? '') }}</h1>
|
||||
<n-text v-if="firstVisit" depth="3">老师报了题号,在右上角的框里输入就能找到题</n-text>
|
||||
<n-text v-else depth="3">
|
||||
<template v-if="longAway">{{ awayText }} · </template>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import type { ClassToday } from '@oj2/contract'
|
||||
import type { AnnouncementListItem } from 'utils/types'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
@@ -78,7 +79,7 @@ const shortcuts = computed(() =>
|
||||
<template>
|
||||
<div class="home">
|
||||
<section class="hero">
|
||||
<h1 class="hello">{{ greeting }},{{ userStore.user?.username }}</h1>
|
||||
<h1 class="hello">{{ greeting }},{{ displayName(userStore.user?.username ?? '') }}</h1>
|
||||
<n-text v-if="today" depth="3" class="tagline">
|
||||
{{ today.classes.length ? `今天 ${today.classes.length} 个班上过课` : '今天还没有班在上课' }}
|
||||
</n-text>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import { storeToRefs } from 'pinia'
|
||||
import { useProblemStore } from 'oj/store/problem'
|
||||
import { useCollabStore } from 'shared/store/collab'
|
||||
@@ -85,7 +86,7 @@ function finishAndNext() {
|
||||
<button type="button" :class="next ? 'ghost' : 'end'" @click="finish">
|
||||
{{ peek ? '看完了' : '帮完了' }}
|
||||
</button>
|
||||
<button v-if="next" type="button" class="end" @click="finishAndNext">下一个:{{ next.studentName }} {{ next.problemId }} →</button>
|
||||
<button v-if="next" type="button" class="end" @click="finishAndNext">下一个:{{ displayName(next.studentName) }} {{ next.problemId }} →</button>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { getProblemSetClassView } from 'oj/api'
|
||||
import { useTone } from 'shared/composables/tone'
|
||||
@@ -84,8 +85,8 @@ const error = computed(() => tone('error'))
|
||||
><span class="muted">做完</span> <b class="num big">{{ finished }}</b
|
||||
><span class="muted"> 人</span></span
|
||||
>
|
||||
<span v-if="absent.length" class="absent ell" :title="absent.map((s) => s.username).join('、')"
|
||||
>没加入:{{ absent.map((s) => s.username).join('、') }}</span
|
||||
<span v-if="absent.length" class="absent ell" :title="absent.map((s) => displayName(s.username)).join('、')"
|
||||
>没加入:{{ absent.map((s) => displayName(s.username)).join('、') }}</span
|
||||
>
|
||||
<div class="spacer"></div>
|
||||
<span v-if="beforeCount" class="legend-before"><span class="corner"></span>加入前就做对过({{ beforeCount }} 格)</span>
|
||||
@@ -116,7 +117,7 @@ const error = computed(() => tone('error'))
|
||||
target="_blank"
|
||||
class="box"
|
||||
:class="{ ok: c.solvedTime, bad: !c.solvedTime && c.wrongCount }"
|
||||
:title="`${s.username} · ${data.problems[i]!.title}${c.solvedBefore ? ' · 加入前就做对过' : ''}`"
|
||||
:title="`${displayName(s.username)} · ${data.problems[i]!.title}${c.solvedBefore ? ' · 加入前就做对过' : ''}`"
|
||||
>
|
||||
<template v-if="c.solvedTime"
|
||||
>✓ <span class="num">{{ cellTime(c.solvedTime) }}</span></template
|
||||
|
||||
@@ -40,6 +40,8 @@ const props = defineProps<{
|
||||
minis: MiniRank[]
|
||||
/** 手机:前后一名的说明换到名字下面一行,别被截断 */
|
||||
compact?: boolean
|
||||
/** 本年级 / 全服的榜混着好几个班:名字后面淡写班级(本班榜不写) */
|
||||
showClass?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{ moodSaved: [mood: string | null] }>()
|
||||
|
||||
@@ -153,7 +155,7 @@ function threatNote(me: RankRow, behind: RankRow) {
|
||||
<RankAvatar :username="ahead.user.username" :avatar="ahead.avatar" :size="20" />
|
||||
<div class="rival-body">
|
||||
<div class="rival-line">
|
||||
<UserName :username="ahead.user.username" class="rival-name" />
|
||||
<UserName :username="ahead.user.username" :class-name="showClass ? ahead.className : null" class="rival-name" />
|
||||
<span class="note">{{ chaseNote(me, ahead) }}</span>
|
||||
</div>
|
||||
<span v-if="ahead.mood" class="rival-mood" :title="ahead.mood">“{{ ahead.mood }}”</span>
|
||||
@@ -164,7 +166,7 @@ function threatNote(me: RankRow, behind: RankRow) {
|
||||
<RankAvatar :username="behind.user.username" :avatar="behind.avatar" :size="20" />
|
||||
<div class="rival-body">
|
||||
<div class="rival-line">
|
||||
<UserName :username="behind.user.username" class="rival-name" />
|
||||
<UserName :username="behind.user.username" :class-name="showClass ? behind.className : null" class="rival-name" />
|
||||
<span class="note">{{ threatNote(me, behind) }}</span>
|
||||
</div>
|
||||
<span v-if="behind.mood" class="rival-mood" :title="behind.mood">“{{ behind.mood }}”</span>
|
||||
|
||||
@@ -19,6 +19,8 @@ const props = defineProps<{
|
||||
chaseId?: number
|
||||
threatId?: number
|
||||
teacher?: boolean
|
||||
/** 本年级 / 全服的榜混着好几个班:名字后面淡写班级(本班榜不写) */
|
||||
showClass?: boolean
|
||||
}>()
|
||||
defineEmits<{ open: [username: string]; clearMood: [row: RankRow] }>()
|
||||
|
||||
@@ -59,7 +61,7 @@ const HEIGHTS: Record<number, number> = { 1: 56, 2: 44, 3: 34 }
|
||||
<div class="words">
|
||||
<div class="name-line">
|
||||
<button class="name" @click="$emit('open', row.user.username)">
|
||||
<UserName :username="row.user.username" />
|
||||
<UserName :username="row.user.username" :class-name="showClass ? row.className : null" />
|
||||
</button>
|
||||
<span v-if="row.user.id === chaseId" class="tag chase">前一名</span>
|
||||
<span v-else-if="row.user.id === threatId" class="tag threat">后一名</span>
|
||||
|
||||
@@ -43,6 +43,8 @@ const props = defineProps<{
|
||||
expanding?: boolean
|
||||
/** 手机上只有末尾一段折起来时按钮上的字,比如「看全班 39 人」 */
|
||||
wholeLabel?: string
|
||||
/** 本年级 / 全服的榜混着好几个班:名字后面淡写班级(本班榜不写) */
|
||||
showClass?: boolean
|
||||
}>()
|
||||
const emit = defineEmits<{
|
||||
open: [username: string]
|
||||
@@ -188,7 +190,7 @@ function onMenu(key: string, row: RankRow) {
|
||||
<div class="pmain">
|
||||
<div class="pname">
|
||||
<button class="who" @click="emit('open', item.row.user.username)">
|
||||
<UserName :username="item.row.user.username" />
|
||||
<UserName :username="item.row.user.username" :class-name="showClass ? item.row.className : null" />
|
||||
</button>
|
||||
<span v-if="item.row.user.id === chaseId" class="tag chase">前一名</span>
|
||||
<span v-else-if="item.row.user.id === threatId" class="tag threat">后一名</span>
|
||||
@@ -211,7 +213,7 @@ function onMenu(key: string, row: RankRow) {
|
||||
<span class="rank">{{ item.row.solved ? item.row.rank : '—' }}</span>
|
||||
<RankAvatar :username="item.row.user.username" :avatar="item.row.avatar" :size="20" :me="item.row.user.id === meId" />
|
||||
<button class="who" @click="emit('open', item.row.user.username)">
|
||||
<UserName :username="item.row.user.username" />
|
||||
<UserName :username="item.row.user.username" :class-name="showClass ? item.row.className : null" />
|
||||
</button>
|
||||
<span class="bar-box" :style="{ background: palette.track }">
|
||||
<span class="bar" :style="{ width: width(item.row), background: barColor(item.row) }" />
|
||||
|
||||
@@ -287,7 +287,7 @@ function openSubmissions(username: string) {
|
||||
function hide(row: RankRow) {
|
||||
dialog.warning({
|
||||
title: '不计入排名',
|
||||
content: `${row.user.username} 会从所有排名里消失(本班、本年级、全服、首页周榜),本人会看到「不计入排名」。比赛排名不受影响,随时能恢复。`,
|
||||
content: `${displayName(row.user.username)} 会从所有排名里消失(本班、本年级、全服、首页周榜),本人会看到「不计入排名」。比赛排名不受影响,随时能恢复。`,
|
||||
positiveText: '不计入排名',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
@@ -314,7 +314,7 @@ function onMoodSaved(mood: string | null) {
|
||||
function confirmClearMood(row: RankRow) {
|
||||
dialog.warning({
|
||||
title: '清空个性签名',
|
||||
content: `${row.user.username} 的签名「${row.mood}」会被清空。只清这一句,之后还能重新写。`,
|
||||
content: `${displayName(row.user.username)} 的签名「${row.mood}」会被清空。只清这一句,之后还能重新写。`,
|
||||
positiveText: '清空',
|
||||
negativeText: '取消',
|
||||
onPositiveClick: async () => {
|
||||
@@ -369,6 +369,7 @@ async function restore(userId: number) {
|
||||
</div>
|
||||
<RankPodium
|
||||
:rows="podium"
|
||||
:show-class="board.scope !== 'class'"
|
||||
:me-id="meId"
|
||||
:chase-id="board.ahead?.user.id"
|
||||
:threat-id="board.behind?.user.id"
|
||||
@@ -377,6 +378,7 @@ async function restore(userId: number) {
|
||||
@clear-mood="confirmClearMood"
|
||||
/>
|
||||
<RankTrack
|
||||
:show-class="board.scope !== 'class'"
|
||||
:key="`${board.scope}|${board.period}|${board.className}`"
|
||||
:rows="trackRows"
|
||||
:whole-label="board.scope === 'class' ? `看全班 ${board.total} 人` : undefined"
|
||||
@@ -407,6 +409,7 @@ async function restore(userId: number) {
|
||||
<template v-if="board">
|
||||
<RankMe
|
||||
v-if="me"
|
||||
:show-class="board.scope !== 'class'"
|
||||
:me="me"
|
||||
:ahead="board.ahead"
|
||||
:behind="board.behind"
|
||||
|
||||
@@ -63,6 +63,8 @@ const narrowList = computed(() => isDesktop.value && splitWidth.value > 0 && spl
|
||||
const twoLine = computed(() => isMobile.value || narrowList.value)
|
||||
|
||||
const inContest = computed(() => route.name === 'contest submissions')
|
||||
/** 没按班筛、又不在比赛里:列表混着好几个班,名字后面淡写班级(同名的人靠它分开) */
|
||||
const mixedClasses = computed(() => !inContest.value && !query.className && query.myself !== '1')
|
||||
|
||||
/**
|
||||
* 手机上点开的那一条,记在地址的 `?id=` 上:详情是从右边滑出的整屏,学生下意识按返回 ——
|
||||
@@ -754,7 +756,7 @@ function dayBreak(index: number) {
|
||||
<div class="row" :class="{ on: row.id === selectedId && !isMobile, 'two-line': twoLine }" :data-row-id="row.id" @click="rowClicked(row)">
|
||||
<template v-if="!twoLine">
|
||||
<span class="c-time">{{ submissionClockText(row.createTime) }}</span>
|
||||
<span class="c-user"><UserName :username="row.username" /></span>
|
||||
<span class="c-user"><UserName :username="row.username" :class-name="mixedClasses ? row.className : null" short-class /></span>
|
||||
<span class="c-problem" :title="`${row.problemDisplayId} ${row.problemTitle}`">
|
||||
<span class="pid">{{ row.problemDisplayId }}</span> {{ row.problemTitle }}
|
||||
</span>
|
||||
@@ -764,7 +766,7 @@ function dayBreak(index: number) {
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="m-line">
|
||||
<UserName :username="row.username" />
|
||||
<UserName :username="row.username" :class-name="mixedClasses ? row.className : null" short-class />
|
||||
<div class="spacer"></div>
|
||||
<FlowchartState :status="row.status" :grade="row.aiGrade" />
|
||||
</div>
|
||||
@@ -785,7 +787,7 @@ function dayBreak(index: number) {
|
||||
<template v-if="!twoLine">
|
||||
<span class="c-time">{{ submissionClockText(row.createTime) }}</span>
|
||||
<span class="c-user">
|
||||
<UserName :username="row.username" :muted="othersRow(row)" />
|
||||
<UserName :username="row.username" :class-name="mixedClasses ? row.className : null" short-class :muted="othersRow(row)" />
|
||||
</span>
|
||||
<span class="c-problem" :title="`${row.problemDisplayId} ${row.problemTitle}`">
|
||||
<span class="title">
|
||||
@@ -808,7 +810,7 @@ function dayBreak(index: number) {
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="m-line">
|
||||
<UserName :username="row.username" :muted="othersRow(row)" />
|
||||
<UserName :username="row.username" :class-name="mixedClasses ? row.className : null" short-class :muted="othersRow(row)" />
|
||||
<div class="spacer"></div>
|
||||
<StatusPill :result="row.result" />
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { displayName } from '@oj2/contract'
|
||||
import type { LessonMode } from '@oj2/contract'
|
||||
import type { Tone } from 'shared/composables/tone'
|
||||
import { parseTime } from 'utils/functions'
|
||||
@@ -114,11 +115,11 @@ export function cellTitle(cell: TaskBoardCell, student: TaskBoardStudent, day: s
|
||||
if (stage === 'draw') {
|
||||
const latest = cell.flowchart?.latest
|
||||
if (latest?.status === 0 || latest?.status === 1) return 'AI 正在评这张图'
|
||||
return latest ? `讲讲 ${student.username} 这张图哪里画错了` : '还没画对'
|
||||
return latest ? `讲讲 ${displayName(student.username)} 这张图哪里画错了` : '还没画对'
|
||||
}
|
||||
if (stage === 'nodraw') return '代码做对了,流程图还没画'
|
||||
if (halfDone(cell, mode)) return '流程图画对了,代码还没交'
|
||||
if (cell.latest) return `讲讲 ${student.username} 这道题的错`
|
||||
if (cell.latest) return `讲讲 ${displayName(student.username)} 这道题的错`
|
||||
if (mode === 'both' && cell.flowchart?.status === 'accepted') return '流程图画对了,代码以前交过、没做对'
|
||||
return cell.attempts ? `今天画了 ${cell.flowchartAttempts} 张流程图` : '以前交过,没做对'
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import type { TaskBoardStudent } from 'utils/types'
|
||||
|
||||
/**
|
||||
@@ -68,7 +69,13 @@ function save() {
|
||||
</n-button>
|
||||
</div>
|
||||
<div class="leave-grid">
|
||||
<n-checkbox v-for="student in section.students" :key="student.userId" :value="student.username" :label="student.username" class="leave-option" />
|
||||
<n-checkbox
|
||||
v-for="student in section.students"
|
||||
:key="student.userId"
|
||||
:value="student.username"
|
||||
:label="displayName(student.username)"
|
||||
class="leave-option"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</n-checkbox-group>
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { displayName } from '@oj2/contract'
|
||||
import { Icon } from '@iconify/vue'
|
||||
import { useThemeVars } from 'naive-ui'
|
||||
import { getTaskBoard } from 'oj/api'
|
||||
@@ -714,7 +715,7 @@ function lastText(student: TaskBoardStudent) {
|
||||
class="plain"
|
||||
:href="submissionsHref(student, undefined, !student.codeToday)"
|
||||
target="_blank"
|
||||
:title="`看 ${student.username} 今天交的全部`"
|
||||
:title="`看 ${displayName(student.username)} 今天交的全部`"
|
||||
>
|
||||
<UserName :username="student.username" />
|
||||
</a>
|
||||
@@ -733,7 +734,7 @@ function lastText(student: TaskBoardStudent) {
|
||||
v-else-if="student.editing"
|
||||
type="button"
|
||||
class="where"
|
||||
:title="`打开 ${student.username} 正在写的 ${student.editing}`"
|
||||
:title="`打开 ${displayName(student.username)} 正在写的 ${student.editing}`"
|
||||
@click="peek(student.userId)"
|
||||
>
|
||||
{{ student.editing }}
|
||||
|
||||
Reference in new issue
Block a user