Files
education/frontend/src/views/teachOffice/shiftSemester/ClassCalendarDialog.vue
T
2026-08-21 18:42:51 +08:00

907 lines
28 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.
<template>
<el-dialog
:visible="dialogVisible"
title="班历设置"
width="94%"
top="4vh"
:close-on-click-modal="false"
class="class-calendar-dialog"
@update:visible="val => dialogVisible = val"
>
<div class="class-calendar">
<!-- ==================== 1. 学期与班次信息 ==================== -->
<div class="info-panel">
<div class="info-title-row">
<span class="semester-name">{{ semesterName }}</span>
<span class="class-name">班次{{ bc }}</span>
</div>
<div class="date-range-row">
<div class="range-item">{{ calStart }}{{ calEnd }}{{ calWeeks }}</div>
<div class="range-item">{{ classStart }}{{ classEnd }}{{ classWeeks }}</div>
</div>
</div>
<!-- ==================== 2. 设置面板事件设置 + 操作按钮 ==================== -->
<div class="settings-panel">
<div class="event-settings-row">
<div class="setting-item">
<span class="setting-label">事件名称</span>
<el-input v-model="eventName" size="small" style="width: 140px" placeholder="" />
</div>
<el-checkbox v-model="isBold" size="small">加粗显示</el-checkbox>
<el-checkbox v-model="isSchedulable" size="small">可排课</el-checkbox>
<el-checkbox v-model="isRegularClass" size="small">正课</el-checkbox>
<el-checkbox v-model="showRemark" size="small">课表备注中显示</el-checkbox>
<div class="setting-item">
<span class="setting-label">备注</span>
<el-input v-model="remark" size="small" style="width: 150px" placeholder="" />
</div>
<el-checkbox v-model="show78" size="small">显示7/8节</el-checkbox>
<el-checkbox v-model="showEvening" size="small">显示晚上</el-checkbox>
<el-checkbox v-model="showNight" size="small">显示夜间</el-checkbox>
</div>
<!-- ==================== 3. 操作按钮区域 ==================== -->
<div class="action-buttons-row">
<button type="button" class="gray-btn" @click="handleAbsorb">吸取</button>
<button type="button" class="gray-btn" @click="handleSet">设定</button>
<button type="button" class="gray-btn" @click="handleDelete">删除</button>
<button type="button" class="blue-btn" @click="handleApplyOther">选定区域设置应用于其它班次</button>
<button type="button" class="gray-btn" @click="handleApplyWhole">整个班历应用于其它班次</button>
<button type="button" class="gray-btn" @click="handleRefresh">刷新</button>
</div>
</div>
<!-- ==================== 4. 时间编排区域 ==================== -->
<div class="table-area">
<div class="calendar-table-wrapper">
<table class="calendar-table">
<thead>
<tr class="head-row-1">
<th rowspan="2" class="cell-week-head">周次</th>
<th rowspan="2" class="cell-range-head">日期段</th>
<th v-for="name in dayNames" :key="name" :colspan="visiblePeriods.length">{{ name }}</th>
</tr>
<tr class="head-row-2">
<template v-for="(name, di) in dayNames">
<th v-for="p in visiblePeriods" :key="name + '-' + p.key">{{ p.label }}</th>
</template>
</tr>
</thead>
<tbody>
<tr v-for="(w, wi) in weekDefs" :key="w.num">
<td class="week-num">{{ w.num }}</td>
<td class="date-range">{{ w.start }}{{ w.end }}</td>
<template v-for="d in 5">
<td
v-for="p in visiblePeriods"
:key="wi + '-' + (d - 1) + '-' + p.key"
class="cell"
:class="cellClass(getCell(wi, d - 1, p.key))"
:style="cellStyle(getCell(wi, d - 1, p.key))"
:title="getCell(wi, d - 1, p.key).isDate ? '该时间格无事件,可选中后设定事件' : '双击可吸取该事件设置'"
@mousedown.prevent="onCellMouseDown(getCell(wi, d - 1, p.key), $event)"
@mousemove="onCellMouseMove(getCell(wi, d - 1, p.key))"
@dblclick="handleCellDblClick(getCell(wi, d - 1, p.key))"
>{{ getCell(wi, d - 1, p.key).text }}</td>
</template>
</tr>
</tbody>
</table>
</div>
<div class="select-tip">
提示在时间编排区域按住鼠标拖动可连片多选按住 Ctrl 键可用鼠标选择多个区域双击事件格可吸取其设置
</div>
</div>
</div>
<!-- ==================== 学员队选取弹窗 ==================== -->
<ClassTeamSelectDialog
:visible="teamSelectVisible"
@update:visible="val => teamSelectVisible = val"
@confirm="handleTeamConfirm"
/>
</el-dialog>
</template>
<script>
import ClassTeamSelectDialog from './ClassTeamSelectDialog.vue'
// ==================== 模块级常量 ====================
/** 时段定义:1-2、3-4、5-6 恒显示;7-8 / 9-10(晚上) / 夜间 由复选框控制 */
const ALL_PERIODS = [
{ key: '1-2', label: '1-2' },
{ key: '3-4', label: '3-4' },
{ key: '5-6', label: '5-6' },
{ key: '7-8', label: '7-8' },
{ key: '9-10', label: '9-10' },
{ key: 'night', label: '夜间' }
]
/** 事件模式下标 → ALL_PERIODS 下标(1-2、3-4、5-6、9-10 */
const PATTERN_PERIOD_IDX = [0, 1, 2, 4]
/** 事件类型 → 颜色 */
const TYPE_STYLE = {
red: { bg: '#d94343', fg: '#ffffff' }, // 正常课程/活动(马基、保密、布雷、维修、布修、战术、国庆节、政工)
dark: { bg: '#2a8a7f', fg: '#ffffff' }, // 深青/墨绿:游泳、道德、日期标记
green: { bg: '#4f9d57', fg: '#ffffff' }, // 特殊课程/活动(核心、组训)
cyan: { bg: '#8fd4e0', fg: '#14505a' } // 青/浅蓝:学前、元旦
}
const E = (text, type) => ({ text, type })
const D = null // 日期格:显示当天日期
/** 班历事件(第8周~第29周,严格按截图还原;null 为日期格) */
const weekDefs = [
// 学前适应(青)
{
num: 8, start: '08-21', end: '08-27',
monday: [E('学前', 'cyan'), E('学前', 'cyan'), E('学前', 'cyan'), E('学前', 'cyan')],
others: [E('学前', 'cyan'), E('学前', 'cyan'), E('学前', 'cyan'), E('学前', 'cyan')]
},
// 马基(红)+ 游泳(墨绿)
{
num: 9, start: '08-28', end: '09-03',
monday: [E('马基', 'red'), E('游泳', 'dark'), D, D],
others: [E('马基', 'red'), E('游泳', 'dark'), D, D]
},
{
num: 10, start: '09-04', end: '09-10',
monday: [E('马基', 'red'), E('游泳', 'dark'), D, D],
others: [E('马基', 'red'), E('游泳', 'dark'), D, D]
},
// 马基(红)+ 保密(红)
{
num: 11, start: '09-11', end: '09-17',
monday: [E('马基', 'red'), E('保密', 'red'), D, D],
others: [E('马基', 'red'), E('保密', 'red'), D, D]
},
{
num: 12, start: '09-18', end: '09-24',
monday: [E('马基', 'red'), E('保密', 'red'), D, D],
others: [E('马基', 'red'), E('保密', 'red'), D, D]
},
// 布雷(红)
{
num: 13, start: '09-25', end: '10-01',
monday: [E('布雷', 'red'), E('布雷', 'red'), D, D],
others: [E('布雷', 'red'), E('布雷', 'red'), D, D]
},
// 国庆节(红)
{
num: 14, start: '10-02', end: '10-08',
monday: [E('国庆节', 'red'), E('国庆节', 'red'), E('国庆节', 'red'), E('国庆节', 'red')],
others: [E('国庆节', 'red'), E('国庆节', 'red'), E('国庆节', 'red'), E('国庆节', 'red')]
},
{
num: 15, start: '10-09', end: '10-15',
monday: [E('布雷', 'red'), E('布雷', 'red'), D, D],
others: [E('布雷', 'red'), E('布雷', 'red'), D, D]
},
{
num: 16, start: '10-16', end: '10-22',
monday: [E('布雷', 'red'), E('布雷', 'red'), D, D],
others: [E('布雷', 'red'), E('布雷', 'red'), D, D]
},
{
num: 17, start: '10-23', end: '10-29',
monday: [E('布雷', 'red'), E('布雷', 'red'), D, D],
others: [E('布雷', 'red'), E('布雷', 'red'), D, D]
},
// 维修(红)
{
num: 18, start: '10-30', end: '11-05',
monday: [E('维修', 'red'), E('维修', 'red'), D, D],
others: [E('维修', 'red'), E('维修', 'red'), D, D]
},
// 维修(红)+ 核心(绿)
{
num: 19, start: '11-06', end: '11-12',
monday: [E('维修', 'red'), E('维修', 'red'), E('核心', 'green'), D],
others: [E('维修', 'red'), E('维修', 'red'), E('核心', 'green'), D]
},
// 布修(红)
{
num: 20, start: '11-13', end: '11-19',
monday: [E('布修', 'red'), E('布修', 'red'), D, D],
others: [E('布修', 'red'), E('布修', 'red'), D, D]
},
{
num: 21, start: '11-20', end: '11-26',
monday: [E('布修', 'red'), E('布修', 'red'), D, D],
others: [E('布修', 'red'), E('布修', 'red'), D, D]
},
{
num: 22, start: '11-27', end: '12-03',
monday: [E('布修', 'red'), E('布修', 'red'), D, D],
others: [E('布修', 'red'), E('布修', 'red'), D, D]
},
// 维修(红)
{
num: 23, start: '12-04', end: '12-10',
monday: [E('维修', 'red'), E('维修', 'red'), D, D],
others: [E('维修', 'red'), E('维修', 'red'), D, D]
},
{
num: 24, start: '12-11', end: '12-17',
monday: [E('维修', 'red'), E('维修', 'red'), D, D],
others: [E('维修', 'red'), E('维修', 'red'), D, D]
},
{
num: 25, start: '12-18', end: '12-24',
monday: [E('维修', 'red'), E('维修', 'red'), D, D],
others: [E('维修', 'red'), E('维修', 'red'), D, D]
},
// 战术(红)
{
num: 26, start: '12-25', end: '12-31',
monday: [E('战术', 'red'), E('战术', 'red'), E('战术', 'red'), E('战术', 'red')],
others: [E('战术', 'red'), E('战术', 'red'), E('战术', 'red'), E('战术', 'red')]
},
// 元旦(青,周一)+ 组训(绿,周二~周五)
{
num: 27, start: '01-01', end: '01-07',
monday: [E('元旦', 'cyan'), E('元旦', 'cyan'), E('元旦', 'cyan'), E('元旦', 'cyan')],
others: [E('组训', 'green'), E('组训', 'green'), E('组训', 'green'), E('组训', 'green')]
},
// 政工(红)+ 道德(墨绿)
{
num: 28, start: '01-08', end: '01-14',
monday: [E('政工', 'red'), E('道德', 'dark'), E('道德', 'dark'), E('0106', 'dark')],
others: [E('政工', 'red'), E('道德', 'dark'), E('道德', 'dark'), D]
},
{
num: 29, start: '01-15', end: '01-21',
monday: [E('政工', 'red'), E('道德', 'dark'), E('道德', 'dark'), E('0115', 'dark')],
others: [E('政工', 'red'), E('道德', 'dark'), E('道德', 'dark'), D]
}
]
/** 第8周周一(起始日期) */
const START_DATE = '2017-08-21'
export default {
name: 'ClassCalendarDialog',
components: { ClassTeamSelectDialog },
props: {
visible: { type: Boolean, default: false },
/** 班次号(如 0203 */
bc: { type: String, default: '0399' },
/** 学期名称 */
semesterName: { type: String, default: '2017年秋季学期' }
},
data() {
return {
// ==================== 学期与班次信息 ====================
/** 校历范围 */
calStart: '2017-07-08',
calEnd: '2018-02-11',
calWeeks: 32,
/** 班历范围(校历基础上为班次安排) */
classStart: '2017-08-24',
classEnd: '2018-02-05',
classWeeks: 25,
// ==================== 事件设置区域 ====================
eventName: '',
isBold: false, // 加粗显示
isSchedulable: false, // 可排课
isRegularClass: false, // 正课
showRemark: false, // 课表备注中显示
remark: '',
show78: false, // 显示7/8节
showEvening: true, // 显示晚上(已勾选)
showNight: false, // 显示夜间
// ==================== 班历表格数据 ====================
dayNames: ['一', '二', '三', '四', '五'],
weekDefs,
allCells: this.buildCells(),
cellMap: new Map(),
selectedIds: new Set(),
// ==================== 学员队选取弹窗 ====================
teamSelectVisible: false,
/** 当前应用模式:region=选定区域 / whole=整个班历 */
applyMode: 'region'
}
},
computed: {
dialogVisible: {
get() {
return this.visible
},
set(val) {
this.$emit('update:visible', val)
}
},
/** 时间编排区域实际显示的时段(由三个复选框控制) */
visiblePeriods() {
return ALL_PERIODS.filter((p) => {
if (p.key === '7-8') return this.show78
if (p.key === '9-10') return this.showEvening
if (p.key === 'night') return this.showNight
return true
})
}
},
watch: {
// 每次打开对话框时重置选择状态
visible(val) {
if (val) {
this.selectedIds = new Set()
}
}
},
created() {
// 初始化非响应式拖拽状态
this.dragActive = false
this.dragMoved = false
this.dragCtrl = false
this.anchorCell = null
this.anchorPos = { r: 0, c: 0 }
this.lastPos = { r: 0, c: 0 }
this.dragSnapshot = new Set()
// 构建 cellMap 索引
this.allCells.forEach((c) => this.cellMap.set(c.id, c))
},
mounted() {
document.addEventListener('mouseup', this.endSelect)
document.addEventListener('click', this.onDocClick)
},
beforeDestroy() {
document.removeEventListener('mouseup', this.endSelect)
document.removeEventListener('click', this.onDocClick)
},
methods: {
/** 计算从起始日期偏移 n 周 + m 天后的 MMDD(替代 dayjs */
formatDateOffset(weeks, days) {
const date = new Date(START_DATE + 'T00:00:00')
date.setDate(date.getDate() + weeks * 7 + days)
const mm = String(date.getMonth() + 1).padStart(2, '0')
const dd = String(date.getDate()).padStart(2, '0')
return mm + dd
},
/** 根据周定义生成时间格数据(周一~周五 × 全部时段) */
buildCells() {
const cells = []
weekDefs.forEach((w, wi) => {
for (let d = 0; d < 5; d++) {
const dateText = this.formatDateOffset(wi, d)
const pattern = d === 0 ? w.monday : w.others
for (let pi = 0; pi < ALL_PERIODS.length; pi++) {
const period = ALL_PERIODS[pi]
const isSpecialPeriod = period.key === '7-8' || period.key === 'night'
const dj = PATTERN_PERIOD_IDX.indexOf(pi)
const def = isSpecialPeriod ? null : (pattern[dj] !== undefined ? pattern[dj] : null)
const isEvent = !!(def && def.text)
cells.push({
id: `${wi}-${d}-${period.key}`,
weekIdx: wi,
day: d,
periodKey: period.key,
dateText,
text: isEvent ? def.text : dateText,
isDate: !isEvent,
type: isEvent ? def.type : 'dark',
bold: false,
schedulable: true,
regular: false,
remark: false
})
}
}
})
return cells
},
getCell(weekIdx, day, periodKey) {
return this.cellMap.get(`${weekIdx}-${day}-${periodKey}`)
},
/** 单元格样式:事件格用事件色;日期格按星期交替背景色(一、三、五深绿 / 二、四浅绿) */
cellStyle(cell) {
if (cell.isDate) {
return cell.day % 2 === 0
? { backgroundColor: '#2a8a7f', color: '#ffffff' }
: { backgroundColor: '#d6e9e2', color: '#20604f' }
}
const st = TYPE_STYLE[cell.type]
return { backgroundColor: st.bg, color: st.fg }
},
/** 单元格类:选中态 / 选项语义 */
cellClass(cell) {
return {
'is-event': !cell.isDate,
'is-date': cell.isDate,
'is-bold': !cell.isDate && cell.bold,
selected: this.selectedIds.has(cell.id),
'not-schedulable': !cell.isDate && !cell.schedulable,
'is-regular': !cell.isDate && cell.regular
}
},
// ==================== 多选交互(拖动连片 + Ctrl 多区域) ====================
/** 计算格子在当前显示列布局下的行列坐标 */
getPos(cell) {
const periodIdx = this.visiblePeriods.findIndex((p) => p.key === cell.periodKey)
return { r: cell.weekIdx, c: cell.day * this.visiblePeriods.length + periodIdx }
},
/** 计算 anchor 与 last 之间矩形内的所有格子 id */
rectIds(a, b) {
const r1 = Math.min(a.r, b.r), r2 = Math.max(a.r, b.r)
const c1 = Math.min(a.c, b.c), c2 = Math.max(a.c, b.c)
const ids = []
this.allCells.forEach((cell) => {
const p = this.getPos(cell)
if (p.r >= r1 && p.r <= r2 && p.c >= c1 && p.c <= c2) ids.push(cell.id)
})
return ids
},
updateSelection() {
const ids = this.rectIds(this.anchorPos, this.lastPos)
this.selectedIds = this.dragCtrl
? new Set([...this.dragSnapshot, ...ids])
: new Set(ids)
},
onCellMouseDown(cell, e) {
if (e.button !== 0) return
this.dragActive = true
this.dragMoved = false
this.dragCtrl = e.ctrlKey || e.metaKey
this.dragSnapshot = new Set(this.selectedIds)
this.anchorCell = cell
this.anchorPos = this.getPos(cell)
this.lastPos = { ...this.anchorPos }
// 非 Ctrl 按下时立即清空旧选择(Ctrl 拖动则保留并追加)
if (!this.dragCtrl) this.selectedIds = new Set()
},
onCellMouseMove(cell) {
if (!this.dragActive) return
const pos = this.getPos(cell)
if (pos.r === this.lastPos.r && pos.c === this.lastPos.c) return
this.lastPos = pos
this.dragMoved = true
this.updateSelection()
},
endSelect() {
if (!this.dragActive) return
this.dragActive = false
if (!this.dragMoved && this.anchorCell) {
if (this.dragCtrl) {
// Ctrl + 单击:切换单个格子
const set = new Set(this.selectedIds)
if (set.has(this.anchorCell.id)) set.delete(this.anchorCell.id)
else set.add(this.anchorCell.id)
this.selectedIds = set
} else {
// 普通单击:单选该格
this.selectedIds = new Set([this.anchorCell.id])
}
}
},
/** 点击时间编排区域以外的空白区域时取消选择(按钮/输入框等交互元素不触发) */
onDocClick(e) {
const t = e.target
if (!t || !t.closest) return
if (t.closest('.calendar-table')) return
if (t.closest('button, input, .el-button, .el-checkbox, .el-select, .el-input, .el-date-editor')) return
this.selectedIds = new Set()
},
// ==================== 事件操作 ====================
/** 将事件格设置复制到工具栏(吸取) */
applyToToolbar(cell) {
this.eventName = cell.text
this.isBold = cell.bold
this.isSchedulable = cell.schedulable
this.isRegularClass = cell.regular
this.showRemark = cell.remark
},
/** 吸取:将选中事件格的名称和选项设置复制到工具栏 */
handleAbsorb() {
const cell = [...this.selectedIds].map((id) => this.cellMap.get(id)).find((c) => c && !c.isDate)
if (!cell) {
this.$message.warning('请先选择要吸取的事件格')
return
}
this.applyToToolbar(cell)
this.$message.success('已吸取事件设置到工具栏,可像刷子一样快速设定')
},
/** 双击事件格:直接吸取 */
handleCellDblClick(cell) {
if (cell.isDate) {
this.$message.info('该时间格没有事件,无法吸取')
return
}
this.applyToToolbar(cell)
this.$message.success(`已吸取「${cell.text}」的设置到工具栏`)
},
/** 设定:将事件名称和选项应用到所选时间格 */
handleSet() {
const ids = [...this.selectedIds]
if (!ids.length) {
this.$message.warning('请先在时间编排区域选择时间格')
return
}
const name = this.eventName.trim()
if (!name) {
this.$message.warning('请先输入事件名称')
return
}
ids.forEach((id) => {
const c = this.cellMap.get(id)
c.text = name
c.isDate = false
c.type = 'red'
c.bold = this.isBold
c.schedulable = this.isSchedulable
c.regular = this.isRegularClass
c.remark = this.showRemark
})
this.$message.success(`已为 ${ids.length} 个时间格设定事件「${name}`)
},
/** 删除:清除所选时间格的事件,恢复日期显示、低暗背景 */
handleDelete() {
const ids = [...this.selectedIds]
if (!ids.length) {
this.$message.warning('请先选择要删除事件的时间格')
return
}
ids.forEach((id) => {
const c = this.cellMap.get(id)
c.isDate = true
c.text = c.dateText
c.bold = false
c.schedulable = true
c.regular = false
c.remark = false
})
this.$message.success(`已删除 ${ids.length} 个时间格的事件`)
},
/** 选定区域设置应用于其它班次:先选择时间格,再弹出学员队选取窗口 */
handleApplyOther() {
if (!this.selectedIds.size) {
this.$message.warning('请先在时间编排区域选择时间格')
return
}
this.applyMode = 'region'
this.teamSelectVisible = true
},
/** 整个班历应用于其它班次:弹出学员队选取窗口 */
handleApplyWhole() {
this.applyMode = 'whole'
this.teamSelectVisible = true
},
/** 学员队选取确认后应用 */
handleTeamConfirm() {
if (this.applyMode === 'whole') {
// 后端接口未提供,仅前端演示:将当前班历完整复制到所选班次
this.$message.success('整个班历已完整复制到所选班次(后端接口未提供,前端演示)')
} else {
this.$message.success('已应用选定区域设置到所选班次(前端演示)')
}
},
/** 刷新:恢复初始班历数据 */
handleRefresh() {
this.allCells.splice(0, this.allCells.length, ...this.buildCells())
this.cellMap.clear()
this.allCells.forEach((c) => this.cellMap.set(c.id, c))
this.selectedIds = new Set()
this.eventName = ''
this.$message.success('已刷新班历')
}
}
}
</script>
<style scoped lang="scss">
// `class` 会直接加在 .el-dialog 元素自身,因此高度直接写在这里(不用 :deep)
.class-calendar-dialog {
display: flex;
flex-direction: column;
height: 92vh;
margin: 4vh auto;
overflow: hidden;
::v-deep .el-dialog__header {
flex-shrink: 0;
margin-right: 0;
}
::v-deep .el-dialog__body {
display: flex;
flex-direction: column;
padding: 14px 18px 18px;
overflow: hidden;
flex: 1;
min-height: 0;
}
}
.class-calendar {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
// ========== 1. 学期与班次信息 ==========
.info-panel {
flex-shrink: 0;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 20px;
margin-bottom: 12px;
.info-title-row {
display: flex;
align-items: baseline;
gap: 20px;
margin-bottom: 8px;
.semester-name,
.class-name {
font-size: 22px;
font-weight: 700;
color: #1e6fff;
}
}
.date-range-row {
display: flex;
flex-direction: column;
gap: 4px;
.range-item {
font-size: 13px;
font-weight: 600;
color: #1e6fff;
}
}
}
// ========== 2. 设置面板 ==========
.settings-panel {
flex-shrink: 0;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
padding: 12px 20px;
margin-bottom: 12px;
.event-settings-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 14px;
margin-bottom: 12px;
.setting-item {
display: flex;
align-items: center;
gap: 6px;
.setting-label {
font-size: 13px;
color: #606266;
white-space: nowrap;
}
}
}
.action-buttons-row {
display: flex;
align-items: center;
flex-wrap: wrap;
gap: 8px;
padding-top: 10px;
border-top: 1px solid #ebeef5;
}
}
// ========== 3. 银灰色 / 蓝色边框按钮 ==========
.gray-btn {
min-width: 80px;
height: 28px;
padding: 0 14px;
font-size: 12px;
color: #000;
background: linear-gradient(180deg, #fdfdfd 0%, #ececec 50%, #dcdcdc 100%);
border: 1px solid #7a7a7a;
border-radius: 2px;
cursor: pointer;
&:hover {
background: linear-gradient(180deg, #ffffff 0%, #f4f4f4 50%, #e6e6e6 100%);
}
&:active {
background: #c8c8c8;
}
}
.blue-btn {
min-width: 190px;
height: 28px;
padding: 0 14px;
font-size: 12px;
color: #1e6fff;
background: #ecf5ff;
border: 1px solid #1e6fff;
border-radius: 2px;
cursor: pointer;
&:hover {
background: #d9ecff;
}
&:active {
background: #c6e2ff;
}
}
// ========== 4. 时间编排区域 ==========
.table-area {
flex: 1;
min-height: 0;
display: flex;
flex-direction: column;
background: #fff;
border: 1px solid #ebeef5;
border-radius: 4px;
overflow: hidden;
.calendar-table-wrapper {
flex: 1;
min-height: 0;
overflow: auto;
&::-webkit-scrollbar {
width: 8px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background: #dcdfe6;
border-radius: 4px;
&:hover { background: #c0c4cc; }
}
&::-webkit-scrollbar-track {
background: #f5f7fa;
}
}
.select-tip {
flex-shrink: 0;
padding: 6px 12px;
font-size: 12px;
color: #909399;
border-top: 1px solid #ebeef5;
background: #fafafa;
}
.calendar-table {
width: 100%;
border-collapse: collapse;
table-layout: fixed;
font-size: 12px;
user-select: none;
thead {
position: sticky;
top: 0;
z-index: 2;
th {
background: #f5f7fa;
border: 1px solid #ebeef5;
color: #303133;
font-weight: 600;
height: 28px;
padding: 0 4px;
text-align: center;
min-width: 52px;
white-space: nowrap;
}
}
tbody {
td {
&.week-num,
&.date-range {
background: #fafafa;
color: #303133;
font-weight: 600;
}
&.week-num {
width: 48px;
}
&.date-range {
width: 130px;
}
}
.cell {
position: relative;
height: 30px;
padding: 0 2px;
text-align: center;
vertical-align: middle;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
border: 1px solid #ebeef5;
cursor: default;
transition: filter 0.1s;
&:hover {
filter: brightness(0.92);
}
&.is-event {
cursor: pointer;
font-weight: 500;
}
// 选中高亮:蓝色描边
&.selected {
outline: 2px solid #1e6fff;
outline-offset: -2px;
z-index: 1;
}
// 加粗显示
&.is-bold {
font-weight: 700;
}
// 可排课未勾选 → 左上角灰色小方块(该时段全校不允许排课)
&.not-schedulable::before {
content: '';
position: absolute;
top: 1px;
left: 1px;
width: 7px;
height: 7px;
background: #c8c8c8;
border: 1px solid #909399;
}
// 正课 → 右上角蓝色小方块
&.is-regular::after {
content: '';
position: absolute;
top: 1px;
right: 1px;
width: 7px;
height: 7px;
background: #1e6fff;
border: 1px solid #0b4bc0;
}
}
}
}
}
}
</style>