- 后台课堂任务 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>
This commit is contained in:
1 parent
29c31640b5
commit
48590eee60
37 files changed
+1238
-1236
No files matched your search
@@ -0,0 +1,172 @@
|
||||
<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>
|
||||
Reference in new issue
Block a user