1
0

transfer.stories.jsx 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787
  1. import React, { useState, useRef } from 'react';
  2. import { Transfer, Button } from '../../index';
  3. import Table from '../../table';
  4. import Avatar from '../../avatar';
  5. import Checkbox from '../../checkbox';
  6. import Icon from '../../icons';
  7. import Tree from '../../tree';
  8. import Input from '../../input';
  9. import { omit, values } from 'lodash';
  10. import './transfer.scss';
  11. import { SortableContainer, SortableElement, sortableHandle } from 'react-sortable-hoc';
  12. import { IconClose, IconSearch, IconHandle } from '@douyinfe/semi-icons';
  13. export default {
  14. title: 'Transfer'
  15. }
  16. const commonProps = {
  17. onSelect: (...args) => {
  18. console.log('onSelect');
  19. console.log(...args);
  20. },
  21. onChange: (...args) => {
  22. console.log('onChange');
  23. console.log(...args);
  24. },
  25. onDeselect: (...args) => {
  26. console.log('onDeselect');
  27. console.log(...args);
  28. },
  29. onSearch: (...args) => {
  30. console.log('onSearch');
  31. console.log(...args);
  32. },
  33. };
  34. const data = Array.from({ length: 100 }, (v, i) => {
  35. return {
  36. label: `选项名称${i}`,
  37. value: i,
  38. disabled: false,
  39. key: i,
  40. };
  41. });
  42. const treeData = [
  43. {
  44. label: 'Asia',
  45. value: 'Asia',
  46. key: '0',
  47. children: [
  48. {
  49. label: 'China',
  50. value: 'China',
  51. key: '0-0',
  52. children: [
  53. {
  54. label: 'Beijing',
  55. value: 'Beijing',
  56. key: '0-0-0',
  57. disabled: true,
  58. },
  59. {
  60. label: 'ShanghaionChangeonChangeonChange',
  61. value: 'Shanghai',
  62. key: '0-0-1',
  63. },
  64. {
  65. label: 'Chengdu',
  66. value: 'Chengdu',
  67. key: '0-0-2',
  68. },
  69. {
  70. label: 'Chongqing',
  71. value: 'Chongqing',
  72. key: '0-0-3',
  73. },
  74. ],
  75. },
  76. {
  77. label: 'Japan',
  78. value: 'Japan',
  79. key: '0-1',
  80. children: [
  81. {
  82. label: 'Osaka',
  83. value: 'Osaka',
  84. key: '0-1-0',
  85. },
  86. ],
  87. },
  88. ],
  89. },
  90. {
  91. label: 'North America',
  92. value: 'North America',
  93. key: '1',
  94. children: [
  95. {
  96. label: 'United States',
  97. value: 'United States',
  98. key: '1-0',
  99. },
  100. {
  101. label: 'Canada',
  102. value: 'Canada',
  103. key: '1-1',
  104. },
  105. ],
  106. },
  107. ];
  108. const dataWithGroup = [
  109. {
  110. title: '类别A',
  111. children: [
  112. { label: '选项名称1', value: 1, disabled: false, key: 1 },
  113. { label: '选项名称2', value: 2, disabled: false, key: 2 },
  114. { label: '选项名称3', value: 3, disabled: false, key: 3 },
  115. ],
  116. },
  117. {
  118. title: '类别B',
  119. children: [
  120. { label: '选项名称1', value: 4, disabled: true, key: 4 },
  121. { label: '选项名称2', value: 5, disabled: false, key: 5 },
  122. { label: '选项名称3', value: 6, disabled: false, key: 6 },
  123. ],
  124. },
  125. {
  126. title: '类别C',
  127. children: [
  128. { label: '选项名称1', value: 7, disabled: false, key: 7 },
  129. { label: '选项名称2', value: 8, disabled: false, key: 8 },
  130. { label: '选项名称3', value: 9, disabled: false, key: 9 },
  131. { label: '选项名称3', value: 10, disabled: false, key: 10 },
  132. { label: '选项名称3', value: 11, disabled: false, key: 11 },
  133. { label: '选项名称3', value: 12, disabled: false, key: 12 },
  134. { label: '选项名称3', value: 13, disabled: false, key: 13 },
  135. ],
  136. },
  137. ];
  138. const DefaultTransfer = () => {
  139. const [dataSource, setDataSource] = useState([]);
  140. const [treeDataSource, setTreeDataSource] = useState([]);
  141. return (
  142. <div style={{ margin: 10, padding: 10, width: 600 }}>
  143. <Button onClick={() => setDataSource(data)}>更改列表数据源</Button>
  144. <Transfer {...commonProps} dataSource={dataSource} defaultValue={[]} />
  145. <Button onClick={() => setTreeDataSource(treeData)}>更改树数据源</Button>
  146. <Transfer type="treeList" dataSource={treeDataSource}></Transfer>
  147. </div>
  148. );
  149. };
  150. export const _Transfer = DefaultTransfer;
  151. export const TransferDraggable = () => (
  152. <div style={{ margin: 10, padding: 10, width: 600 }}>
  153. <Transfer {...commonProps} dataSource={data} defaultValue={[2, 4]} draggable />
  154. </div>
  155. );
  156. TransferDraggable.story = {
  157. name: 'Transfer draggable',
  158. };
  159. export const TransferDraggableAndDisabled = () => {
  160. const data = Array.from({ length: 30 }, (v, i) => {
  161. return {
  162. label: `选项名称 ${i}`,
  163. value: i,
  164. key: i,
  165. disabled: true,
  166. };
  167. });
  168. return (
  169. <>
  170. <div>Transfer设置draggable, 并且左侧面板中的选项disabled </div>
  171. <div>符合预期的行为: 右侧面板hover不会出现删除按钮,因此不可以点击删除,但是可以拖拽 </div>
  172. <Transfer
  173. style={{ width: 568, height: 416 }}
  174. dataSource={data}
  175. defaultValue={[2, 4]}
  176. draggable
  177. onChange={(values, items) => console.log(values, items)}
  178. />
  179. </>
  180. );
  181. };
  182. TransferDraggableAndDisabled.story = {
  183. name: 'transfer draggable and disabled',
  184. }
  185. const ControlledTransfer = () => {
  186. const [value, setValue] = useState([2, 3]);
  187. const handleChange = value => {
  188. setValue(value);
  189. };
  190. return (
  191. <div style={{ margin: 10, padding: 10, width: 600 }}>
  192. <Transfer {...commonProps} dataSource={data} value={value} onChange={handleChange} />
  193. </div>
  194. );
  195. };
  196. export const ControlledTransferDemo = () => <ControlledTransfer />;
  197. ControlledTransferDemo.story = {
  198. name: '受控Transfer',
  199. };
  200. export const Loading = () => <Transfer loading />;
  201. Loading.story = {
  202. name: 'loading',
  203. };
  204. export const GroupTransfer = () => (
  205. <div style={{ margin: 10, padding: 10, width: 600 }}>
  206. <Transfer {...commonProps} dataSource={dataWithGroup} type="groupList" />
  207. <Transfer {...commonProps} dataSource={dataWithGroup} defaultValue={[2, 4]} type="groupList" />
  208. </div>
  209. );
  210. GroupTransfer.story = {
  211. name: '分组Transfer',
  212. };
  213. const customFilter = (sugInput, item) => {
  214. return item.value.includes(sugInput) || item.label.includes(sugInput);
  215. };
  216. export const CustomFilterRenderSourceItemRenderSelectedItem = () => {
  217. const data = [
  218. {
  219. label: '夏可漫',
  220. value: '[email protected]',
  221. abbr: '夏',
  222. color: 'amber',
  223. area: 'US',
  224. key: 1,
  225. },
  226. {
  227. label: '申悦',
  228. value: '[email protected]',
  229. abbr: '申',
  230. color: 'indigo',
  231. area: 'UK',
  232. key: 2,
  233. },
  234. {
  235. label: '文嘉茂',
  236. value: '[email protected]',
  237. abbr: '文',
  238. color: 'cyan',
  239. area: 'HK',
  240. key: 3,
  241. },
  242. {
  243. label: '曲晨一',
  244. value: '[email protected]',
  245. abbr: '一',
  246. color: 'blue',
  247. area: 'India',
  248. key: 4,
  249. },
  250. {
  251. label: '曲晨二',
  252. value: '[email protected]',
  253. abbr: '二',
  254. color: 'blue',
  255. area: 'India',
  256. key: 5,
  257. },
  258. {
  259. label: '曲晨三',
  260. value: '[email protected]',
  261. abbr: '三',
  262. color: 'blue',
  263. area: 'India',
  264. key: 6,
  265. },
  266. ];
  267. const renderSourceItem = item => {
  268. return (
  269. <div className="components-transfer-demo-source-item">
  270. <Checkbox
  271. onChange={() => {
  272. item.onChange();
  273. }}
  274. key={item.label}
  275. checked={item.checked}
  276. style={{ paddingLeft: 12, height: 52 }}
  277. >
  278. <Avatar color={item.color} size="small">
  279. {item.abbr}
  280. </Avatar>
  281. <div className="info">
  282. <div className="name">{item.label}</div>
  283. <div className="email">{item.value}</div>
  284. </div>
  285. </Checkbox>
  286. </div>
  287. );
  288. };
  289. const renderSelectedItem = item => {
  290. return (
  291. <div className="components-transfer-demo-selected-item" key={item.label}>
  292. <Avatar color={item.color} size="small">
  293. {item.abbr}
  294. </Avatar>
  295. <div className="info">
  296. <div className="name">{item.label}</div>
  297. <div className="email">{item.value}</div>
  298. </div>
  299. <IconClose onClick={item.onRemove} />
  300. </div>
  301. );
  302. };
  303. return (
  304. <div style={{ margin: 10, padding: 10, width: 600 }}>
  305. <Transfer
  306. {...commonProps}
  307. dataSource={data}
  308. filter={customFilter}
  309. defaultValue={['[email protected]']}
  310. renderSelectedItem={renderSelectedItem}
  311. renderSourceItem={renderSourceItem}
  312. inputProps={{ placeholder: '可通过邮箱或者姓名搜索' }}
  313. />
  314. </div>
  315. );
  316. };
  317. CustomFilterRenderSourceItemRenderSelectedItem.story = {
  318. name: 'custom filter, renderSourceItem, renderSelectedItem',
  319. };
  320. const TreeTransferDemo = () => {
  321. const treeData = [
  322. {
  323. label: 'Asia',
  324. value: 'Asia',
  325. key: '0',
  326. children: [
  327. {
  328. label: 'China',
  329. value: 'China',
  330. key: '0-0',
  331. children: [
  332. {
  333. label: 'Beijing',
  334. value: 'Beijing',
  335. key: '0-0-0',
  336. disabled: true,
  337. },
  338. {
  339. label: 'ShanghaionChangeonChangeonChange',
  340. value: 'Shanghai',
  341. key: '0-0-1',
  342. },
  343. {
  344. label: 'Chengdu',
  345. value: 'Chengdu',
  346. key: '0-0-2',
  347. },
  348. {
  349. label: 'Chongqing',
  350. value: 'Chongqing',
  351. key: '0-0-3',
  352. },
  353. ],
  354. },
  355. {
  356. label: 'Japan',
  357. value: 'Japan',
  358. key: '0-1',
  359. children: [
  360. {
  361. label: 'Osaka',
  362. value: 'Osaka',
  363. key: '0-1-0',
  364. },
  365. ],
  366. },
  367. ],
  368. },
  369. {
  370. label: 'North America',
  371. value: 'North America',
  372. key: '1',
  373. children: [
  374. {
  375. label: 'United States',
  376. value: 'United States',
  377. key: '1-0',
  378. },
  379. {
  380. label: 'Canada',
  381. value: 'Canada',
  382. key: '1-1',
  383. },
  384. ],
  385. },
  386. ];
  387. const [value, $value] = useState(['Shanghai']);
  388. const onSearch = v => {
  389. console.log(v);
  390. };
  391. const onChange = v => {
  392. console.log(v);
  393. $value(v);
  394. };
  395. const flatTreeData = dataSource => {
  396. let newData = [];
  397. let stack = [...dataSource].reverse();
  398. while (stack.length) {
  399. const current = stack.pop();
  400. if (current.children && Array.isArray(current.children)) {
  401. const nodes = current.children;
  402. for (let i = nodes.length - 1; i >= 0; i--) {
  403. const child = { ...nodes[i] };
  404. stack.push(child);
  405. }
  406. } else {
  407. current.isLeaf = true;
  408. }
  409. newData.push(omit(current, ['children']));
  410. }
  411. return newData;
  412. };
  413. const flatNodes = flatTreeData(treeData);
  414. const renderSourcePanel = ({ value, onSelect }) => {
  415. return (
  416. <section style={{ width: '50%' }}>
  417. <Tree
  418. defaultExpandAll
  419. multiple
  420. treeData={treeData}
  421. disableStrictly
  422. value={value}
  423. onChange={onSelect}
  424. ></Tree>
  425. </section>
  426. );
  427. };
  428. return (
  429. <div style={{ margin: 10, padding: 10, width: 600 }}>
  430. <Transfer
  431. type="treeList"
  432. draggable
  433. dataSource={treeData}
  434. value={value}
  435. onChange={onChange}
  436. onSearch={onSearch}
  437. ></Transfer>
  438. <Transfer
  439. type="treeList"
  440. draggable
  441. dataSource={treeData}
  442. value={value}
  443. renderSourcePanel={renderSourcePanel}
  444. onChange={onChange}
  445. onSearch={onSearch}
  446. ></Transfer>
  447. </div>
  448. );
  449. };
  450. export const TreeTransfer = () => <TreeTransferDemo />;
  451. TreeTransfer.story = {
  452. name: 'tree transfer',
  453. };
  454. class CustomRenderDemo extends React.Component {
  455. constructor(props) {
  456. super(props);
  457. this.state = {
  458. dataSource: Array.from({ length: 100 }, (v, i) => ({
  459. label: `海底捞门店 ${i}`,
  460. value: i,
  461. disabled: false,
  462. key: i,
  463. })),
  464. };
  465. this.renderSourcePanel = this.renderSourcePanel.bind(this);
  466. this.renderSelectedPanel = this.renderSelectedPanel.bind(this);
  467. this.renderItem = this.renderItem.bind(this);
  468. }
  469. renderItem(type, item, onItemAction, selectedItems) {
  470. let buttonText = '删除';
  471. if (type === 'source') {
  472. let checked = selectedItems.has(item.key);
  473. buttonText = checked ? '删除' : '添加';
  474. }
  475. return (
  476. <div className="semi-transfer-item panel-item" key={item.label}>
  477. <p>{item.label}</p>
  478. <Button
  479. theme="borderless"
  480. type="primary"
  481. onClick={() => onItemAction(item)}
  482. className="panel-item-remove"
  483. size="small"
  484. >
  485. {buttonText}
  486. </Button>
  487. </div>
  488. );
  489. }
  490. renderSourcePanel(props) {
  491. const {
  492. loading,
  493. noMatch,
  494. filterData,
  495. selectedItems,
  496. allChecked,
  497. onAllClick,
  498. inputValue,
  499. onSearch,
  500. onSelectOrRemove,
  501. } = props;
  502. let content;
  503. switch (true) {
  504. case loading:
  505. content = <Spin loading />;
  506. break;
  507. case noMatch:
  508. content = <div className="empty sp-font">{inputValue ? '无搜索结果' : '暂无内容'}</div>;
  509. break;
  510. case !noMatch:
  511. content = filterData.map(item =>
  512. this.renderItem('source', item, onSelectOrRemove, selectedItems)
  513. );
  514. break;
  515. default:
  516. content = null;
  517. break;
  518. }
  519. return (
  520. <section className="source-panel">
  521. <div className="panel-header sp-font">门店列表</div>
  522. <div className="panel-main">
  523. <Input
  524. style={{ width: 454, margin: '12px 14px' }}
  525. prefix={<IconSearch />}
  526. onChange={onSearch}
  527. showClear
  528. />
  529. <div className="panel-controls sp-font">
  530. <span>待选门店: {filterData.length}</span>
  531. <Button onClick={onAllClick} theme="borderless" size="small">
  532. {allChecked ? '取消全选' : '全选'}
  533. </Button>
  534. </div>
  535. <div className="panel-list">{content}</div>
  536. </div>
  537. </section>
  538. );
  539. }
  540. renderSelectedPanel(props) {
  541. const { selectedData, onClear, clearText, onRemove } = props;
  542. let mainContent = selectedData.map(item => this.renderItem('selected', item, onRemove));
  543. if (!selectedData.length) {
  544. mainContent = <div className="empty sp-font">暂无数据,请从左侧筛选</div>;
  545. }
  546. return (
  547. <section className="selected-panel">
  548. <div className="panel-header sp-font">
  549. <div>已选同步门店: {selectedData.length}</div>
  550. <Button theme="borderless" type="primary" onClick={onClear} size="small">
  551. {clearText || '清空 '}
  552. </Button>
  553. </div>
  554. <div className="panel-main">{mainContent}</div>
  555. </section>
  556. );
  557. }
  558. render() {
  559. const { dataSource } = this.state;
  560. return (
  561. <Transfer
  562. onChange={values => console.log(values)}
  563. className="component-transfer-demo-custom-panel"
  564. renderSourcePanel={this.renderSourcePanel}
  565. renderSelectedPanel={this.renderSelectedPanel}
  566. dataSource={dataSource}
  567. />
  568. );
  569. }
  570. }
  571. export const CustomRender = () => <CustomRenderDemo />;
  572. CustomRender.story = {
  573. name: 'customRender',
  574. };
  575. class CustomRenderDragDemo extends React.Component {
  576. constructor(props) {
  577. super(props);
  578. this.state = {
  579. dataSource: Array.from({ length: 100 }, (v, i) => ({
  580. label: `海底捞门店 ${i}`,
  581. value: i,
  582. disabled: false,
  583. key: i,
  584. })),
  585. };
  586. this.renderSourcePanel = this.renderSourcePanel.bind(this);
  587. this.renderSelectedPanel = this.renderSelectedPanel.bind(this);
  588. this.renderItem = this.renderItem.bind(this);
  589. }
  590. renderItem(type, item, onItemAction, selectedItems) {
  591. let buttonText = '删除';
  592. let newItem = item;
  593. if (type === 'source') {
  594. let checked = selectedItems.has(item.key);
  595. buttonText = checked ? '删除' : '添加';
  596. } else {
  597. // delete newItem._optionKey;
  598. newItem = { ...item, key: item._optionKey };
  599. delete newItem._optionKey;
  600. }
  601. const DragHandle = sortableHandle(() => <IconHandle className="pane-item-drag-handler" />);
  602. return (
  603. <div className="semi-transfer-item panel-item" key={item.label}>
  604. {type === 'source' ? null : <DragHandle />}
  605. <div className="panel-item-main" style={{ flexGrow: 1 }}>
  606. <p>{item.label}</p>
  607. <Button
  608. theme="borderless"
  609. type="primary"
  610. onClick={() => onItemAction(newItem)}
  611. className="panel-item-remove"
  612. size="small"
  613. >
  614. {buttonText}
  615. </Button>
  616. </div>
  617. </div>
  618. );
  619. }
  620. renderSourcePanel(props) {
  621. const {
  622. loading,
  623. noMatch,
  624. filterData,
  625. selectedItems,
  626. allChecked,
  627. onAllClick,
  628. inputValue,
  629. onSearch,
  630. onSelectOrRemove,
  631. } = props;
  632. let content;
  633. switch (true) {
  634. case loading:
  635. content = <Spin loading />;
  636. break;
  637. case noMatch:
  638. content = <div className="empty sp-font">{inputValue ? '无搜索结果' : '暂无内容'}</div>;
  639. break;
  640. case !noMatch:
  641. content = filterData.map(item =>
  642. this.renderItem('source', item, onSelectOrRemove, selectedItems)
  643. );
  644. break;
  645. default:
  646. content = null;
  647. break;
  648. }
  649. return (
  650. <section className="source-panel">
  651. <div className="panel-header sp-font">门店列表</div>
  652. <div className="panel-main">
  653. <Input
  654. style={{ width: 454, margin: '12px 14px' }}
  655. prefix={<IconSearch />}
  656. onChange={onSearch}
  657. showClear
  658. />
  659. <div className="panel-controls sp-font">
  660. <span>待选门店: {filterData.length}</span>
  661. <Button onClick={onAllClick} theme="borderless" size="small">
  662. {allChecked ? '取消全选' : '全选'}
  663. </Button>
  664. </div>
  665. <div className="panel-list">{content}</div>
  666. </div>
  667. </section>
  668. );
  669. }
  670. renderSelectedPanel(props) {
  671. const { selectedData, onClear, clearText, onRemove, onSortEnd } = props;
  672. let mainContent = null;
  673. if (!selectedData.length) {
  674. mainContent = <div className="empty sp-font">暂无数据,请从左侧筛选</div>;
  675. }
  676. const SortableItem = SortableElement(item => this.renderItem('selected', item, onRemove));
  677. const SortableList = SortableContainer(
  678. ({ items }) => {
  679. return (
  680. <div className="panel-main">
  681. {items.map((item, index) => (
  682. // sortableElement will take over the property 'key', so use another '_optionKey' to pass
  683. // otherwise you can't get `key` property in this.renderItem
  684. <SortableItem
  685. key={item.label}
  686. index={index}
  687. {...item}
  688. _optionKey={item.key}
  689. ></SortableItem>
  690. ))}
  691. </div>
  692. );
  693. },
  694. { distance: 10 }
  695. );
  696. mainContent = (
  697. <SortableList useDragHandle onSortEnd={onSortEnd} items={selectedData}></SortableList>
  698. );
  699. return (
  700. <section className="selected-panel">
  701. <div className="panel-header sp-font">
  702. <div>已选同步门店: {selectedData.length}</div>
  703. <Button theme="borderless" type="primary" onClick={onClear} size="small">
  704. {clearText || '清空 '}
  705. </Button>
  706. </div>
  707. {mainContent}
  708. </section>
  709. );
  710. }
  711. render() {
  712. const { dataSource } = this.state;
  713. return (
  714. <Transfer
  715. defaultValue={[2, 4]}
  716. onChange={values => console.log(values)}
  717. className="component-transfer-demo-custom-panel"
  718. renderSourcePanel={this.renderSourcePanel}
  719. renderSelectedPanel={this.renderSelectedPanel}
  720. dataSource={dataSource}
  721. />
  722. );
  723. }
  724. }
  725. export const CustomRenderWithDragSort = () => <CustomRenderDragDemo />;
  726. CustomRenderWithDragSort.story = {
  727. name: 'customRender with drag sort',
  728. };