<template>
|
<div class="app-container labor-issue">
|
<div class="config-wrap">
|
<div class="left">
|
<div class="header">
|
<div>部门与岗位</div>
|
<div>
|
<el-button size="small" type="primary" @click="addDepartment">新增部门</el-button>
|
<el-button size="small" @click="addSubDepartment" :disabled="!currentDept">新增子部门</el-button>
|
<el-button size="small" @click="addPosition" :disabled="!currentDept">新增岗位</el-button>
|
</div>
|
</div>
|
<el-tree
|
:data="deptTree"
|
node-key="id"
|
:props="{ label: 'label', children: 'children' }"
|
@node-click="onNodeClick"
|
highlight-current
|
v-model:expanded-keys="expandedKeys"
|
default-expand-all
|
class="tree"
|
>
|
<template #default="{ data }">
|
<span>
|
<el-tag size="small" :type="data.type===1 ? 'success' : 'warning'" effect="plain" style="margin-right:4px;">
|
{{ data.type === 1 ? '部门' : '岗位' }}
|
</el-tag>
|
{{ data.label }}
|
</span>
|
<span class="ops">
|
<el-button link size="small" type="primary" @click.stop="openRenameDialog(data)">重命名</el-button>
|
<el-button link size="small" type="danger" @click.stop="confirmRemoveNode(data)">删除</el-button>
|
</span>
|
</template>
|
</el-tree>
|
</div>
|
<div class="right">
|
<div v-if="currentPosition" class="position-config">
|
<div class="title">
|
<span>岗位配置:{{ currentPosition.label }}</span>
|
</div>
|
<div class="q-toolbar">
|
<el-button size="small" type="primary" @click="openAddItemDialog">新增用品</el-button>
|
</div>
|
<el-table :data="laborConfList" border size="small">
|
<el-table-column label="用品名称" prop="dictName" />
|
<el-table-column label="数量" prop="num" width="140" align="center" />
|
<el-table-column label="季度" width="180" align="center">
|
<template #default="scope">
|
{{ quarterLabelFromNumber(scope.row.quarter) }}
|
</template>
|
</el-table-column>
|
<el-table-column label="操作" width="160" align="center">
|
<template #default="scope">
|
<el-button link type="primary" size="small" @click="openEditItemDialog(scope.row)">编辑</el-button>
|
<el-button link type="danger" size="small" @click="onDeleteItem(scope.row)">删除</el-button>
|
</template>
|
</el-table-column>
|
</el-table>
|
</div>
|
<div v-else class="empty">请选择左侧岗位进行配置</div>
|
</div>
|
</div>
|
<!-- 新增部门/岗位弹窗 -->
|
<el-dialog v-model="addDialogVisible" :title="addDialogTitle" width="420px">
|
<el-form :model="addForm" :rules="addRules" ref="addFormRef" label-width="80px">
|
<el-form-item label="名称" prop="name">
|
<el-input v-model="addForm.name" placeholder="请输入名称" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="onCancelAdd">取 消</el-button>
|
<el-button type="primary" @click="onConfirmAdd" :loading="addLoading">确 认</el-button>
|
</template>
|
</el-dialog>
|
<!-- 重命名弹窗 -->
|
<el-dialog v-model="renameDialogVisible" :title="renameDialogTitle" width="420px">
|
<el-form :model="renameForm" :rules="addRules" ref="renameFormRef" label-width="80px">
|
<el-form-item label="名称" prop="name">
|
<el-input v-model="renameForm.name" placeholder="请输入名称" />
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="onCancelRename">取 消</el-button>
|
<el-button type="primary" @click="onConfirmRename" :loading="renameLoading">确 认</el-button>
|
</template>
|
</el-dialog>
|
|
<!-- 新增/编辑用品弹窗(字典选择) -->
|
<el-dialog v-model="addItemDialogVisible" :title="addItemDialogTitle" width="520px">
|
<el-form :model="addItemForm" label-width="90px">
|
<el-form-item label="用品名称">
|
<el-select v-model="addItemForm.dictId" filterable placeholder="请选择用品">
|
<el-option v-for="opt in sys_lavor_issue" :key="opt.value" :label="opt.label" :value="opt.value" />
|
</el-select>
|
</el-form-item>
|
<el-form-item label="数量">
|
<el-input-number v-model="addItemForm.quantity" :min="0" :precision="0" />
|
</el-form-item>
|
<el-form-item label="季度">
|
<el-select v-model="addItemForm.quarter" placeholder="选择季度" style="width: 160px">
|
<el-option :value="1" :label="quarterLabelFromNumber(1)" />
|
<el-option :value="2" :label="quarterLabelFromNumber(2)" />
|
<el-option :value="3" :label="quarterLabelFromNumber(3)" />
|
<el-option :value="4" :label="quarterLabelFromNumber(4)" />
|
</el-select>
|
</el-form-item>
|
</el-form>
|
<template #footer>
|
<el-button @click="onCancelAddItem">取 消</el-button>
|
<el-button type="primary" :loading="addItemSaving" @click="onConfirmAddItem">保 存</el-button>
|
</template>
|
</el-dialog>
|
</div>
|
</template>
|
|
<script setup>
|
import { ref, reactive, computed, watch, onMounted } from 'vue'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
import { useDict } from '@/utils/dict'
|
import { getDeptPositionTree, addDeptPosition, updateDeptPosition, deleteDeptPosition, laborConfListPage, addLaborConf, updateLaborConf, deleteLaborConf } from '@/api/lavorissce/issue'
|
|
// 字典:劳保用品字典
|
const { sys_lavor_issue } = useDict('sys_lavor_issue')
|
|
// 结构:部门树 -> 岗位
|
const departments = reactive([]) // [{id,name,children:[],positions:[{id,label,itemsByQuarter:{Q1:[{name,quantity}],...}}]}]
|
|
// 加载部门岗位树
|
async function loadDeptTree() {
|
try {
|
const res = await getDeptPositionTree()
|
const data = res?.data || res || []
|
// 覆盖 reactive 数组内容
|
departments.splice(0, departments.length, ...data)
|
} catch (e) {
|
// 静默失败,保留本地示例数据
|
}
|
}
|
|
// 左侧树(部门/岗位)支持多级
|
function mapDeptToTree(d) {
|
// d: { id, name, type:1, children: [...] }
|
const node = {
|
id: `dept-${d.id}`,
|
label: d.name,
|
type: 1,
|
raw: d,
|
children: [],
|
}
|
const kids = Array.isArray(d.children) ? d.children : []
|
for (const c of kids) {
|
if (c.type === 1) {
|
node.children.push(mapDeptToTree(c))
|
} else if (c.type === 2) {
|
// position leaf
|
const rawPos = {
|
id: c.id,
|
label: c.name,
|
// ensure items container exists for UI editing
|
itemsByQuarter: { Q1: [], Q2: [], Q3: [], Q4: [] },
|
}
|
node.children.push({
|
id: `pos-${d.id}-${c.id}`,
|
label: c.name,
|
type: 2,
|
raw: rawPos,
|
parentDeptId: d.id,
|
})
|
}
|
}
|
return node
|
}
|
const deptTree = computed(() => departments.map(d => mapDeptToTree(d)))
|
|
const expandedKeys = ref([])
|
function collectDeptKeys(list, acc) {
|
for (const d of list) {
|
acc.push(`dept-${d.id}`)
|
if (Array.isArray(d.children)) collectDeptKeys(d.children, acc)
|
}
|
}
|
watch(
|
() => departments,
|
() => {
|
const acc = []
|
collectDeptKeys(departments, acc)
|
expandedKeys.value = acc
|
},
|
{ deep: true, immediate: true }
|
)
|
|
const currentDept = ref(null)
|
const currentPosition = ref(null)
|
// 右侧:当前岗位的用品配置可选列表(来自 laborConfListPage)
|
const laborConfList = ref([])
|
const laborConfLoading = ref(false)
|
|
function quarterLabelFromNumber(n) {
|
return n === 1 ? '第一季度' : n === 2 ? '第二季度' : n === 3 ? '第三季度' : n === 4 ? '第四季度' : ''
|
}
|
|
async function loadLaborConf(deptPositionId) {
|
try {
|
laborConfLoading.value = true
|
const res = await laborConfListPage({ deptPositionId })
|
laborConfList.value = res.data.records.map(it => ({
|
...it,
|
id: it.id ?? it.dictId ?? it.value,
|
name: it.dictName,
|
quantity: it.num,
|
quarter: it.quarter,
|
quarterLabel: quarterLabelFromNumber(it.quarter),
|
}))
|
} finally {
|
laborConfLoading.value = false
|
}
|
}
|
|
// 新增用品弹窗状态与逻辑(供模板使用)
|
const addItemDialogVisible = ref(false)
|
const addItemDialogTitle = ref('新增用品')
|
const addItemSaving = ref(false)
|
const addItemForm = reactive({ id: undefined, dictId: undefined, quantity: 0, quarter: 1 })
|
|
function openAddItemDialog() {
|
if (!currentPosition.value) return
|
addItemDialogTitle.value = '新增用品'
|
addItemForm.id = undefined
|
addItemForm.dictId = undefined
|
addItemForm.quantity = 0
|
addItemForm.quarter = 1
|
addItemDialogVisible.value = true
|
}
|
|
function openEditItemDialog(row) {
|
if (!currentPosition.value || !row) return
|
addItemDialogTitle.value = '编辑用品'
|
addItemForm.id = row.id
|
addItemForm.dictId = row.dictId
|
addItemForm.quantity = Number(row.num) || 0
|
addItemForm.quarter = Number(row.quarter) || 1
|
addItemDialogVisible.value = true
|
}
|
|
function onCancelAddItem() {
|
addItemDialogVisible.value = false
|
}
|
|
async function onConfirmAddItem() {
|
if (!currentPosition.value) return
|
addItemSaving.value = true
|
try {
|
const qNum = Number(addItemForm.quarter) || 1
|
const deptPositionId = currentPosition.value.id ?? currentPosition.value.raw?.id ?? currentPosition.value?.id
|
if (addItemForm.id) {
|
await updateLaborConf({
|
id: addItemForm.id,
|
dictId: addItemForm.dictId,
|
num: Number(addItemForm.quantity) || 0,
|
quarter: qNum,
|
})
|
} else {
|
await addLaborConf({
|
deptPositionId,
|
dictId: addItemForm.dictId,
|
num: Number(addItemForm.quantity) || 0,
|
quarter: qNum,
|
})
|
}
|
const posId = currentPosition.value.raw?.id ?? currentPosition.value.id
|
await loadLaborConf(posId)
|
addItemDialogVisible.value = false
|
ElMessage.success('保存成功')
|
} catch (e) {
|
ElMessage.error((e && (e.msg || e.message)) || '保存失败')
|
} finally {
|
addItemSaving.value = false
|
}
|
}
|
|
async function onDeleteItem(row) {
|
if (!currentPosition.value || !row) return
|
try {
|
await ElMessageBox.confirm('确定删除该用品配置吗?', '提示', { type: 'warning' })
|
} catch {
|
return
|
}
|
try {
|
await deleteLaborConf([row.id])
|
const posId = currentPosition.value.raw?.id ?? currentPosition.value.id
|
await loadLaborConf(posId)
|
ElMessage.success('删除成功')
|
} catch (e) {
|
ElMessage.error((e && (e.msg || e.message)) || '删除失败')
|
}
|
}
|
|
// 新增弹窗状态
|
const addDialogVisible = ref(false)
|
const addDialogTitle = ref('新增')
|
const addLoading = ref(false)
|
const addFormRef = ref()
|
const addForm = reactive({ name: '', type: 1, parentId: undefined })
|
const addRules = { name: [{ required: true, message: '请输入名称', trigger: 'blur' }] }
|
|
// 重命名弹窗状态
|
const renameDialogVisible = ref(false)
|
const renameLoading = ref(false)
|
const renameFormRef = ref()
|
const renameForm = reactive({ id: undefined, type: 1, name: '' })
|
const renameDialogTitle = ref('重命名')
|
|
function newPosition() {
|
return {
|
id: Date.now(),
|
label: '新岗位',
|
hrPositionId: undefined,
|
itemsByQuarter: {
|
Q1: [], Q2: [], Q3: [], Q4: []
|
}
|
}
|
}
|
|
async function addDepartment() {
|
openAddDialog({ type: 1 })
|
}
|
|
async function addPosition() {
|
if (!currentDept.value) return
|
openAddDialog({ type: 2, parentId: currentDept.value?.raw?.id })
|
}
|
|
async function addSubDepartment() {
|
if (!currentDept.value) return
|
openAddDialog({ type: 1, parentId: currentDept.value?.raw?.id })
|
}
|
|
function openAddDialog({ type, parentId }) {
|
addForm.name = ''
|
addForm.type = type
|
addForm.parentId = parentId
|
addDialogTitle.value = type === 1 ? '新增部门' : '新增岗位'
|
addDialogVisible.value = true
|
}
|
|
function onCancelAdd() {
|
addDialogVisible.value = false
|
}
|
|
async function onConfirmAdd() {
|
addFormRef.value?.validate(async (valid) => {
|
if (!valid) return
|
try {
|
addLoading.value = true
|
const payload = { name: addForm.name, type: addForm.type, parentId: addForm.parentId ?? 0 }
|
await addDeptPosition(payload)
|
addDialogVisible.value = false
|
await loadDeptTree()
|
} finally {
|
addLoading.value = false
|
}
|
})
|
}
|
|
function onNodeClick(node) {
|
if (node.type === 1) {
|
currentDept.value = node
|
currentPosition.value = null
|
} else if (node.type === 2) {
|
const dept = findDeptById(departments, node.parentDeptId)
|
if (dept) currentDept.value = mapDeptToTree(dept)
|
currentPosition.value = node.raw
|
// 选择岗位时,按要求查询用品配置列表
|
loadLaborConf(node.raw.id)
|
}
|
}
|
|
function openRenameDialog(node) {
|
renameForm.id = node.raw.id
|
renameForm.type = node.type === 1 ? 1 : 2
|
renameForm.name = node.label
|
renameDialogTitle.value = node.type === 1 ? '重命名部门' : '重命名岗位'
|
renameDialogVisible.value = true
|
}
|
|
function onCancelRename() {
|
renameDialogVisible.value = false
|
}
|
|
async function onConfirmRename() {
|
renameFormRef.value?.validate(async (valid) => {
|
if (!valid) return
|
try {
|
renameLoading.value = true
|
await updateDeptPosition({ id: renameForm.id, name: renameForm.name, type: renameForm.type })
|
renameDialogVisible.value = false
|
await loadDeptTree()
|
} finally {
|
renameLoading.value = false
|
}
|
})
|
}
|
|
async function confirmRemoveNode(node) {
|
const type = node.type === 1 ? 1 : 2
|
const id = node.type === 1 ? node.raw.id : node.raw.id
|
// 简单确认
|
try {
|
await deleteDeptPosition([id])
|
await loadDeptTree()
|
} catch (e) {
|
// ignore errors
|
}
|
}
|
|
// 递归查找/删除部门
|
function findDeptById(list, id) {
|
for (const d of list) {
|
if (d.id === id) return d
|
if (Array.isArray(d.children)) {
|
const found = findDeptById(d.children, id)
|
if (found) return found
|
}
|
}
|
return null
|
}
|
function removeDeptById(list, id) {
|
const idx = list.findIndex(d => d.id === id)
|
if (idx > -1) {
|
list.splice(idx, 1)
|
return true
|
}
|
for (const d of list) {
|
if (Array.isArray(d.children) && removeDeptById(d.children, id)) return true
|
}
|
return false
|
}
|
|
// 初始化一个示例结构,便于开箱体验(含子部门)
|
if (departments.length === 0) {
|
const d1 = { id: 1, name: '生产部', positions: [ newPosition(), newPosition() ], children: [] }
|
d1.positions[0].label = '一线工'
|
d1.positions[1].label = '质检'
|
d1.positions[0].itemsByQuarter.Q1.push({ name: '安全手套', quantity: 2 })
|
d1.positions[0].itemsByQuarter.Q2.push({ name: '防护眼镜', quantity: 1 })
|
const d11 = { id: 11, name: '生产一部', positions: [ newPosition() ], children: [] }
|
d11.positions[0].label = '线体A'
|
d1.children.push(d11)
|
departments.push(d1)
|
}
|
|
// 工具:获取指定部门的岗位列表(不含子部门)
|
function getDeptPositions(deptId) {
|
const d = findDeptById(departments, deptId)
|
return (d && Array.isArray(d.positions)) ? d.positions : []
|
}
|
|
onMounted(() => {
|
loadDeptTree()
|
})
|
</script>
|
|
<style scoped>
|
.labor-issue {
|
display: flex;
|
flex-direction: column;
|
gap: 12px;
|
}
|
.config-wrap {
|
display: flex;
|
gap: 16px;
|
align-items: stretch;
|
}
|
.left, .right {
|
background: #fff;
|
border: 1px solid var(--el-border-color, #ebeef5);
|
border-radius: 8px;
|
box-shadow: var(--el-box-shadow-light, 0 2px 12px 0 rgba(0,0,0,.1));
|
}
|
.left { width: 340px; padding: 12px; }
|
.right { flex: 1; padding: 14px; }
|
.header {
|
display: flex;
|
align-items: flex-start;
|
margin-bottom: 10px;
|
flex-direction: column;
|
}
|
.header :deep(.el-button+.el-button) { margin-left: 8px; }
|
.tree {
|
max-height: calc(100vh - 300px);
|
overflow: auto;
|
padding: 6px;
|
border-radius: 6px;
|
background: #fafafa;
|
}
|
.ops { margin-left: 8px; opacity: 0.6; transition: opacity .2s; }
|
:deep(.el-tree-node__content):hover .ops { opacity: 1; }
|
|
.q-toolbar { margin-bottom: 10px; }
|
.empty { color: #999; padding: 48px; text-align: center; }
|
|
.summary-wrap { display: flex; flex-direction: column; gap: 16px; }
|
.people, .summary {
|
background: #fff;
|
border: 1px solid var(--el-border-color, #ebeef5);
|
border-radius: 8px;
|
padding: 12px;
|
box-shadow: var(--el-box-shadow-light, 0 2px 12px 0 rgba(0,0,0,.06));
|
}
|
.summary .header {
|
font-weight: 600;
|
margin-bottom: 12px;
|
}
|
</style>
|