feat: 新增考勤模板管理功能

- 后端新增 WmsAttendanceTemplate CRUD(Controller/Service/Mapper/Domain)
- 前端新增 attendanceTemplate API 对接
- 前端新增 AttendanceTemplateManager 组件(拖拽排序 + 模板编辑)
- 优化考勤 drag.vue 页面交互
This commit is contained in:
2026-06-06 17:08:31 +08:00
parent 696f6d9ee0
commit 1a2fc9852d
11 changed files with 969 additions and 58 deletions

View File

@@ -226,8 +226,15 @@
</el-dialog>
<!-- 班次模板选择弹窗 -->
<el-dialog title="选择人员模板" :visible.sync="showTemplateDialog" width="500px">
<el-table :data="templateList" border style="width: 100%;">
<el-dialog title="选择人员模板" :visible.sync="showTemplateDialog" width="540px">
<div class="template-dialog-tabs">
<div :class="['template-dialog-tab', { active: templateDialogTab === 'personal' }]" @click="switchTemplateDialogTab('personal')">自用模板</div>
<div :class="['template-dialog-tab', { active: templateDialogTab === 'shared' }]" @click="switchTemplateDialogTab('shared')">共享模板</div>
</div>
<div class="template-dialog-toolbar" v-show="templateDialogTab === 'shared'">
<el-input v-model="sharedDialogSearch" placeholder="搜索模板名称" size="small" clearable prefix-icon="el-icon-search" @input="handleDialogSearch" style="width: 200px;" />
</div>
<el-table :data="filteredTemplateList" border style="width: 100%;" v-loading="sharedDialogLoading">
<el-table-column prop="name" label="模板名称" />
<el-table-column prop="employeeCount" label="员工数量" align="center" />
<el-table-column prop="createTime" label="创建时间" />
@@ -238,6 +245,16 @@
</template>
</el-table-column>
</el-table>
<div class="template-dialog-pagination" v-show="templateDialogTab === 'shared' && sharedDialogTotal > sharedDialogPageSize">
<el-pagination
background
layout="prev, pager, next"
:current-page="sharedDialogPageNum"
:page-size="sharedDialogPageSize"
:total="sharedDialogTotal"
@current-change="handleDialogPageChange"
/>
</div>
<div slot="footer" class="dialog-footer">
<el-button @click="showTemplateDialog = false">关闭</el-button>
</div>
@@ -358,6 +375,7 @@ import { listAttendanceSchedule, generateenerateSchedule, updateAttendanceSchedu
import { listShift } from '@/api/wms/attendanceShift'
import { listAttendanceShiftRule } from '@/api/wms/attendanceShiftRule'
import { listEmployeeInfo } from '@/api/wms/employeeInfo'
import { listAttendanceTemplate, delAttendanceTemplate } from '@/api/wms/attendanceTemplate'
export default {
name: 'AttendanceSchedule',
@@ -387,6 +405,15 @@ export default {
editDialogVisible: false,
showTemplateDialog: false,
showTemplateManager: false,
templateDialogTab: 'personal',
sharedDialogList: [],
sharedDialogLoading: false,
sharedDialogPageNum: 1,
sharedDialogPageSize: 20,
sharedDialogTotal: 0,
sharedDialogSearch: '',
sharedTemplateList: [],
personalTemplateList: [],
templateList: [],
currentShiftIndex: -1,
employeeList: [],
@@ -543,6 +570,12 @@ export default {
if (!this.batchCellEditShiftId) return ''
const shift = this.shiftList.find(s => s.shiftId === this.batchCellEditShiftId)
return shift ? shift.shiftType : ''
},
filteredTemplateList() {
if (this.templateDialogTab === 'shared') {
return this.sharedDialogList
}
return this.personalTemplateList
}
},
created() {
@@ -1323,22 +1356,51 @@ export default {
this.getScheduleList()
},
// 加载模板列表
// 加载模板列表(共享 + 自用)
loadTemplates() {
try {
const templates = localStorage.getItem('attendanceTemplates')
this.templateList = templates ? JSON.parse(templates) : []
this.personalTemplateList = (templates ? JSON.parse(templates) : []).map(t => ({ ...t, source: 'personal' }))
} catch (e) {
this.templateList = []
this.personalTemplateList = []
}
listAttendanceTemplate().then(res => {
this.sharedTemplateList = (res.rows || []).map(row => ({
id: row.templateId,
name: row.templateName,
employeeIds: this.parseTemplateContent(row.templateContent),
employeeCount: this.parseTemplateContent(row.templateContent).length,
createTime: row.createTime,
source: 'shared'
}))
this.templateList = [...this.sharedTemplateList, ...this.personalTemplateList]
}).catch(() => {
this.templateList = [...this.personalTemplateList]
})
},
parseTemplateContent(content) {
if (!content) return []
try {
const parsed = JSON.parse(content)
return Array.isArray(parsed) ? parsed : []
} catch (e) {
return content.split(',').filter(Boolean)
}
},
// 保存模板到localStorage
saveTemplates() {
localStorage.setItem('attendanceTemplates', JSON.stringify(this.templateList))
// 保存自用模板到localStorage
savePersonalTemplates() {
const plainList = this.personalTemplateList.map(t => {
const { source, ...rest } = t
return rest
})
localStorage.setItem('attendanceTemplates', JSON.stringify(plainList))
this.templateList = [...this.sharedTemplateList, ...this.personalTemplateList]
},
// 保存单个班次为模板
// 保存单个班次为模板(保存到自用模板)
saveSingleTemplate(index) {
const shiftItem = this.form.shiftList[index]
if (!shiftItem.employeeIds || !shiftItem.employeeIds.trim()) {
@@ -1357,17 +1419,18 @@ export default {
}
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')
createTime: new Date().toLocaleString('zh-CN'),
source: 'personal'
}
this.templateList.push(template)
this.saveTemplates()
this.personalTemplateList.push(template)
this.savePersonalTemplates()
this.$message.success('模板保存成功')
}).catch(() => {
this.$message.info('已取消保存')
@@ -1378,6 +1441,57 @@ export default {
openTemplateDialog(index) {
this.currentShiftIndex = index
this.showTemplateDialog = true
if (this.templateDialogTab === 'shared') {
this.sharedDialogPageNum = 1
this.sharedDialogSearch = ''
this.loadSharedDialogTemplates()
}
},
// 切换模板弹窗的 tab
switchTemplateDialogTab(tab) {
if (this.templateDialogTab === tab) return
this.templateDialogTab = tab
if (tab === 'shared') {
this.sharedDialogPageNum = 1
this.sharedDialogSearch = ''
this.loadSharedDialogTemplates()
}
},
// 加载共享模板弹窗列表(带分页)
loadSharedDialogTemplates() {
this.sharedDialogLoading = true
const query = {
pageNum: this.sharedDialogPageNum,
pageSize: this.sharedDialogPageSize
}
if (this.sharedDialogSearch) {
query.templateName = this.sharedDialogSearch
}
listAttendanceTemplate(query).then(res => {
this.sharedDialogList = (res.rows || []).map(row => ({
id: row.templateId,
name: row.templateName,
employeeIds: this.parseTemplateContent(row.templateContent),
employeeCount: this.parseTemplateContent(row.templateContent).length,
createTime: row.createTime,
source: 'shared'
}))
this.sharedDialogTotal = res.total || 0
}).finally(() => {
this.sharedDialogLoading = false
})
},
handleDialogSearch() {
this.sharedDialogPageNum = 1
this.loadSharedDialogTemplates()
},
handleDialogPageChange(pageNum) {
this.sharedDialogPageNum = pageNum
this.loadSharedDialogTemplates()
},
// 应用单个模板到当前班次
@@ -1392,16 +1506,26 @@ export default {
// 删除单个模板
deleteSingleTemplate(template) {
this.$confirm('确定要删除这个模板吗?', '提示', {
const confirmMsg = template.source === 'shared'
? '删除后该模板所有人都无法使用,确定删除?'
: '确定要删除这个模板吗?'
this.$confirm(confirmMsg, '提示', {
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()
}).then(async () => {
if (template.source === 'shared') {
await delAttendanceTemplate(template.id)
this.$message.success('删除成功')
this.loadTemplates()
this.loadSharedDialogTemplates()
} else {
const index = this.personalTemplateList.findIndex(t => t.id === template.id)
if (index > -1) {
this.personalTemplateList.splice(index, 1)
this.savePersonalTemplates()
this.$message.success('删除成功')
}
}
}).catch(() => {
this.$message.info('已取消删除')
@@ -1782,4 +1906,41 @@ export default {
justify-content: flex-end;
gap: 12px;
}
.template-dialog-tabs {
display: flex;
border-bottom: 1px solid #e4e7ed;
margin-bottom: 12px;
}
.template-dialog-tab {
padding: 6px 14px;
font-size: 13px;
color: #909399;
cursor: pointer;
border-bottom: 2px solid transparent;
margin-bottom: -1px;
transition: color 0.2s, border-color 0.2s;
user-select: none;
}
.template-dialog-tab:hover {
color: #606266;
}
.template-dialog-tab.active {
color: #1890ff;
border-bottom-color: #1890ff;
font-weight: 500;
}
.template-dialog-toolbar {
margin-bottom: 10px;
}
.template-dialog-pagination {
display: flex;
justify-content: center;
padding-top: 10px;
}
</style>