Files
klp-oa/klp-ui/src/views/wms/hrm/attendance/drag.vue
砂糖 501abc4821 feat(wms/attendance): 新增批量修改排班功能,优化设备巡检表格展示
1. 新增批量修改排班API接口和页面弹窗功能
2. 设备巡检表格移除固定宽度并添加溢出提示,新增现场图像展示列
2026-05-25 15:39:03 +08:00

1377 lines
42 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div class="app-container">
<!-- 日期范围选择 -->
<div class="date-range-section">
<TimeRangePicker v-model="dateRangeParams" startKey="scheduleDateStart" endKey="scheduleDateEnd"
:defaultStartTime="defaultStartTime" :defaultEndTime="defaultEndTime" @change="handleDateRangeChange"
@quick-select="getScheduleList" />
</div>
<!-- 操作栏 -->
<div class="operation-bar">
<el-button plain type="primary" icon="el-icon-plus" @click="handleCreate">创建排班</el-button>
<el-button plain type="info" icon="el-icon-refresh" @click="handleRefresh">刷新</el-button>
<el-button plain type="success" icon="el-icon-setting" @click="showTemplateManager = true">管理模板</el-button>
<el-button plain type="warning" icon="el-icon-edit" @click="handleBatchUpdate">批量修改</el-button>
</div>
<el-alert type="info" title="提示:双击排班单元格可编辑排班"></el-alert>
<!-- 排班表格 -->
<div class="schedule-table-wrapper">
<el-table v-loading="loading" :data="scheduleData" border stripe height="calc(100vh - 200px)">
<!-- 员工列 -->
<el-table-column prop="employeeName" label="员工" width="120" fixed="left" />
<!-- 操作列 -->
<el-table-column label="操作" width="120" fixed="right">
<template slot-scope="scope">
<el-button size="mini" type="danger" @click="handleDeleteRow(scope.row)">删除整行</el-button>
</template>
</el-table-column>
<!-- 动态日期列 -->
<el-table-column v-for="date in dateList" :key="date" :label="formatDateLabel(date)" width="150" align="center">
<template slot-scope="scope">
<div class="schedule-cell" :class="{ 'has-data': scope.row[date], 'empty-cell': !scope.row[date] }"
@dblclick="handleCellDoubleClick(scope.row, date)">
<template v-if="scope.row[date]">
<div class="shift-name" :class="getShiftTypeClass(scope.row[date].shiftType)">
{{ scope.row[date].shiftName }}
</div>
<div class="shift-time-info">
<span v-if="scope.row[date].shiftStartTime">{{ scope.row[date].shiftStartTime }}-{{
scope.row[date].shiftEndTime }}</span>
<span v-if="scope.row[date].shiftStartTime2" class="second-shift">
{{ scope.row[date].shiftStartTime2 }}-{{ scope.row[date].shiftEndTime2 }}
</span>
</div>
</template>
<template v-else>
<span class="empty-hint">双击排班</span>
</template>
</div>
</template>
</el-table-column>
</el-table>
</div>
<!-- 创建排班弹窗 -->
<el-dialog title="创建排班" :visible.sync="dialogVisible" width="900px">
<!-- 步骤指示器 -->
<div class="steps">
<div :class="['step', { active: currentStep >= 1, done: currentStep > 1 }]">
<span class="step-number">1</span>
<span class="step-text">选择班次</span>
</div>
<div class="step-arrow"></div>
<div :class="['step', { active: currentStep >= 2, done: currentStep > 2 }]">
<span class="step-number">2</span>
<span class="step-text">分配人员</span>
</div>
<div class="step-arrow"></div>
<div :class="['step', { active: currentStep >= 3 }]">
<span class="step-number">3</span>
<span class="step-text">确认生成</span>
</div>
</div>
<!-- 步骤1选择班次配置 -->
<div v-if="currentStep === 1" class="step-content">
<el-form ref="form" :model="form" label-width="100px">
<el-form-item label="时间段">
<el-date-picker v-model="form.dateRange" type="daterange" range-separator="至"
start-placeholder="开始日期" end-placeholder="结束日期"
value-format="yyyy-MM-dd" style="width: 100%;" />
</el-form-item>
<el-form-item label="班次配置">
<div class="shift-config-panel">
<div v-for="(item, index) in form.shiftList" :key="index" class="shift-config-row">
<div class="shift-config-header">
<span class="config-label">班次 {{ index + 1 }}</span>
<el-button icon="el-icon-plus" size="mini" @click="addShiftItem">添加班次</el-button>
<el-button v-if="form.shiftList.length > 1" icon="el-icon-delete" size="mini"
@click="removeShiftItem(index)">删除</el-button>
</div>
<div class="shift-config-fields">
<el-select v-model="item.shiftId" placeholder="选择班次" style="width: 180px;" clearable>
<el-option v-for="shift in shiftList" :key="shift.shiftId"
:label="shift.shiftName" :value="shift.shiftId" />
</el-select>
<span v-if="item.shiftId" class="shift-time-display">
{{ getShiftTime(item.shiftId) }}
</span>
<el-select v-model="item.ruleId" placeholder="倒班规则(可选)" style="width: 180px;" clearable>
<el-option v-for="rule in shiftRuleList" :key="rule.ruleId"
:label="rule.changeDays" :value="rule.ruleId" />
</el-select>
</div>
</div>
</div>
</el-form-item>
</el-form>
</div>
<!-- 步骤2为每个班次分配人员 -->
<div v-if="currentStep === 2" class="step-content">
<div v-for="(item, index) in form.shiftList" :key="index" class="shift-assignment">
<div class="assignment-header">
<el-tag type="primary">{{ getShiftName(item.shiftId) || '未选择班次' }}</el-tag>
<span class="assignment-count">
已分配 {{ item.employeeIds ? item.employeeIds.split(',').filter(id => id.trim()).length : 0 }}
</span>
<div class="import-export-buttons">
<el-button icon="el-icon-download" size="mini" type="success" @click="exportCsv(index)"
title="导出CSV">导出人员</el-button>
<el-button icon="el-icon-upload" size="mini" type="info" @click="importCsv(index)"
title="导入CSV">导入人员</el-button>
<el-button icon="el-icon-save" size="mini" type="warning" @click="saveSingleTemplate(index)"
title="保存为模板">存储为模板</el-button>
<el-button icon="el-icon-folder-open" size="mini" type="primary" @click="openTemplateDialog(index)"
title="使用模板">使用模板</el-button>
</div>
</div>
<EmployeeSelector
v-model="item.employeeIds"
:multiple="true"
:disabled-names="getExcludedIds(index)"
placeholder="选择该班次的员工"
title="选择班次员工" />
<input type="file" ref="fileInput" class="file-input" accept=".csv" @change="handleFileChange($event, index)" />
</div>
<!-- <div class="quick-actions">
<el-button type="success" icon="el-icon-random" @click="quickAssignByDepartment">
按部门自动分配
</el-button>
</div> -->
</div>
<!-- 步骤3确认生成 -->
<div v-if="currentStep === 3" class="step-content">
<el-table :data="previewData" border style="width: 100%;">
<el-table-column prop="shiftName" label="班次" />
<el-table-column prop="employeeNames" label="员工列表" />
<el-table-column prop="count" label="人数" align="center" />
</el-table>
<div class="preview-summary">
<span> {{ totalEmployeeCount }} 名员工{{ totalScheduleCount }} 条排班记录</span>
</div>
</div>
<!-- 步骤导航按钮 -->
<div slot="footer" class="dialog-footer">
<el-button @click="cancel" v-if="currentStep === 1">取消</el-button>
<template v-else>
<el-button @click="prevStep">上一步</el-button>
</template>
<template v-if="currentStep < 3">
<el-button type="primary" @click="nextStep" :disabled="!canProceed" :loading="buttonLoading">
{{ currentStep === 1 ? '下一步:分配人员' : '下一步:确认' }}
</el-button>
</template>
<template v-else>
<el-button type="primary" @click="submitForm" :loading="buttonLoading">确认生成</el-button>
</template>
</div>
</el-dialog>
<!-- 模板管理弹窗 -->
<el-dialog title="人员列表模板管理" :visible.sync="showTemplateManager" width="1000px" :close-on-click-modal="false">
<AttendanceTemplateManager
:employee-list="employeeListForTransfer"
@update="loadTemplates"
/>
</el-dialog>
<!-- 班次模板选择弹窗 -->
<el-dialog title="选择人员模板" :visible.sync="showTemplateDialog" width="500px">
<el-table :data="templateList" border style="width: 100%;">
<el-table-column prop="name" label="模板名称" />
<el-table-column prop="employeeCount" label="员工数量" align="center" />
<el-table-column prop="createTime" label="创建时间" />
<el-table-column label="操作" align="center">
<template slot-scope="scope">
<el-button size="mini" type="primary" @click.stop="applySingleTemplate(scope.row)">应用</el-button>
<el-button size="mini" type="danger" @click.stop="deleteSingleTemplate(scope.row)">删除</el-button>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="showTemplateDialog = false">关闭</el-button>
</div>
</el-dialog>
<!-- 批量修改弹窗 -->
<el-dialog title="批量修改排班" :visible.sync="batchDialogVisible" width="600px">
<el-form ref="batchForm" :model="batchForm" label-width="100px">
<el-form-item label="日期" prop="workDate" :rules="[{ required: true, message: '请选择日期', trigger: 'change' }]">
<el-date-picker v-model="batchForm.workDate" type="date" placeholder="选择日期"
value-format="yyyy-MM-dd" style="width: 100%;" />
</el-form-item>
<el-form-item label="班次" prop="shiftId" :rules="[{ required: true, message: '请选择班次', trigger: 'change' }]">
<el-select v-model="batchForm.shiftId" placeholder="选择班次" style="width: 100%;" clearable>
<el-option v-for="shift in shiftList" :key="shift.shiftId" :label="shift.shiftName" :value="shift.shiftId" />
</el-select>
<div v-if="batchForm.shiftId" class="shift-detail" style="margin-top: 8px;">
<div>
<span class="time-label">时段一</span>
<span class="time-value">{{ getShiftTime(batchForm.shiftId) }}</span>
</div>
</div>
</el-form-item>
<el-form-item label="员工" prop="userIds" :rules="[{ required: true, message: '请选择员工', trigger: 'change' }]">
<EmployeeSelector v-model="batchForm.userIds" :multiple="true" placeholder="选择要修改的员工"
title="选择员工" />
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="batchDialogVisible = false">取消</el-button>
<el-button type="primary" @click="submitBatchUpdate" :loading="buttonLoading">确定修改</el-button>
</div>
</el-dialog>
<!-- 编辑班次弹窗 -->
<el-dialog :title="editDialogTitle" :visible.sync="editDialogVisible" :width="editForm.batchEdit ? '600px' : '400px'">
<el-form ref="editForm" :model="editForm" label-width="80px">
<el-form-item label="批量编辑">
<el-checkbox v-model="editForm.batchEdit" @change="handleBatchEditChange">勾选后可选择多个员工批量修改</el-checkbox>
</el-form-item>
<el-form-item v-if="editForm.batchEdit" label="员工">
<EmployeeSelector v-model="editForm.batchUserIds" :multiple="true" placeholder="选择要批量修改的员工"
title="选择员工" />
</el-form-item>
<el-form-item label="班次">
<el-select v-model="editForm.shiftId" placeholder="选择班次" style="width: 100%;">
<el-option v-for="shift in shiftList" :key="shift.shiftId" :label="shift.shiftName"
:value="shift.shiftId" />
</el-select>
</el-form-item>
<el-form-item label="工作时间">
<div v-if="currentShift" class="shift-detail">
<div>
<span class="time-label">时段一</span>
<span class="time-value">{{ currentShift.startTime }} - {{ currentShift.endTime }}</span>
</div>
<div v-if="currentShift.startTime2">
<span class="time-label">时段二</span>
<span class="time-value">{{ currentShift.startTime2 }} - {{ currentShift.endTime2 }}</span>
</div>
</div>
<span v-else>请选择班次</span>
</el-form-item>
</el-form>
<div slot="footer" class="dialog-footer">
<el-button @click="cancelEdit">取消</el-button>
<el-button type="danger"
v-if="!editForm.batchEdit && currentEditRow && currentEditRow[currentEditDate] && currentEditRow[currentEditDate].scheduleId"
@click="handleDelete">删除排班</el-button>
<el-button type="primary" @click="submitEdit" :disabled="!editForm.shiftId">确定</el-button>
</div>
</el-dialog>
</div>
</template>
<script>
import TimeRangePicker from '@/views/wms/report/components/timeRangePicker.vue'
import EmployeeSelector from '@/components/EmployeeSelector/index.vue'
import AttendanceTemplateManager from '@/components/AttendanceTemplateManager/index.vue'
import { listAttendanceSchedule, generateenerateSchedule, updateAttendanceSchedule, addAttendanceSchedule, delAttendanceSchedule, batchUpdateSchedule } from '@/api/wms/attendanceSchedule'
import { listShift } from '@/api/wms/attendanceShift'
import { listAttendanceShiftRule } from '@/api/wms/attendanceShiftRule'
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
export default {
name: 'AttendanceSchedule',
components: { TimeRangePicker, EmployeeSelector, AttendanceTemplateManager },
data() {
return {
loading: false,
buttonLoading: false,
dateRangeParams: {},
defaultStartTime: '',
defaultEndTime: '',
dateList: [],
scheduleData: [],
shiftList: [],
shiftRuleList: [],
dialogVisible: false,
batchDialogVisible: false,
batchForm: {
workDate: '',
shiftId: '',
userIds: ''
},
editDialogVisible: false,
showTemplateDialog: false,
showTemplateManager: false,
templateList: [],
currentShiftIndex: -1,
employeeList: [],
currentStep: 1, // 步骤1-选择班次2-分配人员3-确认
form: {
dateRange: [],
shiftList: [
{ shiftId: '', ruleId: '', employeeIds: '' }
]
},
editForm: {
shiftId: '',
batchEdit: false,
batchUserIds: ''
},
currentEditRow: null,
currentEditDate: '',
rules: {
dateRange: [
{ required: true, message: '请选择时间段', trigger: 'change' }
],
selectedEmployees: [
{ required: true, message: '请选择员工', trigger: 'change' }
]
}
}
},
computed: {
// 检查是否可以进入下一步
canProceed() {
if (this.currentStep === 1) {
// 步骤1必须选择时间段和至少一个有效班次
if (!this.form.dateRange || this.form.dateRange.length === 0) return false
return this.form.shiftList.some(item => item.shiftId)
} else if (this.currentStep === 2) {
// 步骤2至少有一个班次分配了人员
return this.form.shiftList.some(item => item.employeeIds && item.employeeIds.trim())
}
return true
},
// 预览数据
previewData() {
return this.form.shiftList
.filter(item => item.shiftId && item.employeeIds)
.map(item => ({
shiftName: this.getShiftName(item.shiftId),
employeeNames: this.getEmployeeNames(item.employeeIds),
count: item.employeeIds.split(',').filter(id => id.trim()).length
}))
},
// 总员工数
totalEmployeeCount() {
const allIds = new Set()
this.form.shiftList.forEach(item => {
if (item.employeeIds) {
item.employeeIds.split(',').forEach(id => {
if (id.trim()) allIds.add(id.trim())
})
}
})
return allIds.size
},
// 总排班记录数
totalScheduleCount() {
let count = 0
this.form.shiftList.forEach(item => {
if (item.employeeIds) {
count += item.employeeIds.split(',').filter(id => id.trim()).length
}
})
return count
},
editDialogTitle() {
if (this.editForm.batchEdit) {
const employeeIds = this.editForm.batchUserIds ? this.editForm.batchUserIds.split(',').filter(id => id.trim()) : []
return `批量编辑排班 - ${this.currentEditDate || ''} (已选 ${employeeIds.length} 人)`
}
return `编辑排班 - ${this.currentEditDate || ''}`
},
currentShift() {
if (!this.editForm.shiftId) {
return null
}
return this.shiftList.find(s => s.shiftId === this.editForm.shiftId)
},
employeeListForTransfer() {
return this.employeeList.map(emp => ({
key: emp.id,
label: emp.name
}))
}
},
created() {
this.initDateRange()
this.getShiftList()
this.getShiftRuleList()
this.loadTemplates()
this.getEmployeeList()
},
methods: {
// 刷新排班
handleRefresh() {
this.getScheduleList()
},
// 初始化日期范围为当前月份
initDateRange() {
const now = new Date()
const firstDay = new Date(now.getFullYear(), now.getMonth(), 1)
const lastDay = new Date(now.getFullYear(), now.getMonth() + 1, 0)
this.defaultStartTime = this.formatDate(firstDay) + ' 00:00:00'
this.defaultEndTime = this.formatDate(lastDay) + ' 23:59:59'
this.dateRangeParams = {
scheduleDateStart: this.defaultStartTime,
scheduleDateEnd: this.defaultEndTime
}
this.generateDateList(firstDay, lastDay)
this.getScheduleList()
},
// 格式化日期
formatDate(date) {
const year = date.getFullYear()
const month = (date.getMonth() + 1).toString().padStart(2, '0')
const day = date.getDate().toString().padStart(2, '0')
return `${year}-${month}-${day}`
},
// 生成日期列表
generateDateList(startDate, endDate) {
this.dateList = []
let currentDate = new Date(startDate)
while (currentDate <= endDate) {
this.dateList.push(this.formatDate(currentDate))
currentDate.setDate(currentDate.getDate() + 1)
}
},
handleDeleteEmployee(index) {
this.form.shiftConfig.splice(index, 1)
},
// 格式化日期标签
formatDateLabel(date) {
const dateObj = new Date(date)
const weekDays = ['日', '一', '二', '三', '四', '五', '六']
const month = dateObj.getMonth() + 1
const day = dateObj.getDate()
const weekDay = weekDays[dateObj.getDay()]
return `${month}${day}日 周${weekDay}`
},
// 日期范围变化
handleDateRangeChange() {
if (this.dateRangeParams.scheduleDateStart && this.dateRangeParams.scheduleDateEnd) {
const startDate = new Date(this.dateRangeParams.scheduleDateStart.split(' ')[0])
const endDate = new Date(this.dateRangeParams.scheduleDateEnd.split(' ')[0])
this.generateDateList(startDate, endDate)
this.getScheduleList()
}
},
// 获取排班列表
getScheduleList() {
this.loading = true
listAttendanceSchedule(this.dateRangeParams).then(response => {
this.scheduleData = this.transformScheduleData(response.rows || [])
this.loading = false
}).catch(() => {
this.loading = false
})
},
// 转换排班数据
transformScheduleData(rows) {
const dataMap = {}
rows.forEach(record => {
if (!dataMap[record.userId]) {
dataMap[record.userId] = {
employeeName: record.employeeName,
employeeId: record.userId
}
}
if (record.workDate) {
const dateKey = record.workDate.split(' ')[0]
dataMap[record.userId][dateKey] = {
scheduleId: record.scheduleId,
shiftId: record.shiftId,
shiftName: record.shiftName,
shiftType: record.shiftType,
shiftStartTime: record.shiftStartTime,
shiftEndTime: record.shiftEndTime,
shiftStartTime2: record.shiftStartTime2,
shiftEndTime2: record.shiftEndTime2
}
}
})
return Object.values(dataMap)
},
// 获取班次列表
getShiftList() {
listShift().then(response => {
this.shiftList = response.rows || []
})
},
// 获取倒班规则列表
getShiftRuleList() {
listAttendanceShiftRule().then(response => {
this.shiftRuleList = response.rows || []
})
},
// 获取班次时间显示
getShiftTime(shiftId) {
const shift = this.shiftList.find(s => s.shiftId === shiftId)
if (!shift) return ''
const startTime = shift.startTime ? shift.startTime.substring(0, 5) : ''
const endTime = shift.endTime ? shift.endTime.substring(0, 5) : ''
if (shift.isCrossDay) {
return `${startTime} - 次日${endTime}`
}
return `${startTime} - ${endTime}`
},
// 获取班次类型样式
getShiftTypeClass(shiftType) {
return shiftType === '夜班' ? 'night-shift' : 'day-shift'
},
// 双击单元格处理
handleCellDoubleClick(row, date) {
this.currentEditRow = row
this.currentEditDate = date
if (row[date]) {
this.editForm.shiftId = row[date].shiftId
} else {
this.editForm.shiftId = ''
}
this.editForm.batchEdit = false
this.editForm.batchUserIds = ''
this.editDialogVisible = true
},
// 取消编辑
cancelEdit() {
this.editDialogVisible = false
this.editForm.shiftId = ''
this.editForm.batchEdit = false
this.editForm.batchUserIds = ''
this.currentEditRow = null
this.currentEditDate = ''
},
// 提交编辑
submitEdit() {
if (!this.editForm.shiftId || (!this.currentEditRow && !this.editForm.batchEdit)) {
return
}
const shift = this.shiftList.find(s => s.shiftId === this.editForm.shiftId)
if (!shift) {
return
}
const date = this.currentEditDate
if (this.editForm.batchEdit) {
const userIds = this.editForm.batchUserIds.split(',').filter(id => id.trim())
if (userIds.length === 0) {
this.$message.warning('请选择要批量修改的员工')
return
}
this.buttonLoading = true
batchUpdateSchedule({
userIds: userIds,
workDate: date,
shiftId: shift.shiftId,
shiftName: shift.shiftName
}).then(() => {
this.$message.success(`批量修改成功,共修改 ${userIds.length} 名员工`)
this.buttonLoading = false
this.getScheduleList()
this.cancelEdit()
}).catch(() => {
this.buttonLoading = false
})
return
}
const employeeId = this.currentEditRow.employeeId
if (this.currentEditRow[date]) {
updateAttendanceSchedule({
scheduleId: this.currentEditRow[date].scheduleId,
userId: employeeId,
workDate: date,
shiftId: shift.shiftId,
shiftName: shift.shiftName,
}).then(_ => {
this.$message.success('修改成功')
this.getScheduleList()
}).catch(() => {
this.$message.error('修改失败')
this.getScheduleList()
})
} else {
addAttendanceSchedule({
shiftId: shift.shiftId,
userId: employeeId,
workDate: date,
shiftName: shift.shiftName,
}).then(_ => {
this.$message.success('添加成功')
this.getScheduleList()
}).catch(() => {
this.$message.error('添加失败')
this.getScheduleList()
})
}
this.cancelEdit()
},
// 批量编辑模式切换
handleBatchEditChange(val) {
if (!val) {
this.editForm.batchUserIds = ''
}
},
// 删除排班
handleDelete() {
const scheduleId = this.currentEditRow[this.currentEditDate]?.scheduleId
if (!scheduleId) {
return
}
this.$confirm('确定要删除该排班吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delAttendanceSchedule(scheduleId).then(() => {
this.$message.success('删除成功')
delete this.currentEditRow[this.currentEditDate]
this.cancelEdit()
}).catch(() => {
this.$message.error('删除失败,正在重新获取数据')
this.getScheduleList()
this.cancelEdit()
})
}).catch(() => {
this.$message.info('已取消删除')
})
},
// 删除整行排班
handleDeleteRow(row) {
const scheduleIds = []
this.dateList.forEach(date => {
if (row[date] && row[date].scheduleId) {
scheduleIds.push(row[date].scheduleId)
}
})
if (scheduleIds.length === 0) {
this.$message.info('该行没有排班记录')
return
}
this.$confirm(`确定要删除该员工的 ${scheduleIds.length} 条排班记录吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
delAttendanceSchedule(scheduleIds.join(',')).then(() => {
this.$message.success('删除成功')
this.getScheduleList()
}).catch(() => {
this.$message.error('删除失败,正在重新获取数据')
this.getScheduleList()
})
}).catch(() => {
this.$message.info('已取消删除')
})
},
// 批量修改排班
handleBatchUpdate() {
this.batchForm = {
workDate: '',
shiftId: '',
userIds: ''
}
this.batchDialogVisible = true
},
// 提交批量修改
submitBatchUpdate() {
this.$refs['batchForm'].validate(valid => {
if (!valid) return
const userIds = this.batchForm.userIds.split(',').filter(id => id.trim())
if (userIds.length === 0) {
this.$message.warning('请选择员工')
return
}
const shift = this.shiftList.find(s => s.shiftId === this.batchForm.shiftId)
if (!shift) {
this.$message.warning('请选择班次')
return
}
this.$confirm(`确定要为 ${userIds.length} 名员工修改 ${this.batchForm.workDate} 的班次为「${shift.shiftName}」吗?`, '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
this.buttonLoading = true
batchUpdateSchedule({
userIds: userIds,
workDate: this.batchForm.workDate,
shiftId: shift.shiftId,
shiftName: shift.shiftName
}).then(() => {
this.$message.success('批量修改成功')
this.batchDialogVisible = false
this.buttonLoading = false
this.getScheduleList()
}).catch(() => {
this.buttonLoading = false
})
}).catch(() => {
this.$message.info('已取消')
})
})
},
// 处理班次变更
handleShiftChange(employeeId, date, shiftId) {
if (!shiftId) {
return
}
const shift = this.shiftList.find(s => s.shiftId === shiftId)
if (!shift) {
return
}
const scheduleItem = this.scheduleData.find(item => item.employeeId === employeeId)
if (scheduleItem && scheduleItem[date]) {
updateAttendanceSchedule({
...scheduleItem[date],
shiftId: scheduleItem.shiftId
}).then(_ => {
this.$message.success('修改成功')
}).catch(() => {
this.$message.success('修改成功')
this.getScheduleList()
})
scheduleItem[date].shiftId = shift.shiftId
scheduleItem[date].shiftName = shift.shiftName
scheduleItem[date].shiftType = shift.shiftType
scheduleItem[date].shiftStartTime = shift.startTime
scheduleItem[date].shiftEndTime = shift.endTime
scheduleItem[date].shiftStartTime2 = shift.startTime2
scheduleItem[date].shiftEndTime2 = shift.endTime2
} else if (scheduleItem) {
scheduleItem[date] = {
shiftId: shift.shiftId,
shiftName: shift.shiftName,
shiftType: shift.shiftType,
shiftStartTime: shift.startTime,
shiftEndTime: shift.endTime,
shiftStartTime2: shift.startTime2,
shiftEndTime2: shift.endTime2
}
}
},
// 创建排班
handleCreate() {
this.reset()
this.dialogVisible = true
},
// 重置表单
reset() {
this.currentStep = 1
this.form = {
dateRange: [],
shiftList: [
{ shiftId: '', ruleId: '', employeeIds: '' }
]
}
if (this.$refs['form']) {
this.$refs['form'].resetFields()
}
},
// 添加班次配置项
addShiftItem() {
this.form.shiftList.push({ shiftId: '', ruleId: '', employeeIds: '' })
},
// 移除班次配置项
removeShiftItem(index) {
this.form.shiftList.splice(index, 1)
},
// 复制班次配置(不复制人员)
copyShiftItem(index) {
const source = this.form.shiftList[index]
this.form.shiftList.push({
shiftId: source.shiftId,
ruleId: source.ruleId,
employeeIds: ''
})
},
// 获取排除的员工ID避免重复分配
getExcludedIds(currentIndex) {
const excluded = []
this.form.shiftList.forEach((item, index) => {
if (index !== currentIndex && item.employeeIds) {
excluded.push(...item.employeeIds.split(','))
}
})
return excluded.join(',').trim()
},
// 获取班次名称
getShiftName(shiftId) {
const shift = this.shiftList.find(s => s.shiftId === shiftId)
return shift ? shift.shiftName : '未知班次'
},
// 获取员工姓名临时实现实际应从API获取
getEmployeeNames(employeeIds) {
if (!employeeIds) return '未分配'
return employeeIds.split(',').filter(id => id.trim()).length + ' 名员工'
},
// 下一步
nextStep() {
if (this.currentStep < 3) {
this.currentStep++
}
},
// 上一步
prevStep() {
if (this.currentStep > 1) {
this.currentStep--
}
},
// 按部门自动分配
quickAssignByDepartment() {
this.$message.info('按部门自动分配功能开发中...')
// 实际实现时可以调用API获取部门员工列表并分配
},
// 提交表单
submitForm() {
this.buttonLoading = true
// 构建提交数据
const list = []
const startDate = this.form.dateRange[0]
const endDate = this.form.dateRange[1]
this.form.shiftList.forEach(shiftItem => {
if (!shiftItem.shiftId || !shiftItem.employeeIds) return
const employeeIds = shiftItem.employeeIds.split(',').filter(id => id.trim())
employeeIds.forEach(employeeId => {
list.push({
userId: employeeId,
shiftId: shiftItem.shiftId,
ruleId: shiftItem.ruleId,
startDate: startDate,
endDate: endDate
})
})
})
// 调用API
generateenerateSchedule(list).then(response => {
this.$modal.msgSuccess('生成成功')
this.dialogVisible = false
this.buttonLoading = false
this.getScheduleList()
}).catch(() => {
this.buttonLoading = false
})
},
// 取消
cancel() {
this.dialogVisible = false
this.reset()
},
// 导出CSV文件
exportCsv(index) {
const shiftItem = this.form.shiftList[index]
const shiftName = this.getShiftName(shiftItem.shiftId) || '班次'
const ruleName = shiftItem.ruleId ? this.getRuleName(shiftItem.ruleId) : ''
let csvContent = '名字\n'
if (shiftItem.employeeIds) {
const ids = shiftItem.employeeIds.split(',').filter(id => id.trim())
ids.forEach(id => {
csvContent += `${id}\n`
})
}
const blob = new Blob([csvContent], { type: 'text/csv;charset=utf-8;' })
const link = document.createElement('a')
const url = URL.createObjectURL(blob)
const fileName = `${shiftName}${ruleName ? '_' + ruleName : ''}_员工列表.csv`
link.setAttribute('href', url)
link.setAttribute('download', fileName)
link.style.visibility = 'hidden'
document.body.appendChild(link)
link.click()
document.body.removeChild(link)
},
// 导入CSV文件
importCsv(index) {
const fileInput = document.querySelector('.file-input')
if (fileInput) {
fileInput.click()
}
},
// 处理文件选择
handleFileChange(event, index) {
const file = event.target.files[0]
if (!file) return
const reader = new FileReader()
reader.onload = (e) => {
const content = e.target.result
const lines = content.split('\n').filter(line => line.trim())
if (lines.length === 0) {
this.$message.warning('CSV文件内容为空')
return
}
const header = lines[0].trim()
if (header !== '名字') {
this.$message.warning('CSV文件格式不正确第一行应为"名字"')
return
}
const names = lines.slice(1).map(line => line.trim()).filter(name => name)
if (names.length === 0) {
this.$message.warning('CSV文件中没有有效的员工名字')
return
}
const shiftItem = this.form.shiftList[index]
shiftItem.employeeIds = names.join(',')
this.$message.success(`成功导入 ${names.length} 名员工`)
}
reader.readAsText(file, 'UTF-8')
event.target.value = ''
},
// 获取规则名称
getRuleName(ruleId) {
const rule = this.shiftRuleList.find(r => r.ruleId === ruleId)
return rule ? rule.changeDays : ''
},
// 获取员工列表
getEmployeeList() {
const params = {
pageNum: 1,
pageSize: 9999,
}
listEmployeeInfo(params).then(response => {
// 过滤掉已离职的员工
const filteredList = (response.rows || []).filter(employee => {
return employee.isLeave !== 1 && employee.isLeave !== '1'
})
this.employeeList = filteredList.map(item => ({
id: item.infoId,
name: item.name
}))
}).catch(() => {
this.employeeList = []
})
},
// 加载模板列表
loadTemplates() {
try {
const templates = localStorage.getItem('attendanceTemplates')
this.templateList = templates ? JSON.parse(templates) : []
} catch (e) {
this.templateList = []
}
},
// 保存模板到localStorage
saveTemplates() {
localStorage.setItem('attendanceTemplates', JSON.stringify(this.templateList))
},
// 保存单个班次为模板
saveSingleTemplate(index) {
const shiftItem = this.form.shiftList[index]
if (!shiftItem.employeeIds || !shiftItem.employeeIds.trim()) {
this.$message.warning('请先分配人员')
return
}
this.$prompt('请输入模板名称', '保存模板', {
confirmButtonText: '确定',
cancelButtonText: '取消',
inputValue: this.getShiftName(shiftItem.shiftId) + '_' + new Date().toLocaleDateString()
}).then(({ value }) => {
if (!value.trim()) {
this.$message.warning('模板名称不能为空')
return
}
const employeeIds = shiftItem.employeeIds.split(',').filter(id => id.trim())
const template = {
id: Date.now().toString(),
name: value.trim(),
employeeIds: employeeIds,
employeeCount: employeeIds.length,
createTime: new Date().toLocaleString('zh-CN')
}
this.templateList.push(template)
this.saveTemplates()
this.$message.success('模板保存成功')
}).catch(() => {
this.$message.info('已取消保存')
})
},
// 打开模板选择弹窗
openTemplateDialog(index) {
this.currentShiftIndex = index
this.showTemplateDialog = true
},
// 应用单个模板到当前班次
applySingleTemplate(template) {
if (!template || !template.employeeIds || this.currentShiftIndex < 0) return
this.form.shiftList[this.currentShiftIndex].employeeIds = template.employeeIds.join(',')
this.showTemplateDialog = false
this.currentShiftIndex = -1
this.$message.success('模板应用成功')
},
// 删除单个模板
deleteSingleTemplate(template) {
this.$confirm('确定要删除这个模板吗?', '提示', {
confirmButtonText: '确定',
cancelButtonText: '取消',
type: 'warning'
}).then(() => {
const index = this.templateList.findIndex(t => t.id === template.id)
if (index > -1) {
this.templateList.splice(index, 1)
this.saveTemplates()
this.$message.success('删除成功')
}
}).catch(() => {
this.$message.info('已取消删除')
})
}
}
}
</script>
<style scoped>
.app-container {
padding: 20px;
}
.date-range-section {
margin-bottom: 20px;
}
.operation-bar {
margin-bottom: 20px;
}
.schedule-table-wrapper {
overflow-x: auto;
}
.schedule-cell {
padding: 4px;
min-height: 60px;
cursor: pointer;
transition: background-color 0.2s;
}
.schedule-cell:hover {
background-color: #f5f7fa;
}
.has-data {
padding: 8px 4px;
}
.empty-cell {
display: flex;
align-items: center;
justify-content: center;
}
.empty-hint {
font-size: 12px;
color: #c0c4cc;
}
.shift-info {
text-align: center;
}
.shift-name {
display: inline-block;
padding: 2px 6px;
border-radius: 4px;
font-size: 11px;
color: #fff;
margin-bottom: 2px;
}
.day-shift {
background-color: #67c23a;
}
.night-shift {
background-color: #409eff;
}
.shift-time-info {
font-size: 10px;
color: #606266;
line-height: 1.4;
}
.second-shift {
display: block;
margin-top: 2px;
}
.shift-config {
width: 100%;
}
.shift-config-item {
margin-bottom: 12px;
padding: 12px;
background-color: #f5f7fa;
border-radius: 4px;
display: flex;
align-items: center;
gap: 12px;
}
.employee-info {
min-width: 100px;
}
.shift-time {
flex: 1;
margin-left: 12px;
}
.time-label {
font-size: 12px;
color: #909399;
}
.time-value {
font-size: 12px;
color: #606266;
}
/* ===== 优化后的排班弹窗样式 ===== */
/* 步骤指示器 */
.steps {
display: flex;
align-items: center;
justify-content: center;
margin-bottom: 24px;
padding-bottom: 20px;
border-bottom: 1px solid #e4e7ed;
}
.step {
display: flex;
flex-direction: column;
align-items: center;
min-width: 80px;
}
.step-number {
width: 32px;
height: 32px;
line-height: 32px;
border-radius: 50%;
background-color: #e4e7ed;
color: #909399;
font-size: 14px;
font-weight: bold;
text-align: center;
transition: all 0.3s;
}
.step.active .step-number {
background-color: #409eff;
color: #fff;
}
.step.done .step-number {
background-color: #67c23a;
color: #fff;
}
.step-text {
margin-top: 8px;
font-size: 13px;
color: #909399;
}
.step.active .step-text,
.step.done .step-text {
color: #606266;
}
.step-arrow {
margin: 0 12px;
color: #c0c4cc;
font-size: 16px;
}
/* 步骤内容 */
.step-content {
min-height: 200px;
}
/* 班次配置面板 */
.shift-config-panel {
margin-top: 12px;
}
.shift-config-row {
margin-bottom: 16px;
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
}
.shift-config-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.config-label {
font-weight: 600;
color: #606266;
}
.shift-config-fields {
display: flex;
align-items: center;
gap: 16px;
}
.shift-time-display {
font-size: 13px;
color: #67c23a;
padding: 4px 8px;
background-color: #f0f9eb;
border-radius: 4px;
}
/* 班次分配区域 */
.shift-assignment {
margin-bottom: 20px;
padding: 16px;
background-color: #fafafa;
border-radius: 4px;
}
.assignment-header {
display: flex;
align-items: center;
gap: 12px;
margin-bottom: 12px;
}
.assignment-count {
font-size: 13px;
color: #909399;
}
.import-export-buttons {
display: flex;
gap: 8px;
margin-left: auto;
}
.file-input {
display: none;
}
/* 快速操作按钮 */
.quick-actions {
margin-top: 16px;
padding-top: 16px;
border-top: 1px dashed #e4e7ed;
}
/* 预览摘要 */
.preview-summary {
margin-top: 16px;
padding: 12px;
background-color: #f0f9eb;
border-radius: 4px;
text-align: center;
color: #67c23a;
font-size: 14px;
}
/* 对话框按钮 */
.dialog-footer {
display: flex;
justify-content: flex-end;
gap: 12px;
}
</style>