新增组织结构树页面

This commit is contained in:
zhigang.li 2018-12-20 19:07:47 +08:00
parent 5e345e02f5
commit e78f77fc76
16 changed files with 498 additions and 5 deletions

View File

@ -9,7 +9,9 @@ module.exports = {
'generator-star-spacing': 'off', 'generator-star-spacing': 'off',
// allow debugger during development // allow debugger during development
'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off', 'no-debugger': process.env.NODE_ENV === 'production' ? 'error' : 'off',
'vue/no-parsing-error': [2, { 'x-invalid-end-tag': false }], 'vue/no-parsing-error': [2, {
'x-invalid-end-tag': false
}],
'no-undef': 'off', 'no-undef': 'off',
'camelcase': 'off' 'camelcase': 'off'
}, },

8
package-lock.json generated
View File

@ -11973,6 +11973,14 @@
"resolved": "https://registry.npmjs.org/v-click-outside-x/-/v-click-outside-x-3.5.6.tgz", "resolved": "https://registry.npmjs.org/v-click-outside-x/-/v-click-outside-x-3.5.6.tgz",
"integrity": "sha512-41j5mdZ8NzGJM5Or0BTADKU1B0vBi5a29g9ieVNTorg3FTiep6zOXjZeLpMmSH/koon1JP6S5uvwI2OfSVCgSA==" "integrity": "sha512-41j5mdZ8NzGJM5Or0BTADKU1B0vBi5a29g9ieVNTorg3FTiep6zOXjZeLpMmSH/koon1JP6S5uvwI2OfSVCgSA=="
}, },
"v-org-tree": {
"version": "1.0.6",
"resolved": "http://registry.npm.taobao.org/v-org-tree/download/v-org-tree-1.0.6.tgz",
"integrity": "sha1-uh4EL9bExFb9/FmFtlvClFpAaWc=",
"requires": {
"clonedeep": "2.0.0"
}
},
"validate-npm-package-license": { "validate-npm-package-license": {
"version": "3.0.4", "version": "3.0.4",
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz", "resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",

View File

@ -25,6 +25,7 @@
"simplemde": "^1.11.2", "simplemde": "^1.11.2",
"sortablejs": "^1.7.0", "sortablejs": "^1.7.0",
"tree-table-vue": "^1.1.0", "tree-table-vue": "^1.1.0",
"v-org-tree": "^1.0.6",
"vue": "^2.5.10", "vue": "^2.5.10",
"vue-i18n": "^7.8.0", "vue-i18n": "^7.8.0",
"vue-router": "^3.0.1", "vue-router": "^3.0.1",

View File

@ -35,3 +35,10 @@ export const uploadImg = formData => {
data: formData data: formData
}) })
} }
export const getOrgData = () => {
return axios.request({
url: 'get_org_data',
method: 'get'
})
}

View File

@ -38,5 +38,6 @@ export default {
params: 'Params', params: 'Params',
cropper_page: 'Cropper', cropper_page: 'Cropper',
message_page: 'Message Center', message_page: 'Message Center',
tree_table_page: 'Tree Table' tree_table_page: 'Tree Table',
org_tree_page: 'Org Tree'
} }

View File

@ -38,5 +38,6 @@ export default {
params: '动态路由', params: '动态路由',
cropper_page: '图片裁剪', cropper_page: '图片裁剪',
message_page: '消息中心', message_page: '消息中心',
tree_table_page: '树状表格' tree_table_page: '树状表格',
org_tree_page: '组织结构树'
} }

View File

@ -38,5 +38,6 @@ export default {
params: '動態路由', params: '動態路由',
cropper_page: '圖片裁剪', cropper_page: '圖片裁剪',
message_page: '消息中心', message_page: '消息中心',
tree_table_page: '樹狀表格' tree_table_page: '樹狀表格',
org_tree_page: '組織結構樹'
} }

View File

@ -8,10 +8,13 @@ import iView from 'iview'
import i18n from '@/locale' import i18n from '@/locale'
import config from '@/config' import config from '@/config'
import importDirective from '@/directive' import importDirective from '@/directive'
import { directive as clickOutside } from 'v-click-outside-x'
import installPlugin from '@/plugin' import installPlugin from '@/plugin'
import './index.less' import './index.less'
import '@/assets/icons/iconfont.css' import '@/assets/icons/iconfont.css'
import TreeTable from 'tree-table-vue' import TreeTable from 'tree-table-vue'
import VOrgTree from 'v-org-tree'
import 'v-org-tree/dist/v-org-tree.css'
// 实际打包时应该不引入mock // 实际打包时应该不引入mock
/* eslint-disable */ /* eslint-disable */
if (process.env.NODE_ENV !== 'production') require('@/mock') if (process.env.NODE_ENV !== 'production') require('@/mock')
@ -20,6 +23,7 @@ Vue.use(iView, {
i18n: (key, value) => i18n.t(key, value) i18n: (key, value) => i18n.t(key, value)
}) })
Vue.use(TreeTable) Vue.use(TreeTable)
Vue.use(VOrgTree)
/** /**
* @description 注册admin内置插件 * @description 注册admin内置插件
*/ */
@ -36,6 +40,7 @@ Vue.prototype.$config = config
* 注册指令 * 注册指令
*/ */
importDirective(Vue) importDirective(Vue)
Vue.directive('clickOutside', clickOutside)
/* eslint-disable no-new */ /* eslint-disable no-new */
new Vue({ new Vue({

View File

@ -1,5 +1,6 @@
import Mock from 'mockjs' import Mock from 'mockjs'
import { doCustomTimes } from '@/libs/util' import { doCustomTimes } from '@/libs/util'
import orgData from './data/org-data'
const Random = Mock.Random const Random = Mock.Random
export const getTableData = req => { export const getTableData = req => {
@ -28,3 +29,7 @@ export const getDragList = req => {
export const uploadImage = req => { export const uploadImage = req => {
return Promise.resolve() return Promise.resolve()
} }
export const getOrgData = req => {
return orgData
}

45
src/mock/data/org-data.js Normal file
View File

@ -0,0 +1,45 @@
export default {
id: 0,
label: 'XXX科技有限公司',
children: [
{
id: 2,
label: '产品研发部',
children: [
{
id: 5,
label: '研发-前端'
}, {
id: 6,
label: '研发-后端'
}, {
id: 9,
label: 'UI设计'
}, {
id: 10,
label: '产品经理'
}
]
},
{
id: 3,
label: '销售部',
children: [
{
id: 7,
label: '销售一部'
}, {
id: 8,
label: '销售二部'
}
]
},
{
id: 4,
label: '财务部'
}, {
id: 11,
label: 'HR人事'
}
]
}

View File

@ -1,6 +1,6 @@
import Mock from 'mockjs' import Mock from 'mockjs'
import { login, logout, getUserInfo } from './login' import { login, logout, getUserInfo } from './login'
import { getTableData, getDragList, uploadImage } from './data' import { getTableData, getDragList, uploadImage, getOrgData } from './data'
import { getMessageInit, getContentByMsgId, hasRead, removeReaded, restoreTrash, messageCount } from './user' import { getMessageInit, getContentByMsgId, hasRead, removeReaded, restoreTrash, messageCount } from './user'
// 配置Ajax请求延时可用来测试网络延迟大时项目中一些效果 // 配置Ajax请求延时可用来测试网络延迟大时项目中一些效果
@ -22,5 +22,6 @@ Mock.mock(/\/message\/has_read/, hasRead)
Mock.mock(/\/message\/remove_readed/, removeReaded) Mock.mock(/\/message\/remove_readed/, removeReaded)
Mock.mock(/\/message\/restore/, restoreTrash) Mock.mock(/\/message\/restore/, restoreTrash)
Mock.mock(/\/message\/count/, messageCount) Mock.mock(/\/message\/count/, messageCount)
Mock.mock(/\/get_org_data/, getOrgData)
export default Mock export default Mock

View File

@ -125,6 +125,15 @@ export default [
}, },
component: () => import('@/view/components/drag-list/drag-list.vue') component: () => import('@/view/components/drag-list/drag-list.vue')
}, },
{
path: 'org_tree_page',
name: 'org_tree_page',
meta: {
icon: 'ios-people',
title: '组织结构树'
},
component: () => import('@/view/components/org-tree')
},
{ {
path: 'tree_table_page', path: 'tree_table_page',
name: 'tree_table_page', name: 'tree_table_page',

View File

@ -0,0 +1,174 @@
<template>
<div
ref="dragWrapper"
class="org-tree-drag-wrapper"
@mousedown="mousedownView"
@contextmenu="handleDocumentContextmenu"
>
<div class="org-tree-wrapper" :style="orgTreeStyle">
<v-org-tree
v-if="data"
:data="data"
:node-render="nodeRender"
:expand-all="true"
@on-node-click="handleNodeClick"
collapsable
></v-org-tree>
</div>
</div>
</template>
<script>
import { on, off } from '@/libs/tools'
const menuList = [
{
key: 'edit',
label: '编辑部门'
},
{
key: 'detail',
label: '查看部门'
},
{
key: 'new',
label: '新增子部门'
},
{
key: 'delete',
label: '删除部门'
}
]
export default {
name: 'OrgView',
props: {
zoomHandled: {
type: Number,
default: 1
},
data: Object
},
data () {
return {
currentContextMenuId: '',
orgTreeOffsetLeft: 0,
orgTreeOffsetTop: 0,
initPageX: 0,
initPageY: 0,
oldMarginLeft: 0,
oldMarginTop: 0,
canMove: false
}
},
computed: {
orgTreeStyle () {
return {
transform: `translate(-50%, -50%) scale(${this.zoomHandled}, ${
this.zoomHandled
})`,
marginLeft: `${this.orgTreeOffsetLeft}px`,
marginTop: `${this.orgTreeOffsetTop}px`
}
}
},
methods: {
handleNodeClick (e, data, expand) {
expand()
},
closeMenu () {
this.currentContextMenuId = ''
},
getBgColor (data) {
return this.currentContextMenuId === data.id
? data.isRoot
? '#0d7fe8'
: '#5d6c7b'
: ''
},
nodeRender (h, data) {
return (
<div
class={[
'custom-org-node',
data.children && data.children.length ? 'has-children-label' : ''
]}
on-mousedown={event => event.stopPropagation()}
on-contextmenu={this.contextmenu.bind(this, data)}
>
{data.label}
<dropdown
trigger="custom"
class="context-menu"
visible={this.currentContextMenuId === data.id}
nativeOn-click={this.handleDropdownClick}
on-on-click={this.handleContextMenuClick.bind(this, data)}
style={{
transform: `scale(${1 / this.zoomHandled}, ${1 /
this.zoomHandled})`
}}
v-click-outside={this.closeMenu}
>
<dropdown-menu slot="list">
{menuList.map(item => {
return (
<dropdown-item name={item.key}>{item.label}</dropdown-item>
)
})}
</dropdown-menu>
</dropdown>
</div>
)
},
contextmenu (data, $event) {
let event = $event || window.event
event.preventDefault
? event.preventDefault()
: (event.returnValue = false)
this.currentContextMenuId = data.id
},
setDepartmentData (data) {
data.isRoot = true
this.departmentData = data
},
mousedownView (event) {
this.canMove = true
this.initPageX = event.pageX
this.initPageY = event.pageY
this.oldMarginLeft = this.orgTreeOffsetLeft
this.oldMarginTop = this.orgTreeOffsetTop
on(document, 'mousemove', this.mousemoveView)
on(document, 'mouseup', this.mouseupView)
},
mousemoveView (event) {
if (!this.canMove) return
const { pageX, pageY } = event
this.orgTreeOffsetLeft = this.oldMarginLeft + pageX - this.initPageX
this.orgTreeOffsetTop = this.oldMarginTop + pageY - this.initPageY
},
mouseupView () {
this.canMove = false
off(document, 'mousemove', this.mousemoveView)
off(document, 'mouseup', this.mouseupView)
},
handleDropdownClick (event) {
event.stopPropagation()
},
handleDocumentContextmenu () {
this.canMove = false
},
handleContextMenuClick (data, key) {
this.$emit('on-menu-click', { data, key })
}
},
mounted () {
on(document, 'mousedown', this.mousedownView)
on(document, 'contextmenu', this.handleDocumentContextmenu)
},
beforeDestroy () {
off(document, 'mousedown', this.mousedownView)
off(document, 'contextmenu', this.handleDocumentContextmenu)
}
}
</script>
<style>
</style>

View File

@ -0,0 +1,81 @@
<template>
<div class="zoom-wrapper">
<button class="zoom-button" @click="scale('down')">
<Icon type="md-remove" :size="14" color="#fff"/>
</button>
<span class="zoom-number">{{ value }}%</span>
<button class="zoom-button" @click="scale('up')">
<Icon type="md-add" :size="14" color="#fff"/>
</button>
</div>
</template>
<script>
export default {
name: 'ZoomController',
props: {
value: {
type: Number,
default: 100
},
step: {
type: Number,
default: 20
},
min: {
type: Number,
default: 10
},
max: {
type: Number,
default: 200
}
},
methods: {
scale (type) {
const zoom = this.value + (type === 'down' ? -this.step : this.step)
if (
(zoom < this.min && type === 'down') ||
(zoom > this.max && type === 'up')
) {
return
}
this.$emit('input', zoom)
}
}
}
</script>
<style lang="less">
.trans(@duration) {
transition: ~"all @{duration} ease-in";
}
.zoom-wrapper {
.zoom-button {
width: 20px;
height: 20px;
line-height: 10px;
border-radius: 50%;
background: rgba(157, 162, 172, 1);
box-shadow: 0px 2px 8px 0px rgba(218, 220, 223, 0.7);
border: none;
cursor: pointer;
outline: none;
&:active {
box-shadow: 0px 0px 2px 2px rgba(218, 220, 223, 0.2) inset;
}
.trans(0.1s);
&:hover {
background: #1890ff;
.trans(0.1s);
}
}
.zoom-number {
color: #657180;
padding: 0 8px;
display: inline-block;
width: 46px;
text-align: center;
}
}
</style>

View File

@ -0,0 +1,75 @@
@wrapper: ~'department';
.percent-100 {
width: 100%;
height: 100%;
}
.@{wrapper}-outer {
.percent-100;
overflow: hidden;
.tip-box{
position: absolute;
left: 20px;
top: 20px;
z-index: 12;
}
.zoom-box {
position: absolute;
right: 30px;
bottom: 30px;
z-index: 2;
}
.view-box {
position: absolute;
top: 0;
bottom: 0;
left: 0;
right: 0;
z-index: 1;
cursor: move;
.org-tree-drag-wrapper {
width: 100%;
height: 100%;
}
.org-tree-wrapper {
display: inline-block;
position: absolute;
left: 50%;
top: 50%;
transition: transform 0.2s ease-out;
.org-tree-node-label {
box-shadow: 0px 2px 12px 0px rgba(143, 154, 165, 0.4);
border-radius: 4px;
.org-tree-node-label-inner {
padding: 0;
.custom-org-node {
padding: 14px 41px;
background: #738699;
user-select: none;
word-wrap: none;
white-space: nowrap;
border-radius: 4px;
color: #ffffff;
font-size: 14px;
font-weight: 500;
line-height: 20px;
transition: background 0.1s ease-in;
cursor: default;
&:hover {
background: #5d6c7b;
transition: background 0.1s ease-in;
}
&.has-children-label {
cursor: pointer;
}
.context-menu{
position: absolute;
right: -10px;
bottom: 20px;
z-index: 10;
}
}
}
}
}
}
}

View File

@ -0,0 +1,77 @@
<template>
<Card shadow style="height: 100%;width: 100%;overflow:hidden">
<div class="department-outer">
<div class="tip-box">
<b style="margin-right: 20px;">powered by <a target="blank" href="https://github.com/lison16">Lison</a></b>
<a target="blank" href="https://github.com/lison16/v-org-tree" style="margin-right: 10px;">v-org-tree文档</a>
</div>
<div class="zoom-box">
<zoom-controller v-model="zoom" :min="20" :max="200"></zoom-controller>
</div>
<div class="view-box">
<org-view
v-if="data"
:data="data"
:zoom-handled="zoomHandled"
@on-menu-click="handleMenuClick"
></org-view>
</div>
</div>
</Card>
</template>
<script>
import OrgView from './components/org-view.vue'
import ZoomController from './components/zoom-controller.vue'
import { getOrgData } from '@/api/data'
import './index.less'
const menuDic = {
edit: '编辑部门',
detail: '查看部门',
new: '新增子部门',
delete: '删除部门'
}
export default {
name: 'org_tree_page',
components: {
OrgView,
ZoomController
},
data () {
return {
data: null,
zoom: 100
}
},
computed: {
zoomHandled () {
return this.zoom / 100
}
},
methods: {
setDepartmentData (data) {
data.isRoot = true
return data
},
handleMenuClick ({ data, key }) {
console.log(data, key)
this.$Message.success({
duration: 5,
content: `点击了《${data.label}》节点的'${menuDic[key]}'菜单`
})
},
getDepartmentData () {
getOrgData().then(res => {
const { data } = res
this.data = data
})
}
},
mounted () {
this.getDepartmentData()
}
}
</script>
<style>
</style>