feat(wms/attendance): 新增批量单元格编辑排班功能

1. 新增批量修改排班班次的API接口
2. 重构排班页面工具栏,新增部门和员工姓名筛选功能
3. 新增批量编辑模式,支持选择多个已有排班单元格进行批量修改
4. 新增批量编辑弹窗,可统一修改所选排班的班次
5. 优化页面样式布局,添加批量操作相关的交互样式
This commit is contained in:
2026-05-27 17:51:34 +08:00
parent d0a15032f2
commit 6de1bbfe0b
2 changed files with 339 additions and 22 deletions

View File

@@ -69,3 +69,12 @@ export function batchUpdateSchedule(data) {
data: data
})
}
// 批量修改排班班次(按主键)
export function batchUpdateShiftByIds(data) {
return request({
url: '/wms/attendanceSchedule/batchUpdateShift',
method: 'put',
data: data
})
}

View File

@@ -1,21 +1,46 @@
<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 class="tool-bar">
<div class="tool-bar-left">
<TimeRangePicker v-model="dateRangeParams" startKey="scheduleDateStart" endKey="scheduleDateEnd"
:defaultStartTime="defaultStartTime" :defaultEndTime="defaultEndTime" @change="handleDateRangeChange"
@quick-select="getScheduleList" />
<el-select v-model="queryParams.employeeDept" placeholder="部门" clearable style="width: 160px;" @change="handleFilterChange">
<el-option v-for="dept in deptList" :key="dept" :label="dept" :value="dept" />
</el-select>
<el-autocomplete
v-model="queryParams.employeeName"
:fetch-suggestions="querySearchAsync"
placeholder="员工姓名"
style="width: 180px;"
:trigger-on-focus="false"
@select="handleFilterChange"
clearable
@clear="handleFilterChange"
/>
</div>
<div class="tool-bar-right">
<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>
<el-button plain type="primary" icon="el-icon-s-grid" @click="enterBatchCellMode">批量编辑</el-button>
</div>
</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 v-if="!batchCellSelectMode" type="info" title="提示:双击排班单元格可编辑排班"></el-alert>
<el-alert type="info" title="提示:双击排班单元格可编辑排班"></el-alert>
<!-- 批量选择模式提示栏 -->
<div v-if="batchCellSelectMode" class="batch-select-bar">
<span class="batch-select-info">
<i class="el-icon-info" /> 批量编辑模式点击单元格选择仅可选择已有排班的单元格已选 <strong>{{ batchSelectedCount }}</strong>
</span>
<div class="batch-select-actions">
<el-button size="mini" type="primary" :disabled="batchSelectedCount === 0" @click="confirmBatchCellSelection">确认选择</el-button>
<el-button size="mini" @click="cancelBatchCellSelection">取消</el-button>
</div>
</div>
<!-- 排班表格 -->
<div class="schedule-table-wrapper">
@@ -31,8 +56,11 @@
<!-- 动态日期列 -->
<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)">
<div
class="schedule-cell"
:class="cellClass(scope.row, date)"
@dblclick="!batchCellSelectMode && handleCellDoubleClick(scope.row, date)"
@click="batchCellSelectMode && handleCellClick(scope.row, date)">
<template v-if="scope.row[date]">
<div class="shift-name" :class="getShiftTypeClass(scope.row[date].shiftType)">
{{ scope.row[date].shiftName }}
@@ -46,8 +74,12 @@
</div>
</template>
<template v-else>
<span class="empty-hint">双击排班</span>
<span class="empty-hint" v-if="!batchCellSelectMode">双击排班</span>
</template>
<span v-if="batchCellSelectMode" class="cell-checkbox">
<i :class="isCellSelected(scope.row, date) ? 'el-icon-check' : 'el-icon-circle-check'"
:style="{ color: isCellSelected(scope.row, date) ? '#fff' : '#c0c4cc' }" />
</span>
</div>
</template>
</el-table-column>
@@ -266,6 +298,43 @@
<el-button type="primary" @click="submitEdit" :disabled="!editForm.shiftId">确定</el-button>
</div>
</el-dialog>
<!-- 批量单元格编辑弹窗 -->
<el-dialog title="批量编辑排班" :visible.sync="batchCellDialogVisible" width="900px">
<div class="batch-cell-summary">
已选择 <strong>{{ batchEditTableData.length }}</strong> 个排班单元格
</div>
<el-form label-width="80px" class="batch-cell-form">
<el-form-item label="统一班次">
<el-select v-model="batchCellEditShiftId" placeholder="选择要设置的新班次" style="width: 280px;">
<el-option v-for="shift in shiftList" :key="shift.shiftId" :label="shift.shiftName" :value="shift.shiftId" />
</el-select>
</el-form-item>
</el-form>
<el-table :data="batchEditTableData" border stripe max-height="400">
<el-table-column prop="employeeName" label="员工" width="120" />
<el-table-column prop="workDate" label="日期" width="120" />
<el-table-column prop="oldShiftName" label="原班次" width="120">
<template slot-scope="scope">
<span :class="getShiftTypeClass(scope.row.oldShiftType)">
{{ scope.row.oldShiftName }}
</span>
</template>
</el-table-column>
<el-table-column prop="newShiftName" label="新班次">
<template slot-scope="scope">
<span v-if="getNewShiftName()" :class="getShiftTypeClass(batchCellEditShiftShiftType)">
{{ getNewShiftName() }}
</span>
<span v-else class="empty-hint">请选择班次</span>
</template>
</el-table-column>
</el-table>
<div slot="footer" class="dialog-footer">
<el-button @click="batchCellDialogVisible = false">取消</el-button>
<el-button type="primary" :disabled="!batchCellEditShiftId" :loading="buttonLoading" @click="submitBatchCellEdit">确定修改</el-button>
</div>
</el-dialog>
</div>
</template>
@@ -273,7 +342,7 @@
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 { listAttendanceSchedule, generateenerateSchedule, updateAttendanceSchedule, addAttendanceSchedule, delAttendanceSchedule, batchUpdateSchedule, batchUpdateShiftByIds } from '@/api/wms/attendanceSchedule'
import { listShift } from '@/api/wms/attendanceShift'
import { listAttendanceShiftRule } from '@/api/wms/attendanceShiftRule'
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
@@ -292,6 +361,10 @@ export default {
scheduleData: [],
shiftList: [],
shiftRuleList: [],
queryParams: {
employeeDept: '',
employeeName: ''
},
dialogVisible: false,
batchDialogVisible: false,
batchForm: {
@@ -319,6 +392,10 @@ export default {
},
currentEditRow: null,
currentEditDate: '',
batchCellSelectMode: false,
batchCellSelection: {},
batchCellDialogVisible: false,
batchCellEditShiftId: '',
rules: {
dateRange: [
{ required: true, message: '请选择时间段', trigger: 'change' }
@@ -395,6 +472,44 @@ export default {
key: emp.id,
label: emp.name
}))
},
deptList() {
const depts = new Set()
this.employeeList.forEach(emp => {
if (emp.dept) depts.add(emp.dept)
})
return Array.from(depts).sort()
},
employeeNameList() {
return this.employeeList.map(emp => ({
value: emp.name,
dept: emp.dept || ''
}))
},
batchSelectedCount() {
return Object.keys(this.batchCellSelection).length
},
batchEditTableData() {
const result = []
Object.keys(this.batchCellSelection).forEach(key => {
const cell = this.batchCellSelection[key]
if (cell) {
result.push({
key,
employeeName: cell.employeeName,
workDate: cell.workDate,
scheduleId: cell.scheduleId,
oldShiftName: cell.shiftName,
oldShiftType: cell.shiftType
})
}
})
return result
},
batchCellEditShiftShiftType() {
if (!this.batchCellEditShiftId) return ''
const shift = this.shiftList.find(s => s.shiftId === this.batchCellEditShiftId)
return shift ? shift.shiftType : ''
}
},
created() {
@@ -472,7 +587,10 @@ export default {
// 获取排班列表
getScheduleList() {
this.loading = true
listAttendanceSchedule(this.dateRangeParams).then(response => {
const params = { ...this.dateRangeParams }
if (this.queryParams.employeeDept) params.employeeDept = this.queryParams.employeeDept
if (this.queryParams.employeeName) params.employeeName = this.queryParams.employeeName
listAttendanceSchedule(params).then(response => {
this.scheduleData = this.transformScheduleData(response.rows || [])
this.loading = false
}).catch(() => {
@@ -544,6 +662,96 @@ export default {
return shiftType === '夜班' ? 'night-shift' : 'day-shift'
},
// ========== 批量单元格编辑 ==========
cellKey(row, date) {
return `${row.employeeId}_${date}`
},
cellClass(row, date) {
const hasData = !!row[date]
const selected = this.batchCellSelectMode && this.isCellSelected(row, date)
return {
'has-data': hasData,
'empty-cell': !hasData,
'batch-mode': this.batchCellSelectMode,
'selected-cell': selected
}
},
isCellSelected(row, date) {
return !!this.batchCellSelection[this.cellKey(row, date)]
},
handleCellClick(row, date) {
if (!this.batchCellSelectMode) return
if (!row[date]) return
const key = this.cellKey(row, date)
if (this.batchCellSelection[key]) {
this.$delete(this.batchCellSelection, key)
} else {
this.$set(this.batchCellSelection, key, {
scheduleId: row[date].scheduleId,
employeeName: row.employeeName,
workDate: date,
shiftName: row[date].shiftName,
shiftType: row[date].shiftType
})
}
},
enterBatchCellMode() {
this.batchCellSelectMode = true
this.batchCellSelection = {}
},
cancelBatchCellSelection() {
this.batchCellSelectMode = false
this.batchCellSelection = {}
},
confirmBatchCellSelection() {
if (this.batchSelectedCount === 0) {
this.$message.warning('请至少选择一个排班单元格')
return
}
this.batchCellSelectMode = false
this.batchCellEditShiftId = ''
this.batchCellDialogVisible = true
},
getNewShiftName() {
if (!this.batchCellEditShiftId) return ''
const shift = this.shiftList.find(s => s.shiftId === this.batchCellEditShiftId)
return shift ? shift.shiftName : ''
},
submitBatchCellEdit() {
if (!this.batchCellEditShiftId) {
this.$message.warning('请选择新班次')
return
}
const shiftId = this.batchCellEditShiftId
const list = this.batchEditTableData.map(item => ({
scheduleId: item.scheduleId,
shiftId: shiftId
}))
this.buttonLoading = true
batchUpdateShiftByIds(list).then(() => {
this.$message.success(`已批量修改 ${list.length} 条排班`)
this.buttonLoading = false
this.batchCellDialogVisible = false
this.batchCellEditShiftId = ''
this.batchCellSelection = {}
this.getScheduleList()
}).catch(() => {
this.$message.error('批量修改失败,正在重新获取数据')
this.buttonLoading = false
this.getScheduleList()
})
},
// 双击单元格处理
handleCellDoubleClick(row, date) {
this.currentEditRow = row
@@ -1014,13 +1222,29 @@ export default {
})
this.employeeList = filteredList.map(item => ({
id: item.infoId,
name: item.name
name: item.name,
dept: item.dept || ''
}))
}).catch(() => {
this.employeeList = []
})
},
querySearchAsync(queryString, cb) {
if (!queryString) {
cb([])
return
}
const results = this.employeeNameList.filter(item => {
return item.value.toLowerCase().includes(queryString.toLowerCase())
})
cb(results)
},
handleFilterChange() {
this.getScheduleList()
},
// 加载模板列表
loadTemplates() {
try {
@@ -1114,12 +1338,30 @@ export default {
padding: 20px;
}
.date-range-section {
.tool-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 20px;
padding: 12px 16px;
background: #fafafa;
border-radius: 4px;
flex-wrap: wrap;
gap: 10px;
}
.operation-bar {
margin-bottom: 20px;
.tool-bar-left {
display: flex;
align-items: center;
gap: 12px;
flex-wrap: wrap;
}
.tool-bar-right {
display: flex;
align-items: center;
gap: 8px;
flex-shrink: 0;
}
.schedule-table-wrapper {
@@ -1152,6 +1394,72 @@ export default {
color: #c0c4cc;
}
/* 批量选择模式 */
.batch-select-bar {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
padding: 10px 16px;
background: #ecf5ff;
border: 1px solid #b3d8ff;
border-radius: 4px;
}
.batch-select-info {
font-size: 14px;
color: #409eff;
}
.batch-select-actions {
display: flex;
gap: 8px;
}
.schedule-cell.batch-mode {
position: relative;
border: 1px dashed #c0c4cc;
cursor: pointer;
}
.schedule-cell.batch-mode.empty-cell {
cursor: not-allowed;
opacity: 0.5;
}
.schedule-cell.selected-cell {
background-color: #ecf5ff !important;
border-color: #409eff;
border-style: solid;
}
.cell-checkbox {
position: absolute;
top: 2px;
right: 2px;
display: flex;
align-items: center;
justify-content: center;
width: 18px;
height: 18px;
border-radius: 50%;
}
.schedule-cell.selected-cell .cell-checkbox {
background-color: #409eff;
}
/* 批量单元格编辑弹窗 */
.batch-cell-summary {
margin-bottom: 16px;
font-size: 14px;
color: #606266;
}
.batch-cell-form {
margin-bottom: 16px;
}
.shift-info {
text-align: center;
}