feat(attendance): 新增考勤模板管理功能,支持模板的增删改查和导入导出

1. 新增AttendanceTemplateManager组件,实现考勤模板的本地管理
2. 在排班页面添加模板管理入口和相关操作按钮
3. 支持将班次配置保存为模板、从模板快速导入人员
4. 优化班次选择器的交互体验,添加清空功能
5. 新增单个班次的人员导入导出功能
This commit is contained in:
2026-05-23 14:48:22 +08:00
parent 40067a3680
commit a33db26838
3 changed files with 577 additions and 8 deletions

View File

@@ -0,0 +1,318 @@
<template>
<div class="template-manager">
<div class="left-panel">
<div class="panel-header">
<span class="panel-title">模板列表</span>
<el-button icon="el-icon-plus" size="mini" type="primary" @click="addTemplate">新增</el-button>
</div>
<div class="template-list">
<div
v-for="template in templateList"
:key="template.id"
:class="['template-item', { active: currentTemplate.id === template.id }]"
@click="selectTemplate(template)"
>
<div class="template-name">{{ template.name }}</div>
<div class="template-meta">
<span>{{ template.employeeCount }}</span>
<span class="create-time">{{ template.createTime }}</span>
</div>
<div class="template-actions">
<el-button size="mini" type="text" @click.stop="deleteTemplate(template)">删除</el-button>
</div>
</div>
<div v-if="templateList.length === 0" class="empty-list">
<el-empty description="暂无模板" />
</div>
</div>
</div>
<div class="right-panel">
<div class="panel-header">
<span class="panel-title">员工配置</span>
</div>
<div class="template-form">
<el-form :model="currentTemplate" label-width="70px" inline>
<el-form-item label="模板名称">
<el-input
v-model="currentTemplate.name"
placeholder="请输入模板名称"
style="width: 200px;"
/>
</el-form-item>
</el-form>
</div>
<div class="transfer-container">
<el-transfer
v-model="selectedEmployeeIds"
:data="employeeList"
:titles="['可选员工', '已选员工']"
:button-texts="['移除', '添加']"
filterable
filter-placeholder="搜索员工"
:width="['160px', '160px']"
:height="280"
/>
</div>
<div class="right-panel-footer">
<el-button type="primary" @click="saveTemplate" :disabled="!currentTemplate.name">保存</el-button>
<el-button @click="resetForm">重置</el-button>
</div>
</div>
</div>
</template>
<script>
export default {
name: 'AttendanceTemplateManager',
props: {
employeeList: {
type: Array,
default: () => []
}
},
data() {
return {
templateList: [],
currentTemplate: {
id: '',
name: '',
employeeIds: [],
employeeCount: 0,
createTime: ''
},
selectedEmployeeIds: [],
}
},
mounted() {
this.loadTemplates()
},
methods: {
loadTemplates() {
try {
const templates = localStorage.getItem('attendanceTemplates')
this.templateList = templates ? JSON.parse(templates) : []
} catch (e) {
this.templateList = []
}
},
saveTemplates() {
localStorage.setItem('attendanceTemplates', JSON.stringify(this.templateList))
this.$emit('update')
},
addTemplate() {
this.currentTemplate = {
id: '',
name: '',
employeeIds: [],
employeeCount: 0,
createTime: ''
}
this.selectedEmployeeIds = []
},
editTemplate(template) {
this.currentTemplate = JSON.parse(JSON.stringify(template))
this.selectedEmployeeIds = [...template.employeeIds]
},
deleteTemplate(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('删除成功')
if (this.currentTemplate.id === template.id) {
this.resetForm()
}
}
}).catch(() => {
this.$message.info('已取消')
})
},
selectTemplate(template) {
this.currentTemplate = JSON.parse(JSON.stringify(template))
this.selectedEmployeeIds = [...template.employeeIds]
},
saveTemplate() {
if (!this.currentTemplate.name.trim()) {
this.$message.warning('请输入模板名称')
return
}
const templateData = {
id: this.currentTemplate.id || Date.now().toString(),
name: this.currentTemplate.name.trim(),
employeeIds: [...this.selectedEmployeeIds],
employeeCount: this.selectedEmployeeIds.length,
createTime: this.currentTemplate.id ? this.currentTemplate.createTime : new Date().toLocaleString('zh-CN')
}
if (this.currentTemplate.id) {
const index = this.templateList.findIndex(t => t.id === this.currentTemplate.id)
if (index > -1) {
this.templateList[index] = templateData
}
this.$message.success('修改成功')
} else {
this.templateList.push(templateData)
this.$message.success('新增成功')
}
this.saveTemplates()
},
resetForm() {
this.currentTemplate = {
id: '',
name: '',
employeeIds: [],
employeeCount: 0,
createTime: ''
}
this.selectedEmployeeIds = []
}
}
}
</script>
<style scoped>
.template-manager {
display: flex;
height: 520px;
gap: 24px;
}
.left-panel {
flex: 1;
display: flex;
flex-direction: column;
border-right: 1px solid #e4e7ed;
padding-right: 20px;
}
.right-panel {
width: 600px;
display: flex;
flex-direction: column;
}
.panel-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 14px;
padding-bottom: 10px;
border-bottom: 1px solid #e4e7ed;
}
.panel-title {
font-weight: 600;
font-size: 14px;
color: #303133;
}
.template-list {
flex: 1;
overflow-y: auto;
padding-right: 4px;
}
.template-list::-webkit-scrollbar {
width: 6px;
}
.template-list::-webkit-scrollbar-thumb {
background-color: #dcdfe6;
border-radius: 3px;
}
.template-item {
display: flex;
align-items: center;
padding: 10px 12px;
margin-bottom: 6px;
background-color: #fafafa;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
}
.template-item:hover {
background-color: #f0f0f0;
}
.template-item.active {
background-color: #e6f7ff;
border-left: 3px solid #1890ff;
}
.template-name {
flex: 1;
font-size: 13px;
font-weight: 500;
color: #303133;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.template-meta {
display: flex;
flex-direction: column;
align-items: flex-end;
gap: 2px;
margin-right: 12px;
font-size: 11px;
color: #909399;
}
.create-time {
font-size: 10px;
}
.template-actions {
display: flex;
gap: 8px;
}
.template-actions .el-button {
padding: 0 4px;
font-size: 11px;
color: #606266;
}
.template-actions .el-button:hover {
color: #409eff;
}
.empty-list {
padding: 40px 0;
}
.template-form {
margin-bottom: 14px;
}
.transfer-container {
flex: 1;
overflow: hidden;
}
.right-panel-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
margin-top: 14px;
padding-top: 10px;
border-top: 1px solid #e4e7ed;
}
</style>

View File

@@ -20,7 +20,7 @@
<div v-if="multiple" class="custom-transfer" v-loading="loading">
<!-- 左侧可选列表 -->
<div class="transfer-panel">
<div class="panel-header">可选员工</div>
<div class="panel-header">可选员工 <span class="count-info">(已选 {{ leftSelectedKeys.length }}/总计 {{ availableList.length }})</span></div>
<div class="panel-search">
<input
type="text"
@@ -73,7 +73,7 @@
<!-- 右侧已选列表 -->
<div class="transfer-panel">
<div class="panel-header">已选员工</div>
<div class="panel-header">已选员工 <span class="count-info">( {{ selectedList.length }}/已选 {{ rightSelectedKeys.length }})</span></div>
<div class="panel-search">
<input
type="text"
@@ -601,6 +601,13 @@ export default {
background: #f5f7fa;
}
.count-info {
font-weight: normal;
font-size: 12px;
color: #909399;
margin-left: 8px;
}
.panel-search {
padding: 10px;
border-bottom: 1px solid #f2f6fc;