新建时可选“自动生成顺序号”:例如 数据库实验 01、02…

或选“逐行粘贴名称”:每行一个名称,按实际生成顺序对应项目。
草稿项目支持勾选后“批量改名”,可直接粘贴多行名称。
服务端会校验名称数量、长度和草稿状态,避免错位或修改已发布项目。
This commit is contained in:
2026-08-10 09:45:02 +08:00 Unverified
parent 4be61ab921
commit 3bcb976aa4
3 changed files with 207 additions and 4 deletions
+92 -3
View File
@@ -42,12 +42,18 @@ const projectForm = reactive({
scheduleEntryId: '',
code: '',
name: '',
projectNames: '',
nameMode: 'Sequence',
arrangementMode: 'Centralized',
description: '',
requirements: '',
dates: [] as string[],
})
const batchRenameDialog = ref(false)
const batchRenameProjectIds = ref<string[]>([])
const batchRenameNames = ref('')
const sessionDialog = ref(false)
const selectedProject = ref<any>(null)
const sessionForm = reactive({
@@ -119,6 +125,9 @@ const draftSelectedProjectIds = computed(() =>
selectedProjectIds.value.filter((id) =>
projects.value.some((project) => project.id === id && project.status === 'Draft')),
)
const batchRenameProjects = computed(() =>
projects.value.filter((project) => batchRenameProjectIds.value.includes(project.id)),
)
const allPageDraftSelected = computed(() => {
const ids = projects.value.filter((project) => project.status === 'Draft').map((project) => project.id)
return ids.length > 0 && ids.every((id) => selectedProjectIds.value.includes(id))
@@ -154,6 +163,8 @@ function resetProjectForm() {
scheduleEntryId: '',
code: '',
name: '',
projectNames: '',
nameMode: 'Sequence',
arrangementMode: 'Centralized',
description: '',
requirements: '',
@@ -191,6 +202,8 @@ function openEditProject(project: any) {
scheduleEntryId: project.scheduleEntryId ?? '',
code: project.code,
name: project.name,
projectNames: '',
nameMode: 'Sequence',
arrangementMode: project.arrangementMode,
description: project.description ?? '',
requirements: project.requirements ?? '',
@@ -199,11 +212,21 @@ function openEditProject(project: any) {
projectDialog.value = true
}
function projectNameLines(value: string) {
return value.split(/\r?\n/).map((name) => name.trim()).filter(Boolean)
}
async function saveProject() {
const names = projectNameLines(projectForm.projectNames)
const hasValidName = editingProjectId.value
? !!projectForm.name.trim()
: projectForm.nameMode === 'InputNames'
? names.length > 0
: !!projectForm.name.trim()
if (!projectForm.teachingTaskIds.length ||
(editingProjectId.value && projectForm.arrangementMode === 'Centralized' && !projectForm.scheduleEntryId) ||
!projectForm.code.trim()
|| !projectForm.name.trim() || projectForm.dates.length !== 2) {
|| !hasValidName || projectForm.dates.length !== 2) {
ElMessage.warning(projectForm.arrangementMode === 'Centralized' && editingProjectId.value
? '请选择课表实验课,并填写项目编码、名称和开放日期'
: '请填写教学任务、项目编码、名称和开放日期')
@@ -213,7 +236,7 @@ async function saveProject() {
teachingTaskId: projectForm.teachingTaskIds[0],
scheduleEntryId: projectForm.scheduleEntryId || null,
code: projectForm.code,
name: projectForm.name,
name: projectForm.nameMode === 'InputNames' && !editingProjectId.value ? names[0] : projectForm.name,
arrangementMode: projectForm.arrangementMode,
description: projectForm.description || null,
requirements: projectForm.requirements || null,
@@ -229,6 +252,8 @@ async function saveProject() {
...payload,
teachingTaskId: undefined,
teachingTaskIds: projectForm.teachingTaskIds,
names: projectForm.nameMode === 'InputNames' ? names : undefined,
generateSequentialNames: projectForm.nameMode === 'Sequence',
})
ElMessage.success(projectForm.arrangementMode === 'Centralized'
? `已按 ${projectForm.teachingTaskIds.length} 个教学班的全部实验课生成项目`
@@ -395,6 +420,30 @@ async function deleteProject(project: any) {
}
}
function openBatchRename() {
batchRenameProjectIds.value = projects.value
.filter((project) => draftSelectedProjectIds.value.includes(project.id))
.map((project) => project.id)
batchRenameNames.value = batchRenameProjects.value.map((project) => project.name).join('\n')
batchRenameDialog.value = true
}
async function saveBatchRename() {
const names = projectNameLines(batchRenameNames.value)
if (names.length !== batchRenameProjectIds.value.length) {
ElMessage.warning(`已选 ${batchRenameProjectIds.value.length} 个项目,请粘贴 ${batchRenameProjectIds.value.length} 行项目名称`)
return
}
try {
await http.put('/experiments/batch/names', { projectIds: batchRenameProjectIds.value, names })
ElMessage.success('实验项目名称已批量更新')
batchRenameDialog.value = false
await load()
} catch (error) {
ElMessage.error(apiErrorMessage(error))
}
}
function togglePageDraftSelection(value: unknown) {
const ids = projects.value.filter((project) => project.status === 'Draft').map((project) => project.id)
selectedProjectIds.value = Boolean(value)
@@ -715,6 +764,7 @@ onMounted(async () => {
选中当前页全部草稿
</el-checkbox>
<span>已选 {{ draftSelectedProjectIds.length }} 个草稿项目</span>
<el-button size="small" :disabled="!draftSelectedProjectIds.length" @click="openBatchRename">批量改名</el-button>
<el-button size="small" type="primary" :disabled="!draftSelectedProjectIds.length" @click="publishSelectedProjects">批量发布</el-button>
<el-button size="small" type="danger" plain :disabled="!draftSelectedProjectIds.length" @click="deleteSelectedProjects">批量删除</el-button>
</section>
@@ -979,10 +1029,31 @@ onMounted(async () => {
<el-form-item label="项目编码" required>
<el-input v-model="projectForm.code" maxlength="40" placeholder="如 LAB-01" />
</el-form-item>
<el-form-item label="项目名称" required>
<el-form-item v-if="editingProjectId" label="项目名称" required>
<el-input v-model="projectForm.name" maxlength="120" placeholder="如 数据库事务与并发实验" />
</el-form-item>
</div>
<template v-if="!editingProjectId">
<el-form-item label="项目命名方式" required>
<el-radio-group v-model="projectForm.nameMode">
<el-radio-button value="Sequence">自动生成顺序号</el-radio-button>
<el-radio-button value="InputNames">逐行粘贴名称</el-radio-button>
</el-radio-group>
</el-form-item>
<el-form-item v-if="projectForm.nameMode === 'Sequence'" label="项目名称前缀" required>
<el-input v-model="projectForm.name" maxlength="116" placeholder="如 数据库实验;将生成“数据库实验 01、数据库实验 02”" />
</el-form-item>
<el-form-item v-else label="实验项目名称" required>
<el-input
v-model="projectForm.projectNames"
type="textarea"
:rows="6"
maxlength="120000"
placeholder="每行一个名称,可直接从 Excel 粘贴;系统按实际生成顺序逐行对应"
/>
<small class="form-help">已输入 {{ projectNameLines(projectForm.projectNames).length }} 行有效名称</small>
</el-form-item>
</template>
</div>
<div class="form-section">
@@ -1043,6 +1114,23 @@ onMounted(async () => {
</template>
</el-dialog>
<el-dialog v-model="batchRenameDialog" title="批量修改实验项目名称" width="680px">
<el-alert
:title="`已选择 ${batchRenameProjects.length} 个草稿项目。请按下方显示顺序,每行填写一个名称。`"
type="info"
:closable="false"
show-icon
/>
<ol class="batch-rename-preview">
<li v-for="project in batchRenameProjects" :key="project.id">{{ project.code }} · {{ project.name }}</li>
</ol>
<el-input v-model="batchRenameNames" type="textarea" :rows="Math.min(12, Math.max(5, batchRenameProjects.length))" maxlength="12000" placeholder="每行一个实验项目名称" />
<template #footer>
<el-button @click="batchRenameDialog = false">取消</el-button>
<el-button type="primary" @click="saveBatchRename">保存名称</el-button>
</template>
</el-dialog>
<el-dialog
v-model="sessionDialog"
:title="`安排场次 · ${selectedProject?.name ?? ''}`"
@@ -1253,6 +1341,7 @@ onMounted(async () => {
.result-note { margin-left: auto; color: var(--muted); font-size: 12px; }
.experiment-batch-bar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; min-height: 46px; margin: -6px 0 16px; padding: 8px 12px; border: 1px solid #cddce3; background: #f7fafb; }
.experiment-batch-bar > span { margin-right: auto; color: #5c7481; font-size: 12px; }
.batch-rename-preview { max-height: 180px; margin: 14px 0; overflow: auto; padding: 10px 12px 10px 32px; border: 1px solid #dce5e9; background: #f8fbfc; color: #526b78; font-size: 12px; line-height: 1.8; }
.experiment-pagination { justify-content: flex-end; margin-top: 4px; }
.project-list { display: grid; gap: 14px; min-height: 160px; }
.project-group { overflow: hidden; border: 1px solid #cddce3; background: #f6fafb; }