ic-value-picker.vue 17 KB

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