Files
OJ2/apps/web/src/admin/task/components/LessonEditModal.vue
T
xuyueandClaude Opus 5.5 48590eee60
Deploy / deploy (push) Canceled after 0s
refactor(目录): 目录名对上界面上的名字,自学的「一课」不再也叫 lesson
- 后台课堂任务 admin/lesson → admin/task(/admin/task),和前台 oj/task 对上
- 后台首页 admin/setting/home.vue → admin/home/,设置 → admin/config/
- 卡点分析、年度趋势从 admin/problem 搬到 admin/analytics(/admin/analytics/stuck|trend),和菜单的「学情分析」、后端 analytics.ts 对上
- 首页 dashboard.vue / TeacherHome.vue → student.vue / teacher.vue;自学首页 home.vue → index.vue,看一课 index.vue → tutorial.vue
- lesson 只表示课堂上的一节:自学那边改叫 tutorial(oj/learn/lesson.ts → utils.ts、isLessonDone → isTutorialDone、Lesson*.vue → Tutorial*.vue、自学情况的 lessons 字段 → tutorials)
- 旧地址不留跳转

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 22:59:43 +08:00

173 lines
5.8 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<script setup lang="ts">
import { Icon } from '@iconify/vue'
import { LESSON_MODE_LABEL, lessonNeeds, type AdminLesson, type LessonLanguage, type LessonMode } from '@oj2/contract'
import ProblemPicker from 'oj/task/components/ProblemPicker.vue'
import { useConfigStore } from 'shared/store/config'
import { errorMessage } from 'utils/api'
import { groupClassOptions } from 'utils/classes'
import { LANGUAGE_SHOW_VALUE } from 'utils/constants'
import { parseTime } from 'utils/functions'
import { createAdminLesson, updateAdminLesson } from '../../api'
/**
* 后台改一节 / 补一节。改的时候班级、日子、时刻都能动 —— 任务页上点错了班、隔天才想起来布置,
* 都在这里纠正;补的是任务页外上的课,补了数据统计才认得到。题号可以敲,也可以点「选题」挑
*/
const props = defineProps<{
/** null = 补一节 */
lesson: AdminLesson | null
}>()
const show = defineModel<boolean>('show', { required: true })
const emit = defineEmits<{ saved: [] }>()
const message = useMessage()
const configStore = useConfigStore()
const LESSON_LANGUAGES: LessonLanguage[] = ['Python', 'C', 'C++', 'SQL']
const languageOptions = LESSON_LANGUAGES.map((value) => ({ label: LANGUAGE_SHOW_VALUE[value], value }))
const modeOptions = (['code', 'both', 'flowchart'] as const).map((value) => ({ label: LESSON_MODE_LABEL[value], value }))
const classOptions = computed(() => groupClassOptions(configStore.config?.classList ?? []))
const className = ref<string | null>(null)
const day = ref<string | null>(null)
const time = ref<string | null>(null)
const language = ref<LessonLanguage>('Python')
/** 做什么:画流程图不用选语言(存成 null) */
const mode = ref<LessonMode>('code')
const needsCode = computed(() => lessonNeeds(mode.value).code)
const idsInput = ref('')
const saving = ref(false)
watch(show, (open) => {
if (!open) return
const lesson = props.lesson
const now = new Date()
className.value = lesson?.className ?? null
// 日子、时刻都按东八区:n-date-picker / n-time-picker 绑的是格式化后的文本,不经过浏览器时区
day.value = lesson?.day ?? parseTime(now, 'YYYY-MM-DD')
time.value = parseTime(lesson?.createdAt ?? now, 'HH:mm')
language.value = lesson?.language ?? 'Python'
mode.value = lesson?.mode ?? 'code'
idsInput.value = lesson?.problems.map((p) => p.problemDisplayId).join(' ') ?? ''
})
function inputIds() {
return idsInput.value.split(/[\s,,、;;]+/).filter(Boolean)
}
// ---- 选题弹框,和任务页上那个是同一个 ----
const picking = ref(false)
const pickerInitial = ref<{ id: string; title?: string }[]>([])
function openPicker() {
const titles = new Map(props.lesson?.problems.map((p) => [p.problemDisplayId.toLowerCase(), p.title]) ?? [])
pickerInitial.value = inputIds().map((id) => ({ id, title: titles.get(id.toLowerCase()) }))
picking.value = true
}
function picked(ids: string[]) {
idsInput.value = ids.join(' ')
picking.value = false
}
const ready = computed(() => !!className.value && !!day.value && !!time.value && inputIds().length > 0)
async function save() {
if (!ready.value) return
saving.value = true
const body = {
className: className.value!,
day: day.value!,
time: time.value!,
language: needsCode.value ? language.value : null,
mode: mode.value,
problemDisplayIds: inputIds(),
}
try {
if (props.lesson) await updateAdminLesson(props.lesson.id, body)
else await createAdminLesson(body)
message.success(props.lesson ? '改好了' : '补上了')
show.value = false
emit('saved')
} catch (error) {
message.error(errorMessage(error))
} finally {
saving.value = false
}
}
</script>
<template>
<n-modal v-model:show="show" preset="card" :title="lesson ? '改这一节' : '补一节'" :bordered="false" :style="{ width: 'min(520px, calc(100vw - 32px))' }">
<n-form label-placement="left" label-width="64">
<n-form-item label="班级">
<n-select v-model:value="className" :options="classOptions" filterable placeholder="选班级" />
</n-form-item>
<n-form-item label="上课">
<div class="when">
<n-date-picker v-model:formatted-value="day" value-format="yyyy-MM-dd" type="date" class="day" />
<n-time-picker v-model:formatted-value="time" value-format="HH:mm" format="HH:mm" class="time" />
</div>
</n-form-item>
<n-form-item label="做什么">
<div class="when">
<n-select v-model:value="mode" :options="modeOptions" class="lang" />
<n-select v-if="needsCode" v-model:value="language" :options="languageOptions" aria-label="用什么语言写" class="lang" />
</div>
</n-form-item>
<n-form-item label="题目">
<div class="ids">
<n-input v-model:value="idsInput" placeholder="题号,用空格隔开" @keyup.enter="save" />
<n-button @click="openPicker">
<template #icon><Icon icon="ph:list-checks" /></template>
选题
</n-button>
</div>
</n-form-item>
</n-form>
<template #footer>
<div class="foot">
<n-button @click="show = false">取消</n-button>
<n-button type="primary" :disabled="!ready" :loading="saving" @click="save">{{ lesson ? '保存' : '补上' }}</n-button>
</div>
</template>
<ProblemPicker
v-model:show="picking"
v-model:language="language"
v-model:lesson-mode="mode"
:initial="pickerInitial"
:saving="false"
mode="pick"
@confirm="picked"
/>
</n-modal>
</template>
<style scoped>
.when,
.ids {
display: flex;
gap: 8px;
width: 100%;
}
.day {
flex: 1 1 0;
}
.time {
width: 110px;
}
.lang {
width: 140px;
}
.ids .n-input {
flex: 1 1 0;
}
.foot {
display: flex;
justify-content: flex-end;
gap: 8px;
}
</style>