audit-value-picker.vue 16 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546
  1. <script>
  2. import { Transfer, Tabs, Icon, Button, Modal } from 'ant-design-vue'
  3. import uniqueArray from '@/common/services/unique-array'
  4. import sdDraggableModal from '@/common/components/sd-draggable-modal'
  5. import pickerMixinInternal from '@/common/components/sd-picker/picker-mixin-internal'
  6. import pickerListRight from '@/common/components/sd-picker/sd-picker-list-right.vue'
  7. import SdSelect from '@/common/components/sd-select.vue'
  8. import components from './_import-components/audit-value-picker-import'
  9. /**
  10. * input框,通过弹出对话框选择数据,支持列表、树等多种数据源
  11. * @displayName SdValuePicker 值选择器
  12. */
  13. export default {
  14. name: 'AuditValuePicker',
  15. components,
  16. mixins: [pickerMixinInternal],
  17. props: {
  18. selectclick: {
  19. type: Function,
  20. default: null,
  21. },
  22. /**
  23. * 根节点{code:'200000',name:'西安分公司'}
  24. */
  25. rootNode: {
  26. type: [Object, Array],
  27. default: undefined,
  28. },
  29. },
  30. data() {
  31. return {
  32. activeTab: 0, // 当前激活的tab页
  33. pickerSources: [], // 各数据源的属性,如标题、图标等
  34. modalVisible: false,
  35. targetKeys: [], // 已经选到右边的key,
  36. dataSourceInTargetKeys: [], // 已经选到右边的key,每一项都保存一份数据源信息
  37. selectedKeys: [],
  38. dataSource: {},
  39. showSearch: {},
  40. searchValue: {},
  41. }
  42. },
  43. computed: {
  44. singleMode() {
  45. return this.single || this.singleColumn
  46. },
  47. keysToInput() {
  48. // 不管什么模式,最终将要写入Input框的keys
  49. return this.singleMode ? this.selectedKeys : this.targetKeys
  50. },
  51. dblClickMode() {
  52. // 单列模式不能使用点击移动
  53. return this.dblClickTransfer || this.singleMode
  54. },
  55. allDataSourceOriginal() {
  56. return this.allDataSource.map((item) => item.originalValue)
  57. },
  58. allDataSource() {
  59. const currentTabDataSource = this.dataSource[this.activeTab] || []
  60. // 当前tab页的数据源+已选中到右边的数据源
  61. return this.dataSourceInTargetKeys
  62. .filter(
  63. // 过滤掉已经在数据源里面的
  64. (item) =>
  65. !currentTabDataSource.find(
  66. (item2) => item[this.optionValue] === item2[this.optionValue]
  67. )
  68. )
  69. .concat(
  70. currentTabDataSource.filter(
  71. // 把disabledKeys中的数据源去掉
  72. (item) => this.disabledKeys.indexOf(item[this.optionValue]) === -1
  73. )
  74. )
  75. .map((item) => {
  76. return {
  77. key: item[this.optionValue],
  78. title: item[this.optionLabel],
  79. disabled: item.disabled === true,
  80. originalValue: item,
  81. }
  82. })
  83. },
  84. dataSourceRight() {
  85. // 已选中部分的数据源
  86. return this.targetKeys.map(
  87. (key) => this.allDataSource.find((item) => item.key === key).originalValue
  88. )
  89. },
  90. },
  91. watch: {
  92. targetKeys(keys) {
  93. // 已经选到右边的key,每一项都保存一份数据源信息
  94. const dataSource = keys.map((key) =>
  95. this.allDataSourceOriginal.find((data) => key === data[this.optionValue])
  96. )
  97. this.dataSourceInTargetKeys = dataSource
  98. },
  99. },
  100. mounted() {
  101. // 计算有几个数据源
  102. this.pickerSources = this.$scopedSlots
  103. .default()
  104. .filter((vnode) => !!vnode.componentOptions)
  105. .map((vnode) => {
  106. return {
  107. title: vnode.componentOptions.propsData.title,
  108. icon: vnode.componentOptions.propsData.icon,
  109. }
  110. })
  111. },
  112. methods: {
  113. change(keys, direction, moveKeys) {
  114. if (direction === 'right') {
  115. this.targetKeys = this.targetKeys.concat(moveKeys)
  116. } else {
  117. this.targetKeys = keys
  118. }
  119. },
  120. selectChange(keys) {
  121. // 单选模式,始终只选中一个
  122. if (this.single) {
  123. if (keys.length > 0) this.selectedKeys = [keys[keys.length - 1]]
  124. } else if (this.singleColumn) {
  125. this.selectedKeys = keys
  126. }
  127. },
  128. closeModal() {
  129. this.modalVisible = false
  130. },
  131. inputClick() {
  132. // 只读模式不弹窗,直接返回
  133. if (this.readOnly) return
  134. if (this.selectclick !== null) {
  135. this.selectclick()
  136. } else {
  137. this.$refs.select.blur()
  138. // 初始化各种选择器相关的数据
  139. this.activeTab = 0
  140. this.dataSource = {}
  141. this.dataSourceInTargetKeys = this.value
  142. this.showSearch = {}
  143. this.targetKeys = this.selectedKeys = this.keyInValue()
  144. if (this.rootNode !== undefined) {
  145. if (this.rootNode.id !== '') {
  146. this.modalVisible = true
  147. } else {
  148. let lx = ''
  149. if (this.rootNode.name.indexOf('内控') > -1) {
  150. lx = '内控'
  151. }else if(this.rootNode.name.indexOf('风控') > -1){
  152. lx = '风控'
  153. }
  154. Modal.info({
  155. content: '请配置组织对应的' + lx + '机构',
  156. })
  157. }
  158. } else {
  159. this.modalVisible = true
  160. }
  161. }
  162. },
  163. handleOk() {
  164. const keys = this.keysToInput
  165. const value = keys.map((key) => {
  166. return this.allDataSource.find((item) => item.key === key).originalValue
  167. })
  168. this.$emit('change', value)
  169. this.closeModal()
  170. },
  171. updateDataSource(data) {
  172. // 先把数组去重处理
  173. this.dataSource[this.activeTab] = uniqueArray(data, (item) => item[this.optionValue])
  174. this.dataSource = { ...this.dataSource }
  175. },
  176. updateShowSearch(data) {
  177. this.showSearch[this.activeTab] = data
  178. this.showSearch = { ...this.showSearch }
  179. },
  180. itemDblClick(evt, key, direction) {
  181. if (direction !== 'right') {
  182. if (this.single) {
  183. this.handleOk()
  184. } else {
  185. if (this.singleColumn) return
  186. this.targetKeys.push(key)
  187. }
  188. } else {
  189. this.targetKeys.splice(this.targetKeys.indexOf(key), 1)
  190. }
  191. },
  192. removeTargetKeyItem(key) {
  193. this.targetKeys.splice(this.targetKeys.indexOf(key), 1)
  194. },
  195. keyInValue() {
  196. return this.value ? this.value.map((v) => v[this.optionValue]) : []
  197. },
  198. },
  199. /**
  200. * input框最后的图标
  201. * @slot suffixIcon
  202. */
  203. /**
  204. * 选择器的数据源,可以是多个
  205. * @slot default
  206. * @binding scope {object} 各种值和工具函数,无需解析直接传递给内部的数据源即可
  207. */
  208. /**
  209. * 右侧列表顶部
  210. * <p>8.0.7 新增</p>
  211. * @slot targetListHeader
  212. * @binding targetKeys {array} 右侧列表所有项目的 key 值
  213. * @binding targetValues {array} 右侧列表所有项目的完整 Object 值
  214. */
  215. render(h) {
  216. const renderTransfer = (index) => {
  217. return (
  218. <div
  219. class={{ [this.$style.wrapper]: true, [this.$style.singleClickMode]: !this.dblClickMode }}
  220. >
  221. {this.singleMode || index !== this.activeTab ? (
  222. undefined
  223. ) : (
  224. <div class={this.$style.targetListHeader}>
  225. {this.$scopedSlots.targetListHeader?.({
  226. targetKeys: this.targetKeys,
  227. targetValues: this.dataSourceInTargetKeys,
  228. })}
  229. <Button
  230. icon='sd-trash2'
  231. type='link'
  232. title='全部删除'
  233. on={{
  234. click: () => {
  235. this.targetKeys = []
  236. },
  237. }}
  238. ></Button>
  239. </div>
  240. )}
  241. {this.singleMode ? (
  242. undefined
  243. ) : (
  244. <div class={this.$style.sourceListHeader}>
  245. <Button
  246. icon='double-right'
  247. type='link'
  248. title='全部增加'
  249. on={{
  250. click: () =>
  251. this.allDataSource.forEach((item) => {
  252. if (!this.targetKeys.includes(item.key)) {
  253. this.targetKeys.push(item.key)
  254. }
  255. }),
  256. }}
  257. ></Button>
  258. </div>
  259. )}
  260. <Transfer
  261. class={[
  262. this.$style.transfer,
  263. {
  264. [this.$style.single]: this.singleMode,
  265. [this.$style.singleColumn]: this.singleColumn,
  266. [this.$style.showSearch]: !this.singleColumn && this.showSearch[this.activeTab],
  267. },
  268. ]}
  269. showSelectAll={!this.single}
  270. dataSource={this.allDataSource}
  271. targetKeys={this.singleMode ? undefined : this.targetKeys}
  272. selectedKeys={!this.singleMode ? undefined : this.selectedKeys}
  273. lazy={false}
  274. showSearch={true} // 用css隐藏搜索框,改prop的值会导致内部picker-source组件重新创建
  275. filterOption={() => true} // 不过滤任何数据,只是为了显示出搜索框来
  276. on={{
  277. change: this.change,
  278. search: (direction, value) => {
  279. if (direction === 'left') this.searchValue[index] = value
  280. },
  281. selectChange: this.selectChange,
  282. }}
  283. scopedSlots={{
  284. children: (scope) => {
  285. if (scope.props.direction !== 'left')
  286. return (
  287. <pickerListRight
  288. itemSelect={
  289. this.dblClickMode
  290. ? scope.on.itemSelect
  291. : (key, checked) => this.itemDblClick({}, key, 'right')
  292. }
  293. dataSource={this.dataSourceRight}
  294. selectedKeys={scope.props.selectedKeys}
  295. targetKeys={this.targetKeys}
  296. itemDblClick={this.dblClickTransfer ? this.itemDblClick : () => {}}
  297. render={this.render}
  298. optionValue={this.optionValue}
  299. optionLabel={this.optionLabel}
  300. on={{
  301. updateTargetKeys: (keys) => {
  302. this.targetKeys = keys
  303. },
  304. }}
  305. />
  306. )
  307. return [
  308. this.$scopedSlots
  309. .default({
  310. render: this.render,
  311. single: this.single,
  312. selectedKeys: scope.props.selectedKeys,
  313. targetKeys: this.singleMode ? [] : this.targetKeys,
  314. disabledKeys: this.disabledKeys,
  315. itemSelect: this.dblClickMode
  316. ? this.singleColumn
  317. ? (...args) => {
  318. // 单列多选模式时,对于selectedKeys的处理有些问题,加个timeout
  319. setTimeout(() => scope.on.itemSelect(...args))
  320. }
  321. : scope.on.itemSelect
  322. : (key, checked) => this.itemDblClick({}, key, 'left'),
  323. updateDataSource: this.updateDataSource,
  324. dataSource: index === this.activeTab ? this.allDataSourceOriginal : [],
  325. itemDblClick: this.dblClickTransfer ? this.itemDblClick : () => {},
  326. optionValue: this.optionValue,
  327. optionLabel: this.optionLabel,
  328. updateShowSearch: this.updateShowSearch,
  329. searchValue: this.searchValue[index],
  330. })
  331. .filter((vnode) => !!vnode.componentOptions)[index],
  332. ]
  333. },
  334. }}
  335. ></Transfer>
  336. </div>
  337. )
  338. }
  339. return (
  340. <span
  341. class={{
  342. [this.$style.allowClear]: this.value?.length > 1,
  343. [this.$style.selectWrapper]: true,
  344. }}
  345. >
  346. <SdSelect
  347. ref='select'
  348. value={this.value}
  349. optionLabel={this.optionLabel}
  350. optionValue={this.optionValue}
  351. options={[]}
  352. open={false}
  353. readOnly={this.readOnly}
  354. mode='multiple'
  355. showArrow={false}
  356. on={{
  357. ...this.$listeners,
  358. }}
  359. nativeOnClick={this.inputClick}
  360. />
  361. {this.$scopedSlots.suffixIcon && !this.readOnly ? (
  362. <span class={this.$style.icon}>{this.$scopedSlots.suffixIcon()}</span>
  363. ) : (
  364. undefined
  365. )}
  366. <sdDraggableModal
  367. title={this.title}
  368. visible={this.modalVisible}
  369. destroyOnClose={true}
  370. width={
  371. this.modalProps.width ||
  372. (this.single ? 600 : 800) + (this.pickerSources.length > 1 ? 100 : 0)
  373. }
  374. okButtonProps={
  375. this.required && this.keysToInput.length === 0 ? { props: { disabled: true } } : {}
  376. }
  377. attrs={this.modalProps}
  378. on={{
  379. cancel: () => {
  380. this.closeModal()
  381. this.$emit('cancel')
  382. },
  383. ok: this.handleOk,
  384. }}
  385. >
  386. {this.pickerSources.length > 1 ? (
  387. <Tabs
  388. class={this.$style.tab}
  389. tabPosition='left'
  390. on={{ change: (tab) => (this.activeTab = tab) }}
  391. >
  392. {this.pickerSources.map((source, index) => {
  393. return (
  394. <Tabs.TabPane key={index}>
  395. <div slot='tab' class={this.$style.tabTitle}>
  396. {source.icon ? <Icon type={source.icon} /> : undefined}
  397. {source.title ? <div>{source.title}</div> : undefined}
  398. </div>
  399. {renderTransfer(index)}
  400. </Tabs.TabPane>
  401. )
  402. })}
  403. </Tabs>
  404. ) : (
  405. renderTransfer(0)
  406. )}
  407. </sdDraggableModal>
  408. </span>
  409. )
  410. },
  411. }
  412. </script>
  413. <style module lang="scss">
  414. @use '@/common/design' as *;
  415. .wrapper {
  416. position: relative;
  417. }
  418. .select-wrapper {
  419. position: relative;
  420. display: inline-block;
  421. width: 100%;
  422. :global .ant-select {
  423. padding: 4px 0; // 保持上下边距相等,目前还没弄清为什么必须要加padding
  424. vertical-align: middle;
  425. .ant-select-selection {
  426. max-height: 86px; // 最多显示三行
  427. overflow-y: auto;
  428. /* stylelint-disable-next-line */
  429. .ant-select-selection__clear {
  430. top: 50%;
  431. }
  432. }
  433. }
  434. }
  435. .allow-clear:hover {
  436. .icon {
  437. opacity: 0;
  438. }
  439. }
  440. .icon {
  441. position: absolute;
  442. top: 50%;
  443. right: 11px;
  444. margin-top: -$font-size-base / 2;
  445. line-height: $font-size-base;
  446. color: $disabled-color;
  447. pointer-events: none;
  448. transition: opacity 0.15s ease;
  449. }
  450. .transfer {
  451. display: flex;
  452. :global .ant-transfer-list {
  453. flex: 1;
  454. height: 500px;
  455. .ant-transfer-list-body {
  456. display: flex;
  457. flex-direction: column;
  458. }
  459. .ant-transfer-list-body-search-wrapper {
  460. padding-bottom: 12px;
  461. }
  462. .ant-transfer-list-content-item {
  463. white-space: normal;
  464. user-select: none;
  465. }
  466. .ant-radio-wrapper {
  467. margin-right: 0;
  468. }
  469. }
  470. // 右侧待选区,不显示搜索框
  471. :global .ant-transfer-list:last-child {
  472. .ant-transfer-list-body-search-wrapper {
  473. display: none;
  474. }
  475. }
  476. &:not(.show-search) {
  477. :global .ant-transfer-list-body-search-wrapper {
  478. // display: none;
  479. }
  480. }
  481. }
  482. .tab {
  483. &:global(.ant-tabs .ant-tabs-left-bar .ant-tabs-tab) {
  484. padding: 8px 15px 8px 0;
  485. .tab-title {
  486. text-align: center;
  487. :global(.anticon) {
  488. margin-right: 0;
  489. font-size: $font-size-base * 1.5;
  490. }
  491. }
  492. }
  493. }
  494. .single {
  495. :global .ant-transfer-operation,
  496. :global .ant-transfer-list:last-child {
  497. display: none;
  498. }
  499. &:not(.single-column) {
  500. :global(.ant-transfer-list-header) {
  501. display: none;
  502. }
  503. :global .ant-transfer-list {
  504. padding-top: 0;
  505. }
  506. }
  507. }
  508. .list-text {
  509. display: inline-block;
  510. min-width: 200px;
  511. }
  512. .target-list-header {
  513. position: absolute;
  514. right: 0;
  515. z-index: 1;
  516. padding: 3px 0;
  517. }
  518. .source-list-header {
  519. position: absolute;
  520. left: 50%;
  521. z-index: 1;
  522. padding: 3px 0;
  523. margin-left: -52px;
  524. }
  525. .single-click-mode {
  526. :global(.ant-transfer-list-content .ant-checkbox-wrapper),
  527. :global(.ant-transfer-list-header .ant-checkbox-wrapper) {
  528. width: 0;
  529. margin-right: -8px;
  530. visibility: hidden;
  531. }
  532. }
  533. </style>