2025-09-20 11:26:02 +08:00
|
|
|
|
<template>
|
|
|
|
|
|
<div class="schedule-table-container">
|
|
|
|
|
|
<el-table
|
|
|
|
|
|
:data="scheduleData"
|
|
|
|
|
|
style="width: 100%"
|
|
|
|
|
|
max-height="600"
|
|
|
|
|
|
stripe
|
|
|
|
|
|
border
|
2025-09-23 20:15:50 +08:00
|
|
|
|
v-loading="loading"
|
2025-09-20 11:26:02 +08:00
|
|
|
|
>
|
|
|
|
|
|
<!-- 固定列 -->
|
|
|
|
|
|
<el-table-column fixed prop="name" label="姓名" width="120" align="center" />
|
2025-09-23 20:15:50 +08:00
|
|
|
|
<el-table-column fixed="left" prop="postName" label="岗位" width="120" align="center" />
|
2025-09-20 11:26:02 +08:00
|
|
|
|
<el-table-column fixed="left" prop="weeklyHours" label="周总计/小时" width="120" align="center" />
|
|
|
|
|
|
|
|
|
|
|
|
<!-- 日期列 - 纵向显示号数和星期几 -->
|
|
|
|
|
|
<el-table-column
|
|
|
|
|
|
v-for="(dateInfo, index) in currentMonthDates"
|
|
|
|
|
|
:key="index"
|
|
|
|
|
|
:prop="`day${index + 1}`"
|
|
|
|
|
|
width="80"
|
|
|
|
|
|
align="center"
|
|
|
|
|
|
>
|
|
|
|
|
|
<template #header>
|
|
|
|
|
|
<div class="vertical-header">
|
|
|
|
|
|
<div class="date-number">{{ dateInfo.date }}</div>
|
|
|
|
|
|
<div class="week-day">{{ dateInfo.weekDay }}</div>
|
|
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-09-22 16:15:50 +08:00
|
|
|
|
<template #default="scope">
|
|
|
|
|
|
<div
|
|
|
|
|
|
class="schedule-cell"
|
2025-09-23 20:15:50 +08:00
|
|
|
|
:class="getShiftClass(scope.row[`day${index + 1}`])"
|
2025-09-22 16:15:50 +08:00
|
|
|
|
@click="handleCellClick(scope.row, {...dateInfo, index}, scope)"
|
|
|
|
|
|
>
|
2025-09-23 20:15:50 +08:00
|
|
|
|
{{ formatShiftText(scope.row[`day${index + 1}`]) }}
|
2025-09-22 16:15:50 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
2025-09-20 11:26:02 +08:00
|
|
|
|
</el-table-column>
|
|
|
|
|
|
</el-table>
|
2025-09-22 20:47:13 +08:00
|
|
|
|
|
|
|
|
|
|
<!-- 分页组件 -->
|
2025-09-23 20:15:50 +08:00
|
|
|
|
<!-- <div class="pagination-container">
|
2025-09-22 20:47:13 +08:00
|
|
|
|
<el-pagination
|
|
|
|
|
|
v-model:current-page="currentPage"
|
|
|
|
|
|
v-model:page-size="pageSize"
|
|
|
|
|
|
:page-sizes="[10, 20, 50, 100]"
|
|
|
|
|
|
layout="total, sizes, prev, pager, next, jumper"
|
|
|
|
|
|
:total="total"
|
|
|
|
|
|
@size-change="handleSizeChange"
|
|
|
|
|
|
@current-change="handleCurrentChange"
|
|
|
|
|
|
/>
|
2025-09-23 20:15:50 +08:00
|
|
|
|
</div> -->
|
2025-09-20 11:26:02 +08:00
|
|
|
|
</div>
|
|
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2025-09-23 20:15:50 +08:00
|
|
|
|
import { ref, computed, onMounted, watch } from 'vue';
|
|
|
|
|
|
import { getCurrentMonthDates, getMonthDates } from '@/utils/getDate';
|
|
|
|
|
|
import { ElMessage } from 'element-plus';
|
2025-09-20 11:26:02 +08:00
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 定义排班类型接口
|
|
|
|
|
|
interface UserTypePair {
|
|
|
|
|
|
schedulingDate: string;
|
|
|
|
|
|
schedulingType: string;
|
|
|
|
|
|
schedulingTypeName: string;
|
|
|
|
|
|
// 可能还有其他字段
|
|
|
|
|
|
[key: string]: any;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 定义员工排班信息接口
|
|
|
|
|
|
interface ScheduleItem {
|
|
|
|
|
|
opsUserId: number;
|
|
|
|
|
|
opsUserName: string;
|
|
|
|
|
|
durationCount: number;
|
|
|
|
|
|
postName: string;
|
|
|
|
|
|
userTypePairs: UserTypePair[];
|
|
|
|
|
|
// 可能还有其他字段
|
|
|
|
|
|
[key: string]: any;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 定义日期信息接口
|
|
|
|
|
|
interface DateInfo {
|
|
|
|
|
|
date: number;
|
|
|
|
|
|
weekDay: string;
|
|
|
|
|
|
fullDate: string;
|
|
|
|
|
|
year: number;
|
|
|
|
|
|
month: number;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 定义表格行数据接口
|
|
|
|
|
|
interface TableRowData {
|
|
|
|
|
|
opsUserId: number;
|
|
|
|
|
|
name: string;
|
|
|
|
|
|
postName: string;
|
|
|
|
|
|
weeklyHours: number;
|
|
|
|
|
|
[key: string]: any; // 动态添加day1, day2等字段
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 定义props接收排班数据
|
|
|
|
|
|
const props = defineProps<{
|
|
|
|
|
|
scheduleList: ScheduleItem[];
|
|
|
|
|
|
loading?: boolean;
|
|
|
|
|
|
// 可选:指定要显示的月份
|
|
|
|
|
|
targetMonth?: { year: number; month: number };
|
2025-09-22 16:15:50 +08:00
|
|
|
|
}>();
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
const emit = defineEmits<{
|
|
|
|
|
|
'edit-schedule': [rowData: TableRowData, columnData: DateInfo & { index: number }, cellEvent: any];
|
|
|
|
|
|
'page-change': [currentPage: number, pageSize: number];
|
|
|
|
|
|
}>();
|
2025-09-22 16:15:50 +08:00
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 排班类型与样式的映射关系
|
|
|
|
|
|
const shiftConfig = {
|
|
|
|
|
|
'早班': { color: '#67c23a', className: 'morning-shift' },
|
|
|
|
|
|
'中班': { color: '#e6a23c', className: 'afternoon-shift' },
|
|
|
|
|
|
'晚班': { color: '#409eff', className: 'evening-shift' },
|
|
|
|
|
|
'休息': { color: '#909399', className: 'rest-day' },
|
2025-09-22 16:15:50 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-20 11:26:02 +08:00
|
|
|
|
// 获取当前月的日期信息
|
2025-09-23 20:15:50 +08:00
|
|
|
|
const currentMonthDates = ref<(DateInfo & { year: number; month: number })[]>([]);
|
|
|
|
|
|
|
|
|
|
|
|
// 分页相关状态
|
|
|
|
|
|
const currentPage = ref(1);
|
|
|
|
|
|
const pageSize = ref(10);
|
|
|
|
|
|
const total = computed(() => props.scheduleList ? props.scheduleList.length : 0);
|
2025-09-20 11:26:02 +08:00
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 格式化排班文本,支持多排班情况
|
|
|
|
|
|
const formatShiftText = (shiftData: any): string => {
|
|
|
|
|
|
if (!shiftData) return '休息';
|
2025-09-20 11:26:02 +08:00
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 如果是字符串,直接返回
|
|
|
|
|
|
if (typeof shiftData === 'string') {
|
|
|
|
|
|
return shiftData;
|
|
|
|
|
|
}
|
2025-09-20 11:26:02 +08:00
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 如果是数组,返回第一个排班类型
|
|
|
|
|
|
if (Array.isArray(shiftData)) {
|
|
|
|
|
|
return shiftData.length > 0 ? shiftData[0].schedulingTypeName || '休息' : '休息';
|
|
|
|
|
|
}
|
2025-09-20 11:26:02 +08:00
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 如果是对象,返回排班类型名称
|
|
|
|
|
|
if (typeof shiftData === 'object' && shiftData.schedulingTypeName) {
|
|
|
|
|
|
return shiftData.schedulingTypeName;
|
2025-09-20 11:26:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
return '休息';
|
2025-09-20 11:26:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 获取排班对应的样式类名
|
|
|
|
|
|
const getShiftClass = (shiftData: any): string => {
|
|
|
|
|
|
const shiftText = formatShiftText(shiftData);
|
|
|
|
|
|
return shiftConfig[shiftText as keyof typeof shiftConfig]?.className || 'unknown-shift';
|
|
|
|
|
|
};
|
2025-09-22 20:47:13 +08:00
|
|
|
|
|
2025-09-20 11:26:02 +08:00
|
|
|
|
// 生成排班数据
|
2025-09-23 20:15:50 +08:00
|
|
|
|
const scheduleData = computed((): TableRowData[] => {
|
2025-09-22 20:47:13 +08:00
|
|
|
|
const startIndex = (currentPage.value - 1) * pageSize.value;
|
|
|
|
|
|
const endIndex = startIndex + pageSize.value;
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 确保 props.scheduleList 存在
|
|
|
|
|
|
const scheduleList = props.scheduleList || [];
|
|
|
|
|
|
|
|
|
|
|
|
// 如果没有数据且loading为false,返回空数组显示空状态
|
|
|
|
|
|
if (scheduleList.length === 0 && !props.loading) {
|
|
|
|
|
|
return [];
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 处理排班数据
|
|
|
|
|
|
return scheduleList.map((item: ScheduleItem) => {
|
|
|
|
|
|
const rowData: TableRowData = {
|
|
|
|
|
|
opsUserId: item.opsUserId,
|
|
|
|
|
|
name: item.opsUserName || `未知员工${item.opsUserId}`,
|
|
|
|
|
|
postName: item.postName || '未知岗位',
|
|
|
|
|
|
weeklyHours: item.durationCount || 0
|
2025-09-20 11:26:02 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 为当月每一天生成排班数据
|
2025-09-23 20:15:50 +08:00
|
|
|
|
currentMonthDates.value.forEach((dateInfo, dayIndex) => {
|
|
|
|
|
|
// 格式化日期为 YYYY-MM-DD
|
|
|
|
|
|
const dateKey = `${dateInfo.year}-${String(dateInfo.month).padStart(2, '0')}-${String(dateInfo.date).padStart(2, '0')}`;
|
|
|
|
|
|
|
|
|
|
|
|
// 从userTypePairs中查找对应日期的所有排班信息
|
|
|
|
|
|
let daySchedule = null;
|
|
|
|
|
|
if (item.userTypePairs && Array.isArray(item.userTypePairs)) {
|
|
|
|
|
|
// 查找对应日期的所有排班信息
|
|
|
|
|
|
const dateSchedules = item.userTypePairs.filter(pair => pair.schedulingDate === dateKey);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果有多个排班,也返回,方便后续扩展显示多个排班
|
|
|
|
|
|
daySchedule = dateSchedules.length > 0 ? dateSchedules : null;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果找到排班信息,存储原始数据;如果没有,设置为'休息'
|
|
|
|
|
|
rowData[`day${dayIndex + 1}`] = daySchedule || '休息';
|
2025-09-20 11:26:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
return rowData;
|
2025-09-22 20:47:13 +08:00
|
|
|
|
}).slice(startIndex, endIndex);
|
2025-09-20 11:26:02 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
// 更新日期列表
|
|
|
|
|
|
const updateDates = () => {
|
|
|
|
|
|
if (props.targetMonth) {
|
|
|
|
|
|
// 使用指定的月份
|
|
|
|
|
|
const dates = getMonthDates(props.targetMonth.year, props.targetMonth.month - 1); // getMonthDates的month参数是0-11
|
|
|
|
|
|
currentMonthDates.value = dates.map(date => ({
|
|
|
|
|
|
...date,
|
|
|
|
|
|
year: props.targetMonth!.year,
|
|
|
|
|
|
month: props.targetMonth!.month
|
|
|
|
|
|
}));
|
|
|
|
|
|
} else {
|
|
|
|
|
|
// 使用当前月份
|
|
|
|
|
|
const today = new Date();
|
|
|
|
|
|
const dates = getCurrentMonthDates();
|
|
|
|
|
|
currentMonthDates.value = dates.map(date => ({
|
|
|
|
|
|
...date,
|
|
|
|
|
|
year: today.getFullYear(),
|
|
|
|
|
|
month: today.getMonth() + 1
|
|
|
|
|
|
}));
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-22 20:47:13 +08:00
|
|
|
|
// 分页大小变化处理
|
|
|
|
|
|
const handleSizeChange = (size: number) => {
|
|
|
|
|
|
pageSize.value = size;
|
|
|
|
|
|
currentPage.value = 1; // 重置为第一页
|
2025-09-23 20:15:50 +08:00
|
|
|
|
emit('page-change', currentPage.value, pageSize.value);
|
2025-09-22 20:47:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 当前页码变化处理
|
|
|
|
|
|
const handleCurrentChange = (current: number) => {
|
|
|
|
|
|
currentPage.value = current;
|
2025-09-23 20:15:50 +08:00
|
|
|
|
emit('page-change', currentPage.value, pageSize.value);
|
2025-09-22 20:47:13 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
2025-09-22 16:15:50 +08:00
|
|
|
|
// 处理单元格点击事件
|
2025-09-23 20:15:50 +08:00
|
|
|
|
const handleCellClick = (rowData: TableRowData, columnData: DateInfo & { index: number }, cellEvent: any) => {
|
|
|
|
|
|
// 获取当前单元格的排班数据
|
|
|
|
|
|
const cellData = rowData[`day${columnData.index + 1}`];
|
|
|
|
|
|
const shiftText = formatShiftText(cellData);
|
|
|
|
|
|
|
|
|
|
|
|
// 如果是休息状态,显示提示信息,不触发编辑事件
|
|
|
|
|
|
if (shiftText === '休息') {
|
|
|
|
|
|
ElMessage.warning('请前往管理考勤增加排班');
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 非休息状态,触发编辑事件
|
2025-09-22 16:15:50 +08:00
|
|
|
|
emit('edit-schedule', rowData, columnData, cellEvent);
|
|
|
|
|
|
};
|
2025-09-23 20:15:50 +08:00
|
|
|
|
|
|
|
|
|
|
// 组件挂载时初始化
|
|
|
|
|
|
onMounted(() => {
|
|
|
|
|
|
updateDates();
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 监听目标月份变化,更新日期列表
|
|
|
|
|
|
watch(() => props.targetMonth, () => {
|
|
|
|
|
|
updateDates();
|
|
|
|
|
|
}, { deep: true });
|
|
|
|
|
|
|
|
|
|
|
|
// 监听排班数据变化,重置页码
|
|
|
|
|
|
watch(() => props.scheduleList, () => {
|
|
|
|
|
|
currentPage.value = 1;
|
|
|
|
|
|
}, { deep: true });
|
2025-09-20 11:26:02 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style scoped>
|
|
|
|
|
|
.schedule-table-container {
|
|
|
|
|
|
overflow-x: auto;
|
2025-09-23 20:15:50 +08:00
|
|
|
|
padding: 16px;
|
|
|
|
|
|
background: #fff;
|
|
|
|
|
|
border-radius: 4px;
|
2025-09-20 11:26:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 优化滚动条样式 */
|
|
|
|
|
|
.schedule-table-container::-webkit-scrollbar {
|
|
|
|
|
|
height: 8px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.schedule-table-container::-webkit-scrollbar-track {
|
|
|
|
|
|
background: #f1f1f1;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.schedule-table-container::-webkit-scrollbar-thumb {
|
|
|
|
|
|
background: #c0c4cc;
|
|
|
|
|
|
border-radius: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.schedule-table-container::-webkit-scrollbar-thumb:hover {
|
|
|
|
|
|
background: #909399;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 优化表格样式 */
|
|
|
|
|
|
:deep(.el-table) {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table__header-wrapper th) {
|
|
|
|
|
|
background-color: #fafafa;
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
padding: 0 !important;
|
|
|
|
|
|
height: auto !important;
|
|
|
|
|
|
min-height: 60px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
:deep(.el-table__body-wrapper) {
|
|
|
|
|
|
overflow-x: visible;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 纵向表头样式 */
|
|
|
|
|
|
.vertical-header {
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
justify-content: center;
|
|
|
|
|
|
height: 100%;
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
.date-number {
|
|
|
|
|
|
font-size: 16px;
|
|
|
|
|
|
font-weight: 600;
|
|
|
|
|
|
color: #333;
|
|
|
|
|
|
margin-bottom: 4px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.week-day {
|
|
|
|
|
|
font-size: 12px;
|
|
|
|
|
|
color: #666;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-22 16:15:50 +08:00
|
|
|
|
/* 排班单元格样式 */
|
|
|
|
|
|
.schedule-cell {
|
|
|
|
|
|
padding: 8px 0;
|
|
|
|
|
|
cursor: pointer;
|
|
|
|
|
|
transition: all 0.2s ease;
|
2025-09-23 20:15:50 +08:00
|
|
|
|
text-align: center;
|
|
|
|
|
|
border-radius: 4px;
|
2025-09-22 16:15:50 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.schedule-cell:hover {
|
|
|
|
|
|
background-color: #f5f7fa;
|
|
|
|
|
|
transform: scale(1.05);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
/* 排班类型样式 */
|
|
|
|
|
|
.morning-shift {
|
|
|
|
|
|
color: #67c23a; /* 早班 - 绿色 */
|
|
|
|
|
|
font-weight: 500;
|
2025-09-20 11:26:02 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-09-23 20:15:50 +08:00
|
|
|
|
.afternoon-shift {
|
|
|
|
|
|
color: #e6a23c; /* 中班 - 橙色 */
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.evening-shift {
|
|
|
|
|
|
color: #409eff; /* 晚班 - 蓝色 */
|
|
|
|
|
|
font-weight: 500;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
.rest-day {
|
|
|
|
|
|
color: #909399; /* 休息 - 灰色 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
.unknown-shift {
|
|
|
|
|
|
color: #333; /* 未知类型 - 默认黑色 */
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 分页容器样式 */
|
|
|
|
|
|
.pagination-container {
|
|
|
|
|
|
margin-top: 16px;
|
|
|
|
|
|
display: flex;
|
|
|
|
|
|
justify-content: flex-end;
|
|
|
|
|
|
align-items: center;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 分页组件样式优化 */
|
|
|
|
|
|
:deep(.el-pagination) {
|
|
|
|
|
|
font-size: 14px;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
/* 加载状态样式优化 */
|
|
|
|
|
|
:deep(.el-loading-mask) {
|
|
|
|
|
|
background-color: rgba(255, 255, 255, 0.8);
|
|
|
|
|
|
}
|
2025-09-20 11:26:02 +08:00
|
|
|
|
</style>
|