| ¶Ô±ÈÐÂÎļþ |
| | |
| | | <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> |