zhengnaiwen_citu 6 kuukautta sitten
vanhempi
commit
cc6a7555c3

+ 33 - 4
src/api/system.js

@@ -145,13 +145,13 @@ export function uploadRoster (params) {
 }
 
 // 花名册导出
-export function exportRoster (params) {
-  return http.download('/digitizationData/employee/download/export', params)
+export function exportRoster () {
+  return http.download('/digitizationData/employee/download/export')
 }
 
 // 花名册模板下载
-export function downloadRosterTemplate (params) {
-  return http.download('/digitizationData/employee/download/template', params)
+export function downloadRosterTemplate () {
+  return http.download('/digitizationData/employee/download/template')
 }
 
 // 花名册钻取员工
@@ -163,18 +163,47 @@ export function organizationDrill (params) {
 export function getOrganizationAtlas () {
   return http.post('/organization/atlas')
 }
+
 // 机构图钻取员工图例
 export function getOrganizationAtlasEmployee (params) {
   return http.post('/organization/employee/atlas', params)
 }
+
+// 组织结构 导入
+export function importOrganization (params) {
+  return http.upload('/organization/upload', params)
+}
+
+// 组织结构 导出
+export function exportOrganization (params) {
+  return http.download('/organization/download/export', params)
+}
+
+// 组织结构 导出
+export function addOrganization (params) {
+  return http.post('/organization/save', params)
+}
+
+// 组织结构 下载模板
+export function downloadOrganization (params) {
+  return http.download('/organization/download/template', params)
+}
+
+// 组织结构 下载模板
+export function deleteOrganization (params) {
+  return http.download('/organization/del', params)
+}
+
 // 组织结构 保存标签
 export function getLabelPage (params) {
   return http.post('/label/page', params)
 }
+
 // 组织结构 保存标签
 export function saveLabel (params) {
   return http.post('/label/save', params)
 }
+
 // 组织结构 批量保存标签
 export function saveLabelAll (params) {
   return http.post('/label/batch/save', params)

+ 0 - 128
src/components/AutoComponents/MButton/index.vue

@@ -22,133 +22,5 @@ export default {
 </script>
 
 <style lang="scss" scoped>
-// 定义主题颜色(仿照 Element UI)
-$orange-color: $theme-color;
-$theme-colors: (
-  "default": (
-    "base": #ffffff,       // 默认背景色
-    "text": #606266,      // 默认文字颜色
-    "border": #dcdfe6     // 默认边框颜色
-  ),
-  "primary": #409EFF,
-  "success": #67C23A,
-  "warning": #E6A23C,
-  "danger": #F56C6C,
-  "orange": $theme-color  // 新增橙色
-);
-// 生成按钮样式
-@each $type, $color in $theme-colors {
-  @if $type == "default" {
-    .el-button--default {
-      background-color: map-get($color, "base");
-      border-color: map-get($color, "border");
-      color: map-get($color, "text");
-
-      // Hover 状态(浅橙色背景 + 浅橙色边框)
-      &:hover,
-      &:focus {
-        background-color: lighten($orange-color, 42%); // 非常浅的橙色
-        border-color: lighten($orange-color, 20%);    // 浅橙色边框
-        color: $orange-color;                         // 文字变橙色
-      }
-
-      // Active 状态(稍深的橙色边框)
-      &:active {
-        background-color: lighten($orange-color, 38%); // 更浅橙色背景
-        border-color: $orange-color;                   // 主橙色边框
-        color: $orange-color;
-      }
-
-      // Disabled 状态(保持灰色)
-      &.is-disabled {
-        background-color: #ffffff;
-        border-color: #e4e7ed;
-        color: #c0c4cc;
-      }
-
-      // Plain 模式(镂空按钮)
-      &.is-plain {
-        background-color: #ffffff;
-        border-color: #dcdfe6;
-        color: #606266;
-
-        // Hover 状态(浅橙色背景)
-        &:hover,
-        &:focus {
-          background-color: lighten($orange-color, 42%);
-          border-color: lighten($orange-color, 20%);
-          color: $orange-color;
-        }
-
-        // Active 状态(主橙色边框)
-        &:active {
-          background-color: lighten($orange-color, 38%);
-          border-color: $orange-color;
-          color: $orange-color;
-        }
-
-        // Disabled 状态(灰色)
-        &.is-disabled {
-          background-color: #ffffff;
-          border-color: #e4e7ed;
-          color: #c0c4cc;
-        }
-      }
-    }
-  }
-  // 其他颜色类型(primary、success、orange 等)
-  @else {
-    .el-button--#{$type} {
-      background-color: $color;
-      border-color: $color;
-      color: #fff;
-
-      &:hover,
-      &:focus {
-        background-color: lighten($color, 10%);
-        border-color: lighten($color, 10%);
-        color: #FFF;
-      }
-
-      &:active {
-        background-color: darken($color, 10%);
-        border-color: darken($color, 10%);
-      }
-
-      &.is-disabled {
-        background-color: lighten($color, 40%);
-        border-color: lighten($color, 20%);
-        color: #fff;
-        opacity: 0.6;
-      }
-
-      // 镂空按钮样式(is-plain)
-      &.is-plain {
-        background-color: lighten($color, 38%);
-        border-color: lighten($color, 20%);
-        color: $color;
-
-        &:hover,
-        &:focus {
-          background-color: $color;
-          border-color: $color;
-          color: #fff;
-        }
-
-        &:active {
-          background-color: darken($color, 10%);
-          border-color: darken($color, 10%);
-          color: #fff;
-        }
-
-        &.is-disabled {
-          background-color: lighten($color, 45%);
-          border-color: lighten($color, 30%);
-          color: lighten($color, 20%);
-        }
-      }
-    }
-  }
-}
 
 </style>

+ 7 - 0
src/components/AutoComponents/MForm/index.vue

@@ -94,6 +94,13 @@
         <template v-if="item.type === 'datePicker'">
           <el-date-picker v-bind="item.options" v-on="item.handles" v-model="query[item.prop]"></el-date-picker>
         </template>
+        <template v-if="item.type === 'cascader'">
+          <el-cascader
+            v-model="query[item.prop]"
+            v-bind="item.options"
+            v-on="item.handles"
+          ></el-cascader>
+        </template>
         <template v-if="item.slotAfter">
           <slot :name="`${item.prop}.after`"></slot>
         </template>

+ 132 - 1
src/styles/orangeTheme.scss

@@ -27,4 +27,135 @@
   &:hover:not(.active) {
     color: $theme-color;
   }
-}
+}
+
+
+
+// 定义主题颜色(仿照 Element UI)
+$orange-color: $theme-color;
+$theme-colors: (
+  "default": (
+    "base": #ffffff,       // 默认背景色
+    "text": #606266,      // 默认文字颜色
+    "border": #dcdfe6     // 默认边框颜色
+  ),
+  "primary": #409EFF,
+  "success": #67C23A,
+  "warning": #E6A23C,
+  "danger": #F56C6C,
+  "orange": $theme-color  // 新增橙色
+);
+// 生成按钮样式
+@each $type, $color in $theme-colors {
+  @if $type == "default" {
+    .el-button--default {
+      background-color: map-get($color, "base");
+      border-color: map-get($color, "border");
+      color: map-get($color, "text");
+
+      // Hover 状态(浅橙色背景 + 浅橙色边框)
+      &:hover,
+      &:focus {
+        background-color: lighten($orange-color, 42%); // 非常浅的橙色
+        border-color: lighten($orange-color, 20%);    // 浅橙色边框
+        color: $orange-color;                         // 文字变橙色
+      }
+
+      // Active 状态(稍深的橙色边框)
+      &:active {
+        background-color: lighten($orange-color, 38%); // 更浅橙色背景
+        border-color: $orange-color;                   // 主橙色边框
+        color: $orange-color;
+      }
+
+      // Disabled 状态(保持灰色)
+      &.is-disabled {
+        background-color: #ffffff;
+        border-color: #e4e7ed;
+        color: #c0c4cc;
+      }
+
+      // Plain 模式(镂空按钮)
+      &.is-plain {
+        background-color: #ffffff;
+        border-color: #dcdfe6;
+        color: #606266;
+
+        // Hover 状态(浅橙色背景)
+        &:hover,
+        &:focus {
+          background-color: lighten($orange-color, 42%);
+          border-color: lighten($orange-color, 20%);
+          color: $orange-color;
+        }
+
+        // Active 状态(主橙色边框)
+        &:active {
+          background-color: lighten($orange-color, 38%);
+          border-color: $orange-color;
+          color: $orange-color;
+        }
+
+        // Disabled 状态(灰色)
+        &.is-disabled {
+          background-color: #ffffff;
+          border-color: #e4e7ed;
+          color: #c0c4cc;
+        }
+      }
+    }
+  }
+  // 其他颜色类型(primary、success、orange 等)
+  @else {
+    .el-button--#{$type} {
+      background-color: $color;
+      border-color: $color;
+      color: #fff;
+
+      &:hover,
+      &:focus {
+        background-color: lighten($color, 10%);
+        border-color: lighten($color, 10%);
+        color: #FFF;
+      }
+
+      &:active {
+        background-color: darken($color, 10%);
+        border-color: darken($color, 10%);
+      }
+
+      &.is-disabled {
+        background-color: lighten($color, 40%);
+        border-color: lighten($color, 20%);
+        color: #fff;
+        opacity: 0.6;
+      }
+
+      // 镂空按钮样式(is-plain)
+      &.is-plain {
+        background-color: lighten($color, 38%);
+        border-color: lighten($color, 20%);
+        color: $color;
+
+        &:hover,
+        &:focus {
+          background-color: $color;
+          border-color: $color;
+          color: #fff;
+        }
+
+        &:active {
+          background-color: darken($color, 10%);
+          border-color: darken($color, 10%);
+          color: #fff;
+        }
+
+        &.is-disabled {
+          background-color: lighten($color, 45%);
+          border-color: lighten($color, 30%);
+          color: lighten($color, 20%);
+        }
+      }
+    }
+  }
+}

+ 27 - 24
src/utils/antvG6.js

@@ -76,7 +76,7 @@ export class MindMapNode extends Circle {
       backgroundFill: '#ff650e',
       backgroundHeight: 20,
       backgroundWidth: 20,
-      lineHeight: 120,
+      // lineHeight: 20,
       cursor: 'pointer',
       fill: '#fff',
       fontSize: 20,
@@ -89,7 +89,7 @@ export class MindMapNode extends Circle {
 
     this.forwardEvent(btn, CommonEvent.CLICK, async (event) => {
       event.stopPropagation()
-      if (!this.status) {
+      if (!this.status || event.button !== 0) {
         return
       }
       const parent = this.context.graph.getNodeData(this.id)
@@ -109,27 +109,30 @@ export class MindMapNode extends Circle {
   }
 
   // 绘制标签形状
-  drawTapShape (attributes, container) {
-    const nodes = this.context.graph.getNodeData(this.id)
-    if (nodes.depth > 2) {
-      return
-    }
-    const btn = this.upsert('status', Badge, {
-      text: '[ 点击编辑 ]',
-      fontSize: 14,
-      textAlign: 'left',
-      transform: [['translate', 50, 30]],
-      padding: [10],
-      fill: '#ff650e',
-      cursor: 'pointer',
-      zIndex: 5
-    }, container)
-
-    this.forwardEvent(btn, CommonEvent.CLICK, async (event) => {
-      event.stopPropagation()
-      nodes.editTag && nodes.editTag(nodes)
-    })
-  }
+  // drawTapShape (attributes, container) {
+  //   const nodes = this.context.graph.getNodeData(this.id)
+  //   if (nodes.depth > 2) {
+  //     return
+  //   }
+  //   const btn = this.upsert('status', Badge, {
+  //     text: '[ 点击编辑 ]',
+  //     fontSize: 14,
+  //     textAlign: 'left',
+  //     transform: [['translate', 50, 30]],
+  //     padding: [10],
+  //     fill: '#ff650e',
+  //     cursor: 'pointer',
+  //     zIndex: 5
+  //   }, container)
+
+  //   this.forwardEvent(btn, CommonEvent.CLICK, async (event) => {
+  //     event.stopPropagation()
+  //     if (event.button !== 0) {
+  //       return
+  //     }
+  //     nodes.editTag && nodes.editTag(nodes)
+  //   })
+  // }
 
   async addChildrenData (parent) {
     this.status = false
@@ -161,7 +164,7 @@ export class MindMapNode extends Circle {
     super.render(attributes, container)
 
     this.drawCollapseShape(attributes, container)
-    this.drawTapShape(attributes, container)
+    // this.drawTapShape(attributes, container)
     // this.drawLabelShape(attributes, container)
   }
 }

+ 6 - 0
src/utils/dict.js

@@ -5,3 +5,9 @@ export const MENU_TYPE = {
   BUTTON: 2,
   COMPONENT: 3
 }
+
+export const ORGANIZATION_CATEGORY = [
+  { label: '支行', value: '支行' },
+  { label: '网点', value: '网点' },
+  { label: '部室', value: '部室' }
+]

+ 38 - 0
src/utils/elementUploadAndDownload.js

@@ -0,0 +1,38 @@
+
+import Vue from 'vue'
+
+import { downloadFile } from '@/utils'
+
+/**
+ * 上传
+ * @param {Promise} api 提交api
+ * @param {File} file 文件
+ * @param {Promise} init 实例
+ */
+export async function upload (api, file, init) {
+  const formData = new FormData()
+  formData.append('file', file)
+  try {
+    await api(formData)
+    Vue.prototype.$message.success('导入成功')
+    init && init()
+    return true
+  } catch (error) {
+    Vue.prototype.$message.error(error)
+  }
+}
+
+/**
+ * 下载
+ * @param {Promise} api
+ * @param {Object} params
+ */
+export async function download (api, params) {
+  try {
+    const { data, name } = await api(params)
+    downloadFile(data, name)
+    return true
+  } catch (error) {
+    this.$message.error(error)
+  }
+}

+ 15 - 0
src/utils/recursion.js

@@ -0,0 +1,15 @@
+// 递归相关
+
+// 根据机构编码查找机构节点
+export function findTreeNode (val, items, value = 'organizationNo', children = 'child') {
+  for (const item of items) {
+    if (item[value] === val) {
+      return item
+    }
+    if (item[children] && item[children].length) {
+      const _item = findTreeNode(val, item[children], value, children)
+      if (_item) return _item
+    }
+  }
+  return null
+}

+ 171 - 30
src/views/humanResources/organizationStructure/index.vue

@@ -1,12 +1,34 @@
 <template>
-  <div class="fullBox white pa-3">
+  <div class="fullBox white pa-3 relative" ref="boxRef">
     <div ref="graphRef" v-loading="loading" class="fullBox"></div>
+    <div class="btnBox">
+      <el-upload class="el-button pa-0" action="#" :show-file-list="false" :http-request="onImport">
+        <m-button type="orange" icon="el-icon-upload2" size="small" :loading="importLoading">导入</m-button>
+      </el-upload>
+      <m-button type="orange" icon="el-icon-download" size="small" :loading="exportLoading" @click="onExport">导出</m-button>
+      <m-button type="orange" icon="el-icon-download" size="small" :loading="downloadLoading" @click="onDownload">模板下载</m-button>
+      <m-button type="orange" icon="el-icon-plus" size="small" @click="onAdd">新增机构</m-button>
+    </div>
+    <div ref="contextMenuRefs" class="contextMenu">
+      <el-card shadow="always" :body-style="{padding: 0}">
+        <div
+          v-for="menu in menus"
+          :key="menu.prop"
+          class="contextMenuItem pa-3"
+          @click="onMenuClick(menu.prop)"
+        >
+          {{ menu.label }}
+        </div>
+      </el-card>
+    </div>
     <OrganizationEdit ref="organizationEditRefs" @success="onRefresh"></OrganizationEdit>
+    <OrganizationAdd ref="organizationAddRefs" @success="onRefresh"></OrganizationAdd>
   </div>
 </template>
 
 <script>
 import OrganizationEdit from './organizationEdit.vue'
+import OrganizationAdd from './organizationAdd.vue'
 import {
   CollapseExpandTree,
   MindMapNode
@@ -14,13 +36,24 @@ import {
 import {
   Graph,
   register,
+  NodeEvent,
+  CommonEvent,
+  CanvasEvent,
   ExtensionCategory
 } from '@antv/g6'
 import {
   getOrganizationAtlas,
-  getOrganizationAtlasEmployee
+  getOrganizationAtlasEmployee,
+  importOrganization,
+  exportOrganization,
+  downloadOrganization,
+  deleteOrganization
 } from '@/api/system'
 import { mapGetters } from 'vuex'
+import {
+  upload,
+  download
+} from '@/utils/elementUploadAndDownload'
 
 const NODE_TYPE = {
   type: 'MindMapNode',
@@ -30,9 +63,9 @@ const NODE_TYPE = {
       size: 15,
       label: true,
       labelFontSize: 14,
-      labelLineHeight: d.depth === 3 ? 20 : 48,
+      labelLineHeight: 20,
       labelPlacement: 'right',
-      labelPadding: d.depth === 3 ? [10] : [0, 15, 20, 15],
+      labelPadding: [10],
       labelText: d.name,
       labelOffsetX: d.depth === 3 ? 20 : 50,
       labelBackground: true,
@@ -47,11 +80,20 @@ const NODE_TYPE = {
 export default {
   name: 'organization-structure',
   components: {
-    OrganizationEdit
+    OrganizationEdit,
+    OrganizationAdd
   },
   data () {
     return {
+      menus: [
+        { label: '编辑', prop: 'edit' },
+        { label: '删除', prop: 'delete' }
+      ],
+      nodes: null,
       loading: false,
+      importLoading: false,
+      exportLoading: false,
+      downloadLoading: false,
       graph: null
     }
   },
@@ -61,10 +103,27 @@ export default {
   mounted () {
     register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree)
     register(ExtensionCategory.NODE, 'MindMapNode', MindMapNode)
-    // this.renderGraph(treeToGraphData(this.assignTree(this.organizationTree)[0]))
     this.onInit()
+    this.$nextTick(() => {
+      document.addEventListener('click', this.onClick)
+      this.$refs.boxRef.addEventListener('contextmenu', (e) => {
+        e.preventDefault()
+      })
+    })
+  },
+  beforeDestroy () {
+    if (this.graph) {
+      this.graph.off()
+    }
+    document.removeEventListener('click', this.onClick)
+    this.$refs.boxRef.removeEventListener('contextmenu', (e) => {
+      e.preventDefault()
+    })
   },
   methods: {
+    onClick () {
+      this.$refs.contextMenuRefs.style.display = 'none'
+    },
     async getChildren (organizationNo) {
       try {
         const { data } = await getOrganizationAtlasEmployee({ organizationNo })
@@ -105,6 +164,10 @@ export default {
       })
     },
     async renderGraph (data) {
+      if (this.graph) {
+        this.graph.updateData(data)
+        return
+      }
       const graph = new Graph({
         container: this.$refs.graphRef,
         width: this.$refs.graphRef.clientWidth,
@@ -114,7 +177,7 @@ export default {
         autoResize: false,
         enable: false,
         plugins: [
-          'minimap',
+          // 'minimap',
           // 'contextmenu',
           {
             className: 'toolbar',
@@ -124,9 +187,6 @@ export default {
               { id: 'zoom-in', value: 'zoom-in' },
               { id: 'zoom-out', value: 'zoom-out' },
               { id: 'auto-fit', value: 'auto-fit' }
-              // { id: 'export', value: 'export' },
-              // { id: 'request-fullscreen', value: 'request-fullscreen' },
-              // { id: 'exit-fullscreen', value: 'exit-fullscreen' }
             ],
             onClick: (value) => {
               const zoom = graph.getZoom()
@@ -149,12 +209,10 @@ export default {
         ],
         behaviors: [
           'drag-canvas',
-          // 'zoom-canvas',
           'scroll-canvas',
           'drag-element',
           'collapse-expand-tree'
         ],
-        // transforms: ['assign-color-by-branch'],
         animation: false,
         layout: {
           type: 'compact-box',
@@ -165,22 +223,12 @@ export default {
             return 32
           },
           getVGap: function getVGap (e) {
-            const { depth } = graph.getNodeData(e.id)
-            return depth === 3 ? 8 : 20
+            return 8
           },
           getHGap: function getHGap (e) {
             const { depth } = graph.getNodeData(e.id)
             return depth < 2 ? 100 : 140
           },
-          // preventOverlap: true, // 防止节点重叠
-          // iterations: 200, // 迭代次数
-          // animation: true, // 启用布局动画
-          // direction: 'LR',
-          // nodeSep: 50,
-          // rankSep: 500, // 层间距(px)
-          // ranker: 'tight-tree', // 布局的模式 'network-simplex' | 'tight-tree' | 'longest-path'
-          // rankdir: 'LR', // 布局的方向
-          // nodeSize: 50, // 节点大小(直径)
           radial: false
         },
         node: NODE_TYPE,
@@ -197,15 +245,39 @@ export default {
       })
       // graph.zoomTo(0.7)
       graph.render()
-      // await graph.collapseElement('74bf386cdba4b1ff60c8ac09b01c2e91', {
-      //   align: true
-      // })
       this.graph = graph
+      const contextMenu = this.$refs.contextMenuRefs
+      const { width, height } = this.$refs.boxRef.getBoundingClientRect()
+      const { left, top } = this.$refs.boxRef.getBoundingClientRect()
+      const content = {
+        x: 0,
+        y: 0,
+        width,
+        height
+      }
+      graph.on(NodeEvent.CONTEXT_MENU, (e) => {
+        const { target } = e // 获取被点击节点的 ID
+        this.nodes = graph.getNodeData(target.id)
+        e.preventDefault()
+        const { x, y } = e.client
+        content.x = x
+        content.y = y
+        contextMenu.style.left = x - left + 'px'
+        contextMenu.style.top = y - top + 'px'
+        contextMenu.style.display = 'block'
+      }).on(CommonEvent.WHEEL, () => {
+        contextMenu.style.display = 'none'
+      }).on(CanvasEvent.CONTEXT_MENU, (e) => {
+        contextMenu.style.display = 'none'
+      })
     },
-    editTag (nodes) {
-      this.$refs.organizationEditRefs.open(nodes)
+    async editTag (nodes) {
+      this.loading = true
+      await this.$refs.organizationEditRefs.open(nodes)
+      this.loading = false
     },
     async onInit () {
+      this.loading = true
       try {
         const { data } = await getOrganizationAtlas()
         const { nodes, edges } = data
@@ -213,28 +285,83 @@ export default {
           nodes: nodes.map(e => {
             return {
               ...e,
-              getChildren: this.getChildren,
-              editTag: this.editTag
+              getChildren: this.getChildren
             }
           }),
           edges
         })
       } catch (error) {
         this.$message.error(error)
+      } finally {
+        this.loading = false
       }
     },
     onRefresh () {
       this.graph.render()
+    },
+    async onImport (response) {
+      this.importLoading = true
+      await upload(importOrganization, response.file, this.onInit)
+      this.importLoading = false
+    },
+    async onExport () {
+      this.exportLoading = true
+      await download(exportOrganization)
+      this.exportLoading = false
+    },
+    async onDownload () {
+      this.downloadLoading = true
+      await download(downloadOrganization)
+      this.downloadLoading = false
+    },
+    onAdd () {
+      this.$refs.organizationAddRefs.open()
+    },
+    async onDelete (nodes) {
+      this.$confirm('是否删除该项', '提示', {
+        confirmButtonText: '确定',
+        cancelButtonText: '取消',
+        type: 'warning',
+        confirmButtonClass: 'el-button--orange'
+      }).then(async () => {
+        try {
+          await deleteOrganization({
+            organizationNo: nodes.id
+          })
+          this.$message.success('删除成功')
+        } catch (error) {
+          this.$message.error(error)
+        }
+      }).catch(_ => {})
+    },
+    onMenuClick (prop) {
+      if (prop === 'edit') {
+        this.editTag(this.nodes)
+      }
+      if (prop === 'delete') {
+        this.onDelete(this.nodes)
+      }
     }
   }
 }
 </script>
 
 <style lang="scss" scoped>
+.contextMenu {
+  display: none;
+  position: absolute;
+  width: 200px;
+}
 .fullBox {
   width: 100%;
   height: 100%;
   box-sizing: border-box;
+  position: relative;
+}
+.btnBox {
+  position: absolute;
+  left: 10px;
+  top: 10px;
 }
 
 ::v-deep .toolbar {
@@ -248,4 +375,18 @@ export default {
   }
 }
 
+.relative {
+  position: relative;
+}
+
+.contextMenuItem {
+  font-size: 14px;
+  color: #333;
+  cursor: pointer;
+  &:hover {
+    color: #FFF;
+    background: $theme-color;
+  }
+}
+
 </style>

+ 110 - 0
src/views/humanResources/organizationStructure/organizationAdd.vue

@@ -0,0 +1,110 @@
+<template>
+  <m-dialog title="新增机构" ref="dialog" @sure="onSure">
+    <m-form ref="form" :items="formItems" v-model="formValues" label-width="100px" label-position="right"></m-form>
+  </m-dialog>
+</template>
+
+<script>
+import { mapGetters } from 'vuex'
+import {
+  addOrganization
+} from '@/api/system'
+import {
+  ORGANIZATION_CATEGORY
+} from '@/utils/dict'
+import {
+  findTreeNode
+} from '@/utils/recursion'
+export default {
+  name: 'organizationAdd',
+  data () {
+    return {
+      formValues: {
+        parentOrganizationNo: null,
+        organizationName: null,
+        organizationCategory: null
+      }
+    }
+  },
+  computed: {
+    ...mapGetters(['organizationTree']),
+    formItems () {
+      return [
+        {
+          label: '上级机构',
+          prop: 'parentOrganizationNo',
+          type: 'cascader',
+          rules: { required: true, message: '请选择上级机构', trigger: 'blur' },
+          options: {
+            placeholder: '请选择机构',
+            options: this.organizationTree,
+            showAllLevels: false,
+            props: {
+              emitPath: false,
+              checkStrictly: true,
+              value: 'organizationNo',
+              label: 'organizationName',
+              children: 'child'
+            }
+          }
+        },
+        {
+          label: '机构名称',
+          prop: 'organizationName',
+          type: 'input',
+          rules: { required: true, message: '请输入机构名称', trigger: 'blur' },
+          options: {
+            placeholder: '请输入机构名称'
+          }
+        },
+        {
+          label: '机构类型',
+          prop: 'organizationCategory',
+          type: 'select',
+          rules: { required: true, message: '请输入机构类型', trigger: 'blur' },
+          options: {
+            placeholder: '请选择机构类型',
+            items: ORGANIZATION_CATEGORY
+          }
+        }
+      ]
+    }
+  },
+  methods: {
+    open () {
+      Object.keys(this.formValues).forEach(e => {
+        this.formValues[e] = null
+      })
+      this.$refs.dialog.open()
+    },
+    onSure () {
+      this.$refs.form.validate(async valid => {
+        if (!valid) {
+          return
+        }
+        const {
+          organizationName: parentOrganizationName,
+          organizationCategory: parentOrganizationCategory
+        } = findTreeNode(this.formValues.parentOrganizationNo, this.organizationTree)
+
+        try {
+          await addOrganization({
+            ...this.formValues,
+            parentOrganizationName,
+            parentOrganizationCategory
+          })
+          this.$message.success('新增成功')
+          this.$refs.dialog.close()
+          this.$emit('refresh')
+        } catch (error) {
+          this.$message.error(error)
+        }
+      })
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+
+</style>

+ 17 - 6
src/views/humanResources/organizationStructure/organizationEdit.vue

@@ -20,7 +20,8 @@
 
 <script>
 import {
-  saveLabel
+  saveLabel,
+  getOrganizationDetails
 } from '@/api/system'
 import organizationEditInfo from './organizationEditInfo.vue'
 import organizationEditLabel from './organizationEditLabel.vue'
@@ -43,11 +44,21 @@ export default {
     }
   },
   methods: {
-    open (nodes) {
-      this.activeName = 'Info'
-      this.item = nodes
-      this.key = Date.now()
-      this.drawer = true
+    async open (nodes) {
+      try {
+        const { data } = await getOrganizationDetails({
+          organizationNo: nodes.id
+        })
+        this.item = {
+          ...nodes,
+          origin: data
+        }
+        this.activeName = 'Info'
+        this.key = Date.now()
+        this.drawer = true
+      } catch (error) {
+        this.$message.error(error)
+      }
     },
     async onSure () {
       try {

+ 126 - 12
src/views/humanResources/organizationStructure/organizationEditInfo.vue

@@ -1,15 +1,34 @@
 <template>
-  <el-form label-width="100px" label-position="right" class="demo-table-expand">
-    <el-form-item label="机构名称">
-      <span>{{ item.name }}</span>
-    </el-form-item>
-    <el-form-item label="机构 ID">
-      <span>{{ item.id }}</span>
-    </el-form-item>
-  </el-form>
+  <div>
+    <div class="text-right pb-3">
+      <m-button v-if="isEdit" size="small" @click="onEdit">取消编辑</m-button>
+      <m-button v-if="isEdit" size="small" type="orange" @click="onSave">保存</m-button>
+      <m-button v-else size="small" type="orange" @click="onEdit">编辑</m-button>
+    </div>
+    <m-form ref="form" :items="formItems" v-model="formValues" label-width="100px" label-position="right">
+      <template v-for="item in formItems" v-slot:[item.prop]>
+        <div :key="item.prop">
+          <span v-if="item.prop === 'parentOrganizationNo'">
+            {{ formValues.parentOrganizationName }}
+          </span>
+          <span v-else>{{ formValues[item.prop] }}</span>
+        </div>
+      </template>
+    </m-form>
+  </div>
 </template>
 
 <script>
+import {
+  ORGANIZATION_CATEGORY
+} from '@/utils/dict'
+import {
+  addOrganization
+} from '@/api/system'
+import { mapGetters } from 'vuex'
+import {
+  findTreeNode
+} from '@/utils/recursion'
 export default {
   name: 'organizationEditInfo',
   props: {
@@ -18,14 +37,109 @@ export default {
       default: () => ({})
     }
   },
+  data () {
+    return {
+      isEdit: false,
+      formValues: {
+        organizationName: null,
+        organizationNo: null,
+        organizationCategory: null,
+        parentOrganizationNo: null,
+        parentOrganizationName: null,
+        parentOrganizationCategory: null
+      },
+      originValues: {} // 记录编辑前数据
+    }
+  },
+  computed: {
+    ...mapGetters(['organizationTree']),
+    formItems () {
+      return [
+        {
+          label: '上级机构',
+          prop: 'parentOrganizationNo',
+          type: this.isEdit ? 'cascader' : undefined,
+          options: {
+            placeholder: '请选择机构',
+            options: this.organizationTree,
+            showAllLevels: false,
+            props: {
+              emitPath: false,
+              checkStrictly: true,
+              value: 'organizationNo',
+              label: 'organizationName',
+              children: 'child'
+            }
+          }
+        },
+        {
+          label: '机构名称',
+          prop: 'organizationName',
+          type: this.isEdit ? 'input' : undefined
+        },
+        {
+          label: '机构类型',
+          prop: 'organizationCategory',
+          type: this.isEdit ? 'select' : undefined,
+          options: {
+            placeholder: '请选择机构类型',
+            items: ORGANIZATION_CATEGORY
+          }
+        },
+        {
+          label: '机构 ID',
+          prop: 'organizationNo'
+        }
+      ]
+    }
+  },
   created () {
-    console.log(this.item)
+    const data = this.item.origin?.organization
+    if (!data) {
+      return
+    }
+    Object.keys(this.formValues).forEach(key => {
+      this.formValues[key] = data[key]
+    })
+  },
+  methods: {
+    onEdit () {
+      this.isEdit = !this.isEdit
+      if (this.isEdit) {
+        this.originValues = { ...this.formValues }
+      } else {
+        Object.keys(this.formValues).forEach(key => {
+          this.formValues[key] = this.originValues[key]
+        })
+      }
+    },
+    async onSave () {
+      this.$refs.form.validate(async valid => {
+        if (!valid) {
+          return
+        }
+        const {
+          organizationName: parentOrganizationName,
+          organizationCategory: parentOrganizationCategory
+        } = findTreeNode(this.formValues.parentOrganizationNo, this.organizationTree)
+        try {
+          await addOrganization({
+            ...this.formValues,
+            parentOrganizationName,
+            parentOrganizationCategory
+          })
+          this.$message.success('新增成功')
+          this.$refs.dialog.close()
+          this.$emit('refresh')
+        } catch (error) {
+          this.$message.error(error)
+        }
+      })
+    }
   }
 }
 </script>
 
 <style lang="scss" scoped>
-.demo-table-expand label {
-  color: #99a9bf;
-}
+
 </style>

+ 2 - 2
src/views/humanResources/organizationStructure/organizationEditLabel.vue

@@ -13,8 +13,8 @@
 
     </el-form-item>
     <el-form-item>
-      <m-button @click="onAdd">新增标注</m-button>
-      <m-button type="orange" @click="onSave">保存</m-button>
+      <m-button size="small" @click="onAdd">新增标注</m-button>
+      <m-button size="small" type="orange" @click="onSave">保存</m-button>
     </el-form-item>
   </el-form>
 </template>