law-case-value-picker.vue 16 KB

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