使用了arco.design组件库,发现里面的穿梭框组件没法进行分页搜索和分页查询接口,只能自己封装一个组件
html 父组件调用 getRequestUrl返回的数据格式有一定的要求
mokdata
{
"requestId": "22",
"code": "200",
"data": {
"content": [
{
"id": "1",
"name": "name",
"label": "中文名",
},
{
"id": "2",
"name": "name2",
"label": "中文名2",
},
],
"totalElements": 4029,
"totalPages": 202,
},
"success": true,
"message": null,
"devMsg": null,
"hasPrivilege": true
}
import RemoteTransfer from '@/components/RemoteTransfer';
<RemoteTransfer
key={addDialogTitle}
getMethod="post"
getRequestUrl={()=>{
return 'api/post';
}}
transferValue={remoteTargetKeys}
defaultParams={{
searchSubNode:true
}}
titleTexts={['可选列表','已选择列表']}
transferProps={
{
key: 'id',
search:['keyWord']
}
}
targetFilterInput={
{
key: 'id',
search:['modelName','modelLabel']
}
}
column={transferColumns}
pageSize={100}
paginationConfig={
{
pageNum:'offset',
pageSize:'limit'
}
}
onChange={(value,to)=>remoteTransferonChange(value,to)}
>
</RemoteTransfer>
ts data
const [addDialogTitle,setAddDialogTitle] =useState('新增');
const [remoteTargetKeys, setRemoteTargetKeys] = useState([]);
const transferColumns = [
{
title: '中文名',
dataIndex: 'label',
ellipsis:true,
render:(col, record) => (
<Tooltip content={record.label}>
<span className='transfer-table-cell'>{record.label}</span>
</Tooltip>
)
},
{
title: '英文名',
dataIndex: 'name',
ellipsis:true,
render:(col, record) => (
<Tooltip content={record.name}>
<span className='transfer-table-cell'>{record.name}</span>
</Tooltip>
)
}
];
函数
const remoteTransferonChange=(value,to)=>{
setRemoteTargetKeys(value)
}
子组件

index.tsx
import React, {
forwardRef,
useContext,
PropsWithChildren,
useEffect,
useState,
useMemo,
useRef,
useCallback,
} from 'react';
import useMergeProps from '@arco-design/web-react/lib/_util/hooks/useMergeProps';
import cs from '@arco-design/web-react/lib/_util/classNames';
import axios from 'axios';
import { Button, Input, Table,Checkbox ,Message,Tooltip} from '@arco-design/web-react';
import { IconLeft, IconRight } from '@arco-design/web-react/icon';
import './style/index.less';
import useMergeValue from '@arco-design/web-react/lib/_util/hooks/useMergeValue';
import RemoteTransferList from './list';
import {
RemoteTransferItem,
RemoteTransferProcessDataRetrue,
RemoteTransferProps,
RemoteTransferType,
} from './interface';
require('static/guid.js');
const InputSearch = Input.Search;
// const transferProps: RemoteTransferProps = {
// column: {},
// processData: (raw) => raw,
// props: { key: 'key', search: ['word'] },
// pageSize: 20,
// };
function RemoteTransfer(props: PropsWithChildren<RemoteTransferProps>) {
const {
className,
style,
requestUrl,
getRequestUrl,
processData,
transferValue,
pageSize,
transferProps,
column,
titleTexts,
platformCode,
defaultParams,
disabled,
getMethod,
// paginationConfig,
targetFilterInput,
// dataSource,
onChange,
...rest
} = props;
const prefixCls="remote-transfer";
const [targetValue, setTargetValue] = useMergeValue([], { value: transferValue });
const [dataSource, setDataSource] = useState([]);
const [columns, setColumns] = useState(column);
const [platformCodeNew, setPlatformCodeNew] = useState(platformCode);
const [loading, setLoading] = useState(false);
const [transferChecked, setTransferChecked] = useState(false);
const [selectchecked, setSelectchecked] = useState(false);
const [sourcePagination, setSourcePagination] = useState({ current: 1, pageSize, total: 0 });
const pageLoadedRef = useRef<number[]>([]);
const filterInfo = useMemo(() => {
type FilterInfo = {
rowKey: keyof RemoteTransferItem;
searchKeys: (keyof RemoteTransferItem)[];
};
const { key: rowKey, search } = transferProps;
const searchKeys = [].concat(search);
return { rowKey, searchKeys } as FilterInfo;
}, [transferProps]);
const targetFilterInfo = useMemo(() => {
type targetFilterInfo = {
rowKey: keyof RemoteTransferItem;
searchKeys: (keyof RemoteTransferItem)[];
};
const { key: rowKey, search } = targetFilterInput;
const searchKeys = [].concat(search);
return { rowKey, searchKeys } as targetFilterInfo;
}, [targetFilterInput]);
type ListInfo = {
dataSource: RemoteTransferItem[];
selectedDisabledKeys: string[];
};
const sourceInfo = useMemo<ListInfo>(() => {
const { current, pageSize } = sourcePagination;
const currentLoadedIndex = pageLoadedRef.current.indexOf(current);
return {
dataSource: dataSource.slice(
currentLoadedIndex * pageSize,
(currentLoadedIndex + 1) * pageSize
),
selectedDisabledKeys: targetValue.map((item) => item[filterInfo.rowKey]),
};
}, [dataSource, filterInfo.rowKey, sourcePagination, targetValue]);
const [targetFilterText, setTargetFilterText] = useState('');
const targetInfo = useMemo<ListInfo>(() => {
let filterInfo_={};
if(targetFilterInput){
filterInfo_=targetFilterInfo;
}else{
filterInfo_=filterInfo;
}
return {
dataSource:
targetFilterText && filterInfo_.searchKeys.length
? targetValue.filter(item => {
return filterInfo_.searchKeys.some(key => item[key] && item[key].includes(targetFilterText));
})
: targetValue,
selectedDisabledKeys: [],
};
}, [targetFilterText, targetValue, filterInfo.searchKeys]);
type SelectedInfo = { source: string[]; target: string[] };
const [selectedInfo, setSelectedInfo] = useState<SelectedInfo>({
source: [],
target: [],
});
type RemoteParams = { pageNum: number; [K: string]: any };
const remoteParamsRef = useRef<RemoteParams>({ pageNum: 1 });
const loadRemote = useCallback(
async (reset = false,code) => {
const url = getRequestUrl?.() || requestUrl || '';
let obj={};
filterInfo.searchKeys.forEach(key => {
obj[key] = '';
});
let params={
...obj,
...defaultParams,
...remoteParamsRef.current,
pageNum:remoteParamsRef.current.pageNum-1,
pageSize,
}
if(code){
params.platformCode=code;
}
// if(paginationConfig){
// let pageNum = paginationConfig.pageNum;
// let pageSize_ = paginationConfig.pageSize;
// params[pageNum]=params.pageNum;
// params[pageSize_]=params.pageSize;
// params={params};
// delete params.pageNum;
// delete params.pageSize;
// }
setLoading(true);
const host=`${window.location.host}`;
const protocol=`${window.location.protocol}`;
let res={};
if(getMethod && getMethod === 'post'){
res = await axios({
method:'post',
url:`${protocol}//${host}/${url}` ,
data: params ? params: {},
}).then(res=> res.data).catch(err=>{ console.log(err); setLoading(false);});
}else{
res = await axios({
method:'get',
url:`${protocol}//${host}/${url}` ,
params: params ? params: {},
}).then(res=> res.data).catch(err=>{ console.log(err); setLoading(false);});
}
// await axios.post(`/${APP_NAME}/_api/_/team/changeTeam`, { teamName:'asiainfo' });
setLoading(false);
let data=res?.data;
if (processData) {
data = processData(data);
}
if (data == null || !Array.isArray(data?.content) ) {
console.log('远程穿梭框获取的数据格式不正确');
if(res?.message){
Message.warning(res?.message);
}
}
if(data?.content.length>0){
data.content.forEach((item,index)=>{
if(item?.id){
item.id=item.id;
}else{
item.id=index+'';
}
})
}
const { pageNum } = remoteParamsRef.current;
const { content, totalElements } = data;
setDataSource((source) => {
if (reset) {
pageLoadedRef.current = [pageNum];
return content;
}
pageLoadedRef.current = pageLoadedRef.current.concat(pageNum).sort();
const index = pageLoadedRef.current.indexOf(pageNum);
source.splice(index * pageSize, 0, ...content);
return source;
});
setSourcePagination({
current: pageNum,
pageSize,
total: totalElements || content.length,
});
},
[getRequestUrl, pageSize, processData, requestUrl]
);
useEffect(() => {
if(platformCode){
loadRemote(true,platformCode,);
}else{
loadRemote(true);
}
// loadRemote,platformCode
}, []);
useEffect(() => {
if(column){
setColumns(column);
}
}, [column]);
useEffect(() => {
if(props?.item?.field){
if(props.thisEvent.formRef){
let targetData_ = props.thisEvent.formRef.current.getFieldValue(props?.item?.field);
if(Array.isArray(targetData_) && targetData_.length > 0 ){
setTargetValue(targetData_);
}else{
let initialValue=props?.item?.initialValue;
setTargetValue(Array.isArray(initialValue) && initialValue.length > 0 ? props?.item?.initialValue : []);
}
}
}
}, []);
const moveTo = (to: RemoteTransferType) => {
const selectedKeys = to === 'source' ? selectedInfo.target : selectedInfo.source;
if (!selectedKeys.length) return;
if (to === 'target') {
const newValue = targetValue.concat(
dataSource.filter((item) => selectedKeys.includes(item[filterInfo.rowKey]))
);
if(props?.options){
props?.options?.onChange?.(newValue, to,props);
}else{
props?.onChange?.(newValue, to,props);
}
setTargetValue(newValue);
setSelectedInfo({ ...selectedInfo, source: [] });
} else {
const newValue = targetValue.filter(
(item) => !selectedKeys.includes(item[filterInfo.rowKey])
);
if(props?.options){
props?.options?.onChange?.(newValue, to,props);
}else{
props?.onChange?.(newValue, to,props);
}
setTargetValue(newValue);
setSelectedInfo({ ...selectedInfo, target: [] });
}
};
const handleSelect = (to: RemoteTransferType, selectedKeys: (string | number)[]) => {
const key: keyof SelectedInfo = to === 'source' ? 'source' : 'target';
setSelectedInfo((info) => {
return { ...info, [key]: selectedKeys };
});
if(to=='source'){
setTransferChecked(!transferChecked)
}else if(to=='target'){
setSelectchecked(!selectchecked)
}
};
const handleSourcePageChange = (current: number) => {
if (pageLoadedRef.current.includes(current)) {
setSourcePagination({ ...sourcePagination, current });
return;
}
remoteParamsRef.current.pageNum = current;
loadRemote(true,platformCode);
};
const handleSourceSearch = (value: string) => {
remoteParamsRef.current = filterInfo.searchKeys.reduce(
(params, key) => {
params[key] = value;
return params;
},
{ pageNum: 1 }
);
loadRemote(true,platformCode);
};
const renderOperations = () => {
const leftActive = selectedInfo.target.length > 0;
const rightActive = selectedInfo.source.length > 0;
const buttons: RemoteTransferType[] = ['target', 'source'];
return (
<div className={cs(`${prefixCls}-operations elx-remote-transfer-operate`)}>
{buttons.map((to, index) => {
let Icon;
let _disabled;
if (to === 'source') {
Icon = IconLeft;
_disabled = disabled || !leftActive;
} else {
Icon = IconRight;
_disabled = disabled || !rightActive;
}
return (
<Button
key={index}
className={to?'right':'left'}
tabIndex={-1}
aria-label={`move selected ${to === 'target' ? 'right' : 'left'}`}
type="secondary"
size="small"
shape="round"
disabled={_disabled}
onClick={() => moveTo(to)}
icon={<Icon />}
/>
);
})}
</div>
);
};
const renderList = (to: RemoteTransferType) => {
const onSearch = to === 'target' ? setTargetFilterText : handleSourceSearch;
const info = to === 'target' ? targetInfo : sourceInfo;
const pagination =
to === 'target'
? {}
: {
...sourcePagination,
onChange: handleSourcePageChange,
};
const rowSelection =
to === 'target'
? {}
: {
checkboxProps(record: RemoteTransferItem) {
return {
disabled:
disabled || sourceInfo.selectedDisabledKeys.includes(record[filterInfo.rowKey]),
};
},
};
return (
<RemoteTransferList
{...info}
prefixCls={prefixCls}
className={cs(`${prefixCls}-view`, `${prefixCls}--view-${to}`)}
disabled={disabled}
rowKey={filterInfo.rowKey}
selectchecked={selectchecked}
selectedKeys={selectedInfo[to]}
onSearch={onSearch}
titleTexts={titleTexts}
onSelect={(selectedKeys) => handleSelect(to, selectedKeys)}
columns={columns.map((column, index) => {
return {
...column,
title: column?.label || column?.title,
dataIndex: column?.name || column?.dataIndex,
};
})}
pagination={{ showTotal: true, simple:true, pageSize, ...pagination }}
rowSelection={rowSelection}
/>
);
};
return (
<div className={cs(prefixCls, className)} style={style} {...rest}>
<div className={`elx-remote-transfer-left left-box--view-source`}>
<div className='arco-transfer-view-header'>
<Checkbox checked={selectedInfo?.source.length==0 ? false: transferChecked}>{Array.isArray(titleTexts)?titleTexts[0]:'可选'}</Checkbox>
<div className='arco-transfer-view-header-unit'>{sourceInfo?.dataSource.length}/{selectedInfo?.source.length}</div>
</div>
<InputSearch
className={`left-view-search`}
placeholder="请输入搜索内容"
searchButton
loading={loading}
onSearch={handleSourceSearch}
/>
<Table
loading={loading}
className={`left-view-table`}
rowKey={filterInfo.rowKey}
rowSelection={{
selectedRowKeys: selectedInfo.source,
checkboxProps(record) {
return {
disabled:
disabled || sourceInfo.selectedDisabledKeys.includes(record[filterInfo.rowKey]),
};
},
onChange: (selectedKeys) => {
handleSelect('source', selectedKeys);
},
}}
data={sourceInfo.dataSource}
columns={columns.map((column, index) => {
return {
...column,
title: column?.label || column?.title,
dataIndex: column?.name || column?.dataIndex,
};
})}
pagePosition="bottomCenter"
pagination={{
...sourcePagination,
onChange: handleSourcePageChange,
simple:true,
showTotal: true,
}}
scroll={{ y: '100%' }}
/>
</div>
{renderOperations()}
{renderList('target')}
</div>
);
}
RemoteTransfer.defaultProps = {
requestUrl:'',
// transferValue:[],//已选的key
pageSize:50,
getMethod:'get',
transferProps:{ key: 'key', search: ['word'] },
targetFilterInput:{key: 'key', search: ['name']},
column:[
{
"name": "label",
"label": "中文名"
},
{
"name": "name",
"label": "英文名"
}
],
titleTexts:['可选','已选'],
defaultParams:{},//默认传递的其他搜索值
disabled:false,
}
export default RemoteTransfer;
list.tsx
import { Input, Table,Checkbox } from '@arco-design/web-react';
import cs from '@arco-design/web-react/lib/_util/classNames';
import React from 'react';
function RemoteTransferList(props) {
const {
prefixCls,
className,
disabled,
onSearch,
selectedKeys,
onSelect,
dataSource,
columns,
pagination,
titleTexts,
rowKey,
selectchecked
} = props;
const InputSearch = Input.Search;
return (
<div className={className +' elx-remote-transfer-right'}>
<div className='arco-transfer-view-header'>
<Checkbox checked={selectedKeys.length==0 ? false : selectchecked}>{Array.isArray(titleTexts)?titleTexts[1]:'已选择'}</Checkbox>
<div className='arco-transfer-view-header-unit'>{dataSource.length}/{selectedKeys.length}</div>
</div>
<InputSearch
className={cs(`${prefixCls}-view-search`)}
disabled={disabled}
// allowClear={}
placeholder="请输入搜索内容"
onChange={onSearch}
/>
<Table
className={cs(`${prefixCls}-view-table`)}
rowSelection={{
selectedRowKeys: selectedKeys,
onChange: onSelect,
}}
rowKey={rowKey}
data={dataSource}
columns={columns}
pagePosition="bottomCenter"
pagination={pagination}
scroll={{ y: '100%' }}
/>
</div>
);
}
export default RemoteTransferList;
interface.ts
import { PaginationProps, TableColumnProps, TableRowSelectionProps } from '@arco-design/web-react';
import { CSSProperties } from 'react';
export type RemoteTransferType = 'target' | 'source';
export interface RemoteTransferItem {
[K: string | number | symbol]: any;
}
/**
* @title RemoteTransfer
*/
export interface RemoteTransferProps {
style?: CSSProperties;
className?: string;
/**
* @zh [左栏]数据请求地址(
* @en The remote url
*/
requestUrl?: string;
/**
* @zh [左栏]数据请求地址的回调方法
* @en Get the remote url by function
*/
getRequestUrl?: () => string;
/**
* @zh 处理成组件要求的数据方法
* @en The function for process remote data
*/
processData?: (raw: any) => RemoteTransferProcessDataRetrue;
/**
* @zh 值,`完全受控`
* @en The value, is `Control`
*/
transferValue?: RemoteTransferItem[];
/**
* @zh 所需展示的表格字段以及表头中文名对象(举例:`{ stepInst: '指令',stepInstLabel: '指令名称' }`)
* @en The table columns `dataindex` & `title`, egs, `{ key1: 'Title 1', key2: 'Title 2' }`
*/
column?: Record<string, string>;
/**
* @zh 页码设置
* @en The page size
*/
pageSize?: number;
/**
* @zh 配置选项,key: 每条数据的唯一值字段, search: 搜索框对应的字段
* @en The config, key: the unique key of data, search: keys of filter data by search input
*/
props?: { key: keyof RemoteTransferItem; search: string | string[] };
/**
* @zh 禁用穿梭框
* @en Whether is disabled
*/
disabled?: boolean;
/**
* @zh 选中项在两栏之间转移时的回调
* @en Callback when the transfer between columns is complete
*/
onChange?: (selected: RemoteTransferItem[], type: RemoteTransferType) => void;
}
/**
* @title RemoteRequestParams
*
* @zh 远程请求的入参,请求方法 `get`
* @en The remote request params, request method: `get`.
*/
export interface RemoteRequestParams {
/**
* @zh 页码
* @en The page number
*/
pageNum: number;
/**
* @zh 一页中含数据条数
* @en The page size
*/
pageSize: number;
/**
* @zh
* @en The search keys ard define by `props.search`, value bu search input
*/
[searchKey: string]: any;
}
/**
* @title RemoteTransferProcessDataRetrue
*
* @zh `processData` 返回的数据格式
* @en The type return of `processData`.
*/
export interface RemoteTransferProcessDataRetrue {
/**
* @zh 数据内容
* @en Data Content
*/
content: RemoteTransferItem[];
/**
* @zh 数据总数
* @en Total of data
*/
totalElements: number;
}
export interface RemoteTransferListProps {
prefixCls: string;
className: string;
disabled: boolean;
onSearch: (value: string) => void;
selectedKeys: string[];
onSelect: (selectedKeys: string[]) => void;
dataSource: RemoteTransferItem[];
columns: TableColumnProps[];
pagination: PaginationProps;
rowSelection: TableRowSelectionProps;
}
index.less
.remote-transfer{
display: flex;
min-width: 500px;
width: 100%;
height: 348px;
.arco-transfer-view-header{
height: 36px;
line-height: 36px;
font-weight: bold;
background-color: var(--color-gray-2);
margin-bottom: 10px;
}
.arco-table{
height: calc(100% - 110px);
}
.arco-spin,
.arco-spin-children,
.arco-table-container,
.arco-table-content-scroll,
.arco-table-content-inner,
.arco-table-body {
height: calc(100%);
}
.arco-table-content-scroll{
height: calc(100% - 5px);
.arco-table-body{
height: calc(100% - 40px);
}
}
.arco-table-content-inner{
height: 100%;
padding: 0px !important;
}
.arco-table-container{
padding: 0px !important;
}
.elx-remote-transfer-left{
width: calc(50% - 25px);
padding: 10px;
box-shadow: 0px 0px 5px #d3d3d3;
float: left;
}
.elx-remote-transfer-operate{
width: 50px;
height: 100%;
float: left;
text-align: center;
position: relative;
top:40%;
.right{
margin-bottom: 20px;
}
}
.elx-remote-transfer-right{
width: calc(50% - 25px);
padding: 10px;
box-shadow: 0px 0px 5px #d3d3d3;
float: left;
}
}