| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395 |
- <template>
- <div class="fullBox white pa-3 relative" ref="boxRef">
- <div ref="graphRef" v-loading="loading" class="fullBox"></div>
- <div class="btnBox pa-1">
- <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" @refresh="onRefresh"></OrganizationEdit>
- <OrganizationAdd ref="organizationAddRefs" @refresh="onRefresh"></OrganizationAdd>
- </div>
- </template>
- <script>
- import OrganizationEdit from './organizationEdit.vue'
- import OrganizationAdd from './organizationAdd.vue'
- import {
- CollapseExpandTree,
- MindMapNode
- } from '@/utils/antvG6'
- import {
- Graph,
- register,
- NodeEvent,
- CommonEvent,
- CanvasEvent,
- ExtensionCategory
- } from '@antv/g6'
- import {
- getOrganizationAtlas,
- getOrganizationAtlasEmployee,
- importOrganization,
- exportOrganization,
- downloadOrganization,
- deleteOrganization
- } from '@/api/system'
- import { mapGetters } from 'vuex'
- import {
- upload,
- download
- } from '@/utils/elementUploadAndDownload'
- const NODE_TYPE = {
- type: 'MindMapNode',
- style: function (d) {
- return {
- fill: '#ff650e',
- size: 15,
- label: true,
- labelFontSize: 14,
- labelLineHeight: 20,
- labelPlacement: 'right',
- labelPadding: [10],
- labelText: d.name,
- labelOffsetX: d.depth === 3 ? 20 : 50,
- labelBackground: true,
- labelBackgroundFill: '#EFF0F0',
- labelBackgroundRadius: 8,
- port: true,
- ports: [{ placement: 'right' }, { placement: 'left' }]
- }
- }
- }
- export default {
- name: 'organization-structure',
- components: {
- OrganizationEdit,
- OrganizationAdd
- },
- data () {
- return {
- menus: [
- { label: '编辑', prop: 'edit' },
- { label: '删除', prop: 'delete' }
- ],
- nodes: null,
- loading: false,
- importLoading: false,
- exportLoading: false,
- downloadLoading: false,
- graph: null
- }
- },
- computed: {
- ...mapGetters(['organizationTree'])
- },
- async mounted () {
- register(ExtensionCategory.BEHAVIOR, 'collapse-expand-tree', CollapseExpandTree)
- register(ExtensionCategory.NODE, 'MindMapNode', MindMapNode)
- const graphData = await this.onInit()
- this.$nextTick(() => {
- this.initGraph(graphData)
- 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 initData () {
- await this.$store.dispatch('system/getOrganizationTree')
- const data = await this.onInit()
- return data
- },
- async onRefresh () {
- const data = await this.initData()
- this.renderGraph(data)
- },
- drawGraph (data) {
- this.graph.setData(data)
- this.graph.draw()
- },
- async renderGraph (data) {
- this.graph.setData(data)
- this.graph.render()
- },
- async onInit () {
- this.loading = true
- try {
- const { data } = await getOrganizationAtlas()
- const { nodes, edges } = data
- const graphData = {
- nodes: nodes.map(e => {
- return {
- ...e,
- getChildren: this.getChildren
- }
- }),
- edges
- }
- return graphData
- } catch (error) {
- this.$message.error(error)
- return { nodes: [], edges: [] }
- } finally {
- this.loading = false
- }
- },
- async initGraph (data) {
- this.graph = new Graph({
- container: this.$refs.graphRef,
- width: this.$refs.graphRef.clientWidth,
- height: this.$refs.graphRef.clientHeight,
- data,
- autoFit: 'center',
- autoResize: false,
- enable: false,
- plugins: [
- // 'minimap',
- // 'contextmenu',
- {
- className: 'toolbar',
- position: 'right-top',
- type: 'toolbar',
- getItems: () => [
- { id: 'zoom-in', value: 'zoom-in' },
- { id: 'zoom-out', value: 'zoom-out' },
- { id: 'auto-fit', value: 'auto-fit' }
- ],
- onClick: (value) => {
- const zoom = this.graph.getZoom()
- // 处理按钮点击事件
- if (value === 'zoom-in') {
- if (zoom > 2) {
- return
- }
- this.graph.zoomTo(zoom + 0.1)
- } else if (value === 'zoom-out') {
- if (zoom < 0.5) {
- return
- }
- this.graph.zoomTo(zoom - 0.1)
- } else if (value === 'auto-fit') {
- this.graph.fitView()
- }
- }
- }
- ],
- behaviors: [
- 'drag-canvas',
- 'scroll-canvas',
- 'drag-element',
- 'collapse-expand-tree'
- ],
- animation: false,
- layout: {
- type: 'compact-box',
- getHeight: function getHeight () {
- return 32
- },
- getWidth: function getWidth () {
- return 32
- },
- getVGap: function getVGap (e) {
- return 8
- },
- getHGap: function getHGap (e) {
- return 150
- },
- radial: false
- },
- node: NODE_TYPE,
- edge: {
- type: 'cubic-horizontal',
- style: function (d) {
- return {
- endArrow: true,
- lineWidth: 1,
- stroke: '#aaa'
- }
- }
- }
- })
- await this.graph.render()
- this.onGraphHandles(this.graph)
- data.nodes.forEach(e => {
- if (e.depth === 1) {
- this.graph.collapseElement(e.id)
- }
- })
- // 关闭之后重新排版计算
- this.graph.render()
- },
- onGraphHandles (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'
- })
- },
- async onDelete (nodes) {
- this.$confirm('是否删除该项', '提示').then(async () => {
- try {
- await deleteOrganization({
- organizationNo: nodes.id
- })
- this.$message.success('删除成功')
- const data = await this.initData()
- this.drawGraph(data)
- } catch (error) {
- this.$message.error(error)
- }
- }).catch(_ => {})
- },
- async getChildren (organizationNo) {
- try {
- const { data } = await getOrganizationAtlasEmployee({ organizationNo })
- return {
- nodes: data.nodes.map(e => {
- const { labelOffsetX, ...obj } = NODE_TYPE
- return {
- id: e.id,
- name: e.text,
- hasChildren: false,
- depth: 3,
- ...obj
- }
- }),
- edges: data.lines.map(e => {
- return {
- source: e.from,
- target: e.to
- }
- })
- }
- } catch (error) {
- this.$message.error(error)
- }
- },
- onMenuClick (prop) {
- if (prop === 'edit') {
- this.editTag(this.nodes)
- }
- if (prop === 'delete') {
- this.onDelete(this.nodes)
- }
- },
- async onImport (response) {
- this.importLoading = true
- await upload(importOrganization, response.file, this.onInit)
- this.importLoading = false
- },
- async editTag (nodes) {
- this.loading = true
- await this.$refs.organizationEditRefs.open(nodes)
- this.loading = false
- },
- onAdd () {
- this.$refs.organizationAddRefs.open()
- },
- async onExport () {
- this.exportLoading = true
- await download(exportOrganization)
- this.exportLoading = false
- },
- async onDownload () {
- this.downloadLoading = true
- await download(downloadOrganization)
- this.downloadLoading = false
- }
- }
- }
- </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 {
- width: 50px;
- padding: 20px 0;
- border: 1px solid #ccc;
- .g6-toolbar-item {
- width: 100%;
- height: 20px;
- padding: 10px 0;
- }
- }
- .relative {
- position: relative;
- }
- .contextMenuItem {
- font-size: 14px;
- color: #333;
- cursor: pointer;
- &:hover {
- color: #FFF;
- background: $theme-color;
- }
- }
- </style>
|