<template>
|
<div class="class-page">
|
<div class="search-container">
|
<div class="search-form">
|
<div class="search-row">
|
<div class="search-item">
|
<label class="search-label">选择时间:</label>
|
<div class="search-input-group">
|
<el-date-picker v-model="query.year"
|
type="year"
|
size="small"
|
format="YYYY"
|
placeholder="选择年"
|
@change="refreshTable()"
|
style="width: 90px"
|
:clearable="false" />
|
<el-select v-model="query.month"
|
clearable
|
placeholder="选择月"
|
style="width: 70px; margin-left: 8px"
|
size="small"
|
@change="refreshTable()">
|
<el-option v-for="item in monthOptions"
|
:key="item.value"
|
:label="item.label"
|
:value="item.value" />
|
</el-select>
|
</div>
|
</div>
|
<div class="search-item">
|
<el-input v-model="query.userName"
|
placeholder="请输入人员名称"
|
size="small"
|
style="width: 120px"
|
clearable
|
@keyup.enter="refreshTable()" />
|
</div>
|
<div class="search-item">
|
<el-tree-select v-model="query.sysDeptId"
|
:data="deptOptions"
|
:props="{ value: 'id', label: 'label', children: 'children' }"
|
value-key="id"
|
placeholder="请选择部门"
|
size="small"
|
clearable
|
@change="refreshTable()"
|
style="width: 140px" />
|
</div>
|
<div class="search-actions">
|
<el-button size="small"
|
type="primary"
|
@click="refreshTable()"
|
:icon="Search">
|
查询
|
</el-button>
|
<el-button size="small"
|
@click="refresh()"
|
:icon="Refresh"
|
style="margin-left: 8px">
|
重置
|
</el-button>
|
</div>
|
<div class="search-buttons">
|
<el-button size="small"
|
type="primary"
|
@click="configTime"
|
:icon="Setting">
|
班次配置
|
</el-button>
|
<el-button size="small"
|
type="success"
|
@click="handleDown"
|
:loading="downLoading"
|
:icon="Download"
|
style="margin-left: 8px">
|
导出
|
</el-button>
|
<el-button size="small"
|
type="warning"
|
@click="schedulingVisible = true"
|
:icon="Calendar"
|
style="margin-left: 8px">
|
排班
|
</el-button>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="scheduling-container"
|
v-loading="pageLoading">
|
<!-- 月度排班 -->
|
<div class="scheduling-table"
|
v-show="query.month">
|
<div class="scheduling-left">
|
<div class="scheduling-header">
|
人员名称
|
</div>
|
<div class="scheduling-user"
|
:class="{ 'scheduling-user-hover': currentUserIndex == index }"
|
v-for="(item, index) in listForm"
|
:key="'e' + index"
|
@mouseenter="onMouseEnter(index)"
|
@mouseleave="currentUserIndex = null">
|
<div class="user-avatar">
|
{{ item.name ? item.name.charAt(0) : "" }}
|
</div>
|
<div class="user-details">
|
<h4 class="user-name">{{ item.name }}</h4>
|
<!-- <div class="user-stats">
|
<span class="stat-item">早:{{ item.day0 }}</span>
|
<span class="stat-item">中:{{ item.day1 }}</span>
|
<span class="stat-item">夜:{{ item.day2 }}</span>
|
<span class="stat-item">休:{{ item.day3 }}</span>
|
<span class="stat-item">假:{{ item.day4 }}</span>
|
<span class="stat-item">差:{{ item.day6 }}</span>
|
</div> -->
|
<div class="user-total">
|
<span class="total-label">合计出勤:</span>
|
<span class="total-value">{{ item.monthlyAttendance.totalAttendance }}天</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="scheduling-right">
|
<div class="scheduling-calendar">
|
<div class="calendar-header">
|
<div class="calendar-header-item"
|
v-for="(item, index) in weeks"
|
:key="'b' + index">
|
<span class="week-number"
|
v-if="item.week == '周日'">{{ item.weekNum }}周</span>
|
<div class="day-info">
|
<span class="day-number">{{ item.day }}</span>
|
<span class="day-week">{{ item.week.charAt(1) }}</span>
|
</div>
|
</div>
|
</div>
|
<div class="calendar-body">
|
<div class="calendar-row"
|
v-for="(item, index) in listForm"
|
:key="'c' + index"
|
:class="{ 'calendar-row-hover': currentUserIndex == index }"
|
@mouseenter="onMouseEnter(index)"
|
@mouseleave="currentUserIndex = null">
|
<div class="calendar-cell"
|
v-for="(m, i) in item.list"
|
:key="'d' + i">
|
<el-dropdown trigger="click"
|
placement="bottom"
|
@command="(e) => handleCommand(e, m)"
|
class="shift-dropdown">
|
<div class="shift-box"
|
:class="{
|
'shift-box-early': m.shift === '早班',
|
'shift-box-mid': m.shift === '中班',
|
'shift-box-night': m.shift === '夜班',
|
'shift-box-rest': m.shift === '休息',
|
'shift-box-leave': m.shift === '请假',
|
'shift-box-other': m.shift === '夜11',
|
'shift-box-business': m.shift === '夜12',
|
}">
|
<span class="shift-text">{{ getShiftNameByValue(m.shift) || '—' }}</span>
|
</div>
|
<template #dropdown>
|
<el-dropdown-menu>
|
<el-dropdown-item v-for="(n, j) in classType"
|
:key="'h' + j"
|
:command="n.id">{{ n.shift || '—'
|
}}</el-dropdown-item>
|
</el-dropdown-menu>
|
</template>
|
</el-dropdown>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<!-- 年度排班 -->
|
<div class="yearly-table"
|
v-show="!query.month">
|
<div class="scheduling-left">
|
<div class="scheduling-header">
|
人员名称
|
</div>
|
<div class="scheduling-user"
|
:class="{ 'scheduling-user-hover': currentUserIndex == index }"
|
v-for="(item, index) in yearList"
|
:key="'e' + index"
|
@mouseenter="onMouseEnter(index)"
|
@mouseleave="currentUserIndex = null">
|
<div class="user-avatar">
|
{{ item.name ? item.name.charAt(0) : "" }}
|
</div>
|
<div class="user-details">
|
<h4 class="user-name">{{ item.name }}</h4>
|
<!-- <div class="user-stats">
|
<span class="stat-item">早:{{ item.day0 }}</span>
|
<span class="stat-item">中:{{ item.day1 }}</span>
|
<span class="stat-item">夜:{{ item.day2 }}</span>
|
<span class="stat-item">休:{{ item.day3 }}</span>
|
<span class="stat-item">假:{{ item.day4 }}</span>
|
<span class="stat-item">差:{{ item.day6 }}</span>
|
</div> -->
|
<div class="user-total">
|
<span class="total-label">合计出勤:</span>
|
<span class="total-value">{{ item.work_time }}天</span>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div class="scheduling-right">
|
<div class="yearly-calendar">
|
<div class="yearly-header">
|
<div class="yearly-header-item"
|
v-for="(item, index) in monthList"
|
:key="'b' + index">
|
<span class="month-name">{{ item }}月</span>
|
</div>
|
</div>
|
<div class="yearly-body">
|
<div class="yearly-row"
|
v-for="(item, index) in yearList"
|
:key="'c' + index"
|
:class="{ 'calendar-row-hover': currentUserIndex == index }"
|
@mouseenter="onMouseEnter(index)"
|
@mouseleave="currentUserIndex = null">
|
<div class="yearly-cell"
|
v-for="(m, i) in item.monthList"
|
:key="'d' + i">
|
<div class="monthly-attendance">
|
<span class="attendance-label">合计出勤:</span>
|
<span class="attendance-value">{{ m.totalMonthAttendance }}</span>
|
</div>
|
<!-- <div class="monthly-stats">
|
<span class="stat-item">早:{{ m.day0 }}</span>
|
<span class="stat-item">中:{{ m.day1 }}</span>
|
<span class="stat-item">夜:{{ m.day2 }}</span>
|
<span class="stat-item">休:{{ m.day3 }}</span>
|
<span class="stat-item">假:{{ m.day4 }}</span>
|
<span class="stat-item">差:{{ m.day6 }}</span>
|
</div> -->
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
</div>
|
<div style="display: flex; justify-content: flex-end; margin-top: 10px; margin-right: 30px">
|
<el-pagination background
|
@current-change="currentChange"
|
:page-size="pageSize"
|
:current-page="currentPage"
|
layout="total, prev, pager, next, jumper"
|
:total="total">
|
</el-pagination>
|
</div>
|
<el-dialog title="排班"
|
v-model="schedulingVisible"
|
width="400px">
|
<div class="search_thing">
|
<div class="search_label"
|
style="width: 90px">
|
<span style="color: red; margin-right: 4px">*</span>周次:
|
</div>
|
<div class="search_input">
|
<el-date-picker v-model="schedulingQuery.week"
|
type="week"
|
format="YYYY 第 ww 周"
|
placeholder="选择周次"
|
style="width: 100%">
|
</el-date-picker>
|
</div>
|
</div>
|
<div class="search_thing">
|
<div class="search_label"
|
style="width: 90px">
|
<span style="color: red; margin-right: 4px">*</span>人员名称:
|
</div>
|
<div class="search_input">
|
<el-select v-model="schedulingQuery.userId"
|
placeholder="请选择"
|
style="width: 100%"
|
multiple
|
clearable
|
collapse-tags>
|
<el-option v-for="item in personList"
|
:key="item.id"
|
:label="item.staffName"
|
:value="item.id">
|
</el-option>
|
</el-select>
|
</div>
|
</div>
|
<div class="search_thing">
|
<div class="search_label"
|
style="width: 90px">
|
<span style="color: red; margin-right: 4px">*</span>班次:
|
</div>
|
<div class="search_input">
|
<el-select v-model="schedulingQuery.shift"
|
placeholder="请选择"
|
style="width: 100%">
|
<el-option v-for="item in classType"
|
:key="item.id"
|
:label="getShiftNameByValue(item.shift)"
|
:value="item.id">
|
</el-option>
|
</el-select>
|
</div>
|
</div>
|
<template #footer>
|
<div class="dialog-footer">
|
<el-button @click="schedulingVisible = false">取 消</el-button>
|
<el-button type="primary"
|
@click="confirmScheduling"
|
:loading="loading">确 定</el-button>
|
</div>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, onMounted, getCurrentInstance } from "vue";
|
import { useRouter } from "vue-router";
|
import {
|
page,
|
pageYear,
|
add,
|
exportFile,
|
update,
|
staffOnJobListPage,
|
} from "@/api/personnelManagement/class";
|
import { deptTreeSelect } from "@/api/system/user.js";
|
import { getAttendanceRules } from "@/api/personnelManagement/attendanceRules.js";
|
import { useDict } from "@/utils/dict";
|
const { proxy } = getCurrentInstance();
|
const router = useRouter();
|
|
// 查询条件
|
const query = reactive({
|
userName: "",
|
sysDeptId: "",
|
year: new Date(),
|
month: new Date().getMonth() + 1,
|
});
|
// 获取班次字典值
|
const { shifts_list } = useDict("shifts_list");
|
// 月份选项
|
const monthOptions = [
|
{ value: 1, label: "1月" },
|
{ value: 2, label: "2月" },
|
{ value: 3, label: "3月" },
|
{ value: 4, label: "4月" },
|
{ value: 5, label: "5月" },
|
{ value: 6, label: "6月" },
|
{ value: 7, label: "7月" },
|
{ value: 8, label: "8月" },
|
{ value: 9, label: "9月" },
|
{ value: 10, label: "10月" },
|
{ value: 11, label: "11月" },
|
{ value: 12, label: "12月" },
|
];
|
|
// 部门列表
|
const deptOptions = ref([]);
|
|
// 周列表
|
const weeks = ref([]);
|
|
// 班次类型
|
const classType = ref([]);
|
|
// 当前用户索引
|
const currentUserIndex = ref(null);
|
|
// 排班弹窗显示状态
|
const schedulingVisible = ref(false);
|
|
// 人员列表
|
const personList = ref([]);
|
|
// 加载状态
|
const loading = ref(false);
|
|
// 排班查询条件
|
const schedulingQuery = reactive({
|
week: "",
|
userId: null,
|
shift: "",
|
});
|
|
// 列表数据
|
const listForm = ref([]);
|
|
// 当前页
|
const currentPage = ref(1);
|
|
// 每页条数
|
const pageSize = ref(6);
|
|
// 总条数
|
const total = ref(3);
|
|
// 页面加载状态
|
const pageLoading = ref(false);
|
|
// 月份列表
|
const monthList = ref([1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12]);
|
|
// 年度列表
|
const yearList = ref([]);
|
|
// 导出加载状态
|
const downLoading = ref(false);
|
|
// 获取部门列表
|
const fetchDeptOptions = () => {
|
deptTreeSelect().then(response => {
|
deptOptions.value = filterDisabledDept(
|
JSON.parse(JSON.stringify(response.data))
|
);
|
});
|
};
|
|
// 根据班次值获取班次名称
|
const getShiftNameByValue = value => {
|
if (!value) return "";
|
const shift = shifts_list.value.find(item => item.value === value);
|
return shift ? shift.label : value;
|
};
|
|
// 过滤禁用的部门
|
const filterDisabledDept = deptList => {
|
return deptList.filter(dept => {
|
if (dept.disabled) {
|
return false;
|
}
|
if (dept.children && dept.children.length) {
|
dept.children = filterDisabledDept(dept.children);
|
}
|
return true;
|
});
|
};
|
|
// 刷新
|
const refresh = () => {
|
listForm.value = [];
|
yearList.value = [];
|
currentPage.value = 1;
|
query.userName = "";
|
query.sysDeptId = "";
|
query.year = new Date();
|
query.month = new Date().getMonth() + 1;
|
if (query.month) {
|
init();
|
} else {
|
initYear();
|
}
|
};
|
|
// 刷新表格
|
const refreshTable = () => {
|
currentPage.value = 1;
|
if (query.month) {
|
listForm.value = [];
|
init();
|
} else {
|
yearList.value = [];
|
initYear();
|
}
|
};
|
|
// 页码改变
|
const currentChange = num => {
|
currentPage.value = num;
|
if (query.month) {
|
init();
|
} else {
|
initYear();
|
}
|
};
|
|
// 数字转中文
|
const transFromNumber = num => {
|
let changeNum = ["零", "一", "二", "三", "四", "五", "六", "七", "八", "九"];
|
let unit = ["", "十", "百", "千", "万"];
|
num = parseInt(num);
|
let getWan = temp => {
|
let strArr = temp.toString().split("").reverse();
|
let newNum = "";
|
for (var i = 0; i < strArr.length; i++) {
|
newNum =
|
(i == 0 && strArr[i] == 0
|
? ""
|
: i > 0 && strArr[i] == 0 && strArr[i - 1] == 0
|
? ""
|
: changeNum[strArr[i]] + (strArr[i] == 0 ? unit[0] : unit[i])) +
|
newNum;
|
}
|
return newNum;
|
};
|
let overWan = Math.floor(num / 10000);
|
let noWan = num % 10000;
|
if (noWan.toString().length < 4) noWan = "0" + noWan;
|
return overWan ? getWan(overWan) + "万" + getWan(noWan) : getWan(num);
|
};
|
|
// 初始化月度数据
|
const init = () => {
|
pageLoading.value = true;
|
console.log(query.year, "query.year");
|
let year = query.year.getFullYear();
|
let month0 = query.month ? query.month : new Date().getMonth() + 1;
|
let month = month0 > 9 ? month0 : "0" + month0;
|
page({
|
size: pageSize.value,
|
current: currentPage.value,
|
time: year + "-" + month + "-01 00:00:00",
|
userName: query.userName,
|
sysDeptId: query.sysDeptId,
|
})
|
.then(res => {
|
pageLoading.value = false;
|
total.value = res.data.page.total;
|
listForm.value = res.data.page.records.map(item => {
|
for (let key in item.monthlyAttendance) {
|
let type = getDayByDic(key);
|
if (type != undefined || type != null) {
|
item[`day${type}`] = item.monthlyAttendance[key];
|
}
|
}
|
return item;
|
});
|
let headerList = res.data.headerList;
|
weeks.value = [];
|
headerList.forEach(item => {
|
let obj = {
|
weekNum: item.weekly,
|
week: item.headerTime.split(" ")[1],
|
day: item.headerTime.split(" ")[0],
|
};
|
weeks.value.push(obj);
|
});
|
})
|
.catch(() => {
|
pageLoading.value = false;
|
});
|
};
|
|
// 初始化年度数据
|
const initYear = () => {
|
pageLoading.value = true;
|
let year = query.year.getFullYear();
|
pageYear({
|
size: pageSize.value,
|
current: currentPage.value,
|
time: year + "-01-01 00:00:00",
|
userName: query.userName,
|
sysDeptId: query.sysDeptId,
|
}).then(res => {
|
pageLoading.value = false;
|
total.value = res.data.total;
|
yearList.value = res.data.records.map(item => {
|
for (let key in item.year) {
|
let type = getDayByDic(key);
|
if (type != undefined || type != null) {
|
item[`day${type}`] = item.year[key];
|
}
|
}
|
item.monthList = [];
|
for (let m in item.month) {
|
let obj = {};
|
for (let key in item.month[m]) {
|
let type = getDayByDic(key);
|
if (type != undefined || type != null) {
|
obj[`day${type}`] = item.month[m][key];
|
}
|
}
|
obj.totalMonthAttendance = item.month[m].totalMonthAttendance;
|
item.monthList.push(obj);
|
}
|
return item;
|
});
|
});
|
};
|
|
// 鼠标进入
|
const onMouseEnter = index => {
|
currentUserIndex.value = index;
|
};
|
|
// 确认排班
|
const confirmScheduling = () => {
|
if (!schedulingQuery.week) {
|
proxy.$modal.msgError("请选择周次");
|
return;
|
}
|
let time = schedulingQuery.week.getTime();
|
|
// 格式化日期为 YYYY-MM-DD 格式
|
const formatDate = date => {
|
const year = date.getFullYear();
|
const month = String(date.getMonth() + 1).padStart(2, "0");
|
const day = String(date.getDate()).padStart(2, "0");
|
return `${year}-${month}-${day}`;
|
};
|
|
let startWeek =
|
formatDate(new Date(time - 24 * 60 * 60 * 1000)) + " 00:00:00";
|
let endWeek =
|
formatDate(new Date(time + 24 * 60 * 60 * 1000 * 5)) + " 00:00:00";
|
|
if (!schedulingQuery.userId || schedulingQuery.userId.length == 0) {
|
proxy.$modal.msgError("请选择人员");
|
return;
|
}
|
if (!schedulingQuery.shift) {
|
proxy.$modal.msgError("请选择班次");
|
return;
|
}
|
loading.value = true;
|
add({
|
startWeek,
|
endWeek,
|
staffOnJobId: schedulingQuery.userId.join(","),
|
personalAttendanceLocationConfigId: schedulingQuery.shift,
|
})
|
.then(res => {
|
loading.value = false;
|
proxy.$modal.msgSuccess("操作成功");
|
schedulingVisible.value = false;
|
schedulingQuery.week = "";
|
schedulingQuery.userId = null;
|
schedulingQuery.shift = "";
|
refresh();
|
})
|
.catch(err => {
|
loading.value = false;
|
});
|
};
|
|
// 时间配置
|
const configTime = () => {
|
// 跳转到考勤打卡页面
|
router.push({
|
path: "/personnelManagement/checkinRules",
|
});
|
};
|
|
// 判断是否为空对象
|
const isObjectEmpty = obj => {
|
return Object.keys(obj).some(key => !obj[key]);
|
};
|
|
// 导出
|
const handleDown = () => {
|
let year = query.year.getFullYear();
|
let time = "";
|
if (query.month) {
|
let month = query.month > 9 ? query.month : "0" + query.month;
|
time = year + "-" + month + "-01 00:00:00";
|
} else {
|
time = year + "-01-01 00:00:00";
|
}
|
downLoading.value = true;
|
exportFile({
|
time,
|
userName: query.userName,
|
sysDeptId: query.sysDeptId,
|
isMonth: query.month ? true : false,
|
})
|
.then(res => {
|
proxy.$modal.msgSuccess("下载成功");
|
downLoading.value = false;
|
const blob = new Blob([res], {
|
type: "application/force-download",
|
});
|
let fileName = "";
|
if (query.month) {
|
fileName = year + "-" + query.month + " 班次信息";
|
} else {
|
fileName = year + " 班次汇总";
|
}
|
proxy.$download.saveAs(blob, fileName + ".xlsx");
|
})
|
.catch(err => {
|
downLoading.value = false;
|
});
|
};
|
// 处理命令
|
const handleCommand = (e, m) => {
|
// if (e != m.shift) {
|
update({
|
id: m.id,
|
personalAttendanceLocationConfigId: e,
|
}).then(res => {
|
proxy.$modal.msgSuccess("操作成功");
|
// m.shift = e;
|
if (query.month) {
|
init();
|
} else {
|
initYear();
|
}
|
});
|
// }
|
};
|
// 查询规则列表
|
const fetchData = () => {
|
getAttendanceRules({ current: -1, size: -1 }).then(res => {
|
classType.value = res.data.records;
|
});
|
};
|
// 获取用户
|
const getUsers = () => {
|
// selectUserCondition({ type: 1 }).then(res => {
|
// let arr = res.data;
|
// personList.value = arr;
|
// });
|
staffOnJobListPage({
|
current: -1,
|
size: -1,
|
staffState: 1,
|
}).then(res => {
|
let arr = res.data.records;
|
personList.value = arr;
|
});
|
};
|
|
// 根据字典获取日期
|
const getDayByDic = e => {
|
let obj = classType.value.find(m => m.shift == e);
|
if (obj) {
|
return obj.id;
|
}
|
};
|
|
// 根据字典获取班次
|
const getShiftByDic = e => {
|
let obj = classType.value.find(m => m.shift == e);
|
if (obj) {
|
return obj.shift;
|
}
|
return "无";
|
};
|
|
// 初始化
|
onMounted(() => {
|
fetchData();
|
getUsers();
|
fetchDeptOptions();
|
if (query.month) {
|
init();
|
} else {
|
initYear();
|
}
|
monthList.value = [];
|
for (let i = 12; i > 0; i--) {
|
monthList.value.push(i);
|
}
|
monthList.value.reverse();
|
});
|
</script>
|
|
<style scoped>
|
.class-page {
|
padding: 16px;
|
}
|
|
.form_title {
|
height: 36px;
|
display: flex;
|
flex-direction: row;
|
justify-content: space-between;
|
font-weight: 800;
|
}
|
|
/* 搜索区域样式 */
|
.search-container {
|
background: #f9fafb;
|
border-radius: 8px;
|
padding: 20px;
|
margin-bottom: 20px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
}
|
|
.search-form {
|
display: flex;
|
flex-direction: column;
|
gap: 16px;
|
}
|
|
.search-row {
|
display: flex;
|
align-items: center;
|
gap: 16px;
|
flex-wrap: nowrap;
|
overflow-x: auto;
|
}
|
|
.search-item {
|
display: flex;
|
align-items: center;
|
gap: 6px;
|
}
|
|
.search-label {
|
font-size: 14px;
|
font-weight: 500;
|
color: #333;
|
min-width: 65px;
|
text-align: right;
|
}
|
|
.search-input-group {
|
display: flex;
|
align-items: center;
|
}
|
|
.search-actions {
|
display: flex;
|
align-items: center;
|
margin-left: 8px;
|
}
|
|
.search-buttons {
|
display: flex;
|
align-items: center;
|
justify-content: flex-end;
|
flex: 1;
|
}
|
|
/* 响应式调整 */
|
@media (max-width: 1200px) {
|
.search-row {
|
gap: 12px;
|
}
|
|
.search-item {
|
gap: 4px;
|
}
|
|
.search-label {
|
min-width: 60px;
|
font-size: 13px;
|
}
|
|
.search-actions {
|
margin-left: 4px;
|
}
|
|
.search-buttons {
|
margin-left: 12px;
|
}
|
}
|
|
@media (max-width: 992px) {
|
.search-row {
|
flex-wrap: wrap;
|
justify-content: flex-start;
|
}
|
|
.search-buttons {
|
margin-left: 0;
|
margin-top: 12px;
|
width: 100%;
|
justify-content: flex-start;
|
}
|
}
|
|
/* 排班容器 */
|
.scheduling-container {
|
width: 100%;
|
min-height: calc(100vh - 280px);
|
background-color: #fff;
|
border-radius: 8px;
|
box-shadow: 0 2px 12px 0 rgba(0, 0, 0, 0.1);
|
overflow: hidden;
|
margin-bottom: 20px;
|
}
|
|
/* 排班表格 */
|
.scheduling-table {
|
display: flex;
|
width: 100%;
|
height: calc(100vh - 280px);
|
}
|
|
/* 左侧人员信息 */
|
.scheduling-left {
|
width: 240px;
|
min-width: 240px;
|
background-color: #f9fafb;
|
border-right: 1px solid #e5e7eb;
|
}
|
|
/* 右侧排班内容 */
|
.scheduling-right {
|
flex: 1;
|
overflow-x: auto;
|
}
|
|
/* 表头 */
|
.scheduling-header {
|
height: 48px;
|
line-height: 48px;
|
padding: 0 20px;
|
font-size: 14px;
|
font-weight: 600;
|
color: #333;
|
background-color: #f3f4f6;
|
border-bottom: 1px solid #e5e7eb;
|
}
|
|
/* 人员信息行 */
|
.scheduling-user {
|
display: flex;
|
align-items: center;
|
padding: 10px 10px;
|
border-bottom: 1px solid #e5e7eb;
|
transition: all 0.3s ease;
|
height: 65px;
|
box-sizing: border-box;
|
}
|
|
.scheduling-user:hover,
|
.scheduling-user-hover {
|
background-color: rgba(59, 130, 246, 0.05);
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.05);
|
}
|
|
/* 用户头像 */
|
.user-avatar {
|
width: 42px;
|
height: 42px;
|
border-radius: 50%;
|
background: linear-gradient(135deg, #3b82f6, #60a5fa);
|
color: #fff;
|
font-size: 18px;
|
font-weight: 600;
|
text-align: center;
|
line-height: 42px;
|
margin-right: 16px;
|
box-shadow: 0 2px 4px rgba(59, 130, 246, 0.2);
|
transition: all 0.3s ease;
|
}
|
|
.scheduling-user:hover .user-avatar {
|
transform: scale(1.05);
|
box-shadow: 0 4px 8px rgba(59, 130, 246, 0.3);
|
}
|
|
/* 用户详情 */
|
.user-details {
|
flex: 1;
|
display: flex;
|
flex-direction: column;
|
justify-content: center;
|
height: 100%;
|
}
|
|
/* 用户名 */
|
.user-name {
|
font-size: 14px;
|
font-weight: 600;
|
color: #333;
|
margin: 0 0 6px 0;
|
line-height: 1.2;
|
}
|
|
/* 用户统计 */
|
.user-stats {
|
/* display: flex; */
|
/* flex-wrap: wrap;
|
gap: 10px; */
|
margin-bottom: 4px;
|
}
|
|
.stat-item {
|
font-size: 12px;
|
color: #666;
|
/* background-color: #f9fafb; */
|
/* padding: 2px 8px; */
|
padding-right: 4px;
|
/* border-radius: 10px; */
|
/* border: 1px solid #e5e7eb; */
|
/* transition: all 0.3s ease; */
|
}
|
|
.scheduling-user:hover .stat-item {
|
background-color: rgba(59, 130, 246, 0.1);
|
border-color: rgba(59, 130, 246, 0.3);
|
}
|
|
/* 合计出勤 */
|
.user-total {
|
display: flex;
|
align-items: center;
|
}
|
|
.total-label {
|
font-size: 12px;
|
color: #666;
|
margin-right: 6px;
|
font-weight: 500;
|
}
|
|
.total-value {
|
font-size: 14px;
|
font-weight: 600;
|
color: #3b82f6;
|
background-color: rgba(59, 130, 246, 0.1);
|
padding: 2px 10px;
|
border-radius: 10px;
|
border: 1px solid rgba(59, 130, 246, 0.2);
|
}
|
|
/* 日历头部 */
|
.calendar-header {
|
display: flex;
|
|
border-bottom: 1px solid #e5e7eb;
|
}
|
|
.calendar-header-item {
|
width: 50px;
|
min-width: 50px;
|
height: 48px;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
justify-content: center;
|
border-right: 1px solid #e5e7eb;
|
background-color: #f3f4f6;
|
position: relative;
|
}
|
|
.week-number {
|
position: absolute;
|
top: 6px;
|
font-size: 10px;
|
font-weight: 600;
|
color: #3b82f6;
|
background-color: #dbeafe;
|
padding: 3px 6px;
|
border-radius: 12px;
|
box-shadow: 0 1px 3px rgba(59, 130, 246, 0.2);
|
transition: all 0.3s ease;
|
}
|
|
.week-number:hover {
|
background-color: #3b82f6;
|
color: #fff;
|
transform: translateY(-1px);
|
}
|
|
.day-info {
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
.day-number {
|
font-size: 14px;
|
font-weight: 500;
|
color: #333;
|
}
|
|
.day-week {
|
font-size: 12px;
|
color: #666;
|
}
|
|
/* 日历主体 */
|
.calendar-body {
|
display: flex;
|
flex-direction: column;
|
}
|
|
/* 日历行 */
|
.calendar-row {
|
display: flex;
|
border-bottom: 1px solid #e5e7eb;
|
transition: all 0.3s ease;
|
}
|
|
.calendar-row:hover,
|
.calendar-row-hover {
|
background-color: rgba(59, 130, 246, 0.03);
|
}
|
|
/* 日历单元格 */
|
.calendar-cell {
|
width: 50px;
|
min-width: 50px;
|
height: 65px;
|
border-right: 1px solid #e5e7eb;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
/* 班次下拉框 */
|
.shift-dropdown {
|
width: 100%;
|
height: 100%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
}
|
|
/* 班次框 */
|
.shift-box {
|
width: 90%;
|
height: 80%;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-radius: 6px;
|
font-size: 12px;
|
font-weight: 500;
|
transition: all 0.3s ease;
|
}
|
|
.shift-box:hover {
|
transform: scale(1.05);
|
}
|
|
/* 班次类型样式 */
|
.shift-box-early {
|
background: rgba(59, 130, 246, 0.15);
|
color: #3b82f6;
|
}
|
|
.shift-box-mid {
|
background: rgba(139, 92, 246, 0.15);
|
color: #8b5cf6;
|
}
|
|
.shift-box-night {
|
background: rgba(245, 158, 11, 0.15);
|
color: #f59e0b;
|
}
|
|
.shift-box-rest {
|
background: rgba(16, 185, 129, 0.15);
|
color: #10b981;
|
}
|
|
.shift-box-leave {
|
background: rgba(239, 68, 68, 0.15);
|
color: #ef4444;
|
}
|
|
.shift-box-other {
|
background: rgba(236, 72, 153, 0.15);
|
color: #ec4899;
|
}
|
|
.shift-box-business {
|
background: rgba(17, 24, 39, 0.15);
|
color: #111827;
|
}
|
|
/* 班次文本 */
|
.shift-text {
|
text-align: center;
|
}
|
|
/* 年度表格 */
|
.yearly-table {
|
display: flex;
|
width: 100%;
|
height: calc(100vh - 280px);
|
}
|
|
/* 年度日历 */
|
.yearly-calendar {
|
width: 100%;
|
}
|
|
/* 年度表头 */
|
.yearly-header {
|
display: grid;
|
grid-template-columns: repeat(12, 1fr);
|
background-color: #f3f4f6;
|
border-bottom: 1px solid #e5e7eb;
|
}
|
|
.yearly-header-item {
|
height: 48px;
|
display: flex;
|
align-items: center;
|
justify-content: center;
|
border-right: 1px solid #e5e7eb;
|
}
|
|
.month-name {
|
font-size: 14px;
|
font-weight: 500;
|
color: #333;
|
}
|
|
/* 年度主体 */
|
.yearly-body {
|
display: flex;
|
flex-direction: column;
|
}
|
|
/* 年度行 */
|
.yearly-row {
|
display: grid;
|
grid-template-columns: repeat(12, 1fr);
|
border-bottom: 1px solid #e5e7eb;
|
transition: all 0.3s ease;
|
}
|
|
/* 年度单元格 */
|
.yearly-cell {
|
padding: 12px;
|
border-right: 1px solid #e5e7eb;
|
display: flex;
|
flex-direction: column;
|
align-items: center;
|
}
|
|
/* 月度出勤 */
|
.monthly-attendance {
|
margin-bottom: 8px;
|
}
|
|
.attendance-label {
|
font-size: 12px;
|
color: #666;
|
margin-right: 4px;
|
}
|
|
.attendance-value {
|
font-size: 14px;
|
font-weight: 600;
|
color: #333;
|
}
|
|
/* 月度统计 */
|
.monthly-stats {
|
display: flex;
|
flex-wrap: wrap;
|
gap: 4px;
|
justify-content: center;
|
}
|
|
.monthly-stats .stat-item {
|
font-size: 11px;
|
}
|
|
/* 滚动条样式 */
|
.scheduling-right::-webkit-scrollbar {
|
height: 8px;
|
}
|
|
.scheduling-right::-webkit-scrollbar-track {
|
background: #f1f1f1;
|
}
|
|
.scheduling-right::-webkit-scrollbar-thumb {
|
background: #c1c1c1;
|
border-radius: 4px;
|
}
|
|
.scheduling-right::-webkit-scrollbar-thumb:hover {
|
background: #a8a8a8;
|
}
|
|
.search_label {
|
font-size: 14px;
|
font-weight: 500;
|
color: #333;
|
margin-bottom: 8px;
|
margin-top: 12px;
|
}
|
</style>
|