di-person-picker.vue 9.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. <template>
  2. <view>
  3. <!-- 结果列表 -->
  4. <view>
  5. <u-tag v-for="item in checkedOrgs" :key="item.name" :text="item.name" class="tag" />
  6. <u-tag v-for="item in checkedUsers" :key="item.realname" :text="item.realname" class="tag" type="success" />
  7. <view style="margin-left: 10px; color: #2979FF; display: inline-block;" @click="visible = true">
  8. {{ checkedOrgs.length || checkedUsers.length ? '重选' : '添加' }}
  9. </view>
  10. </view>
  11. <!-- 弹出 -->
  12. <u-popup v-model="visible" mode="bottom" @open="openPopup">
  13. <view id="top" style="padding-top: 5px;">
  14. <!-- 操作按钮 -->
  15. <view class="division" style="display: flex;padding-bottom: 5px;">
  16. <u-button size="mini" plain @click="cancel">取消</u-button>
  17. <view style="position: absolute; right: 10px;">
  18. <u-button size="mini" plain type="primary" @click="submit">
  19. 确定({{ checkedOrgs.length + checkedUsers.length }})
  20. </u-button>
  21. </view>
  22. </view>
  23. <!-- 用户模糊搜索 -->
  24. <view v-if="type !== 'org'" style="padding: 0 10px;">
  25. <u-form-item left-icon="search">
  26. <u-input v-model="searchValue" clearable placeholder="搜索用户" @input="search" />
  27. </u-form-item>
  28. </view>
  29. <!-- 已选列表 -->
  30. <view class="division">
  31. <u-tag v-for="(item, index) in checkedOrgs" :key="item.name" :text="item.name" closeable class="tag"
  32. @close="remove(checkedOrgs,index)" />
  33. <u-tag v-for="(item, index) in checkedUsers" :key="item.realname" :text="item.realname" closeable
  34. class="tag" type="success" @close="remove(checkedUsers,index)" />
  35. </view>
  36. <!-- 面包屑 -->
  37. <view class="division" v-if="!searchValue">
  38. <text v-for="(item, index) in breadList" :key="index" @click="goOrg(item)">
  39. <text v-if="index > 0">{{' '}} / {{' '}}</text>
  40. <text :style="{color: activateBread(index) ? '' : '#3370ff'}">{{ item.name }}</text>
  41. </text>
  42. </view>
  43. </view>
  44. <scroll-view :style="{height: `${rollHeight}px`}" scroll-y @scrolltolower="handleOnreachBottom">
  45. <!-- 组织架构 -->
  46. <view v-if="!searchValue" v-for="item in orgList" :key="item[orgPrimaryKey]" style="display: flex;">
  47. <view @click="isCheckedOrg(item)" style="margin-left: 10px;">
  48. <radio v-if="type !== 'user'" :value="item[orgPrimaryKey]" :checked="item._checked || false" />
  49. <u-icon name="list" />{{' '}} {{ item.name }}
  50. </view>
  51. <!-- 下级按钮 -->
  52. <view style="position: absolute; right: 10px; padding-left: 15px;" :key="item.loading"
  53. @click="nextLayer(item)">
  54. <u-icon name="arrow-right" />
  55. </view>
  56. </view>
  57. <!-- 人员 -->
  58. <view v-for="item in list" :key="item[primaryKey]">
  59. <view @click="isCheckedUser(item)" style="margin-left: 10px;">
  60. <radio :value="item[primaryKey]" :checked="item._checked || false" />
  61. <u-icon name="account-fill" />{{' '}} {{ item.realname }}
  62. </view>
  63. </view>
  64. <u-empty v-if="isEmpty">
  65. <u-button slot="bottom" size="mini" plain @click="callback">返回上级</u-button>
  66. </u-empty>
  67. <u-loadmore v-else-if="type !== 'org'" :status="status" :loadText='loadText' margin-top="24"
  68. margin-bottom="24" />
  69. </scroll-view>
  70. </u-popup>
  71. </view>
  72. </template>
  73. <script>
  74. import list from '@/mixins/list'
  75. import {
  76. dibootApi
  77. } from '@/utils/dibootApi'
  78. export default {
  79. name: "di-person-picker",
  80. mixins: [list],
  81. props: {
  82. // 类型
  83. type: {
  84. // 用户选择、组织机构选择、人员架构混选
  85. type: 'user' | 'org' | 'default',
  86. default: 'default'
  87. },
  88. // 初始值 (type为 'user' | 'org' 时应,初始值应为对应的对象列表;type为 'default' 时,初始值应为 { orgs?: 组织对象列表, users?: 用户对象列表 } )
  89. initValue: {
  90. type: Array | Object,
  91. default: () => []
  92. }
  93. },
  94. data() {
  95. return {
  96. visible: false,
  97. confirmLoading: false,
  98. getListFromMixin: false,
  99. baseApi: '/workflow/userGroup',
  100. listApi: 'userList',
  101. queryParam: {
  102. orgId: '0'
  103. },
  104. oldQueryParam: null,
  105. searchValue: '',
  106. orgPrimaryKey: 'id',
  107. breadList: [{
  108. id: '0',
  109. name: '组织结构',
  110. children: []
  111. }],
  112. checkedOrgs: [],
  113. checkedUsers: [],
  114. oldCheckedOrgs: [],
  115. oldCkedUsers: [],
  116. topHeight: 50,
  117. windowHeight: 500
  118. };
  119. },
  120. created() {
  121. switch (this.type) {
  122. case 'user':
  123. this.checkedUsers = this.initValue
  124. break
  125. case 'org':
  126. this.checkedOrgs = this.initValue
  127. break
  128. default:
  129. this.checkedOrgs = this.initValue.orgs || []
  130. this.checkedUsers = this.initValue.users || []
  131. }
  132. // 获取组织数据
  133. dibootApi.get(`${this.baseApi}/orgTree`).then(res => {
  134. if (res.ok) {
  135. this.breadList[0].children = this.dataFilter(res.data, this.checkedOrgs)
  136. } else {
  137. this.showToast(res.msg)
  138. }
  139. })
  140. // 获取人员信息
  141. this.type !== 'org' && this.getList(true)
  142. // 获取屏幕可用高度
  143. let self = this;
  144. uni.getSystemInfo({
  145. success: function(res) {
  146. self.windowHeight = res.windowHeight
  147. }
  148. });
  149. },
  150. computed: {
  151. // 滚动区域高度
  152. rollHeight() {
  153. return this.windowHeight - this.topHeight
  154. },
  155. orgList() {
  156. return this.breadList[this.breadList.length - 1].children || []
  157. },
  158. isEmpty() {
  159. return this.orgList.length + ((this.list || []).length || 0) === 0
  160. }
  161. },
  162. watch: {
  163. visible(value) {
  164. value && this.calcHeight()
  165. }
  166. },
  167. methods: {
  168. async calcHeight() {
  169. await this.$nextTick();
  170. uni.createSelectorQuery().in(this).select('#top').boundingClientRect(data => {
  171. this.topHeight = data.height
  172. }).exec()
  173. },
  174. openPopup() {
  175. this.oldCheckedOrgs = this.$u.deepClone(this.checkedOrgs)
  176. this.oldCheckedUsers = this.$u.deepClone(this.checkedUsers)
  177. },
  178. activateBread(index) {
  179. return this.breadList.length - 1 === index
  180. },
  181. isCheckedOrg(item) {
  182. if (this.type === 'user') return
  183. item._checked = !item._checked
  184. if (item._checked)
  185. this.checkedOrgs.push(item)
  186. else
  187. this.checkedOrgs.splice(this.checkedOrgs.indexOf(item), 1);
  188. this.calcHeight()
  189. },
  190. isCheckedUser(item) {
  191. item._checked = !item._checked
  192. if (item._checked)
  193. this.checkedUsers.push(item)
  194. else
  195. this.checkedUsers.splice(this.checkedUsers.indexOf(item), 1);
  196. this.calcHeight()
  197. },
  198. async nextLayer(item) {
  199. item.loading = true
  200. this.$forceUpdate()
  201. this.queryParam.orgId = item[this.orgPrimaryKey]
  202. this.type !== 'org' && await this.getList(true)
  203. this.breadList.push(item)
  204. item.loading = false
  205. this.$forceUpdate()
  206. this.calcHeight()
  207. },
  208. goOrg(item) {
  209. this.breadList.splice(this.breadList.indexOf(item) + 1)
  210. if (this.type !== 'org') {
  211. this.list = []
  212. this.queryParam.orgId = item[this.orgPrimaryKey]
  213. this.getList(true)
  214. }
  215. this.calcHeight()
  216. },
  217. callback() {
  218. this.goOrg(this.breadList[this.breadList.length - 2])
  219. },
  220. remove(list, index) {
  221. list.splice(index, 1)[0]._checked = false
  222. this.calcHeight()
  223. },
  224. submit() {
  225. switch (this.type) {
  226. case 'user':
  227. const userIds = this.checkedUsers.map(e => e[this.primaryKey])
  228. this.$emit('input', userIds)
  229. this.$emit('change', userIds)
  230. break
  231. case 'org':
  232. const orgIds = this.checkedOrgs.map(e => e[this.orgPrimaryKey])
  233. this.$emit('input', orgIds)
  234. this.$emit('change', orgIds)
  235. break
  236. default:
  237. const data = {
  238. orgs: this.checkedOrgs,
  239. users: this.checkedUsers
  240. }
  241. this.$emit('input', data)
  242. this.$emit('change', data)
  243. }
  244. this.visible = false
  245. },
  246. cancel() {
  247. this.visible = false
  248. this.checkedOrgs = this.$u.deepClone(this.oldCheckedOrgs)
  249. this.checkedUsers = this.$u.deepClone(this.oldCheckedUsers)
  250. },
  251. // 数据过滤(用于回显选项)
  252. dataFilter(data, list) {
  253. if (list.length === 0) return data
  254. data.forEach(item => {
  255. const index = list.findIndex(e => e[this.primaryKey] === item[this.primaryKey])
  256. index >= 0 && (item._checked = true) && list.splice(index, 1, item);
  257. item.children != null && this.dataFilter(item.children, list)
  258. });
  259. this.$forceUpdate()
  260. return data
  261. },
  262. /**
  263. * 获取数据列表
  264. */
  265. async getList(replace = false) {
  266. try {
  267. this.status = 'loading'
  268. const res = await dibootApi.get(this.listApi ? `${this.baseApi}/${this.listApi}` :
  269. `${this.baseApi}/list`, this.queryParam)
  270. if (res.code === 0) {
  271. const data = this.dataFilter(res.data, this.checkedUsers)
  272. this.list = replace ? data : this.list.concat(data)
  273. this.page = res.page
  274. this.page.pageIndex++
  275. } else {
  276. this.showToast(res.msg)
  277. }
  278. } catch (e) {
  279. //TODO handle the exception
  280. } finally {
  281. this.triggered = false
  282. this.status = (this.list || []).length == this.page.totalCount ? 'nomore' : 'loadmore'
  283. }
  284. },
  285. // 搜索用户(附带防抖)
  286. search(value) {
  287. clearTimeout(this.searchDebounce);
  288. this.searchDebounce = setTimeout(() => {
  289. if (value) {
  290. if (this.oldQueryParam == null) this.oldQueryParam = this.$u.deepClone(this.queryParam)
  291. this.queryParam = {
  292. realname: value
  293. }
  294. this.getList(true)
  295. } else {
  296. this.queryParam = this.$u.deepClone(this.oldQueryParam)
  297. this.oldQueryParam = null
  298. this.handlePullDownRefresh()
  299. }
  300. this.$forceUpdate()
  301. }, 300)
  302. }
  303. }
  304. }
  305. </script>
  306. <style scoped>
  307. .division {
  308. padding-left: 10px;
  309. border-bottom-style: solid;
  310. border-width: 1px;
  311. border-color: #cccccc;
  312. }
  313. .tag+.tag {
  314. margin-left: 5px;
  315. }
  316. </style>