优化排班功能

This commit is contained in:
hero920103 committed 2026-11-05 18:00:50 +08:00
1 parent 8acbe732ff
commit 8ba15d87b3
13 files changed
+2463 -565

No files matched your search

+6
View File
@@ -34,6 +34,12 @@ export function getScheduleBoard(params) {
return request({ url: '/backend/oa/scheduling/board', method: 'get', params })
}
// 批量按格子编辑排班:cells 为排班板多选的「员工 × 日期」任意组合
// shift_id>0 设为该班次(有排班=调班,无排班=新增);shift_id=0 清除(休息/请假)
export function saveScheduleEntryCells(data) {
return request({ url: '/backend/oa/scheduling/entries/save-cells', method: 'post', data })
}
/* ----------------------------- 排班模板 ----------------------------- */
// 平台预置 + 自定义模板(只读);租户调用后规则会复制到自己的方案
@@ -0,0 +1,180 @@
<template>
<el-dialog
v-model="visible"
title="批量编辑所选格子"
width="420px"
append-to-body
destroy-on-close
@open="handleOpen"
>
<div class="batch-summary">
已选 <b>{{ cells.length }}</b> 个格子(<b>{{ employeeCount }}</b> 人 · <b>{{ dateCount }}</b> 天),
选了哪些格子就改哪些格子。
</div>
<el-form label-width="82px" @submit.prevent>
<el-form-item label="操作">
<el-radio-group v-model="action">
<el-radio value="shift">设置班次(上班/调班)</el-radio>
<el-radio value="clear">清除排班(休息/请假)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item v-if="action === 'shift'" label="班次">
<el-select v-model="shiftId" placeholder="选择班次" style="width: 100%">
<el-option
v-for="item in shifts"
:key="item.id"
:value="item.id"
:label="optionLabel(item)"
>
<span class="shift-option">
<span class="shift-dot" :style="{ background: item.color }" />
<span>{{ optionLabel(item) }}</span>
</span>
</el-option>
</el-select>
<div v-if="shiftHint" class="shift-hint">{{ shiftHint }}</div>
</el-form-item>
<el-form-item v-if="action === 'shift'" label="类型">
<el-radio-group v-model="source">
<el-radio value="temp">临时调整(显示「临」标记)</el-radio>
<el-radio value="normal">正常排班(不显示标记)</el-radio>
</el-radio-group>
</el-form-item>
<el-form-item label="备注">
<el-input
v-model="note"
:placeholder="action === 'clear' ? '如:请假、休息' : '如:加班支援(可选)'"
maxlength="60"
/>
</el-form-item>
</el-form>
<template #footer>
<el-button @click="visible = false">取消</el-button>
<el-button type="primary" :loading="saving" @click="handleSave">
{{ action === 'clear' ? '清除所选' : '应用到所选' }}
</el-button>
</template>
</el-dialog>
</template>
<script setup>
import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { getShiftList, saveScheduleEntryCells } from '@/api/oaScheduling'
const props = defineProps({
modelValue: Boolean,
// 排班板多选的格子:[{ employee_id, employee_name, date }]
cells: { type: Array, default: () => [] }
})
const emit = defineEmits(['update:modelValue', 'changed'])
const visible = computed({
get: () => props.modelValue,
set: (val) => emit('update:modelValue', val)
})
const action = ref('shift')
const shiftId = ref(null)
const source = ref('temp')
const note = ref('')
const shifts = ref([])
const saving = ref(false)
const employeeCount = computed(() => new Set(props.cells.map((item) => item.employee_id)).size)
const dateCount = computed(() => new Set(props.cells.map((item) => item.date)).size)
function optionLabel(item) {
const remark = item.remark ? ` · ${item.remark}` : ''
return `${item.name}(${item.work_start.slice(0, 5)}-${item.work_end.slice(0, 5)}${remark})`
}
const shiftHint = computed(() => {
const found = shifts.value.find((item) => item.id === shiftId.value)
return found ? `${found.name}:${found.work_start.slice(0, 5)} - ${found.work_end.slice(0, 5)}` : ''
})
async function handleOpen() {
action.value = 'shift'
shiftId.value = null
source.value = 'temp'
note.value = ''
if (!shifts.value.length) {
const res = await getShiftList()
shifts.value = res.data?.list || []
}
if (shifts.value.length && !shiftId.value) {
shiftId.value = shifts.value[0].id
}
}
async function handleSave() {
if (!props.cells.length) {
ElMessage.warning('没有选中的格子')
return
}
if (action.value === 'shift' && !shiftId.value) {
ElMessage.warning('请选择班次')
return
}
saving.value = true
try {
const res = await saveScheduleEntryCells({
cells: props.cells.map((item) => ({ employee_id: item.employee_id, schedule_date: item.date })),
shift_id: action.value === 'shift' ? shiftId.value : 0,
source: source.value,
note: note.value
})
const stats = res.data || {}
const parts = []
if (stats.created) parts.push(`新增 ${stats.created}`)
if (stats.updated) parts.push(`调整 ${stats.updated}`)
if (stats.cleared) parts.push(`清除 ${stats.cleared}`)
if (stats.skipped) parts.push(`跳过 ${stats.skipped}`)
ElMessage.success(`批量编辑完成:${parts.join('、') || '无变更'}`)
visible.value = false
emit('changed')
} catch (error) {
ElMessage.error(error.message || '批量编辑失败')
} finally {
saving.value = false
}
}
</script>
<style scoped>
.batch-summary {
padding: 8px 12px;
margin-bottom: 14px;
border-radius: 6px;
background: #ecf5ff;
color: #409eff;
font-size: 12px;
line-height: 20px;
}
.shift-option {
display: inline-flex;
align-items: center;
gap: 6px;
}
.shift-dot {
width: 8px;
height: 8px;
border-radius: 2px;
flex: none;
}
.shift-hint {
font-size: 12px;
color: #909399;
line-height: 18px;
margin-top: 4px;
}
</style>
File diff suppressed because it is too large. Load diff
+411 -71
View File
@@ -58,17 +58,23 @@
/>
<el-tree-select
v-model="filters.org_id"
:data="orgTree"
:data="orgFilterTree"
:props="{ label: 'org_name', value: 'id', children: 'children' }"
node-key="id"
check-strictly
:render-after-expand="false"
default-expand-all
filterable
clearable
placeholder="全部组织"
:clearable="false"
placeholder="全部"
style="width: 200px"
@change="loadBoard"
/>
<el-checkbox v-model="filters.include_child" @change="loadBoard">含下级组织</el-checkbox>
<el-checkbox
v-model="filters.include_child"
:disabled="!filters.org_id"
@change="loadBoard"
>含下级组织</el-checkbox>
<el-input
v-model="filters.keyword"
placeholder="搜索员工姓名 / 账号"
@@ -78,6 +84,13 @@
/>
<el-checkbox v-model="filters.only_scheduled">只看有排班</el-checkbox>
<el-button :icon="Refresh" @click="loadBoard">刷新</el-button>
<el-button
:type="batchMode ? 'warning' : 'default'"
@click="toggleBatchMode"
>{{ batchMode ? "退出批量" : "批量编辑" }}</el-button>
<el-button :icon="Download" :disabled="!displayEmployees.length" @click="exportBoard">
导出{{ exportCount ? ` ${exportCount} 人` : "全部" }}
</el-button>
<div class="legend">
<span v-for="shift in shifts" :key="shift.id" class="legend-item">
<span class="legend-dot" :style="{ background: shift.color }" />{{ shift.short_name || shift.name }}
@@ -85,41 +98,67 @@
</div>
</div>
<div class="board-wrap" :class="{ 'is-month': isMonth }" v-loading="loading">
<!-- 批量选择操作条 -->
<div v-if="batchMode" class="batch-bar">
<span class="batch-tip">按住拖动连续选中,单击切换选中(员工 × 日期格子)</span>
<span v-if="selectedCount" class="batch-count">
已选 <b>{{ selectedCount }}</b> 格 · <b>{{ selectedEmployees.length }}</b> 人 · <b>{{ selectedDates.length }}</b> 天
</span>
<span v-else class="batch-count muted">尚未选择</span>
<el-button size="small" type="primary" :disabled="!selectedCount" @click="openBatchEntry">批量编辑所选</el-button>
<el-button size="small" :disabled="!selectedCount" @click="clearBatchSelection">清空选择</el-button>
</div>
<div class="board-wrap" :class="{ 'is-month': isMonth, 'batch-mode': batchMode }" v-loading="loading">
<table v-if="displayEmployees.length" class="board-table">
<thead>
<tr>
<th class="sticky-col">员工</th>
<th class="sticky-col check-col">
<label class="export-all" title="全选 / 清空;导出时不勾选默认导出全部">
<input type="checkbox" :checked="exportAllChecked" @change="toggleExportAll" />
</label>
</th>
<th class="sticky-col emp-col">员工</th>
<th
v-for="day in dateCells"
:key="day.date"
:class="{
'is-today': day.is_today,
'is-holiday': holidayOf(day.date)?.type === 1,
'is-makeup': holidayOf(day.date)?.type === 0
'is-holiday-off': holidayOf(day.date)?.type === 1,
'is-holiday-work': holidayOf(day.date)?.type === 0,
'is-weekend': isWeekendDay(day) && !holidayOf(day.date)
}"
>
<div class="date-main">{{ day.date.slice(5) }}</div>
<div class="date-sub">{{ weekdayLabel(day.weekday) }}</div>
<div
v-if="holidayOf(day.date)"
v-if="cellDayLabel(day)"
class="date-holiday"
:title="holidayOf(day.date).name"
:title="cellDayLabel(day).name"
>
<span
class="holiday-tag"
:class="holidayOf(day.date).type === 1 ? 'is-off' : 'is-work'"
:class="cellDayLabel(day).cls"
>
{{ holidayOf(day.date).type === 1 ? "休" : "班" }}
{{ cellDayLabel(day).tag }}
</span>
<span v-if="!isMonth" class="holiday-name">{{ holidayOf(day.date).name }}</span>
<span v-if="!isMonth" class="holiday-name">{{ cellDayLabel(day).name }}</span>
</div>
</th>
</tr>
</thead>
<tbody>
<tr v-for="employee in displayEmployees" :key="employee.id">
<td class="sticky-col employee-cell">
<td class="sticky-col check-col">
<input
type="checkbox"
class="emp-check"
:checked="!!exportSel[employee.id]"
@click.stop
@change="exportSel[employee.id] = $event.target.checked"
/>
</td>
<td class="sticky-col emp-col employee-cell">
<div class="emp-name">{{ employee.name }}</div>
<div class="emp-meta">{{ employee.org_name || "未分配部门" }}</div>
</td>
@@ -127,13 +166,21 @@
v-for="day in dateCells"
:key="day.date"
class="shift-cell"
:class="{ 'is-today': day.is_today }"
@click="openCell(employee, day.date)"
:class="{
'is-today': day.is_today,
'is-holiday-off': holidayOf(day.date)?.type === 1,
'is-holiday-work': holidayOf(day.date)?.type === 0,
'is-weekend': isWeekendDay(day) && !holidayOf(day.date),
'is-selected': batchMode && selection[`${employee.id}|${day.date}`]
}"
@mousedown="batchMode && batchMouseDown($event)"
@mouseover="batchMode && batchMouseOver(employee.id, day.date)"
@click="batchMode ? batchClick(employee.id, day.date) : openCell(employee, day.date)"
>
<div
v-if="employee.days[day.date]"
class="shift-block"
:style="shiftStyle(employee.days[day.date])"
:style="shiftStyle(employee.days[day.date], day.is_today)"
:title="cellTitle(employee.days[day.date])"
>
<span v-if="employee.days[day.date].source === 2" class="temp-flag">临</span>
@@ -155,14 +202,24 @@
</div>
</div>
<shift-dialog v-model="shiftVisible" @changed="loadBoard" />
<plan-dialog v-model="planVisible" @changed="loadBoard" />
<shift-dialog v-model="shiftVisible" @changed="handleShiftChanged" />
<plan-dialog
ref="planDialogRef"
v-model="planVisible"
@changed="loadBoard"
@open-shifts="openShiftDialog"
/>
<entry-dialog
v-model="entryVisible"
:preset-employee-ids="entryPreset.employeeIds"
:preset-date="entryPreset.date"
@changed="handleEntryChanged"
/>
<batch-dialog
v-model="batchVisible"
:cells="batchCells"
@changed="handleBatchChanged"
/>
<swap-dialog
v-model="swapVisible"
:entry="selectedCell.entry"
@@ -199,8 +256,8 @@
</template>
<script setup>
import { computed, onMounted, reactive, ref } from 'vue'
import { ArrowLeft, ArrowRight, Clock, Plus, Refresh, Right, Search, Setting, Tickets } from '@element-plus/icons-vue'
import { computed, onBeforeUnmount, onMounted, reactive, ref } from 'vue'
import { ArrowLeft, ArrowRight, Clock, Download, Plus, Refresh, Right, Search, Setting, Tickets } from '@element-plus/icons-vue'
import { ElMessage } from 'element-plus'
import { oaOrganizationApi } from '@/api/organization'
import { buildOrgTree } from '@/views/apps/organization/composables'
@@ -210,6 +267,7 @@ import ShiftDialog from './components/shiftDialog.vue'
import PlanDialog from './components/planDialog.vue'
import EntryDialog from './components/entryDialog.vue'
import SwapDialog from './components/swapDialog.vue'
import BatchDialog from './components/batchDialog.vue'
const WEEK_LABELS = { 1: '周一', 2: '周二', 3: '周三', 4: '周四', 5: '周五', 6: '周六', 7: '周日' }
@@ -244,9 +302,14 @@ const truncated = ref(false)
const board = ref({ employees: [], dates: [], summary: {}, shifts: [] })
const shifts = ref([])
const orgTree = ref([])
const filters = reactive({ org_id: null, include_child: true, keyword: '', only_scheduled: false })
// 组织筛选树:「全部」根节点 + 完整组织架构,默认显示全部
const orgFilterTree = computed(() => [
{ id: 0, org_name: '全部', children: orgTree.value }
])
const filters = reactive({ org_id: 0, include_child: true, keyword: '', only_scheduled: false })
const shiftVisible = ref(false)
const planDialogRef = ref(null)
const planVisible = ref(false)
const entryVisible = ref(false)
const swapVisible = ref(false)
@@ -256,6 +319,136 @@ const changesLoading = ref(false)
const entryPreset = reactive({ employeeIds: [], date: '' })
const selectedCell = reactive({ entry: null, employeeId: 0, employeeName: '', scheduleDate: '' })
// ===== 批量编辑:员工 × 日期格子的多选 =====
const batchMode = ref(false)
const selection = reactive({})
const batchDragging = ref(false)
const batchDragMoved = ref(false)
const batchVisible = ref(false)
const selectedKeys = computed(() => Object.keys(selection).filter((key) => selection[key]))
const selectedCount = computed(() => selectedKeys.value.length)
const selectedEmployees = computed(() => [...new Set(selectedKeys.value.map((key) => Number(key.split('|')[0])))])
const selectedDates = computed(() => [...new Set(selectedKeys.value.map((key) => key.split('|')[1]))].sort())
function toggleBatchMode() {
batchMode.value = !batchMode.value
if (batchMode.value) {
ElMessage.info('批量模式:按住拖动连续选中格子,单击切换选中,选完后点「批量编辑所选」')
} else {
clearBatchSelection()
}
}
function clearBatchSelection() {
Object.keys(selection).forEach((key) => {
delete selection[key]
})
}
// ===== 导出:员工行勾选(不勾选 = 导出全部)=====
const exportSel = reactive({})
const exportCount = computed(() => displayEmployees.value.filter((e) => exportSel[e.id]).length)
const exportAllChecked = computed(
() => displayEmployees.value.length > 0 && exportCount.value === displayEmployees.value.length
)
function toggleExportAll() {
const all = displayEmployees.value
if (exportAllChecked.value) {
all.forEach((e) => {
delete exportSel[e.id]
})
} else {
all.forEach((e) => {
exportSel[e.id] = true
})
}
}
async function exportBoard() {
const all = displayEmployees.value
if (!all.length) {
ElMessage.warning('当前没有可导出的排班数据')
return
}
const list = exportCount.value ? all.filter((e) => exportSel[e.id]) : all
const days = dateCells.value
if (!days.length) {
ElMessage.warning('当前没有日期范围')
return
}
const XLSX = await import('xlsx')
const header = [
'员工',
'部门',
...days.map((d) => `${d.date.slice(5).replace('-', '/')} ${weekdayLabel(d.weekday)}`)
]
const rows = list.map((emp) => [
emp.name,
emp.org_name || '',
...days.map((d) => {
const entry = emp.days[d.date]
if (!entry) return ''
const name = entry.short_name || entry.shift_name || ''
return entry.source === 2 ? `${name}(临)` : name
})
])
const ws = XLSX.utils.aoa_to_sheet([header, ...rows])
ws['!cols'] = [{ wch: 10 }, { wch: 18 }, ...days.map(() => ({ wch: 8 }))]
const wb = XLSX.utils.book_new()
XLSX.utils.book_append_sheet(wb, ws, '排班表')
const range = `${days[0].date}_${days[days.length - 1].date}`
XLSX.writeFile(wb, `排班表_${range}.xlsx`)
ElMessage.success(`已导出 ${list.length} 名员工、${days.length} 天的排班`)
}
function batchMouseDown(event) {
event.preventDefault()
batchDragging.value = true
batchDragMoved.value = false
}
function batchMouseOver(employeeId, date) {
if (!batchDragging.value) return
batchDragMoved.value = true
selection[`${employeeId}|${date}`] = true
}
function batchClick(employeeId, date) {
// 拖动结束的 click 不做切换,只有真正的单击才切换选中
if (batchDragMoved.value) return
const key = `${employeeId}|${date}`
if (selection[key]) delete selection[key]
else selection[key] = true
}
function onWindowMouseUp() {
batchDragging.value = false
}
// 选中的格子集合:[{ employee_id, employee_name, date }]
const batchCells = computed(() => {
const names = {}
board.value.employees.forEach((item) => {
names[item.id] = item.name
})
return selectedKeys.value.map((key) => {
const [id, date] = key.split('|')
return { employee_id: Number(id), employee_name: names[Number(id)] || '', date }
})
})
function openBatchEntry() {
if (!selectedCount.value) return
batchVisible.value = true
}
function handleBatchChanged() {
clearBatchSelection()
loadBoard()
}
const summary = computed(() => board.value.summary || {})
const dateCells = computed(() => board.value.dates || [])
const rangeText = computed(() => {
@@ -280,21 +473,27 @@ function cellTitle(entry) {
return parts.join(' ')
}
function shiftStyle(entry) {
function shiftStyle(entry, isToday) {
// 按当前班次定义统一取色,避免不同时期生成的记录颜色快照不一致
const def = shifts.value.find((s) => s.id === entry.shift_id)
const color = (def && def.color) || entry.color || '#409EFF'
return {
borderLeftColor: entry.color || '#409EFF',
background: hexToRgba(entry.color || '#409EFF', 0.1),
borderLeftColor: color,
// 不透明预混色:今天列混绿底,其余混白底,避免半透明叠加周末/节假日底色混色
background: mixColor(color, 0.1, isToday ? '#e4f5d2' : '#ffffff'),
color: '#303133'
}
}
function hexToRgba(hex, alpha) {
const value = String(hex || '').replace('#', '')
if (value.length !== 6) return `rgba(64, 158, 255, ${alpha})`
const r = parseInt(value.slice(0, 2), 16)
const g = parseInt(value.slice(2, 4), 16)
const b = parseInt(value.slice(4, 6), 16)
return `rgba(${r}, ${g}, ${b}, ${alpha})`
// hex 颜色按比例混入基色,返回不透明色
function mixColor(hex, ratio, base) {
const v = String(hex || '').replace('#', '')
const b = String(base || '#ffffff').replace('#', '')
if (v.length !== 6 || b.length !== 6) return '#ecf5ff'
const ch = (s, i) => parseInt(s.slice(i, i + 2), 16)
const mix = (i) => Math.round(ratio * ch(v, i) + (1 - ratio) * ch(b, i))
const to = (n) => n.toString(16).padStart(2, '0')
return `#${to(mix(0))}${to(mix(2))}${to(mix(4))}`
}
async function loadOrgTree() {
@@ -354,6 +553,26 @@ function holidayOf(date) {
return holidayMap.value[date] || null
}
function isWeekendDay(day) {
// 法定节假日/调休优先(holidayMap 已覆盖),未标记时按周六=6、周日=7
if (!day) return false
return day.weekday === 6 || day.weekday === 7
}
function cellHolidayType(date) {
// 返回 1=法定放假(休) 0=调休上班(班) null=无
const h = holidayOf(date)
return h ? Number(h.type) : null
}
function cellDayLabel(day) {
if (!day) return null
const h = holidayOf(day.date)
if (h) return h.type === 1 ? { tag: '休', cls: 'is-off', name: h.name } : { tag: '班', cls: 'is-work', name: h.name }
if (isWeekendDay(day)) return { tag: '休', cls: 'is-off-weekend', name: day.weekday === 6 ? '周六' : '周日' }
return null
}
async function loadBoard() {
loading.value = true
try {
@@ -442,6 +661,17 @@ function handleEntryChanged() {
loadBoard()
}
// 排班方案里提示「没有班次」时,直接打开班次管理弹窗
function openShiftDialog() {
shiftVisible.value = true
}
// 班次管理变更后,同步刷新方案弹窗的班次列表
function handleShiftChanged() {
loadBoard()
planDialogRef.value?.reloadShifts?.()
}
async function openChanges() {
changesVisible.value = true
await loadChanges()
@@ -467,9 +697,14 @@ async function loadChanges() {
}
onMounted(async () => {
window.addEventListener('mouseup', onWindowMouseUp)
await Promise.all([loadOrgTree(), loadShifts()])
await loadBoard()
})
onBeforeUnmount(() => {
window.removeEventListener('mouseup', onWindowMouseUp)
})
</script>
<style scoped lang="less">
@@ -505,20 +740,8 @@ onMounted(async () => {
}
.stat-card {
padding: 15px 18px;
background: #fff;
border: 1px solid #ebeef5;
border-top: 3px solid #409eff;
background: #f7f8fa;
border-radius: 8px;
&.purple {
border-top-color: #8b5cf6;
}
&.green {
border-top-color: #67c23a;
}
&.orange {
border-top-color: #e6a23c;
}
}
.stat-label,
.stat-sub {
@@ -571,6 +794,37 @@ onMounted(async () => {
max-height: 640px;
border: 1px solid #ebeef5;
border-radius: 6px;
// 批量模式:格子进入可选状态,禁止拖蓝
&.batch-mode .shift-cell {
cursor: cell;
user-select: none;
}
}
.batch-bar {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
padding: 8px 12px;
margin-bottom: 8px;
background: #fdf6ec;
border: 1px solid #f3d19e;
border-radius: 6px;
font-size: 12px;
color: #b88230;
.batch-tip {
color: #b88230;
}
.batch-count {
b {
color: #c05621;
}
&.muted {
color: #d0a96a;
}
}
}
.board-table {
width: 100%;
@@ -594,7 +848,10 @@ onMounted(async () => {
padding: 8px 4px;
}
thead th.is-today {
background: #ecf5ff;
background: #d9f0c7;
.date-main {
color: #3d8b1e;
}
}
.date-main {
font-size: 13px;
@@ -605,10 +862,16 @@ onMounted(async () => {
font-size: 12px;
color: #909399;
}
// 法定假日表头:浅红底 + 「休」角标;调休上班日标「班」
thead th.is-holiday:not(.is-today) {
// 法定假日表头:浅红底;调休上班:灰底;周末:淡橙底(周末节假日优先用节假日样式)
thead th.is-holiday-off:not(.is-today) {
background: #fffafa;
}
thead th.is-holiday-work:not(.is-today) {
background: #f5f7fa;
}
thead th.is-weekend:not(.is-today) {
background: #fdf6ec;
}
.date-holiday {
display: flex;
align-items: center;
@@ -639,6 +902,11 @@ onMounted(async () => {
color: #909399;
border: 1px solid #e9e9eb;
}
&.is-off-weekend {
background: #fdf6ec;
color: #e6a23c;
border: 1px solid #faecd8;
}
}
.holiday-name {
font-size: 11px;
@@ -646,21 +914,55 @@ onMounted(async () => {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
.is-off-weekend ~ &,
&.is-off-weekend-name {
color: #e6a23c;
}
}
.sticky-col {
position: sticky;
left: 0;
z-index: 1;
width: 150px;
min-width: 150px;
background: #fff;
text-align: left;
padding: 8px 12px;
border-right: 1px solid #ebeef5;
&.check-col {
left: 0;
width: 42px;
min-width: 42px;
padding: 8px 0;
text-align: center;
.emp-check {
cursor: pointer;
accent-color: #409eff;
}
}
&.emp-col {
left: 42px;
width: 150px;
min-width: 150px;
text-align: center;
padding: 8px 10px;
}
}
thead .sticky-col {
z-index: 3;
background: #f5f7fa;
.export-all {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
cursor: pointer;
user-select: none;
input {
cursor: pointer;
accent-color: #409eff;
}
}
}
.employee-cell {
.emp-name {
@@ -679,25 +981,42 @@ onMounted(async () => {
min-width: 118px;
height: 56px;
cursor: pointer;
padding: 4px;
padding: 0 3px;
&.is-today {
background: #f7fbff;
background: #e4f5d2;
}
&.is-holiday-off:not(.is-today) {
background: #fffafa;
}
&.is-holiday-work:not(.is-today) {
background: #fafbfc;
}
&.is-weekend:not(.is-today) {
background: #fdf6ec;
}
&:hover {
background: #f5f7fa;
}
// 批量模式选中格
&.is-selected {
background: #ecf5ff;
outline: 2px solid #409eff;
outline-offset: -2px;
}
}
.shift-block {
position: relative;
display: flex;
flex-direction: column;
align-items: flex-start;
align-items: center;
justify-content: center;
gap: 2px;
padding: 5px 7px;
border-left: 3px solid #409eff;
padding: 2px 6px;
height: 100%;
box-sizing: border-box;
border-radius: 4px;
text-align: left;
text-align: center;
min-height: 38px;
}
.shift-title {
@@ -726,7 +1045,10 @@ onMounted(async () => {
.empty-cell {
color: #dcdfe6;
font-size: 16px;
line-height: 38px;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
}
}
@@ -738,22 +1060,21 @@ onMounted(async () => {
thead th {
padding: 6px 2px;
}
.sticky-col {
.sticky-col.emp-col {
width: 130px;
min-width: 130px;
padding: 6px 10px;
padding: 6px 8px;
}
.shift-cell {
width: 54px;
min-width: 54px;
height: 42px;
padding: 2px;
padding: 0 2px;
}
.shift-block {
gap: 1px;
padding: 3px 4px;
min-height: 30px;
border-left-width: 2px;
padding: 2px 4px;
min-height: 100%;
}
.shift-title {
font-size: 11px;
@@ -818,8 +1139,7 @@ html.dark & {
color: var(--el-text-color-secondary);
}
.stat-card {
background: var(--el-bg-color);
border-color: var(--el-border-color-lighter);
background: var(--el-fill-color-light);
.stat-label,
.stat-sub {
@@ -846,11 +1166,17 @@ html.dark & {
background: var(--el-fill-color-light);
}
thead th.is-today {
background: var(--el-color-primary-light-9);
background: rgba(103, 194, 58, 0.26);
}
thead th.is-holiday:not(.is-today) {
thead th.is-holiday-off:not(.is-today) {
background: rgba(245, 108, 108, 0.08);
}
thead th.is-holiday-work:not(.is-today) {
background: var(--el-fill-color-lighter);
}
thead th.is-weekend:not(.is-today) {
background: rgba(230, 162, 60, 0.08);
}
.holiday-tag {
&.is-off {
background: rgba(245, 108, 108, 0.16);
@@ -862,6 +1188,11 @@ html.dark & {
color: var(--el-text-color-secondary);
border-color: var(--el-border-color-lighter);
}
&.is-off-weekend {
background: rgba(230, 162, 60, 0.18);
color: #ebb563;
border-color: rgba(230, 162, 60, 0.35);
}
}
.holiday-name {
color: #f89898;
@@ -883,7 +1214,16 @@ html.dark & {
}
.shift-cell {
&.is-today {
background: var(--el-color-primary-light-9);
background: rgba(103, 194, 58, 0.16);
}
&.is-holiday-off:not(.is-today) {
background: rgba(245, 108, 108, 0.06);
}
&.is-holiday-work:not(.is-today) {
background: var(--el-fill-color-lighter);
}
&.is-weekend:not(.is-today) {
background: rgba(230, 162, 60, 0.08);
}
&:hover {
background: var(--el-fill-color-light);
@@ -132,7 +132,7 @@
</template>
</el-table-column>
<el-table-column prop="platform" label="平台名称" min-width="160">
<el-table-column prop="platform" label="平台名称" min-width="200">
<template #default="{ row }">
<div class="platform-name-cell">
<el-icon class="platform-icon"><Key /></el-icon>