From 4402ff26c26101745f132e2d8c9b08e2b9ee1d46 Mon Sep 17 00:00:00 2001
From: gaoluyang <2820782392@qq.com>
Date: 星期三, 19 十一月 2025 16:24:18 +0800
Subject: [PATCH] 1.金鹰黄金-添加部门管理页面
---
src/api/lavorissce/issue.js | 75 +++++++
src/views/lavorissue/issue/index.vue | 515 +++++++++++++++++++++++++++++++++++++++++++++++++++
2 files changed, 590 insertions(+), 0 deletions(-)
diff --git a/src/api/lavorissce/issue.js b/src/api/lavorissce/issue.js
new file mode 100644
index 0000000..6b02601
--- /dev/null
+++ b/src/api/lavorissce/issue.js
@@ -0,0 +1,75 @@
+// 閮ㄩ棬绠$悊
+import request from '@/utils/request'
+
+// 閫掑綊鑾峰彇閮ㄩ棬宀椾綅鏍戝舰缁撴瀯
+export function getDeptPositionTree(query) {
+ return request({
+ url: '/deptPosition/getDeptPositionTree',
+ method: 'get',
+ params: query
+ })
+}
+// 娣诲姞閮ㄩ棬宀椾綅
+export function addDeptPosition(query) {
+ return request({
+ url: '/deptPosition/addDeptPosition',
+ method: 'post',
+ data: query
+ })
+}
+// 缂栬緫閮ㄩ棬宀椾綅
+export function editDeptPosition(query) {
+ return request({
+ url: '/deptPosition/editDeptPosition',
+ method: 'post',
+ data: query
+ })
+}
+// 淇敼閮ㄩ棬宀椾綅
+export function updateDeptPosition(query) {
+ return request({
+ url: '/deptPosition/updateDeptPosition',
+ method: 'post',
+ data: query
+ })
+}
+// 鍒犻櫎閮ㄩ棬宀椾綅
+export function deleteDeptPosition(query) {
+ return request({
+ url: '/deptPosition/deleteDeptPosition',
+ method: 'delete',
+ data: query
+ })
+}
+// 鏌ヨ鍔充繚閰嶇疆
+export function laborConfListPage(query) {
+ return request({
+ url: '/laborConf/listPage',
+ method: 'get',
+ params: query
+ })
+}
+// 娣诲姞鍔充繚鐢ㄥ搧
+export function addLaborConf(query) {
+ return request({
+ url: '/laborConf/add',
+ method: 'post',
+ data: query
+ })
+}
+// 淇敼鍔充繚鐢ㄥ搧
+export function updateLaborConf(query) {
+ return request({
+ url: '/laborConf/update',
+ method: 'post',
+ data: query
+ })
+}
+// 鍒犻櫎鍔充繚鐢ㄥ搧
+export function deleteLaborConf(query) {
+ return request({
+ url: '/laborConf/delete',
+ method: 'delete',
+ data: query
+ })
+}
\ No newline at end of file
diff --git a/src/views/lavorissue/issue/index.vue b/src/views/lavorissue/issue/index.vue
new file mode 100644
index 0000000..6222ea0
--- /dev/null
+++ b/src/views/lavorissue/issue/index.vue
@@ -0,0 +1,515 @@
+<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="瀛e害" 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="瀛e害">
+ <el-select v-model="addItemForm.quarter" placeholder="閫夋嫨瀛e害" 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 ? '绗竴瀛e害' : n === 2 ? '绗簩瀛e害' : n === 3 ? '绗笁瀛e害' : n === 4 ? '绗洓瀛e害' : ''
+}
+
+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>
--
Gitblit v1.9.3