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