666 lines
20 KiB
Vue
666 lines
20 KiB
Vue
<template>
|
||
<!-- 考勤管理 -->
|
||
<div class="model">
|
||
<!-- 标题栏 -->
|
||
<el-row :gutter="24">
|
||
<el-col :span="12">
|
||
<TitleComponent title="考勤管理" subtitle="项目出勤情况、人员排班及请假调休管理" />
|
||
</el-col>
|
||
<!-- 外层col:控制整体宽度并右对齐,同时作为flex容器 -->
|
||
<el-col :span="12" style="display: flex; justify-content: flex-end; align-items: center;">
|
||
<!-- 子col1:下拉 -->
|
||
<el-col :span="4">
|
||
<el-select placeholder="选择电站">
|
||
<el-option label="所有电站" value="all"></el-option>
|
||
</el-select>
|
||
</el-col>
|
||
<!-- 子col2:下拉框容器 -->
|
||
<el-col :span="4">
|
||
<el-select placeholder="日期范围">
|
||
<el-option label="所有月份" value="all"></el-option>
|
||
</el-select>
|
||
</el-col>
|
||
<el-col :span="4">
|
||
<el-button type="primary">
|
||
导出数据
|
||
<el-icon class="el-icon--right">
|
||
<UploadFilled />
|
||
</el-icon>
|
||
</el-button>
|
||
</el-col>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 第一行:totalView 和 infoBox -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="17">
|
||
<totalView :totalData="totalData"></totalView>
|
||
</el-col>
|
||
<el-col :span="7">
|
||
<infoBox></infoBox>
|
||
</el-col>
|
||
</el-row>
|
||
|
||
<!-- 第二行:人员排班和出勤趋势分析 -->
|
||
<el-row :gutter="20">
|
||
<el-col :span="17">
|
||
<div class="analysis-content">
|
||
<attendTrend :attendData="attendData"></attendTrend>
|
||
<el-card>
|
||
<div style="display: flex; justify-content: space-between; align-items: center;">
|
||
<TitleComponent title="人员排班" :fontLevel="2" />
|
||
<el-button type="primary" @click="manageAttendDialogVisible = true">
|
||
管理考勤
|
||
</el-button>
|
||
</div>
|
||
<renyuanpaiban @edit-schedule="handleEditSchedule" :schedule-list="scheduleList">
|
||
</renyuanpaiban>
|
||
</el-card>
|
||
</div>
|
||
</el-col>
|
||
|
||
<!-- 右侧日历卡片 -->
|
||
<el-col :span="7">
|
||
<div class="calendar-content">
|
||
<el-card>
|
||
<calendar :calendarData="calendarData"></calendar>
|
||
<todayAttend :todayAttendData="todayAttendData"></todayAttend>
|
||
<approval :approvalData="approvalData"></approval>
|
||
</el-card>
|
||
</div>
|
||
</el-col>
|
||
</el-row>
|
||
<!-- 人员排班弹窗组件 -->
|
||
<renyuanguanliDialog v-model:manageAttendDialogVisible="manageAttendDialogVisible"
|
||
@confirm="handleAttendConfirm" :personnel-list="paibanRenYuanList" :type-list="scheduleTypes" />
|
||
|
||
<!-- 编辑排班弹窗 -->
|
||
<el-dialog v-model="editScheduleDialogVisible" title="修改排班" width="400">
|
||
<el-form :model="editScheduleForm" label-width="100px">
|
||
<el-form-item label="员工姓名">
|
||
<el-input v-model="editScheduleForm.name" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="排班日期">
|
||
<el-input v-model="editScheduleForm.date" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="当前排班">
|
||
<el-input v-model="editScheduleForm.currentShift" disabled />
|
||
</el-form-item>
|
||
<el-form-item label="修改为">
|
||
<el-select v-model="editScheduleForm.newShift" placeholder="请选择排班类型" style="width: 100%;">
|
||
<el-option v-for="option in editscheduleTypes" :key="option.id" :label="option.schedulingName"
|
||
:value="option.id"></el-option>
|
||
</el-select>
|
||
</el-form-item>
|
||
</el-form>
|
||
|
||
<template #footer>
|
||
<div class="dialog-footer">
|
||
<el-button @click="editScheduleDialogVisible = false">取消</el-button>
|
||
<el-button type="primary" @click="handleConfirmEditSchedule">
|
||
确认修改
|
||
</el-button>
|
||
</div>
|
||
</template>
|
||
</el-dialog>
|
||
</div>
|
||
</template>
|
||
<script setup lang="ts">
|
||
import infoBox from '@/views/integratedManage/attendManage/components/infoBox.vue'
|
||
import attendTrend from '@/views/integratedManage/attendManage/components/attendTrend.vue'
|
||
import todayAttend from '@/views/integratedManage/attendManage/components/rightBox/todayAttend.vue'
|
||
import approval from '@/views/integratedManage/attendManage/components/rightBox/approval.vue'
|
||
import calendar from '@/views/integratedManage/attendManage/components/rightBox/calendar.vue'
|
||
import totalView from '@/views/integratedManage/attendManage/components/totalView.vue'
|
||
import renyuanpaiban from '@/views/integratedManage/attendManage/components/renyuanpaiban.vue'
|
||
import renyuanguanliDialog from '@/views/integratedManage/attendManage/components/renyuanguanliDialog.vue'
|
||
|
||
|
||
import { getPaibanRenYuanList, getPaibanRiLiList, savePaiban, updatePaiban, deletePaiban } from '@/api/renyuan/paiban';
|
||
import { SchedulingVO } from '@/api/renyuan/paiban/types';
|
||
import { listSchedulingDate } from '@/api/renyuan/schedulingDate';
|
||
import { ref, onMounted, watch, onUnmounted } from 'vue';
|
||
import { getCurrentMonthDates } from '@/utils/getDate';
|
||
const currentMonthDates = getCurrentMonthDates();
|
||
const { proxy } = getCurrentInstance() as ComponentInternalInstance;
|
||
// 导入用户store
|
||
import { useUserStore } from '@/store/modules/user';
|
||
// 初始化用户store
|
||
const userStore = useUserStore();
|
||
|
||
// 排班人员列表
|
||
const paibanRenYuanList = ref([]);
|
||
|
||
// 排班人员数据
|
||
const scheduleList = ref<SchedulingVO[]>([]);
|
||
|
||
// 排班类型
|
||
const scheduleTypes = ref([]);
|
||
// 修改弹出框的类型下拉
|
||
const editscheduleTypes = ref([]);
|
||
// 编辑排班弹窗
|
||
const editScheduleDialogVisible = ref(false);
|
||
|
||
// 人员排班弹窗
|
||
const manageAttendDialogVisible = ref(false);
|
||
|
||
// 获取排班人员列表
|
||
const fetchPaibanRenYuanList = async (deptId?: string) => {
|
||
try {
|
||
// 如果没有提供deptId,默认使用当前登录用户的部门ID
|
||
const targetDeptId = deptId || userStore.deptId;
|
||
if (!targetDeptId) {
|
||
console.warn('未提供部门ID,无法获取排班人员列表');
|
||
return;
|
||
}
|
||
const response = await getPaibanRenYuanList(targetDeptId);
|
||
// console.log('获取排班人员:', response);
|
||
paibanRenYuanList.value = response.data?.map((user: any) => ({
|
||
label: user.nickName,
|
||
value: user.userId,
|
||
deptId: user.deptId,
|
||
|
||
})) || [];
|
||
} catch (error) {
|
||
console.error('获取排班人员列表失败:', error);
|
||
}
|
||
};
|
||
|
||
// 获取排班数据
|
||
const getscheduleData = async (query?: SchedulingVO) => {
|
||
try {
|
||
if (userStore.selectedProject && userStore.selectedProject.id) {
|
||
const res = await getPaibanRiLiList(query);
|
||
if (res.code === 200) {
|
||
scheduleList.value = res.data || [];
|
||
} else {
|
||
proxy?.$modal.msgError(res.msg || '获取排班数据失败');
|
||
}
|
||
}
|
||
} catch (error) {
|
||
proxy?.$modal.msgError('获取排班数据失败');
|
||
}
|
||
}
|
||
|
||
// 获取排班类型
|
||
const getTypeList = async () => {
|
||
try {
|
||
const res = await listSchedulingDate({ projectId: userStore.selectedProject?.id, pageNum: 1, pageSize: 10 });
|
||
if (res.code === 200) {
|
||
scheduleTypes.value = res.rows || [];
|
||
// 在scheduleTypes基础上新增休息字段
|
||
editscheduleTypes.value = [
|
||
...(res.rows || []),
|
||
{ id: 'rest', schedulingName: '休息' }
|
||
];
|
||
} else {
|
||
proxy?.$modal.msgError(res.msg || '获取排班类型失败');
|
||
// 如果获取失败,至少保留休息选项
|
||
editscheduleTypes.value = [{ id: 'rest', schedulingName: '休息' }];
|
||
}
|
||
} catch (error) {
|
||
console.error('获取排班类型出错:', error);
|
||
proxy?.$modal.msgError('获取排班类型失败');
|
||
// 异常情况下也保留休息选项
|
||
editscheduleTypes.value = [{ id: 'rest', schedulingTypeName: '休息' }];
|
||
}
|
||
}
|
||
|
||
// 安排人员排班
|
||
const arrangePaiban = async (formData: any) => {
|
||
try {
|
||
// 添加projectId到表单数据中
|
||
const dataWithProjectId = {
|
||
...formData,
|
||
projectId: userStore.selectedProject?.id
|
||
};
|
||
const res = await savePaiban(dataWithProjectId);
|
||
if (res.code === 200) {
|
||
proxy?.$modal.msgSuccess('排班成功');
|
||
// 刷新排班数据
|
||
refreshScheduleData(userStore.selectedProject.id);
|
||
// 关闭弹窗
|
||
manageAttendDialogVisible.value = false;
|
||
} else {
|
||
proxy?.$modal.msgError(res.msg || '排班失败');
|
||
}
|
||
} catch (error) {
|
||
proxy?.$modal.msgError('排班失败');
|
||
}
|
||
}
|
||
|
||
|
||
// 修改排班
|
||
const updateSchedule = async (formData: any) => {
|
||
try {
|
||
const res = await updatePaiban(formData);
|
||
if (res.code === 200) {
|
||
proxy?.$modal.msgSuccess('修改排班成功');
|
||
// 刷新排班数据
|
||
refreshScheduleData(userStore.selectedProject.id);
|
||
// 关闭弹窗
|
||
editScheduleDialogVisible.value = false;
|
||
} else {
|
||
proxy?.$modal.msgError(res.msg || '修改排班失败');
|
||
}
|
||
} catch (error) {
|
||
proxy?.$modal.msgError('修改排班失败');
|
||
}
|
||
}
|
||
|
||
// 删除排班
|
||
const deleteSchedule = async (ID: any) => {
|
||
try {
|
||
const res = await deletePaiban(ID);
|
||
if (res.code === 200) {
|
||
proxy?.$modal.msgSuccess('删除排班成功');
|
||
// 刷新排班数据
|
||
refreshScheduleData(userStore.selectedProject.id);
|
||
// 关闭弹窗
|
||
editScheduleDialogVisible.value = false;
|
||
} else {
|
||
proxy?.$modal.msgError(res.msg || '删除排班失败');
|
||
}
|
||
} catch (error) {
|
||
proxy?.$modal.msgError('删除排班失败');
|
||
}
|
||
}
|
||
// 处理考勤管理确认
|
||
const handleAttendConfirm = (formData: any) => {
|
||
// console.log('考勤表单数据:', formData);
|
||
// 这里可以添加表单提交逻辑
|
||
arrangePaiban(formData);
|
||
};
|
||
|
||
|
||
|
||
// 编辑排班表单数据
|
||
const editScheduleForm = ref({
|
||
opsUserId: '', // 新增opsUserId字段
|
||
name: '',
|
||
date: '',
|
||
currentShift: '',
|
||
newShift: '',
|
||
id: '' // 新增scheduledId字段,用于存储排班的id
|
||
});
|
||
|
||
|
||
// 格式化排班文本,用于显示当前排班
|
||
const formatShiftDisplay = (shiftData: any): string => {
|
||
if (!shiftData) return '休息';
|
||
|
||
// 如果是字符串,直接返回
|
||
if (typeof shiftData === 'string') {
|
||
return shiftData;
|
||
}
|
||
|
||
// 如果是数组,返回第一个排班类型的名称
|
||
if (Array.isArray(shiftData) && shiftData.length > 0) {
|
||
if (typeof shiftData[0] === 'object' && shiftData[0].schedulingTypeName) {
|
||
return shiftData[0].schedulingTypeName;
|
||
}
|
||
return shiftData[0].toString();
|
||
}
|
||
|
||
// 如果是对象,返回排班类型名称
|
||
if (typeof shiftData === 'object' && shiftData.schedulingTypeName) {
|
||
return shiftData.schedulingTypeName;
|
||
}
|
||
|
||
// 如果是对象但没有schedulingTypeName属性,尝试返回其字符串表示
|
||
if (typeof shiftData === 'object') {
|
||
return JSON.stringify(shiftData);
|
||
}
|
||
|
||
return '休息';
|
||
};
|
||
|
||
// 处理编辑排班
|
||
const handleEditSchedule = (rowData: any, columnData: any) => {
|
||
|
||
// 设置表单数据
|
||
editScheduleForm.value = {
|
||
opsUserId: rowData.opsUserId || '', // 从opsUserId字段获取用户ID
|
||
name: rowData.name,
|
||
date: `${columnData.fullDate}`,
|
||
currentShift: '',
|
||
newShift: '',
|
||
id: ''
|
||
};
|
||
|
||
// 查找当前排班
|
||
Object.keys(rowData).forEach(key => {
|
||
if (key.startsWith('day')) {
|
||
const dayIndex = parseInt(key.replace('day', '')) - 1;
|
||
if (dayIndex === columnData.index) {
|
||
const formattedShift = formatShiftDisplay(rowData[key]);
|
||
editScheduleForm.value.currentShift = formattedShift;
|
||
editScheduleForm.value.newShift = formattedShift;
|
||
|
||
// 如果不是休息状态,则获取id并赋值到表单中
|
||
if (formattedShift !== '休息' && rowData[key]) {
|
||
// 处理rowData[key]为数组的情况
|
||
if (Array.isArray(rowData[key]) && rowData[key].length > 0 && rowData[key][0].id) {
|
||
editScheduleForm.value.id = rowData[key][0].id;
|
||
}
|
||
// 同时处理rowData[key]为对象的情况作为兼容
|
||
else if (typeof rowData[key] === 'object' && rowData[key].id) {
|
||
editScheduleForm.value.id = rowData[key].id;
|
||
}
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
// 显示弹窗
|
||
editScheduleDialogVisible.value = true;
|
||
};
|
||
|
||
// 确认修改排班
|
||
const handleConfirmEditSchedule = () => {
|
||
// 按照要求的格式准备数据
|
||
const submitData = {
|
||
projectId: userStore.selectedProject?.id,
|
||
opsUserId: editScheduleForm.value.opsUserId,
|
||
schedulingType: editScheduleForm.value.newShift,
|
||
schedulingDate: editScheduleForm.value.date,
|
||
id: editScheduleForm.value.id
|
||
};
|
||
console.log('提交的排班数据格式:', submitData);
|
||
if (submitData.schedulingType == 'rest') {
|
||
deleteSchedule(submitData.id);
|
||
} else {
|
||
|
||
updateSchedule(submitData);
|
||
}
|
||
|
||
};
|
||
|
||
// 出勤数据 - 用于attendTrend组件
|
||
const attendData = ref(
|
||
{
|
||
week: {
|
||
xAxis: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
|
||
actualCount: [40, 20, 30, 15, 22, 63, 58, 43, 39, 36],
|
||
expectedCount: [100, 556, 413, 115, 510, 115, 317, 118, 14, 7]
|
||
},
|
||
month: {
|
||
xAxis: ['第1周', '第2周', '第3周', '第4周'],
|
||
actualData: [280, 360, 320, 400],
|
||
theoreticalData: [300, 400, 350, 450]
|
||
},
|
||
}
|
||
)
|
||
|
||
// Mock数据 - 更新为循环生成所需的数据结构
|
||
const totalData = ref({
|
||
attendance: {
|
||
value: 248,
|
||
change: '+8.2%',
|
||
isPositive: true,
|
||
chartData: [150, 230, 224, 218, 135, 300, 220],
|
||
color: '#FF7D00',
|
||
title: '总出勤人数',
|
||
compareText: '较昨日同期',
|
||
chartType: 'bar'
|
||
},
|
||
rest: {
|
||
value: 8,
|
||
change: '+8.2%',
|
||
isPositive: true,
|
||
chartData: [10, 12, 15, 8, 7, 9, 10],
|
||
color: '#00C48C',
|
||
title: '调休',
|
||
compareText: '较上月同期',
|
||
chartType: 'line'
|
||
},
|
||
leave: {
|
||
value: 24,
|
||
change: '-10%',
|
||
isPositive: false,
|
||
chartData: [30, 25, 28, 22, 20, 26, 24],
|
||
color: '#FF5252',
|
||
title: '本月请假',
|
||
compareText: '较昨日同期',
|
||
chartType: 'line'
|
||
},
|
||
rate: {
|
||
value: '96.8%',
|
||
change: '+10%',
|
||
isPositive: true,
|
||
chartData: [90, 92, 94, 95, 97, 98, 96.8],
|
||
color: '#029CD4',
|
||
title: '平均出勤率',
|
||
compareText: '较昨日同期',
|
||
chartType: 'line'
|
||
}
|
||
});
|
||
|
||
// 审批数据 - 用于approval组件
|
||
const approvalData = ref([
|
||
{
|
||
type: '事假',
|
||
days: 1,
|
||
timeRange: '09.14-09.15',
|
||
people: '水泥班组-王五',
|
||
status: '待审批',
|
||
statusType: 'primary',
|
||
iconPath: '/src/assets/demo/approval.png'
|
||
},
|
||
{
|
||
type: '病假',
|
||
days: 2,
|
||
timeRange: '09.14-09.15',
|
||
people: '水泥班组-王五',
|
||
status: '待审批',
|
||
statusType: 'primary',
|
||
iconPath: '/src/assets/demo/approval.png'
|
||
},
|
||
{
|
||
type: '调休',
|
||
days: 1,
|
||
timeRange: '09.14-09.15',
|
||
people: '水泥班组-王五',
|
||
status: '待审批',
|
||
statusType: 'primary',
|
||
iconPath: '/src/assets/demo/approval.png'
|
||
},
|
||
{
|
||
type: '事假',
|
||
days: 1,
|
||
timeRange: '09.14-09.15',
|
||
people: '水泥班组-王五',
|
||
status: '待审批',
|
||
statusType: 'primary',
|
||
iconPath: '/src/assets/demo/approval.png'
|
||
},
|
||
{
|
||
type: '事假',
|
||
days: 1,
|
||
timeRange: '09.14-09.15',
|
||
people: '水泥班组-王五',
|
||
status: '已通过',
|
||
statusType: 'success',
|
||
iconPath: '/src/assets/demo/approval.png'
|
||
}
|
||
]);
|
||
|
||
// 今日出勤数据 - 用于todayAttend组件
|
||
const todayAttendData = ref({
|
||
attendance: {
|
||
count: 150,
|
||
icon: '/src/assets/demo/qin.png'
|
||
},
|
||
late: {
|
||
count: 5,
|
||
icon: '/src/assets/demo/chi.png'
|
||
},
|
||
earlyLeave: {
|
||
count: 2,
|
||
icon: '/src/assets/demo/tui.png'
|
||
},
|
||
absent: {
|
||
count: 8,
|
||
icon: '/src/assets/demo/que.png'
|
||
}
|
||
});
|
||
|
||
// 日历数据 - 用于calendar组件
|
||
const calendarData = ref({
|
||
// 初始化当前日期
|
||
today: new Date(),
|
||
currentDate: new Date(2025, 8, 27), // 2025年9月27日,截图中显示的日期
|
||
selectedDate: new Date(2025, 8, 27),
|
||
|
||
// 模拟考勤数据
|
||
attendanceData: {
|
||
2025: {
|
||
9: {
|
||
1: 'normal',
|
||
4: 'late',
|
||
8: 'absent',
|
||
10: 'leave',
|
||
15: 'normal',
|
||
20: 'normal',
|
||
25: 'late',
|
||
27: 'normal'
|
||
}
|
||
}
|
||
}
|
||
});
|
||
|
||
|
||
// 封装刷新排班数据的方法
|
||
const refreshScheduleData = (projectId: string) => {
|
||
// 获取排班数据
|
||
getscheduleData({
|
||
projectId: projectId,
|
||
schedulingStartDate: currentMonthDates[0].fullDate,
|
||
schedulingEndDate: currentMonthDates[currentMonthDates.length - 1].fullDate,
|
||
});
|
||
// 获取排班类型
|
||
getTypeList();
|
||
};
|
||
|
||
// 监听projectId变化
|
||
const projectIdWatcher = watch(
|
||
() => userStore.selectedProject?.id,
|
||
(newProjectId, oldProjectId) => {
|
||
if (newProjectId && newProjectId !== oldProjectId) {
|
||
refreshScheduleData(newProjectId);
|
||
}
|
||
},
|
||
{ immediate: false, deep: true }
|
||
);
|
||
|
||
onMounted(() => {
|
||
// 刷新排班数据
|
||
refreshScheduleData(userStore.selectedProject.id);
|
||
|
||
// 获取可以排班的人员列表
|
||
fetchPaibanRenYuanList(String(userStore.deptId));
|
||
});
|
||
|
||
// 组件卸载时移除监听器
|
||
onUnmounted(() => {
|
||
if (projectIdWatcher) {
|
||
projectIdWatcher();
|
||
}
|
||
});
|
||
|
||
</script>
|
||
|
||
<style scoped lang="scss">
|
||
.model {
|
||
padding: 24px 20px;
|
||
background-color: rgba(242, 248, 252, 1);
|
||
}
|
||
|
||
/* 标题栏与内容区域间距 */
|
||
.el-row+.el-row {
|
||
margin-top: 24px;
|
||
}
|
||
|
||
/* 分析内容区域 */
|
||
.analysis-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
gap: 45px;
|
||
// border: 1px solid red;
|
||
}
|
||
|
||
/* 日历内容区域 */
|
||
.calendar-content {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
/* 右侧日历卡片内组件间距 */
|
||
.calendar-content .el-card {
|
||
display: flex;
|
||
flex-direction: column;
|
||
height: 100%;
|
||
}
|
||
|
||
.calendar-content .el-card>* {
|
||
margin-bottom: 16px;
|
||
}
|
||
|
||
.calendar-content .el-card>*:last-child {
|
||
margin-bottom: 0;
|
||
flex: 1;
|
||
}
|
||
|
||
/* 卡片样式统一 */
|
||
.el-card {
|
||
border-radius: 8px !important;
|
||
border: none !important;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.06);
|
||
overflow: hidden;
|
||
}
|
||
|
||
/* 下拉选择器和按钮样式调整 */
|
||
.el-select {
|
||
width: 100%;
|
||
margin-right: 12px;
|
||
}
|
||
|
||
.el-button {
|
||
margin-left: 8px;
|
||
}
|
||
|
||
/* 响应式布局优化 */
|
||
@media screen and (max-width: 1200px) {
|
||
.model {
|
||
padding: 16px;
|
||
}
|
||
|
||
.el-row+.el-row {
|
||
margin-top: 16px;
|
||
}
|
||
|
||
.analysis-content {
|
||
gap: 16px;
|
||
}
|
||
|
||
/* 日历卡片内组件间距 */
|
||
.calendar-content .el-card>* {
|
||
margin-bottom: 12px;
|
||
}
|
||
}
|
||
|
||
/* 更细粒度的响应式调整 */
|
||
@media screen and (max-width: 768px) {
|
||
.model {
|
||
padding: 12px;
|
||
}
|
||
|
||
.el-select {
|
||
margin-right: 8px;
|
||
}
|
||
|
||
.el-button {
|
||
margin-left: 4px;
|
||
padding: 8px 12px;
|
||
}
|
||
}
|
||
</style> |