<script setup lang="ts">
import { computed, onMounted, reactive, ref } from 'vue'
import { Calendar, Check, Plus, Refresh, User } from '@element-plus/icons-vue'
import { ElMessage, ElMessageBox } from 'element-plus'
import http, { apiErrorMessage } from '../api/http'
import { useAuthStore } from '../stores/auth'
import {
  academicTermLabel,
  academicTermOptionClass,
  defaultAcademicTermId,
} from '../utils/academicTerms'

const auth = useAuthStore()
const isStudent = computed(() => auth.user?.roles.includes('Student') ?? false)
const loading = ref(false)
const terms = ref<any[]>([])
const termId = ref('')
const projects = ref<any[]>([])
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const selectedProjectIds = ref<string[]>([])
const options = reactive({
  tasks: [] as any[],
  colleges: [] as any[],
  scheduleEntries: [] as any[],
  classrooms: [] as any[],
  periods: [] as any[],
})
const modeFilter = ref('')
const statusFilter = ref('')
const offeringCollegeId = ref('')
const courseKeyword = ref('')
const classKeyword = ref('')
const teacherKeyword = ref('')
const taskKeyword = ref('')

const projectDialog = ref(false)
const editingProjectId = ref('')
const correctingPublishedProject = ref(false)
const projectForm = reactive({
  teachingTaskIds: [] as string[],
  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({
  classroomId: '',
  sessionDate: '',
  startPeriod: undefined as number | undefined,
  periodCount: 2,
  capacity: 30,
  notes: '',
})

const batchSessionDialog = ref(false)
const batchSessionProjectIds = ref<string[]>([])
const batchSessionRows = ref<any[]>([])

const participantsDialog = ref(false)
const participantSession = ref<any>(null)
const participants = ref<any[]>([])
const participantsLoading = ref(false)

const selectedTask = computed(() =>
  options.tasks.find((task) => task.id === projectForm.teachingTaskIds[0]),
)
const selectedScheduleEntry = computed(() =>
  options.scheduleEntries.find((entry) => entry.id === projectForm.scheduleEntryId),
)
const batchProjectOptions = computed(() =>
  projects.value.filter((project) =>
    project.status !== 'Closed' && project.arrangementMode === 'SelfScheduled'),
)
const taskGroups = computed(() => {
  const groups = new Map<string, any>()
  options.tasks.forEach((task) => {
    const key = `${task.academicTermId}-${task.courseId}`
    const group = groups.get(key) ?? {
      key,
      termName: task.termName,
      courseCode: task.courseCode,
      courseName: task.courseName,
      tasks: [],
    }
    group.tasks.push(task)
    groups.set(key, group)
  })
  return [...groups.values()]
})
const projectGroups = computed(() => {
  const groups = new Map<string, any>()
  projects.value.forEach((project) => {
    const classKey = [...(project.classNames ?? [])].sort().join('|')
    const key = `${project.academicTermId}-${project.courseCode}-${classKey}`
    const group = groups.get(key) ?? {
      key,
      termName: project.termName,
      courseCode: project.courseCode,
      courseName: project.courseName,
      classNames: project.classNames,
      teacherNames: project.teacherNames,
      taskNumbers: [],
      projects: [],
    }
    if (!group.taskNumbers.includes(project.taskNumber)) group.taskNumbers.push(project.taskNumber)
    group.projects.push(project)
    groups.set(key, group)
  })
  return [...groups.values()]
})
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))
})
const activePeriods = computed(() =>
  options.periods.filter((period) =>
    !selectedProject.value
      || period.academicTermId === selectedProject.value.academicTermId,
  ),
)

const modeMeta: Record<string, { label: string; note: string }> = {
  Centralized: {
    label: '集中安排',
    note: '像课程一样统一到场，无需学生预约',
  },
  SelfScheduled: {
    label: '自行安排',
    note: '规定实验项目，学生从开放场次中预约',
  },
}

const statusLabels: Record<string, string> = {
  Draft: '草稿',
  Published: '进行中',
  Closed: '已关闭',
}

function resetProjectForm() {
  editingProjectId.value = ''
  correctingPublishedProject.value = false
  Object.assign(projectForm, {
    teachingTaskIds: [],
    scheduleEntryId: '',
    code: '',
    name: '',
    projectNames: '',
    nameMode: 'Sequence',
    arrangementMode: 'Centralized',
    description: '',
    requirements: '',
    dates: [],
  })
}

function onTaskChange() {
  const task = selectedTask.value
  if (!task) return
  projectForm.dates = [task.termStartDate, task.termEndDate]
}

function onScheduleEntryChange() {
  const entry = selectedScheduleEntry.value
  if (!entry) return
  projectForm.teachingTaskIds = [entry.teachingTaskId]
  onTaskChange()
}

function formatScheduleEntry(entry: any) {
  const classes = entry.classNames?.join('、') || '选课学生'
  return `${entry.courseCode} · ${entry.courseName} · ${entry.taskNumber} · ${classes} · 星期 ${entry.dayOfWeek} 第 ${entry.startPeriod}–${entry.startPeriod + entry.periodCount - 1} 节 · ${entry.campusName} ${entry.buildingName} ${entry.classroomName}`
}

function openCreateProject() {
  resetProjectForm()
  projectDialog.value = true
}

function openEditProject(project: any) {
  editingProjectId.value = project.id
  correctingPublishedProject.value = false
  Object.assign(projectForm, {
    teachingTaskIds: [project.teachingTaskId],
    scheduleEntryId: project.scheduleEntryId ?? '',
    code: project.code,
    name: project.name,
    projectNames: '',
    nameMode: 'Sequence',
    arrangementMode: project.arrangementMode,
    description: project.description ?? '',
    requirements: project.requirements ?? '',
    dates: [project.startDate, project.endDate],
  })
  projectDialog.value = true
}

function openCorrectPublishedProject(project: any) {
  openEditProject(project)
  correctingPublishedProject.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 = correctingPublishedProject.value || editingProjectId.value
    ? !!projectForm.name.trim()
    : projectForm.nameMode === 'InputNames'
      ? names.length > 0
      : !!projectForm.name.trim()
  if ((!correctingPublishedProject.value && !projectForm.teachingTaskIds.length) ||
    (editingProjectId.value && projectForm.arrangementMode === 'Centralized' && !projectForm.scheduleEntryId) ||
    !projectForm.code.trim()
    || !hasValidName || projectForm.dates.length !== 2) {
    ElMessage.warning(projectForm.arrangementMode === 'Centralized' && editingProjectId.value
      ? '请选择课表实验课，并填写项目编码、名称和开放日期'
      : '请填写教学任务、项目编码、名称和开放日期')
    return
  }
  const payload = {
    teachingTaskId: projectForm.teachingTaskIds[0],
    scheduleEntryId: projectForm.scheduleEntryId || null,
    code: projectForm.code,
    name: projectForm.nameMode === 'InputNames' && !editingProjectId.value ? names[0] : projectForm.name,
    arrangementMode: projectForm.arrangementMode,
    description: projectForm.description || null,
    requirements: projectForm.requirements || null,
    startDate: projectForm.dates[0],
    endDate: projectForm.dates[1],
  }
  try {
    if (correctingPublishedProject.value) {
      await http.put(`/experiments/${editingProjectId.value}/published-details`, {
        name: projectForm.name,
        description: projectForm.description || null,
        requirements: projectForm.requirements || null,
      })
      ElMessage.success('已修正已发布项目的教学内容，并通知学生查看')
    } else if (editingProjectId.value) {
      await http.put(`/experiments/${editingProjectId.value}`, payload)
      ElMessage.success('实验项目已更新')
    } else {
      await http.post('/experiments/batch', {
        ...payload,
        teachingTaskId: undefined,
        teachingTaskIds: projectForm.teachingTaskIds,
        names: projectForm.nameMode === 'InputNames' ? names : undefined,
        generateSequentialNames: projectForm.nameMode === 'Sequence',
      })
      ElMessage.success(projectForm.arrangementMode === 'Centralized'
        ? `已按 ${projectForm.teachingTaskIds.length} 个教学班的全部实验课生成项目`
        : `已为 ${projectForm.teachingTaskIds.length} 个教学任务创建实验项目`)
    }
    projectDialog.value = false
    await load()
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

function openBatchSession() {
  batchSessionProjectIds.value = []
  batchSessionRows.value = []
  batchSessionDialog.value = true
}

function syncBatchSessionRows() {
  const existing = new Map(batchSessionRows.value.map((row) => [row.projectId, row]))
  batchSessionRows.value = batchSessionProjectIds.value.map((projectId) => {
    const current = existing.get(projectId)
    if (current) return current
    const project = projects.value.find((item) => item.id === projectId)
    const firstPeriod = options.periods.find((item) =>
      item.academicTermId === project?.academicTermId,
    )
    return {
      projectId,
      classroomId: '',
      sessionDate: project?.startDate ?? '',
      startPeriod: firstPeriod?.periodNumber,
      periodCount: 2,
      capacity: 30,
      notes: '',
    }
  })
}

function applyFirstBatchTime() {
  const first = batchSessionRows.value[0]
  if (!first) return
  batchSessionRows.value.slice(1).forEach((row) => {
    row.sessionDate = first.sessionDate
    row.startPeriod = first.startPeriod
    row.periodCount = first.periodCount
  })
  ElMessage.success('已套用首行的日期和节次，请分别选择不冲突的实验室')
}

function batchProject(projectId: string) {
  return projects.value.find((project) => project.id === projectId)
}

function periodsForProject(projectId: string) {
  const project = batchProject(projectId)
  return options.periods.filter((period) =>
    period.academicTermId === project?.academicTermId,
  )
}

async function saveBatchSessions() {
  if (!batchSessionRows.value.length) {
    ElMessage.warning('请至少选择一个实验项目')
    return
  }
  if (batchSessionRows.value.some((row) =>
    !row.classroomId || !row.sessionDate || !row.startPeriod || !row.periodCount,
  )) {
    ElMessage.warning('请完整填写每个项目的日期、节次、实验室和容量')
    return
  }
  try {
    await http.post('/experiments/sessions/batch', {
      items: batchSessionRows.value,
    })
    batchSessionDialog.value = false
    ElMessage.success(`已批量安排 ${batchSessionRows.value.length} 个实验场次`)
    await load()
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

function openSession(project: any) {
  selectedProject.value = project
  const firstPeriod = options.periods.find((item) =>
    item.academicTermId === project.academicTermId,
  )
  Object.assign(sessionForm, {
    classroomId: '',
    sessionDate: project.startDate,
    startPeriod: firstPeriod?.periodNumber,
    periodCount: 2,
    capacity: 30,
    notes: '',
  })
  sessionDialog.value = true
}

async function saveSession() {
  if (!selectedProject.value || !sessionForm.classroomId
    || !sessionForm.sessionDate || !sessionForm.startPeriod) {
    ElMessage.warning('请选择实验日期、节次和教室')
    return
  }
  try {
    await http.post(`/experiments/${selectedProject.value.id}/sessions`, sessionForm)
    sessionDialog.value = false
    ElMessage.success('实验场次已安排')
    await load()
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

async function publishProject(project: any) {
  try {
    await ElMessageBox.confirm(
      project.arrangementMode === 'Centralized'
        ? '发布后，教学任务名单中的学生将直接看到全部集中实验场次。'
        : '发布后，教学任务名单中的学生可以立即预约开放场次。',
      `发布“${project.name}”`,
      { confirmButtonText: '确认发布', cancelButtonText: '暂不发布', type: 'warning' },
    )
    await http.post(`/experiments/${project.id}/publish`)
    ElMessage.success('实验项目已发布')
    await load()
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

async function closeProject(project: any) {
  try {
    await ElMessageBox.confirm(
      '关闭后保留已有安排和预约记录，但学生不能再预约新场次。',
      `关闭“${project.name}”`,
      { confirmButtonText: '确认关闭', cancelButtonText: '取消', type: 'warning' },
    )
    await http.post(`/experiments/${project.id}/close`)
    ElMessage.success('实验项目已关闭')
    await load()
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

async function deleteProject(project: any) {
  try {
    await ElMessageBox.confirm(
      '草稿项目及其未发布场次将一并删除。',
      `删除“${project.name}”`,
      { confirmButtonText: '删除', cancelButtonText: '取消', type: 'error' },
    )
    await http.delete(`/experiments/${project.id}`)
    ElMessage.success('实验项目已删除')
    await load()
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

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)
    ? [...new Set([...selectedProjectIds.value, ...ids])]
    : selectedProjectIds.value.filter((id) => !ids.includes(id))
}

function toggleProjectSelection(projectId: string, value: unknown) {
  selectedProjectIds.value = Boolean(value)
    ? [...new Set([...selectedProjectIds.value, projectId])]
    : selectedProjectIds.value.filter((id) => id !== projectId)
}

async function publishSelectedProjects() {
  const ids = draftSelectedProjectIds.value
  if (!ids.length) return
  try {
    await ElMessageBox.confirm(
      `将发布选中的 ${ids.length} 个草稿实验项目，并通知对应学生。`,
      '批量发布实验项目',
      { confirmButtonText: '确认发布', cancelButtonText: '取消', type: 'warning' },
    )
    const { data } = await http.post('/experiments/batch/publish', { projectIds: ids })
    ElMessage.success(`已提交 ${ids.length} 个实验项目的后台发布任务`)
    selectedProjectIds.value = []
    void pollPublishJob(data.jobId)
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

async function pollPublishJob(jobId: string) {
  try {
    const { data } = await http.get(`/experiments/batch/publish-jobs/${jobId}`)
    if (data.status === 'Succeeded') {
      ElMessage.success('实验项目已完成发布')
      await load()
      return
    }
    if (data.status === 'Failed') {
      ElMessage.error(data.errorMessage || '实验项目发布失败')
      await load()
      return
    }
    window.setTimeout(() => { void pollPublishJob(jobId) }, 1200)
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
}

async function deleteSelectedProjects() {
  const ids = draftSelectedProjectIds.value
  if (!ids.length) return
  try {
    await ElMessageBox.confirm(
      `将删除选中的 ${ids.length} 个草稿实验项目及其未发布场次。`,
      '批量删除实验项目',
      { confirmButtonText: '删除', cancelButtonText: '取消', type: 'error' },
    )
    await http.delete('/experiments/batch', { data: { projectIds: ids } })
    ElMessage.success(`已删除 ${ids.length} 个实验项目`)
    selectedProjectIds.value = []
    await load()
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

async function cancelSession(project: any, session: any) {
  try {
    await ElMessageBox.confirm(
      project.status === 'Draft'
        ? '该未发布场次将直接删除。'
        : '该场次将被取消；已有学生预约也会取消并收到通知。',
      `${project.status === 'Draft' ? '删除' : '取消'} ${formatSessionTime(session)}`,
      { confirmButtonText: '确认', cancelButtonText: '返回', type: 'warning' },
    )
    await http.delete(`/experiments/sessions/${session.id}`)
    ElMessage.success(project.status === 'Draft' ? '场次已删除' : '场次已取消')
    await load()
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

async function showParticipants(project: any, session: any) {
  participantSession.value = { ...session, project }
  participantsDialog.value = true
  participantsLoading.value = true
  try {
    participants.value = (
      await http.get(`/experiments/sessions/${session.id}/participants`)
    ).data
  } catch (error) {
    participants.value = []
    ElMessage.error(apiErrorMessage(error))
  } finally {
    participantsLoading.value = false
  }
}

async function book(project: any, session: any) {
  try {
    await ElMessageBox.confirm(
      `${formatSessionTime(session)} · ${session.campusName} ${session.buildingName} ${session.classroomName}`,
      `预约“${project.name}”`,
      { confirmButtonText: '确认预约', cancelButtonText: '再看看', type: 'info' },
    )
    await http.post(`/experiments/sessions/${session.id}/book`)
    ElMessage.success('实验场次已预约')
    await load()
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

async function cancelBooking(project: any) {
  if (!project.myBooking) return
  try {
    await ElMessageBox.confirm(
      '取消后名额会立即释放，您可以重新选择该项目的其他场次。',
      `取消“${project.name}”预约`,
      { confirmButtonText: '取消预约', cancelButtonText: '保留预约', type: 'warning' },
    )
    await http.delete(`/experiments/bookings/${project.myBooking.id}`)
    ElMessage.success('预约已取消')
    await load()
  } catch (error: any) {
    if (error === 'cancel' || error === 'close') return
    ElMessage.error(apiErrorMessage(error))
  }
}

function formatSessionTime(session: any) {
  return `${session.sessionDate} · 第 ${session.startPeriod}–${session.startPeriod + session.periodCount - 1} 节`
}

function selectedSession(project: any) {
  return project.sessions.find((session: any) =>
    session.id === project.myBooking?.experimentSessionId,
  )
}

function sessionTagType(session: any) {
  const remaining = session.capacity - session.reservedCount
  if (remaining <= 0) return 'danger'
  if (remaining <= Math.max(2, Math.floor(session.capacity * 0.2))) return 'warning'
  return 'success'
}

async function loadOptions() {
  if (isStudent.value) return
  try {
    const { data } = await http.get('/experiments/options', {
      params: {
        academicTermId: termId.value || undefined,
        offeringCollegeId: offeringCollegeId.value || undefined,
        courseKeyword: courseKeyword.value.trim() || undefined,
      },
    })
    options.tasks = data.tasks
    options.colleges = data.colleges
    options.scheduleEntries = data.scheduleEntries
    options.classrooms = data.classrooms
    options.periods = data.periods
  } catch (error) {
    options.tasks = []
    options.colleges = []
    options.classrooms = []
    options.periods = []
    ElMessage.error(apiErrorMessage(error))
  }
}

async function load() {
  loading.value = true
  try {
    if (isStudent.value) {
      projects.value = (await http.get('/experiments/student', {
        params: { academicTermId: termId.value || undefined },
      })).data
      total.value = projects.value.length
    } else {
      const { data } = await http.get('/experiments/management', {
        params: {
          academicTermId: termId.value || undefined,
          arrangementMode: modeFilter.value || undefined,
          status: statusFilter.value || undefined,
          offeringCollegeId: offeringCollegeId.value || undefined,
          courseKeyword: courseKeyword.value.trim() || undefined,
          classKeyword: classKeyword.value.trim() || undefined,
          teacherKeyword: teacherKeyword.value.trim() || undefined,
          taskKeyword: taskKeyword.value.trim() || undefined,
          page: page.value,
          pageSize: pageSize.value,
        },
      })
      projects.value = data.items
      total.value = data.total
    }
  } catch (error) {
    projects.value = []
    total.value = 0
    ElMessage.error(apiErrorMessage(error))
  } finally {
    loading.value = false
  }
}

async function changeTerm() {
  page.value = 1
  selectedProjectIds.value = []
  await loadOptions()
  await load()
}

async function searchTaskOptions() {
  page.value = 1
  selectedProjectIds.value = []
  await loadOptions()
  await load()
}

async function changePage(nextPage: number) {
  page.value = nextPage
  selectedProjectIds.value = []
  await load()
}

async function changePageSize(size: number) {
  pageSize.value = size
  await changePage(1)
}

onMounted(async () => {
  try {
    terms.value = (await http.get('/base-data/terms')).data
    termId.value = defaultAcademicTermId(terms.value) ?? ''
  } catch (error) {
    ElMessage.error(apiErrorMessage(error))
  }
  await loadOptions()
  await load()
})
</script>

<template>
  <div class="page-stack experiment-page">
    <section class="page-intro experiment-intro">
      <div>
        <span class="section-kicker">LABORATORY OPERATIONS</span>
        <h2>{{ isStudent ? '我的实验' : '实验管理' }}</h2>
        <p v-if="isStudent">查看统一安排的实验课次，或为规定实验项目选择适合自己的开放时段。</p>
        <p v-else>集中实验直接绑定已发布课表；仅自主预约实验需要在此开放场次。</p>
      </div>
      <div class="intro-actions">
        <el-button v-if="!isStudent" :icon="Calendar" @click="openBatchSession">
          批量开放场次
        </el-button>
        <el-button v-if="!isStudent" type="primary" :icon="Plus" @click="openCreateProject">
          批量设置实验任务
        </el-button>
        <el-button :icon="Refresh" @click="load">刷新</el-button>
      </div>
    </section>

    <section v-if="!isStudent" class="lab-rail" aria-label="实验安排方式说明">
      <article class="rail-lane centralized">
        <span class="rail-code">FIXED / 集中</span>
        <b>统一课次</b>
        <p>面向整个教学任务名单，时间和实验室固定，像上课一样按安排到场。</p>
      </article>
      <div class="rail-switch" aria-hidden="true"><i /><i /></div>
      <article class="rail-lane flexible">
        <span class="rail-code">OPEN / 自主</span>
        <b>开放预约</b>
        <p>实验内容保持一致，学生在容量允许的场次中选择时间，每个项目限约一次。</p>
      </article>
    </section>

    <section class="experiment-toolbar">
      <el-select v-model="termId" clearable placeholder="全部学期" @change="changeTerm">
        <el-option
          v-for="term in terms"
          :key="term.id"
          :label="academicTermLabel(term)"
          :value="term.id"
          :class="academicTermOptionClass(term)"
        />
      </el-select>
      <template v-if="!isStudent">
        <el-select v-model="offeringCollegeId" clearable filterable placeholder="开课学院" @change="searchTaskOptions">
          <el-option v-for="college in options.colleges" :key="college.collegeId" :label="college.collegeName" :value="college.collegeId" />
        </el-select>
        <el-input v-model="courseKeyword" clearable placeholder="课程名称或编码" @keyup.enter="searchTaskOptions" @clear="searchTaskOptions" />
        <el-input v-model="classKeyword" clearable placeholder="教学班" @keyup.enter="searchTaskOptions" @clear="searchTaskOptions" />
        <el-input v-model="teacherKeyword" clearable placeholder="任课教师" @keyup.enter="searchTaskOptions" @clear="searchTaskOptions" />
        <el-input v-model="taskKeyword" clearable placeholder="教学任务号" @keyup.enter="searchTaskOptions" @clear="searchTaskOptions" />
        <el-select v-model="modeFilter" clearable placeholder="全部安排方式" @change="load">
          <el-option label="集中安排" value="Centralized" />
          <el-option label="自行安排" value="SelfScheduled" />
        </el-select>
        <el-select v-model="statusFilter" clearable placeholder="全部状态" @change="load">
          <el-option label="草稿" value="Draft" />
          <el-option label="进行中" value="Published" />
          <el-option label="已关闭" value="Closed" />
        </el-select>
      </template>
      <span class="result-note">{{ isStudent ? `共 ${projectGroups.length} 门课程、${total} 个实验项目` : `共 ${total} 个教学班课程` }}</span>
    </section>

    <section v-if="!isStudent" class="experiment-batch-bar">
      <el-checkbox :model-value="allPageDraftSelected" :indeterminate="!!draftSelectedProjectIds.length && !allPageDraftSelected" @change="togglePageDraftSelection">
        选中当前页全部草稿
      </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>

    <section v-loading="loading" class="project-list">
      <section v-for="group in projectGroups" :key="group.key" class="project-group">
        <header class="project-group-head">
          <div>
            <span>{{ group.termName }}</span>
            <h3>{{ group.courseCode }} · {{ group.courseName }}</h3>
          </div>
          <div class="project-group-task">
            <b>{{ group.taskNumbers.join('、') }}</b>
            <span>{{ group.classNames?.join('、') || '选课学生' }}</span>
          </div>
        </header>
        <div class="project-group-projects">
          <el-table v-if="!isStudent" :data="group.projects" class="project-table" size="small">
            <el-table-column width="46" align="center">
              <template #default="{ row }">
                <el-checkbox
                  v-if="row.status === 'Draft'"
                  :model-value="selectedProjectIds.includes(row.id)"
                  @change="toggleProjectSelection(row.id, $event)"
                  aria-label="选择实验项目"
                />
              </template>
            </el-table-column>
            <el-table-column label="实验项目" min-width="220">
              <template #default="{ row }">
                <div class="table-project-name">
                  <b>{{ row.code }} · {{ row.name }}</b>
                  <small v-if="row.description">{{ row.description }}</small>
                </div>
              </template>
            </el-table-column>
            <el-table-column label="实验课次 / 场次" min-width="280">
              <template #default="{ row }">
                <div v-if="row.arrangementMode === 'Centralized' && row.scheduleEntry" class="table-schedule">
                  <b>第 {{ row.scheduleEntry.projectWeek ?? `${row.scheduleEntry.startWeek}–${row.scheduleEntry.endWeek}` }} 周 · 星期 {{ row.scheduleEntry.dayOfWeek }} · 第 {{ row.scheduleEntry.startPeriod }}–{{ row.scheduleEntry.startPeriod + row.scheduleEntry.periodCount - 1 }} 节</b>
                  <small>{{ row.scheduleEntry.campusName }} · {{ row.scheduleEntry.buildingName }} {{ row.scheduleEntry.classroomName }}</small>
                </div>
                <div v-else-if="row.sessions.length" class="table-schedule">
                  <b>{{ row.sessions.length }} 个开放场次</b>
                  <small>{{ row.sessions.map((session: any) => formatSessionTime(session)).join('；') }}</small>
                  <el-popover placement="bottom" :width="520" trigger="click">
                    <template #reference><el-button size="small" text :icon="User">查看场次与名单</el-button></template>
                    <div class="student-session-menu">
                      <div v-for="session in row.sessions" :key="session.id" class="student-session-option">
                        <div><b>{{ formatSessionTime(session) }}</b><span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</span></div>
                        <el-button size="small" text @click="showParticipants(row, session)">预约名单</el-button>
                        <el-button v-if="session.status !== 'Cancelled'" size="small" text type="danger" @click="cancelSession(row, session)">{{ row.status === 'Draft' ? '删除' : '取消' }}</el-button>
                      </div>
                    </div>
                  </el-popover>
                </div>
                <span v-else class="table-muted">尚未安排</span>
              </template>
            </el-table-column>
            <el-table-column label="方式" width="100" align="center">
              <template #default="{ row }"><el-tag :type="row.arrangementMode === 'Centralized' ? 'primary' : 'success'" effect="plain">{{ modeMeta[row.arrangementMode].label }}</el-tag></template>
            </el-table-column>
            <el-table-column label="状态" width="88" align="center">
              <template #default="{ row }"><el-tag :type="row.status === 'Published' ? 'success' : row.status === 'Closed' ? 'info' : 'warning'">{{ statusLabels[row.status] }}</el-tag></template>
            </el-table-column>
            <el-table-column label="操作" width="244" fixed="right" align="right">
              <template #default="{ row }">
                <template v-if="row.status === 'Draft'">
                  <el-button size="small" text @click="openEditProject(row)">编辑</el-button>
                  <el-button size="small" text type="danger" @click="deleteProject(row)">删除</el-button>
                  <el-button
                    size="small"
                    type="primary"
                    :disabled="row.arrangementMode === 'Centralized' ? !row.scheduleEntry : !row.sessions.some((item: any) => item.status !== 'Cancelled')"
                    @click="publishProject(row)"
                  >发布</el-button>
                </template>
                <template v-else-if="row.status === 'Published'">
                  <el-button size="small" text @click="openCorrectPublishedProject(row)">修正内容</el-button>
                  <el-button size="small" text @click="closeProject(row)">关闭项目</el-button>
                </template>
                <el-button
                  v-if="row.status !== 'Closed' && row.arrangementMode === 'SelfScheduled'"
                  size="small"
                  text
                  type="primary"
                  @click="openSession(row)"
                >安排场次</el-button>
              </template>
            </el-table-column>
          </el-table>
          <el-table v-else :data="group.projects" class="project-table student-project-table" size="small">
            <el-table-column type="expand" width="44">
              <template #default="{ row }">
                <div class="student-project-detail">
                  <p v-if="row.description"><b>项目说明</b>{{ row.description }}</p>
                  <p v-if="row.requirements"><b>实验要求</b>{{ row.requirements }}</p>
                  <p><b>开放日期</b>{{ row.startDate }} — {{ row.endDate }}</p>
                  <p><b>任课教师</b>{{ row.teacherNames.join('、') || '待定' }}</p>
                </div>
              </template>
            </el-table-column>
            <el-table-column label="实验项目" min-width="210">
              <template #default="{ row }">
                <div class="table-project-name">
                  <b>{{ row.code }} · {{ row.name }}</b>
                  <small>{{ row.description || modeMeta[row.arrangementMode].note }}</small>
                </div>
              </template>
            </el-table-column>
            <el-table-column label="实验安排" min-width="300">
              <template #default="{ row }">
                <div v-if="row.arrangementMode === 'Centralized' && row.scheduleEntry" class="table-schedule">
                  <b>第 {{ row.scheduleEntry.projectWeek ?? `${row.scheduleEntry.startWeek}–${row.scheduleEntry.endWeek}` }} 周 · 星期 {{ row.scheduleEntry.dayOfWeek }} · 第 {{ row.scheduleEntry.startPeriod }}–{{ row.scheduleEntry.startPeriod + row.scheduleEntry.periodCount - 1 }} 节</b>
                  <small>{{ row.scheduleEntry.campusName }} · {{ row.scheduleEntry.buildingName }} {{ row.scheduleEntry.classroomName }}</small>
                </div>
                <div v-else-if="row.myBooking && selectedSession(row)" class="table-schedule is-booked">
                  <b>{{ formatSessionTime(selectedSession(row)) }}</b>
                  <small>{{ selectedSession(row).campusName }} · {{ selectedSession(row).buildingName }} {{ selectedSession(row).classroomName }}</small>
                </div>
                <div v-else-if="row.sessions.length" class="table-schedule">
                  <b>{{ row.sessions.length }} 个可选场次</b>
                  <small>尚未预约，请在右侧选择实验时间</small>
                </div>
                <span v-else class="table-muted">暂无有效实验安排</span>
              </template>
            </el-table-column>
            <el-table-column label="方式" width="100" align="center">
              <template #default="{ row }"><el-tag :type="row.arrangementMode === 'Centralized' ? 'primary' : 'success'" effect="plain" size="small">{{ modeMeta[row.arrangementMode].label }}</el-tag></template>
            </el-table-column>
            <el-table-column label="状态" width="88" align="center">
              <template #default="{ row }"><el-tag :type="row.status === 'Published' ? 'success' : 'info'" size="small">{{ statusLabels[row.status] }}</el-tag></template>
            </el-table-column>
            <el-table-column label="预约操作" width="190" fixed="right" align="right">
              <template #default="{ row }">
                <template v-if="row.arrangementMode === 'SelfScheduled'">
                  <el-popover placement="bottom-end" :width="540" trigger="click">
                    <template #reference>
                      <el-button size="small" :type="row.myBooking ? 'success' : 'primary'" plain>
                        {{ row.myBooking ? '查看场次' : '选择场次' }}
                      </el-button>
                    </template>
                    <div class="student-session-menu">
                      <header><b>{{ row.code }} · {{ row.name }}</b><span>每个实验项目限约一个场次</span></header>
                      <div v-for="session in row.sessions" :key="session.id" class="student-session-option" :class="{ 'is-selected': row.myBooking?.experimentSessionId === session.id }">
                        <div>
                          <b>{{ formatSessionTime(session) }}</b>
                          <span>{{ session.campusName }} · {{ session.buildingName }} {{ session.classroomName }}</span>
                        </div>
                        <el-tag :type="sessionTagType(session)" size="small" effect="plain">余 {{ session.remainingCount }} / {{ session.capacity }}</el-tag>
                        <el-button v-if="row.myBooking?.experimentSessionId === session.id" size="small" type="success" :icon="Check" disabled>已预约</el-button>
                        <el-button v-else size="small" type="primary" plain :disabled="row.status !== 'Published' || !!row.myBooking || session.remainingCount <= 0" @click="book(row, session)">预约</el-button>
                      </div>
                      <el-empty v-if="!row.sessions.length" :image-size="42" description="暂无有效实验场次" />
                    </div>
                  </el-popover>
                  <el-button v-if="row.myBooking && row.status === 'Published'" size="small" text type="danger" @click="cancelBooking(row)">取消</el-button>
                </template>
                <span v-else class="table-muted">无需预约</span>
              </template>
            </el-table-column>
          </el-table>
        </div>
      </section>

      <el-empty
        v-if="!loading && !projects.length"
        :description="isStudent ? '当前学期没有已发布的实验项目' : '当前筛选条件下暂无实验项目'"
      >
        <el-button v-if="!isStudent" type="primary" @click="openCreateProject">
          新建第一个实验项目
        </el-button>
      </el-empty>
    </section>

    <el-pagination
      v-if="!isStudent && total > pageSize"
      class="experiment-pagination"
      background
      layout="total, sizes, prev, pager, next"
      :current-page="page"
      :page-size="pageSize"
      :page-sizes="[10, 20, 50, 100]"
      :total="total"
      @current-change="changePage"
      @size-change="changePageSize"
    />

    <el-dialog
      v-model="projectDialog"
      :title="correctingPublishedProject ? '修正已发布实验项目内容' : editingProjectId ? '编辑实验项目' : '批量设置实验任务'"
      width="720px"
      top="5vh"
    >
      <el-form label-position="top" class="experiment-form">
        <el-alert
          v-if="correctingPublishedProject"
          title="已发布项目仅可修正名称、实验内容和到场要求；编码、安排方式、课表绑定及开放日期保持不变。保存后将通知相关学生。"
          type="info"
          :closable="false"
        />
        <div class="form-section">
          <header><span>PROJECT</span><b>规定实验项目</b></header>
          <el-form-item v-if="editingProjectId && projectForm.arrangementMode === 'Centralized'" label="已发布课表中的实验课" required>
            <el-select v-model="projectForm.scheduleEntryId" filterable :disabled="!!editingProjectId" placeholder="选择已安排实验室的实验课" @change="onScheduleEntryChange">
              <el-option
                v-for="entry in options.scheduleEntries"
                :key="entry.id"
                :label="formatScheduleEntry(entry)"
                :value="entry.id"
              />
            </el-select>
            <small class="form-help">显示当前管理范围内全部已发布、已安排实验室的实验课；集中实验复用其时间和实验室，不会再生成独立实验场次。</small>
          </el-form-item>
          <el-form-item v-else :label="editingProjectId ? '所属教学任务' : '适用教学班（可多选）'" required>
            <el-select
              v-if="editingProjectId"
              v-model="projectForm.teachingTaskIds"
              filterable
              disabled
              placeholder="所属教学任务"
              @change="onTaskChange"
            >
              <el-option
                v-for="task in options.tasks"
                :key="task.id"
                :label="`${task.courseCode} · ${task.courseName} · ${task.taskNumber}`"
                :value="task.id"
              />
            </el-select>
            <el-checkbox-group
              v-else
              v-model="projectForm.teachingTaskIds"
              class="grouped-task-picker"
              @change="onTaskChange"
            >
              <div class="task-option-filters">
                <el-select v-model="offeringCollegeId" clearable filterable placeholder="开课学院" @change="searchTaskOptions">
                  <el-option v-for="college in options.colleges" :key="college.collegeId" :label="college.collegeName" :value="college.collegeId" />
                </el-select>
                <el-input v-model="courseKeyword" clearable placeholder="课程名称或代码" @keyup.enter="searchTaskOptions" />
                <el-button @click="searchTaskOptions">查询</el-button>
              </div>
              <section v-for="group in taskGroups" :key="group.key" class="task-picker-course">
                <header>
                  <span>{{ group.termName }}</span>
                  <b>{{ group.courseCode }} · {{ group.courseName }}</b>
                </header>
                <div>
                  <el-checkbox
                    v-for="task in group.tasks"
                    :key="task.id"
                    :value="task.id"
                    :disabled="projectForm.arrangementMode === 'SelfScheduled' && !!selectedTask
                      && (task.academicTermId !== selectedTask.academicTermId
                        || task.courseId !== selectedTask.courseId)"
                  >
                    <b>{{ task.taskNumber }}</b>
                    <span>{{ task.classNames.join('、') || task.name }}</span>
                  </el-checkbox>
                </div>
              </section>
              <el-empty v-if="!taskGroups.length" :image-size="46" description="没有匹配的已发布教学班，请调整筛选条件" />
            </el-checkbox-group>
            <small v-if="!editingProjectId" class="form-help">
              <template v-if="projectForm.arrangementMode === 'Centralized'">已选 {{ projectForm.teachingTaskIds.length }} 个教学班；系统会为每个教学班的全部已发布实验课生成项目。</template>
              <template v-else>已选 {{ projectForm.teachingTaskIds.length }} 个教学班；只能勾选同一学期、同一课程，实验编码、名称、内容和开放日期将一次应用到这些教学任务。</template>
            </small>
          </el-form-item>
          <div class="form-grid two">
            <el-form-item label="项目编码" required>
              <el-input v-model="projectForm.code" :disabled="correctingPublishedProject" maxlength="40" placeholder="如 LAB-01" />
            </el-form-item>
            <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">
          <header><span>ROUTE</span><b>选择运行轨道</b></header>
           <el-radio-group v-model="projectForm.arrangementMode" class="mode-choice" :disabled="!!editingProjectId || correctingPublishedProject">
            <el-radio-button value="Centralized">
              <b>集中安排</b><small>复用已发布课表的实验课</small>
            </el-radio-button>
            <el-radio-button value="SelfScheduled">
              <b>自行安排</b><small>开放多个场次，学生自主预约</small>
            </el-radio-button>
          </el-radio-group>
          <el-form-item label="项目开放日期" required>
            <el-date-picker
              v-model="projectForm.dates"
              :disabled="correctingPublishedProject"
              type="daterange"
              value-format="YYYY-MM-DD"
              range-separator="至"
              start-placeholder="开始日期"
              end-placeholder="结束日期"
            />
          </el-form-item>
        </div>

        <div class="form-section">
          <header><span>BRIEF</span><b>说明学生要完成什么</b></header>
          <el-form-item label="实验内容">
            <el-input
              v-model="projectForm.description"
              type="textarea"
              :rows="3"
              maxlength="1000"
              show-word-limit
              placeholder="说明实验目标、任务和成果"
            />
          </el-form-item>
          <el-form-item label="到场与准备要求">
            <el-input
              v-model="projectForm.requirements"
              type="textarea"
              :rows="2"
              maxlength="1000"
              show-word-limit
              placeholder="如携带材料、安装软件、分组要求"
            />
          </el-form-item>
        </div>
      </el-form>
      <template #footer>
        <el-button @click="projectDialog = false">取消</el-button>
        <el-button type="primary" @click="saveProject">
          {{ correctingPublishedProject
            ? '保存修正'
            : editingProjectId
            ? '保存项目'
            : projectForm.teachingTaskIds.length
              ? `创建 ${projectForm.teachingTaskIds.length} 个项目`
              : '创建项目' }}
        </el-button>
      </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 ?? ''}`"
      width="650px"
    >
      <el-form label-position="top" class="experiment-form">
        <el-alert
          :title="selectedProject?.arrangementMode === 'Centralized'
            ? '系统会检查实验室、任课教师和行政班的课表冲突。'
            : '系统会检查实验室占用；学生预约时再检查个人课表和其他实验。'"
          type="info"
          :closable="false"
          show-icon
        />
        <div class="form-grid two">
          <el-form-item label="实验日期" required>
            <el-date-picker
              v-model="sessionForm.sessionDate"
              type="date"
              value-format="YYYY-MM-DD"
              placeholder="选择日期"
            />
          </el-form-item>
          <el-form-item label="起始节次" required>
            <el-select v-model="sessionForm.startPeriod" placeholder="选择节次">
              <el-option
                v-for="period in activePeriods"
                :key="period.periodNumber"
                :label="period.startsAt
                  ? `${period.name} · ${period.startsAt}–${period.endsAt}`
                  : period.name"
                :value="period.periodNumber"
              />
            </el-select>
          </el-form-item>
          <el-form-item label="连续节数" required>
            <el-input-number v-model="sessionForm.periodCount" :min="1" :max="12" />
          </el-form-item>
          <el-form-item label="场次容量" required>
            <el-input-number v-model="sessionForm.capacity" :min="1" :max="10000" />
          </el-form-item>
        </div>
        <el-form-item label="实验室" required>
          <el-select v-model="sessionForm.classroomId" filterable placeholder="选择实验室或教学场所">
            <el-option
              v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
              :key="room.id"
              :label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
              :value="room.id"
            />
          </el-select>
        </el-form-item>
        <el-form-item label="场次备注">
          <el-input
            v-model="sessionForm.notes"
            maxlength="500"
            placeholder="如分组、设备或材料说明"
          />
        </el-form-item>
      </el-form>
      <template #footer>
        <el-button @click="sessionDialog = false">取消</el-button>
        <el-button type="primary" @click="saveSession">保存场次</el-button>
      </template>
    </el-dialog>

    <el-dialog
      v-model="batchSessionDialog"
      title="批量安排实验场次"
      width="min(1180px, 94vw)"
      top="4vh"
    >
      <el-alert
        title="一次提交整批排课；系统会逐条检查管理范围、开放日期、实验室、课表、教师和班级冲突，任何一条失败都不会写入本批次。"
        type="info"
        :closable="false"
        show-icon
      />
      <el-form label-position="top" class="batch-session-form">
        <el-form-item label="选择实验项目" required>
          <el-select
            v-model="batchSessionProjectIds"
            multiple
            filterable
            collapse-tags
            collapse-tags-tooltip
            placeholder="选择需要一起排课的实验项目"
            @change="syncBatchSessionRows"
          >
            <el-option
              v-for="project in batchProjectOptions"
              :key="project.id"
              :label="`${project.courseCode} · ${project.name} · ${project.taskNumber}`"
              :value="project.id"
            />
          </el-select>
        </el-form-item>
        <div v-if="batchSessionRows.length" class="batch-session-tools">
          <span>共 {{ batchSessionRows.length }} 条排课</span>
          <el-button size="small" @click="applyFirstBatchTime">套用首行日期与节次</el-button>
        </div>
        <div class="batch-session-table">
          <article v-for="(row, index) in batchSessionRows" :key="row.projectId" class="batch-session-row">
            <div class="batch-project-cell">
              <span>{{ index + 1 }}</span>
              <div>
                <b>{{ batchProject(row.projectId)?.name }}</b>
                <small>{{ batchProject(row.projectId)?.taskNumber }} · {{ batchProject(row.projectId)?.classNames.join('、') || '选课学生' }}</small>
              </div>
            </div>
            <el-date-picker
              v-model="row.sessionDate"
              type="date"
              value-format="YYYY-MM-DD"
              placeholder="实验日期"
            />
            <el-select v-model="row.startPeriod" placeholder="起始节次">
              <el-option
                v-for="period in periodsForProject(row.projectId)"
                :key="period.periodNumber"
                :label="period.name"
                :value="period.periodNumber"
              />
            </el-select>
            <el-input-number v-model="row.periodCount" :min="1" :max="12" controls-position="right" />
            <el-select v-model="row.classroomId" filterable placeholder="实验室">
              <el-option
                v-for="room in options.classrooms.filter((item: any) => (Number(item.teachingVenueNature) & (2 | 4 | 8 | 16)) !== 0)"
                :key="room.id"
                :label="`${room.campusName} · ${room.buildingName} ${room.name} · ${room.capacity} 人`"
                :value="room.id"
              />
            </el-select>
            <el-input-number v-model="row.capacity" :min="1" :max="10000" controls-position="right" />
          </article>
        </div>
        <el-empty v-if="!batchSessionRows.length" :image-size="60" description="选择实验项目后，在同一张表中完成排课" />
      </el-form>
      <template #footer>
        <el-button @click="batchSessionDialog = false">取消</el-button>
        <el-button type="primary" :disabled="!batchSessionRows.length" @click="saveBatchSessions">
          提交整批排课
        </el-button>
      </template>
    </el-dialog>

    <el-dialog
      v-model="participantsDialog"
      :title="`${participantSession?.project?.arrangementMode === 'Centralized' ? '应到名单' : '预约名单'} · ${participantSession ? formatSessionTime(participantSession) : ''}`"
      width="680px"
    >
      <el-table v-loading="participantsLoading" :data="participants" max-height="480">
        <el-table-column prop="studentNumber" label="学号" min-width="130" />
        <el-table-column prop="name" label="姓名" min-width="100" />
        <el-table-column prop="className" label="行政班" min-width="160" />
        <el-table-column label="参与方式" width="110">
          <template #default="{ row }">
            <el-tag
              :type="row.participationType === 'Centralized' ? 'primary' : 'success'"
              size="small"
              effect="plain"
            >
              {{ row.participationType === 'Centralized' ? '统一安排' : '学生预约' }}
            </el-tag>
          </template>
        </el-table-column>
      </el-table>
      <el-empty v-if="!participantsLoading && !participants.length" description="当前没有学生名单" />
    </el-dialog>
  </div>
</template>

<style scoped>
.experiment-page { --lab-blue: #245d82; --lab-teal: #168276; --lab-ink: #173041; }
.experiment-intro { align-items: flex-start; }
.intro-actions { display: flex; gap: 8px; align-items: center; }
.lab-rail {
  display: grid;
  grid-template-columns: minmax(0, 1fr) 78px minmax(0, 1fr);
  gap: 0;
  margin-bottom: 16px;
  border: 1px solid #ccdce5;
  background: #f7fafc;
}
.rail-lane { position: relative; padding: 17px 20px 18px 24px; overflow: hidden; }
.rail-lane::before {
  content: "";
  position: absolute;
  inset: 0 auto 0 0;
  width: 5px;
  background: var(--lab-blue);
}
.rail-lane.flexible::before { background: var(--lab-teal); }
.rail-lane b { display: block; margin: 4px 0; color: var(--lab-ink); font-size: 15px; }
.rail-lane p { margin: 0; color: var(--muted); font-size: 12px; line-height: 1.65; }
.rail-code { color: #607987; font: 700 10px/1.2 Consolas, monospace; letter-spacing: .08em; }
.rail-switch {
  position: relative;
  display: grid;
  place-content: center;
  gap: 8px;
  background: linear-gradient(90deg, transparent 49%, #b9cbd5 49%, #b9cbd5 51%, transparent 51%);
}
.rail-switch i { width: 30px; height: 7px; border: 2px solid #7794a5; border-radius: 999px; background: #fff; }
.experiment-toolbar { display: flex; flex-wrap: wrap; align-items: center; gap: 10px; margin-bottom: 16px; }
.experiment-toolbar .el-select { width: 210px; }
.experiment-toolbar .el-input { width: 172px; }
.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; }
.project-group-head { display: flex; align-items: center; justify-content: space-between; gap: 16px; padding: 13px 17px; border-left: 5px solid var(--lab-blue); background: #edf4f6; }
.project-group-head > div:first-child { display: grid; gap: 3px; }
.project-group-head h3 { margin: 0; color: var(--lab-ink); font-size: 15px; }
.project-group-head > div:first-child span { color: #68808d; font: 700 10px/1 Consolas, monospace; letter-spacing: .06em; }
.project-group-task { display: grid; justify-items: end; gap: 3px; color: #5c7481; font-size: 11px; text-align: right; }
.project-group-task b { color: #365364; font-size: 12px; }
.project-group-projects { display: grid; gap: 0; padding: 0; }
.project-table { --el-table-border-color: #dce5ea; --el-table-header-bg-color: #f7fafb; --el-table-row-hover-bg-color: #f6fafb; width: 100%; }
.project-table :deep(th.el-table__cell) { padding: 8px 0; color: #617986; font-size: 11px; font-weight: 650; }
.project-table :deep(td.el-table__cell) { padding: 9px 0; vertical-align: top; }
.student-project-table :deep(td.el-table__cell) { vertical-align: middle; }
.table-project-name, .table-schedule { display: grid; gap: 4px; min-width: 0; }
.table-project-name b { color: var(--lab-ink); font-size: 13px; }
.table-project-name small, .table-schedule small { overflow: hidden; color: #667f8d; font-size: 11px; line-height: 1.45; text-overflow: ellipsis; white-space: nowrap; }
.table-schedule b { color: #365364; font-size: 12px; font-weight: 600; }
.table-schedule.is-booked b { color: var(--lab-teal); }
.table-muted { color: #8b9aa4; font-size: 12px; }
.student-project-detail { display: grid; grid-template-columns: repeat(2, minmax(220px, 1fr)); gap: 7px 20px; padding: 11px 18px 13px; background: #f7fafb; }
.student-project-detail p { margin: 0; display: grid; grid-template-columns: 70px 1fr; gap: 8px; color: #5f7682; font-size: 11px; line-height: 1.55; }
.student-project-detail b { color: #304d5d; }
.student-session-menu { display: grid; gap: 7px; }
.student-session-menu > header { display: flex; justify-content: space-between; gap: 12px; padding-bottom: 8px; border-bottom: 1px solid #dfe7ea; }
.student-session-menu > header b { color: var(--lab-ink); font-size: 12px; }
.student-session-menu > header span { color: #71858e; font-size: 10px; }
.student-session-option { display: grid; grid-template-columns: minmax(0, 1fr) auto auto; align-items: center; gap: 9px; padding: 8px 9px; border: 1px solid #e0e7ea; background: #fff; }
.student-session-option.is-selected { border-color: #79afa5; box-shadow: inset 3px 0 0 var(--lab-teal); background: #f3faf8; }
.student-session-option > div { display: grid; gap: 2px; min-width: 0; }
.student-session-option > div b { color: #304d5d; font-size: 11px; }
.student-session-option > div span { overflow: hidden; color: #6a808b; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.project-card {
  overflow: hidden;
  border: 0;
  border-top: 1px solid #dce5ea;
  border-left: 4px solid var(--lab-blue);
  background: #fff;
  box-shadow: none;
}
.project-card.is-flexible { border-top-color: var(--lab-teal); }
.project-card.is-closed { opacity: .82; }
.project-head { display: flex; justify-content: space-between; gap: 16px; padding: 18px 20px 10px; }
.project-code { color: #667f8d; font: 700 10px/1.2 Consolas, monospace; letter-spacing: .06em; text-transform: uppercase; }
.project-identity h3 { margin: 5px 0 4px; color: var(--lab-ink); font-size: 18px; }
.project-identity p { margin: 0; color: var(--muted); font-size: 12px; }
.project-tags { display: flex; gap: 6px; align-items: flex-start; flex-shrink: 0; }
.project-context { display: flex; flex-wrap: wrap; gap: 7px 22px; padding: 0 20px 12px; color: #5c7481; font-size: 12px; }
.project-context span { display: inline-flex; gap: 7px; }
.project-context b { color: #304b5a; font-weight: 600; }
.project-description { margin: 0; padding: 10px 20px; border-top: 1px solid #edf1f3; color: #415b69; font-size: 13px; line-height: 1.7; }
.requirement-strip {
  display: grid;
  grid-template-columns: 74px 1fr;
  gap: 10px;
  margin: 0 20px 14px;
  padding: 10px 12px;
  border-left: 3px solid #e3a13c;
  background: #fff9ef;
  color: #6d5b3e;
  font-size: 12px;
  line-height: 1.6;
}
.session-board { margin: 0 20px 16px; border: 1px solid #dfe8ec; background: #f8fafb; }
.session-board-head { display: flex; align-items: center; justify-content: space-between; gap: 12px; padding: 11px 13px; border-bottom: 1px solid #dfe8ec; }
.session-board-head div { display: grid; gap: 2px; }
.session-board-head span { color: #617986; font: 700 10px/1.2 Consolas, monospace; letter-spacing: .06em; text-transform: uppercase; }
.session-board-head b { color: #304d5d; font-size: 11px; font-weight: 500; }
.session-grid { display: grid; gap: 8px; padding: 10px; }
.session-ticket {
  display: grid;
  grid-template-columns: 58px minmax(0, 1fr) auto;
  gap: 12px;
  align-items: center;
  min-height: 74px;
  border: 1px solid #dce5e9;
  background: #fff;
  transition: border-color .18s ease, box-shadow .18s ease;
}
.session-ticket:hover { border-color: #9fb7c4; box-shadow: 0 5px 14px rgb(40 76 96 / 7%); }
.session-ticket.is-selected { border-color: var(--lab-teal); box-shadow: inset 4px 0 0 var(--lab-teal); }
.session-ticket.is-cancelled { opacity: .58; }
.ticket-date { display: grid; place-content: center; align-self: stretch; border-right: 1px dashed #cddbe2; background: #f0f5f7; text-align: center; }
.ticket-date strong { color: var(--lab-ink); font: 700 22px/1 Consolas, monospace; }
.ticket-date span { margin-top: 4px; color: #657d89; font-size: 10px; }
.ticket-body { display: grid; gap: 3px; min-width: 0; }
.ticket-body b { color: #243f4f; font-size: 13px; }
.ticket-body span { color: #5c7481; font-size: 12px; }
.ticket-body small { overflow: hidden; color: #81929b; font-size: 10px; text-overflow: ellipsis; white-space: nowrap; }
.ticket-action { display: flex; align-items: center; justify-content: flex-end; gap: 5px; padding-right: 10px; }
.project-actions { display: flex; justify-content: flex-end; gap: 8px; padding: 12px 20px; border-top: 1px solid #e5ecef; background: #fbfcfd; }
.student-booking-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 12px;
  padding: 12px 20px;
  border-top: 1px solid #cce3de;
  background: #f0faf7;
}
.student-booking-summary > div { display: flex; align-items: center; gap: 10px; color: var(--lab-teal); }
.student-booking-summary svg { width: 22px; }
.student-booking-summary span { display: grid; gap: 2px; color: #365f5b; font-size: 12px; }
.student-booking-summary b { font-size: 10px; text-transform: uppercase; letter-spacing: .06em; }
.experiment-form { display: grid; gap: 13px; }
.grouped-task-picker { display: grid; gap: 10px; max-height: 310px; overflow: auto; padding: 10px; border: 1px solid #d8e3e8; background: #fff; }
.task-option-filters { display: grid; grid-template-columns: minmax(150px, 1fr) minmax(180px, 1.2fr) auto; gap: 8px; }
.task-picker-course { overflow: hidden; border: 1px solid #e0e8eb; }
.task-picker-course header { display: flex; align-items: baseline; gap: 8px; padding: 8px 10px; background: #f3f7f8; }
.task-picker-course header span { color: #738994; font: 700 10px/1 Consolas, monospace; }
.task-picker-course header b { color: #314f60; font-size: 12px; }
.task-picker-course > div { display: grid; grid-template-columns: repeat(auto-fit, minmax(210px, 1fr)); gap: 2px 12px; padding: 8px 10px; }
.task-picker-course :deep(.el-checkbox) { height: auto; margin-right: 0; white-space: normal; }
.task-picker-course :deep(.el-checkbox__label) { display: inline-flex; gap: 6px; min-width: 0; color: #536b78; font-size: 12px; }
.task-picker-course :deep(.el-checkbox__label b) { flex: 0 0 auto; color: #284757; }
.form-help { display: block; margin-top: 7px; color: var(--muted); line-height: 1.5; }
.form-section { padding: 14px 15px 1px; border: 1px solid var(--line); background: #fbfcfd; }
.form-section > header { display: flex; align-items: baseline; gap: 9px; margin-bottom: 13px; }
.form-section > header span { color: var(--lab-teal); font: 700 10px/1 Consolas, monospace; letter-spacing: .08em; }
.form-section > header b { color: var(--lab-ink); font-size: 14px; }
.mode-choice { display: grid; grid-template-columns: 1fr 1fr; margin-bottom: 16px; }
.mode-choice :deep(.el-radio-button__inner) { display: grid; gap: 5px; width: 100%; padding: 13px; }
.mode-choice b { font-size: 13px; }
.mode-choice small { font-size: 10px; font-weight: 400; }
.batch-session-form { display: grid; gap: 12px; margin-top: 16px; }
.batch-session-tools { display: flex; align-items: center; justify-content: space-between; color: var(--muted); font-size: 12px; }
.batch-session-table { display: grid; gap: 8px; overflow-x: auto; padding-bottom: 4px; }
.batch-session-row {
  display: grid;
  grid-template-columns: minmax(230px, 1.4fr) 150px 120px 110px minmax(230px, 1.3fr) 110px;
  gap: 8px;
  align-items: center;
  min-width: 1000px;
  padding: 10px;
  border: 1px solid #dce5e9;
  background: #f9fbfc;
}
.batch-project-cell { display: flex; align-items: center; gap: 10px; min-width: 0; }
.batch-project-cell > span { display: grid; width: 25px; height: 25px; place-items: center; border-radius: 50%; background: #e5f0f5; color: var(--lab-blue); font: 700 11px/1 Consolas, monospace; }
.batch-project-cell > div { display: grid; gap: 3px; min-width: 0; }
.batch-project-cell b, .batch-project-cell small { overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
.batch-project-cell b { color: var(--lab-ink); font-size: 12px; }
.batch-project-cell small { color: var(--muted); font-size: 10px; }
@media (prefers-reduced-motion: reduce) {
  .session-ticket { transition: none; }
}
@media (max-width: 760px) {
  .lab-rail { grid-template-columns: 1fr; }
  .rail-switch { display: none; }
  .rail-lane { border-bottom: 1px solid #d5e1e7; }
  .experiment-toolbar .el-select { width: 100%; }
  .result-note { width: 100%; margin-left: 0; }
  .project-head { align-items: flex-start; padding: 15px 14px 9px; }
  .project-context, .project-description { padding-inline: 14px; }
  .requirement-strip, .session-board { margin-inline: 14px; }
  .session-ticket { grid-template-columns: 54px minmax(0, 1fr); }
  .ticket-action { grid-column: 1 / -1; justify-content: flex-start; padding: 0 10px 10px; }
  .project-actions, .student-booking-summary { padding-inline: 14px; }
  .student-project-detail { grid-template-columns: 1fr; }
  .mode-choice { grid-template-columns: 1fr; }
  .intro-actions { flex-wrap: wrap; justify-content: flex-end; }
}
</style>
