This commit is contained in:
jhd
2026-05-23 16:21:20 +08:00
3 changed files with 577 additions and 8 deletions

View File

@@ -11,6 +11,7 @@
<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>
</div>
<el-alert type="info" title="提示:双击排班单元格可编辑排班"></el-alert>
@@ -90,16 +91,16 @@
@click="removeShiftItem(index)">删除</el-button>
</div>
<div class="shift-config-fields">
<el-select v-model="item.shiftId" placeholder="选择班次" style="width: 180px;">
<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;">
<el-select v-model="item.ruleId" placeholder="倒班规则(可选)" style="width: 180px;" clearable>
<el-option v-for="rule in shiftRuleList" :key="rule.ruleId"
:label="rule.ruleName" :value="rule.ruleId" />
:label="rule.changeDays" :value="rule.ruleId" />
</el-select>
</div>
</div>
@@ -116,8 +117,16 @@
<span class="assignment-count">
已分配 {{ item.employeeIds ? item.employeeIds.split(',').filter(id => id.trim()).length : 0 }}
</span>
<!-- <el-button icon="el-icon-copy" size="mini" @click="copyShiftItem(index)"
title="复制班次配置">复制</el-button> -->
<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"
@@ -125,6 +134,7 @@
: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">
@@ -162,6 +172,32 @@
</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="editDialogVisible" width="400px">
<el-form ref="editForm" :model="editForm" label-width="80px">
@@ -199,13 +235,15 @@
<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 } 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 },
components: { TimeRangePicker, EmployeeSelector, AttendanceTemplateManager },
data() {
return {
loading: false,
@@ -219,6 +257,11 @@ export default {
shiftRuleList: [],
dialogVisible: false,
editDialogVisible: false,
showTemplateDialog: false,
showTemplateManager: false,
templateList: [],
currentShiftIndex: -1,
employeeList: [],
currentStep: 1, // 步骤1-选择班次2-分配人员3-确认
form: {
dateRange: [],
@@ -294,12 +337,20 @@ export default {
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: {
// 刷新排班
@@ -742,6 +793,189 @@ export default {
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('已取消删除')
})
}
}
}
@@ -977,6 +1211,16 @@ export default {
color: #909399;
}
.import-export-buttons {
display: flex;
gap: 8px;
margin-left: auto;
}
.file-input {
display: none;
}
/* 快速操作按钮 */
.quick-actions {
margin-top: 16px;