前端代码

This commit is contained in:
liumengyu
2026-08-21 18:42:51 +08:00
parent 054947482f
commit 04e06a9c9a
762 changed files with 62992 additions and 0 deletions
@@ -0,0 +1,906 @@
<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>