登录框、提交列表、数据统计、排名、课堂看板、题单班级视图、班级 PK 七处统一走 groupClassOptions。 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
1e441411a5
commit
367cfc9d5d
6 files changed
+71
-59
No files matched your search
@@ -4,7 +4,7 @@ import { MdPreview } from "md-editor-v3"
|
||||
import "md-editor-v3/lib/preview.css"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { getClassBattle, getClassPk } from "oj/api"
|
||||
import { classLabel } from "oj/submission/utils"
|
||||
import { classLabel, groupClassOptions } from "oj/submission/utils"
|
||||
import { useAIStream } from "shared/composables/aiStream"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useUserStore } from "shared/store/user"
|
||||
@@ -102,25 +102,13 @@ function setPeriod(value: ClassPkPeriod) {
|
||||
}
|
||||
|
||||
/** 「+ 加一个班」:这学期在用的班(班级对抗里有的),按年级分组,自己年级放最前 */
|
||||
const addOptions = computed(() => {
|
||||
const grade = selected.value[0]?.slice(0, 2)
|
||||
const groups = new Map<string, { label: string; value: string }[]>()
|
||||
for (const item of battle.value) {
|
||||
if (selected.value.includes(item.className)) continue
|
||||
const key = item.className.slice(0, 2)
|
||||
const list = groups.get(key) ?? []
|
||||
list.push({ label: classLabel(item.className), value: item.className })
|
||||
groups.set(key, list)
|
||||
}
|
||||
return [...groups]
|
||||
.sort((a, b) => (a[0] === grade ? -1 : b[0] === grade ? 1 : b[0].localeCompare(a[0])))
|
||||
.map(([key, children]) => ({
|
||||
type: "group" as const,
|
||||
label: `${key} 级`,
|
||||
key,
|
||||
children: children.sort((a, b) => a.value.localeCompare(b.value, "zh", { numeric: true })),
|
||||
}))
|
||||
})
|
||||
const addOptions = computed(() =>
|
||||
groupClassOptions(
|
||||
battle.value.map((item) => item.className).filter((name) => !selected.value.includes(name)),
|
||||
undefined,
|
||||
selected.value[0]?.slice(0, 2),
|
||||
),
|
||||
)
|
||||
|
||||
const mode = computed(() => {
|
||||
if (!pk.value || pk.value.classes.length < 2) return "empty"
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
<script setup lang="ts">
|
||||
import { getClassBoard, setClassLesson } from "oj/api"
|
||||
import { groupClassOptions } from "oj/submission/utils"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import { useLeaveStudents } from "shared/composables/leaveStudents"
|
||||
import { useCollabStore } from "shared/store/collab"
|
||||
@@ -49,17 +50,9 @@ const languageOptions = LESSON_LANGUAGES.map((value) => ({
|
||||
}))
|
||||
const saving = ref(false)
|
||||
|
||||
function classLabel(name: string) {
|
||||
return /^\d{3,}$/.test(name) ? `${name.slice(0, 2)}计算机${name.slice(2)}班` : name
|
||||
}
|
||||
|
||||
const classOptions = computed(() => {
|
||||
const list = configStore.config?.classList ?? []
|
||||
const names =
|
||||
board.value?.className && !list.includes(board.value.className)
|
||||
? [board.value.className, ...list]
|
||||
: list
|
||||
return names.map((name) => ({ label: classLabel(name), value: name }))
|
||||
return groupClassOptions(board.value?.className ? [board.value.className, ...list] : list)
|
||||
})
|
||||
|
||||
async function load() {
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { getProblemSetClassView } from "oj/api"
|
||||
import { useTone } from "oj/submission/composables/tone"
|
||||
import { classLabel, groupClassOptions } from "oj/submission/utils"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import { parseTime } from "utils/functions"
|
||||
import type { ProblemSetClassView } from "utils/types"
|
||||
@@ -35,16 +36,13 @@ watch(className, (value, old) => {
|
||||
if (old && value !== data.value?.className) load()
|
||||
})
|
||||
|
||||
function classLabel(name: string) {
|
||||
return /^\d{3,}$/.test(name) ? `${name.slice(0, 2)}计算机${name.slice(2)}班` : name
|
||||
}
|
||||
|
||||
const classOptions = computed(() =>
|
||||
(data.value?.classes ?? []).map((item) => ({
|
||||
value: item.className,
|
||||
label: `${classLabel(item.className)}(${item.joined}/${item.size} 人加入)`,
|
||||
})),
|
||||
)
|
||||
const classOptions = computed(() => {
|
||||
const classes = new Map((data.value?.classes ?? []).map((item) => [item.className, item]))
|
||||
return groupClassOptions([...classes.keys()], (name) => {
|
||||
const item = classes.get(name)!
|
||||
return { value: name, label: `${classLabel(name)}(${item.joined}/${item.size} 人加入)` }
|
||||
})
|
||||
})
|
||||
|
||||
const joined = computed(() => data.value?.students.filter((s) => s.joinTime) ?? [])
|
||||
const absent = computed(() => data.value?.students.filter((s) => !s.joinTime) ?? [])
|
||||
|
||||
@@ -9,7 +9,7 @@ import type {
|
||||
} from "@oj2/contract"
|
||||
import { useThemeVars } from "naive-ui"
|
||||
import { clearMood, getClassBattle, getRankBoard, getWeeklyChampions, setRankHidden } from "oj/api"
|
||||
import { classLabel } from "oj/submission/utils"
|
||||
import { classLabel, groupClassOptions } from "oj/submission/utils"
|
||||
import UserName from "shared/components/UserName.vue"
|
||||
import { useBreakpoints } from "shared/composables/breakpoints"
|
||||
import { useConfigStore } from "shared/store/config"
|
||||
@@ -261,12 +261,8 @@ const minis = computed<MiniRank[]>(() => {
|
||||
|
||||
const teacherClassOptions = computed(() => {
|
||||
const list = configStore.config?.classList ?? []
|
||||
const all =
|
||||
teacherClass.value && !list.includes(teacherClass.value) ? [teacherClass.value, ...list] : list
|
||||
return [
|
||||
{ label: "全部", value: "" },
|
||||
...all.map((name) => ({ label: classLabel(name), value: name })),
|
||||
]
|
||||
const all = teacherClass.value ? [teacherClass.value, ...list] : list
|
||||
return [{ label: "全部", value: "" }, ...groupClassOptions(all)]
|
||||
})
|
||||
|
||||
const myBattle = computed(() => battle.value.find((item) => item.className === classContext.value))
|
||||
|
||||
@@ -40,14 +40,50 @@ export function submissionDayText(time: string) {
|
||||
*/
|
||||
export function classLabel(className: string | null) {
|
||||
if (className === null || className === NO_CLASS) return "没填班级"
|
||||
if (!/^\d{3,}$/.test(className)) return className
|
||||
return `${className.slice(0, 2)}计算机${className.slice(2)}班`
|
||||
}
|
||||
|
||||
type ClassOption = { label: string; value: string }
|
||||
|
||||
/**
|
||||
* 班级下拉的选项:配置里的班,最后一项「没填班级」(这批号也要能和普通班一样圈出来看)。
|
||||
* 带进来的班不在配置里(老班、手打的)也要显示得出来,不然框里是空的却在筛
|
||||
* 全站的班级下拉都按年级分组:「25 级」底下列 251、252……,新的年级在上,组里按班号排
|
||||
* (11 班排在 2 班后面)。年级是班号前两位,和排名的「本年级」同一个口径。
|
||||
* `first` 给了就把那个年级提到最前(班级 PK 加班时先给同年级的)。
|
||||
* `toOption` 换掉选项长相(题单的「n/m 人加入」、登录框的 ks 前缀);不是数字班号的
|
||||
* 名字不分组,按原顺序跟在最后。
|
||||
*/
|
||||
export function groupClassOptions<T extends ClassOption = ClassOption>(
|
||||
names: string[],
|
||||
toOption: (name: string) => T = (name) => ({ label: classLabel(name), value: name }) as T,
|
||||
first?: string,
|
||||
) {
|
||||
const groups = new Map<string, string[]>()
|
||||
const rest: string[] = []
|
||||
for (const name of new Set(names)) {
|
||||
if (!/^\d{3,}$/.test(name)) {
|
||||
rest.push(name)
|
||||
continue
|
||||
}
|
||||
const grade = name.slice(0, 2)
|
||||
groups.set(grade, [...(groups.get(grade) ?? []), name])
|
||||
}
|
||||
const grouped = [...groups]
|
||||
.sort((a, b) => (a[0] === first ? -1 : b[0] === first ? 1 : b[0].localeCompare(a[0])))
|
||||
.map(([grade, list]) => ({
|
||||
type: "group" as const,
|
||||
label: `${grade} 级`,
|
||||
key: `grade-${grade}`,
|
||||
children: list.sort((a, b) => a.localeCompare(b, "zh", { numeric: true })).map(toOption),
|
||||
}))
|
||||
return [...grouped, ...rest.map(toOption)]
|
||||
}
|
||||
|
||||
/**
|
||||
* 班级下拉的选项:配置里的班按年级分组,最后一项「没填班级」(这批号也要能和普通班一样
|
||||
* 圈出来看)。带进来的班不在配置里(老班、手打的)也要显示得出来,不然框里是空的却在筛
|
||||
*/
|
||||
export function classSelectOptions(list: string[], current: string) {
|
||||
const all = current && current !== NO_CLASS && !list.includes(current) ? [current, ...list] : list
|
||||
return [...all, NO_CLASS].map((item) => ({ label: classLabel(item), value: item }))
|
||||
const all = current && current !== NO_CLASS ? [current, ...list] : list
|
||||
return [...groupClassOptions(all), { label: classLabel(NO_CLASS), value: NO_CLASS }]
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
<script setup lang="ts">
|
||||
import { classLabel, groupClassOptions } from "oj/submission/utils"
|
||||
import { errorCode } from "utils/api"
|
||||
import { STORAGE_KEY } from "utils/constants"
|
||||
import storage from "utils/storage"
|
||||
@@ -52,14 +53,13 @@ function filterClassUser(pattern: string, option: SelectOption) {
|
||||
// 空串是「没有我所在的班级」,null 才是没选 —— 往届的班级毕业后会从后台配置里删掉,
|
||||
// 这一项是他们唯一的入口,写完整用户名登录
|
||||
const OTHER_CLASS = ""
|
||||
const classList = computed<SelectOption[]>(() => {
|
||||
const configs =
|
||||
configStore.config?.classList.map((item) => ({
|
||||
label: `${item.slice(0, 2)}计算机${item.slice(2)}班`,
|
||||
value: `ks${item}`,
|
||||
})) ?? []
|
||||
return [...configs, { label: "没有我所在的班级", value: OTHER_CLASS }]
|
||||
})
|
||||
const classList = computed<SelectOption[]>(() => [
|
||||
...groupClassOptions(configStore.config?.classList ?? [], (item) => ({
|
||||
label: classLabel(item),
|
||||
value: `ks${item}`,
|
||||
})),
|
||||
{ label: "没有我所在的班级", value: OTHER_CLASS },
|
||||
])
|
||||
// 选了具体班级就是「选姓名」,选了「没有我所在的班级」就是「写用户名」
|
||||
const isClassLogin = computed(() => Boolean(form.value.class))
|
||||
const passwordRule: FormItemRule[] = [
|
||||
@@ -190,7 +190,8 @@ function onUsernamePicked() {
|
||||
function restoreLastClass() {
|
||||
if (form.value.class !== null) return
|
||||
const last = storage.get(STORAGE_KEY.LOGIN_CLASS)
|
||||
if (typeof last === "string" && classList.value.some((item) => item.value === last)) {
|
||||
const known = (configStore.config?.classList ?? []).map((item) => `ks${item}`)
|
||||
if (typeof last === "string" && known.includes(last)) {
|
||||
form.value.class = last
|
||||
}
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user