gaoluyang
2025-11-19 4402ff26c26101745f132e2d8c9b08e2b9ee1d46
1.金鹰黄金-添加部门管理页面
已添加2个文件
590 ■■■■■ 文件已修改
src/api/lavorissce/issue.js 75 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/views/lavorissue/issue/index.vue 515 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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
  })
}
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="季度" 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>