index.vue 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395
  1. <template>
  2. <div class="fullBox white pa-3 relative" ref="boxRef">
  3. <div ref="graphRef" v-loading="loading" class="fullBox"></div>
  4. <div class="btnBox pa-1">
  5. <el-upload class="el-button pa-0" action="#" :show-file-list="false" :http-request="onImport">
  6. <m-button type="orange" icon="el-icon-upload2" size="small" :loading="importLoading">导入</m-button>
  7. </el-upload>
  8. <m-button type="orange" icon="el-icon-download" size="small" :loading="exportLoading" @click="onExport">导出</m-button>
  9. <m-button type="orange" icon="el-icon-download" size="small" :loading="downloadLoading" @click="onDownload">模板下载</m-button>
  10. <m-button type="orange" icon="el-icon-plus" size="small" @click="onAdd">新增机构</m-button>
  11. </div>
  12. <div ref="contextMenuRefs" class="contextMenu">
  13. <el-card shadow="always" :body-style="{padding: 0}">
  14. <div
  15. v-for="menu in menus"
  16. :key="menu.prop"
  17. class="contextMenuItem pa-3"
  18. @click="onMenuClick(menu.prop)"
  19. >
  20. {{ menu.label }}
  21. </div>
  22. </el-card>
  23. </div>
  24. <OrganizationEdit ref="organizationEditRefs" @refresh="onRefresh"></OrganizationEdit>
  25. <OrganizationAdd ref="organizationAddRefs" @refresh="onRefresh"></OrganizationAdd>
  26. </div>
  27. </template>
  28. <script>
  29. import OrganizationEdit from './organizationEdit.vue'
  30. import OrganizationAdd from './organizationAdd.vue'
  31. import {
  32. CollapseExpandTree,
  33. MindMapNode
  34. } from '@/utils/antvG6'
  35. import {
  36. Graph,
  37. register,
  38. NodeEvent,
  39. CommonEvent,
  40. CanvasEvent,
  41. ExtensionCategory
  42. } from '@antv/g6'
  43. import {
  44. getOrganizationAtlas,
  45. getOrganizationAtlasEmployee,
  46. importOrganization,
  47. exportOrganization,
  48. downloadOrganization,
  49. deleteOrganization
  50. } from '@/api/system'
  51. import { mapGetters } from 'vuex'
  52. import {
  53. upload,
  54. download
  55. } from '@/utils/elementUploadAndDownload'
  56. const NODE_TYPE = {
  57. type: 'MindMapNode',
  58. style: function (d) {
  59. return {
  60. fill: '#ff650e',
  61. size: 15,
  62. label: true,
  63. labelFontSize: 14,
  64. labelLineHeight: 20,
  65. labelPlacement: 'right',
  66. labelPadding: [10],
  67. labelText: d.name,
  68. labelOffsetX: d.depth === 3 ? 20 : 50,
  69. labelBackground: true,
  70. labelBackgroundFill: '#EFF0F0',
  71. labelBackgroundRadius: 8,
  72. port: true,
  73. ports: [{ placement: 'right' }, { placement: 'left' }]
  74. }
  75. }
  76. }
  77. export default {
  78. name: 'organization-structure',
  79. components: {
  80. OrganizationEdit,
  81. OrganizationAdd
  82. },
  83. data () {
  84. return {
  85. menus: [
  86. { label: '编辑', prop: 'edit' },
  87. { label: '删除', prop: 'delete' }
  88. ],
  89. nodes: null,
  90. loading: false,
  91. importLoading: false,
  92. exportLoading: false,
  93. downloadLoading: false,
  94. graph: null
  95. }
  96. },
  97. computed: {
  98. ...mapGetters(['organizationTree'])
  99. },
  100. async mounted () {
  101. register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree)
  102. register(ExtensionCategory.NODE, 'MindMapNode', MindMapNode)
  103. const graphData = await this.onInit()
  104. this.$nextTick(() => {
  105. this.initGraph(graphData)
  106. document.addEventListener('click', this.onClick)
  107. this.$refs.boxRef.addEventListener('contextmenu', (e) => {
  108. e.preventDefault()
  109. })
  110. })
  111. },
  112. beforeDestroy () {
  113. if (this.graph) {
  114. this.graph.off()
  115. }
  116. document.removeEventListener('click', this.onClick)
  117. this.$refs.boxRef.removeEventListener('contextmenu', (e) => {
  118. e.preventDefault()
  119. })
  120. },
  121. methods: {
  122. onClick () {
  123. this.$refs.contextMenuRefs.style.display = 'none'
  124. },
  125. async initData () {
  126. await this.$store.dispatch('system/getOrganizationTree')
  127. const data = await this.onInit()
  128. return data
  129. },
  130. async onRefresh () {
  131. const data = await this.initData()
  132. this.renderGraph(data)
  133. },
  134. drawGraph (data) {
  135. this.graph.setData(data)
  136. this.graph.draw()
  137. },
  138. async renderGraph (data) {
  139. this.graph.setData(data)
  140. this.graph.render()
  141. },
  142. async onInit () {
  143. this.loading = true
  144. try {
  145. const { data } = await getOrganizationAtlas()
  146. const { nodes, edges } = data
  147. const graphData = {
  148. nodes: nodes.map(e => {
  149. return {
  150. ...e,
  151. getChildren: this.getChildren
  152. }
  153. }),
  154. edges
  155. }
  156. return graphData
  157. } catch (error) {
  158. this.$message.error(error)
  159. return { nodes: [], edges: [] }
  160. } finally {
  161. this.loading = false
  162. }
  163. },
  164. async initGraph (data) {
  165. this.graph = new Graph({
  166. container: this.$refs.graphRef,
  167. width: this.$refs.graphRef.clientWidth,
  168. height: this.$refs.graphRef.clientHeight,
  169. data,
  170. autoFit: 'center',
  171. autoResize: false,
  172. enable: false,
  173. plugins: [
  174. // 'minimap',
  175. // 'contextmenu',
  176. {
  177. className: 'toolbar',
  178. position: 'right-top',
  179. type: 'toolbar',
  180. getItems: () => [
  181. { id: 'zoom-in', value: 'zoom-in' },
  182. { id: 'zoom-out', value: 'zoom-out' },
  183. { id: 'auto-fit', value: 'auto-fit' }
  184. ],
  185. onClick: (value) => {
  186. const zoom = this.graph.getZoom()
  187. // 处理按钮点击事件
  188. if (value === 'zoom-in') {
  189. if (zoom > 2) {
  190. return
  191. }
  192. this.graph.zoomTo(zoom + 0.1)
  193. } else if (value === 'zoom-out') {
  194. if (zoom < 0.5) {
  195. return
  196. }
  197. this.graph.zoomTo(zoom - 0.1)
  198. } else if (value === 'auto-fit') {
  199. this.graph.fitView()
  200. }
  201. }
  202. }
  203. ],
  204. behaviors: [
  205. 'drag-canvas',
  206. 'scroll-canvas',
  207. 'drag-element',
  208. 'collapse-expand-tree'
  209. ],
  210. animation: false,
  211. layout: {
  212. type: 'compact-box',
  213. getHeight: function getHeight () {
  214. return 32
  215. },
  216. getWidth: function getWidth () {
  217. return 32
  218. },
  219. getVGap: function getVGap (e) {
  220. return 8
  221. },
  222. getHGap: function getHGap (e) {
  223. return 150
  224. },
  225. radial: false
  226. },
  227. node: NODE_TYPE,
  228. edge: {
  229. type: 'cubic-horizontal',
  230. style: function (d) {
  231. return {
  232. endArrow: true,
  233. lineWidth: 1,
  234. stroke: '#aaa'
  235. }
  236. }
  237. }
  238. })
  239. await this.graph.render()
  240. this.onGraphHandles(this.graph)
  241. data.nodes.forEach(e => {
  242. if (e.depth === 1) {
  243. this.graph.collapseElement(e.id)
  244. }
  245. })
  246. // 关闭之后重新排版计算
  247. this.graph.render()
  248. },
  249. onGraphHandles (graph) {
  250. const contextMenu = this.$refs.contextMenuRefs
  251. const { width, height } = this.$refs.boxRef.getBoundingClientRect()
  252. const { left, top } = this.$refs.boxRef.getBoundingClientRect()
  253. const content = {
  254. x: 0,
  255. y: 0,
  256. width,
  257. height
  258. }
  259. graph.on(NodeEvent.CONTEXT_MENU, (e) => {
  260. const { target } = e // 获取被点击节点的 ID
  261. this.nodes = graph.getNodeData(target.id)
  262. e.preventDefault()
  263. const { x, y } = e.client
  264. content.x = x
  265. content.y = y
  266. contextMenu.style.left = x - left + 'px'
  267. contextMenu.style.top = y - top + 'px'
  268. contextMenu.style.display = 'block'
  269. }).on(CommonEvent.WHEEL, () => {
  270. contextMenu.style.display = 'none'
  271. }).on(CanvasEvent.CONTEXT_MENU, (e) => {
  272. contextMenu.style.display = 'none'
  273. })
  274. },
  275. async onDelete (nodes) {
  276. this.$confirm('是否删除该项', '提示').then(async () => {
  277. try {
  278. await deleteOrganization({
  279. organizationNo: nodes.id
  280. })
  281. this.$message.success('删除成功')
  282. const data = await this.initData()
  283. this.drawGraph(data)
  284. } catch (error) {
  285. this.$message.error(error)
  286. }
  287. }).catch(_ => {})
  288. },
  289. async getChildren (organizationNo) {
  290. try {
  291. const { data } = await getOrganizationAtlasEmployee({ organizationNo })
  292. return {
  293. nodes: data.nodes.map(e => {
  294. const { labelOffsetX, ...obj } = NODE_TYPE
  295. return {
  296. id: e.id,
  297. name: e.text,
  298. hasChildren: false,
  299. depth: 3,
  300. ...obj
  301. }
  302. }),
  303. edges: data.lines.map(e => {
  304. return {
  305. source: e.from,
  306. target: e.to
  307. }
  308. })
  309. }
  310. } catch (error) {
  311. this.$message.error(error)
  312. }
  313. },
  314. onMenuClick (prop) {
  315. if (prop === 'edit') {
  316. this.editTag(this.nodes)
  317. }
  318. if (prop === 'delete') {
  319. this.onDelete(this.nodes)
  320. }
  321. },
  322. async onImport (response) {
  323. this.importLoading = true
  324. await upload(importOrganization, response.file, this.onInit)
  325. this.importLoading = false
  326. },
  327. async editTag (nodes) {
  328. this.loading = true
  329. await this.$refs.organizationEditRefs.open(nodes)
  330. this.loading = false
  331. },
  332. onAdd () {
  333. this.$refs.organizationAddRefs.open()
  334. },
  335. async onExport () {
  336. this.exportLoading = true
  337. await download(exportOrganization)
  338. this.exportLoading = false
  339. },
  340. async onDownload () {
  341. this.downloadLoading = true
  342. await download(downloadOrganization)
  343. this.downloadLoading = false
  344. }
  345. }
  346. }
  347. </script>
  348. <style lang="scss" scoped>
  349. .contextMenu {
  350. display: none;
  351. position: absolute;
  352. width: 200px;
  353. }
  354. .fullBox {
  355. width: 100%;
  356. height: 100%;
  357. box-sizing: border-box;
  358. position: relative;
  359. }
  360. .btnBox {
  361. position: absolute;
  362. left: 10px;
  363. top: 10px;
  364. }
  365. ::v-deep .toolbar {
  366. width: 50px;
  367. padding: 20px 0;
  368. border: 1px solid #ccc;
  369. .g6-toolbar-item {
  370. width: 100%;
  371. height: 20px;
  372. padding: 10px 0;
  373. }
  374. }
  375. .relative {
  376. position: relative;
  377. }
  378. .contextMenuItem {
  379. font-size: 14px;
  380. color: #333;
  381. cursor: pointer;
  382. &:hover {
  383. color: #FFF;
  384. background: $theme-color;
  385. }
  386. }
  387. </style>