UNPKG

@douyinfe/semi-ui

Version:

A modern, comprehensive, flexible design system and UI library. Connect DesignOps & DevOps. Quickly build beautiful React apps. Maintained by Douyin-fe team.

1,250 lines (1,108 loc) 45 kB
--- localeCode: zh-CN order: 75 category: 展示类 title: List 列表 icon: doc-list dir: column noInline: true brief: 基础列表组件 --- ## 代码演示 ### 如何引入 ```jsx import import { List } from '@douyinfe/semi-ui'; ``` ### 基本用法 列表的基本用法。可以通过 size 设置尺寸,支持`large`, `default`, `small`。可设置 header 和 footer,来自定义列表头部和尾部。 ```jsx live=true dir="column" noInline=true import React from 'react'; import { List } from '@douyinfe/semi-ui'; function SimpleList() { const data = [ '从明天起,做一个幸福的人', '喂马,劈柴,周游世界', '从明天起,关心粮食和蔬菜', '我有一所房子,面朝大海,春暖花开', ]; return ( <div> <div style={{ marginRight: 16 }}> <h3 style={{ marginBottom: 16 }}>Default Size</h3> <List header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={item => <List.Item>{item}</List.Item>} /> </div> <div style={{ marginRight: 16 }}> <h3 style={{ margin: '16px 0' }}>Small Size</h3> <List size="small" header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={item => <List.Item>{item}</List.Item>} /> </div> <div style={{ marginRight: 16 }}> <h3 style={{ margin: '16px 0' }}>Large Size</h3> <List size="large" header={<div>Header</div>} footer={<div>Footer</div>} bordered dataSource={data} renderItem={item => <List.Item>{item}</List.Item>} /> </div> </div> ); } render(SimpleList); ``` ### 模板用法 列表的 List.Item 内置了简单的结构包含:header,main 和 extra 。其中 header 和 main 的对齐方式可以通过 align 属性设置,支持 `flex-start`(默认), `flex-end`, `center`, `baseline`, 和 `stretch` 。 ```jsx live=true dir="column" noInline=true import React from 'react'; import { List, Avatar, ButtonGroup, Button } from '@douyinfe/semi-ui'; function ContentList() { const data = [ // eslint-disable-next-line react/jsx-key <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0', width: 420, whiteSpace: 'nowrap', overflow: 'hidden', textOverflow: 'ellipsis', }} > Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。 </p>, // eslint-disable-next-line react/jsx-key <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0', width: 500 }}> Semi Design 是由抖音前端团队与 UED 团队共同设计开发并维护的设计系统。设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。 </p>, // eslint-disable-next-line react/jsx-key <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0', width: 500 }}> Semi Design 以用户中心、内容优先、设计人性化的设计系统,打造一致、好看、好用、高效的用户体验。 </p>, ]; return ( <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}> <List dataSource={data} renderItem={item => ( <List.Item header={<Avatar color="blue">SE</Avatar>} main={ <div> <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>示例标题</span> {item} </div> } extra={ <ButtonGroup theme="borderless"> <Button>编辑</Button> <Button>更多</Button> </ButtonGroup> } /> )} /> </div> ); } render(ContentList); ``` ### 布局 通过 layout 属性可以设置列表的布局,支持`vertical`(默认)和`horizontal`。 ```jsx live=true dir="column" noInline=true import React from 'react'; import { List, Avatar } from '@douyinfe/semi-ui'; function LayoutList() { const data = [ { title: 'Semi Design Title 1', color: 'light-blue', }, { title: 'Semi Design Title 2', color: 'grey', }, { title: 'Semi Design Title 3', color: 'light-green', }, ]; return ( <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}> <List dataSource={data} layout="horizontal" renderItem={item => ( <List.Item header={<Avatar color={item.color}>SE</Avatar>} main={ <div> <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span> <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}> Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。 </p> </div> } /> )} /> </div> ); } render(LayoutList); ``` ### 栅格列表 通过 grid 属性可以实现栅格列表,`span` 可设置每项的占格数,`gutter`可设置栅格间隔。 ```jsx live=true dir="column" noInline=true import React from 'react'; import { List, Descriptions, ButtonGroup, Rating, Button } from '@douyinfe/semi-ui'; function LayoutList() { const data = [ { title: 'Semi UI', rating: 4.5, feedbacks: 124, }, { title: 'Semi DSM', rating: 4, feedbacks: 108, }, { title: 'Semi D2C', rating: 4.5, feedbacks: 244, } ]; const style = { border: '1px solid var(--semi-color-border)', backgroundColor: 'var(--semi-color-bg-2)', borderRadius: '3px', paddingLeft: '20px', }; return ( <div> <List grid={{ gutter: 12, span: 6, }} dataSource={data} renderItem={item => ( <List.Item style={style}> <div> <h3 style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</h3> <Descriptions align="center" size="small" row data={[ { key: '满意度', value: <Rating allowHalf size="small" value={item.rating} /> }, { key: '反馈数', value: item.feedbacks }, ]} /> <div style={{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }}> <ButtonGroup theme="borderless" style={{ marginTop: 8 }}> <Button>编辑</Button> <Button>更多</Button> </ButtonGroup> </div> </div> </List.Item> )} /> </div> ); } render(LayoutList); ``` ### 响应式的栅格列表 响应式的栅格列表。响应尺寸与 [Grid](/zh-CN/basic/grid) 保持一致。 ```jsx live=true dir="column" noInline=true hideInDSM import React from 'react'; import { List, Descriptions, Rating, Button, ButtonGroup } from '@douyinfe/semi-ui'; function Responsive() { const data = [ { title: '审核管理平台', rating: 4.5, feedbacks: 124, }, { title: '扁鹊', rating: 4, feedbacks: 108, }, { title: '直播审核平台', rating: 3.5, feedbacks: 244, }, { title: '抖音安全测试', feedbacks: 189, }, { title: '内容平台', rating: 3, feedbacks: 128, }, { title: '策略平台', rating: 4, feedbacks: 156, }, ]; const style = { border: '1px solid var(--semi-color-border)', backgroundColor: 'var(--semi-color-bg-2)', borderRadius: '3px', paddingLeft: '20px', margin: '8px 2px', }; return ( <div> <List grid={{ gutter: 12, xs: 0, sm: 0, md: 12, lg: 8, xl: 8, xxl: 6, }} dataSource={data} renderItem={item => ( <List.Item style={style}> <div> <h3 style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</h3> <Descriptions align="center" size="small" row data={[ { key: '满意度', value: <Rating allowHalf size="small" value={item.rating} /> }, { key: '反馈数', value: item.feedbacks }, ]} /> <div style={{ margin: '12px 0', display: 'flex', justifyContent: 'flex-end' }}> <ButtonGroup theme="borderless" style={{ marginTop: 8 }}> <Button>编辑</Button> <Button>更多</Button> </ButtonGroup> </div> </div> </List.Item> )} /> </div> ); } render(Responsive); ``` ### 加载更多 可通过 loadMore 属性实现加载更多的功能。 ```jsx live=true dir="column" noInline=true hideInDSM import React, { useState, useEffect, useRef, useCallback } from 'react'; import { List, Skeleton, Button, Avatar } from '@douyinfe/semi-ui'; function LoadMoreList() { const count = 3; const data = []; for (let i = 0; i < 40; i++) { data.push({ color: 'grey', title: `Semi Design Title ${i}`, loading: false, }); } const dataRef = useRef(data); const countRef = useRef(0); const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); const [list, setList] = useState([]); const [noMore, setNoMore] = useState(false); const fetchData = useCallback(() => { let placeholders = [0, 1, 2].map(key => ({ loading: true })); setLoading(true); setList(prevList => [...prevList, ...placeholders]); return new Promise((res, rej) => { setTimeout(() => { let newDataSource = dataRef.current.slice(countRef.current * count, countRef.current * count + count); res(newDataSource); }, 1000); }).then(newDataSource => { setDataSource(prevData => { let newData = [...prevData, ...newDataSource]; setLoading(false); setList(newData); setNoMore(!newDataSource.length); return newData; }); }); }, []); useEffect(() => { fetchData(); }, [fetchData]); const onLoadMore = () => { countRef.current++; fetchData(); }; const loadMore = !loading && !noMore ? ( <div style={{ textAlign: 'center', marginTop: 12, height: 32, lineHeight: '32px', }} > <Button onClick={onLoadMore}>显示更多</Button> </div> ) : null; const placeholder = ( <div style={{ display: 'flex', alignItems: 'flex-start', padding: 12, borderBottom: '1px solid var(--semi-color-border)', }} > <Skeleton.Avatar style={{ marginRight: 12 }} /> <div> <Skeleton.Title style={{ width: 120, marginBottom: 12, marginTop: 12 }} /> <Skeleton.Paragraph style={{ width: 600 }} rows={2} /> </div> </div> ); return ( <List loading={loading} loadMore={loadMore} dataSource={list} renderItem={item => ( <Skeleton placeholder={placeholder} loading={item.loading}> <List.Item header={<Avatar color={item.color}>SE</Avatar>} main={ <div> <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span> <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}> Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。 </p> </div> } /> </Skeleton> )} /> ); } render(LoadMoreList); ``` ### 滚动加载 可以通过集成 [react-infinite-scroller](https://github.com/CassetteRocks/react-infinite-scroller) 来实现滚动加载的列表。交互建议符合 semi 交互设计规范,这里采用三次滚加载后出现 load more 按钮的形式。 ```jsx live=true dir="column" noInline=true hideInDSM import React, { useState, useEffect, useRef } from 'react'; import { List, Button, Avatar, Spin } from '@douyinfe/semi-ui'; import InfiniteScroll from 'react-infinite-scroller'; function ScrollLoad() { const count = 5; const dataList = []; for (let i = 0; i < 100; i++) { dataList.push({ color: 'grey', title: `Semi Design Title ${i}`, loading: false, }); } const dataRef = useRef(dataList); const countRef = useRef(0); const [loading, setLoading] = useState(false); const [dataSource, setDataSource] = useState([]); const [hasMore, setHasMore] = useState(true); const fetchData = useCallback(() => { setLoading(true); return new Promise((res, rej) => { setTimeout(() => { let newDataSource = dataRef.current.slice(countRef.current * count, countRef.current * count + count); res(newDataSource); }, 1000); }).then(newDataSource => { setDataSource(prevData => { let newData = [...prevData, ...newDataSource]; countRef.current++; setLoading(false); setHasMore(!!newDataSource.length); return newData; }); }); }, []); useEffect(() => { fetchData(); }, [fetchData]); const showLoadMore = countRef.current % 4 === 0; const loadMore = !loading && hasMore && showLoadMore ? ( <div style={{ textAlign: 'center', marginTop: 12, height: 32, lineHeight: '32px', }} > <Button onClick={fetchData}>显示更多</Button> </div> ) : null; return ( <div className="light-scrollbar" style={{ height: 420, overflow: 'auto', border: '1px solid var(--semi-color-border)', padding: 10 }} > <InfiniteScroll initialLoad={false} pageStart={0} threshold={20} loadMore={fetchData} hasMore={!loading && hasMore && !showLoadMore} useWindow={false} > <List loadMore={loadMore} dataSource={dataSource} renderItem={item => ( <List.Item header={<Avatar color={item.color}>SE</Avatar>} main={ <div> <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}> {item.title} </span> <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}> Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。 </p> </div> } /> )} /> {loading && hasMore && ( <div style={{ textAlign: 'center' }}> <Spin /> </div> )} </InfiniteScroll> </div> ); } render(ScrollLoad); ``` ### 滚动加载无限长列表 可以通过集成 [react-virtualized](https://github.com/bvaughn/react-virtualized) 实现滚动加载无限长列表,带有虚拟化(virtualization)功能,能够提高数据量大时候长列表的性能。 ```jsx live=true dir="column" noInline=true hideInDSM import React, { useState, useRef, useCallback } from 'react'; import { List, Avatar } from '@douyinfe/semi-ui'; import { InfiniteLoader, AutoSizer } from 'react-virtualized'; import VList from 'react-virtualized/dist/commonjs/List'; function VirtualizedScroll() { const dataList = []; for (let i = 0; i < 50; i++) { dataList.push({ color: 'grey', title: `Semi Design Title ${i}`, }); } const dataRef = useRef(dataList); const [dataSource, setDataSource] = useState([]); const [loadedRowsMap, setLoadedRowsMap] = useState({}); const [loadingRowCount, setLoadingRowCount] = useState(0); const statusLoading = 0; const statusLoaded = 1; const loadLimit = dataRef.current.length; const fetchData = useCallback((startIndex, stopIndex) => { return new Promise((res, rej) => { setTimeout(() => { let newDataSource = dataRef.current.slice(startIndex, stopIndex + 1); res(newDataSource); }, 1000); }).then(newDataSource => { setDataSource(prevData => { let newData = [...prevData, ...newDataSource]; const increment = stopIndex - startIndex + 1; const newLoadedRowsMap = { ...loadedRowsMap }; for (let i = startIndex; i <= stopIndex; i++) { newLoadedRowsMap[i] = statusLoaded; } setLoadedRowsMap(newLoadedRowsMap); setLoadingRowCount(prev => prev - increment); return newData; }); }); }, [loadedRowsMap]); const handleInfiniteOnLoad = useCallback(({ startIndex, stopIndex }) => { const increment = stopIndex - startIndex + 1; if (stopIndex >= loadLimit || loadingRowCount > 0) { return; } const newLoadedRowsMap = { ...loadedRowsMap }; for (let i = startIndex; i <= stopIndex; i++) { newLoadedRowsMap[i] = statusLoading; } setLoadedRowsMap(newLoadedRowsMap); setLoadingRowCount(prev => prev + increment); return fetchData(startIndex, stopIndex); }, [loadLimit, loadingRowCount, loadedRowsMap, fetchData]); const isRowLoaded = useCallback(({ index }) => { return !!loadedRowsMap[index]; }, [loadedRowsMap]); const renderItem = useCallback(({ index, key, style }) => { const item = dataSource[index]; if (!item) { return; } const content = ( <List.Item key={key} style={style} header={<Avatar color={item.color}>SE</Avatar>} main={ <div> <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span> <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}> Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。 </p> </div> } /> ); return content; }, [dataSource]); const height = 500; return ( <List style={{ border: '1px solid var(--semi-color-border)', padding: 10 }}> <InfiniteLoader isRowLoaded={isRowLoaded} loadMoreRows={handleInfiniteOnLoad} rowCount={loadLimit} > {({ onRowsRendered, registerChild }) => ( <AutoSizer disableHeight> {({ width }) => ( <VList ref={registerChild} height={height} onRowsRendered={onRowsRendered} rowCount={loadLimit} rowHeight={118} rowRenderer={renderItem} width={width} /> )} </AutoSizer> )} </InfiniteLoader> </List> ); } render(VirtualizedScroll); ``` ### 拖拽排序 使用 [dnd-kit](https://github.com/clauderic/dnd-kit/tree/master) 可轻松实现拖拽排序。 ```jsx live=true dir="column" hideInDSM import React, { useState } from 'react'; import { List, Avatar } from '@douyinfe/semi-ui'; import { DndContext, PointerSensor, MouseSensor, useSensors, useSensor } from '@dnd-kit/core'; import { SortableContext, arrayMove, useSortable, verticalListSortingStrategy } from '@dnd-kit/sortable'; import { restrictToVerticalAxis } from '@dnd-kit/modifiers'; import { CSS as cssDndKit } from '@dnd-kit/utilities'; import classNames from 'classnames'; () => { const data = [ { id: 1, // 添加唯一id title: 'Semi Design Title 1', color: 'red', }, { id: 2, title: 'Semi Design Title 2', color: 'grey', }, { id: 3, title: 'Semi Design Title 3', color: 'light-green', }, { id: 4, title: 'Semi Design Title 4', color: 'light-blue', }, { id: 5, title: 'Semi Design Title 5', color: 'pink', }, ]; const [listItems, setListItems] = useState(data); const sensors = useSensors( useSensor(MouseSensor, { activationConstraint: { distance: 1 }, }) ); const handleDragEnd = event => { const { active, over } = event; if (active.id !== over.id) { setListItems((items) => { const oldIndex = items.findIndex(item => item.id === active.id); const newIndex = items.findIndex(item => item.id === over.id); return arrayMove(items, oldIndex, newIndex); }); } }; const ListItem = (props) => { const { attributes, listeners, setNodeRef, transform, transition, isDragging, isOver } = useSortable({ id: props['id'], }); const styles = { ...props.style, transform: cssDndKit.Transform.toString(transform), transition, border: '1px solid var(--semi-color-border)', marginBottom: 12, cursor: 'grabbing', ...(isDragging ? { zIndex: 999, position: 'relative', backgroundColor: 'var(--semi-color-bg-0)' } : {}), }; const itemCls = classNames( { ['isDragging']: isDragging, ['isOver']: isOver, } ); return ( <div ref={setNodeRef} style={styles} className={itemCls} {...listeners} {...attributes} > <List.Item {...props} ></List.Item> </div> ); }; const RenderDraggable = (item, id) => { return ( <ListItem id={id} {...item} header={<Avatar color={item.color}>SE</Avatar>} main={ <div> <span style={{ color: 'var(--semi-color-text-0)', fontWeight: 500 }}>{item.title}</span> <p style={{ color: 'var(--semi-color-text-2)', margin: '4px 0' }}> Semi Design 设计系统包含设计语言以及一整套可复用的前端组件,帮助设计师与开发者更容易地打造高质量的、用户体验一致的、符合设计规范的 Web 应用。 </p> </div> } /> ); }; return ( <div style={{ padding: 12, border: '1px solid var(--semi-color-border)', margin: 12 }}> <DndContext autoScroll={true} sensors={sensors} modifiers={[restrictToVerticalAxis]} onDragEnd={handleDragEnd} > <SortableContext items={listItems.map(data => data.id)} strategy={verticalListSortingStrategy}> <List dataSource={listItems} renderItem={RenderDraggable} /> </SortableContext> </DndContext> </div> ); }; ``` ### 带分页器 你可以组合使用 Pagination, 实现一个分页的 List ```jsx live=true dir="column" hideInDSM import React, { useState } from 'react'; import { List, Pagination } from '@douyinfe/semi-ui'; () => { const data = [ '围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', ]; const [page, onPageChange] = useState(1); let pageSize = 4; const getData = (page) => { let start = (page - 1) * pageSize; let end = page * pageSize; return data.slice(start, end); }; return ( <div> <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}> <List dataSource={getData(page)} split={false} size='small' className='component-list-demo-booklist' style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }} renderItem={item => <List.Item className='list-item'>{item}</List.Item>} /> <Pagination size='small' style={{ width: '100%', flexBasis: '100%', justifyContent: 'center' }} pageSize={pageSize} total={data.length} currentPage={page} onChange={cPage => onPageChange(cPage)} /> </div> </div> ); }; ``` ### 带筛选器 你可以通过组装 Input 使用,实现对 List 列表的筛选 ```jsx live=true dir="column" hideInDSM import React, { useState } from 'react'; import { List, Input } from '@douyinfe/semi-ui'; import { IconSearch } from '@douyinfe/semi-icons'; () => { const data = [ '围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', ]; const [list, setList] = useState(data); const onSearch = (string) => { let newList; if (string) { newList = data.filter(item => item.includes(string)); } else { newList = data; } setList(newList); }; return ( <div> <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap', border: '1px solid var(--semi-color-border)' }}> <List className='component-list-demo-booklist' dataSource={list} split={false} header={<Input onCompositionEnd={(v) => onSearch(v.target.value)} onChange={(v) => !v ? onSearch() : null} placeholder='搜索' prefix={<IconSearch />} />} size='small' style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }} renderItem={item => <List.Item className='list-item'>{item}</List.Item> } /> </div> </div> ); }; ``` ### 添加删除项 ```jsx live=true dir="column" hideInDSM import React, { useState } from 'react'; import { List, Input, Button } from '@douyinfe/semi-ui'; import { IconMinusCircle, IconPlusCircle } from '@douyinfe/semi-icons'; () => { const data = [ '围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数', ]; const [list, setList] = useState(data.slice(0, 8)); const updateList = (item) => { let newList; if (item) { newList = list.filter(i => item !== i); } else { newList = list.concat(data.slice(list.length, list.length + 1)); } setList(newList); }; return ( <div> <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap', border: '1px solid var(--semi-color-border)' }}> <List className='component-list-demo-booklist' dataSource={list} split={false} size='small' style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }} renderItem={item => <div style={{ margin: 4 }} className='list-item'> <Button type='danger' theme='borderless' icon={<IconMinusCircle />} onClick={() => updateList(item)} style={{ marginRight: 4 }} /> {item} </div> } /> <div style={{ margin: 4, fontSize: 14 }} onClick={() => updateList()}> <Button theme='borderless' icon={<IconPlusCircle />} style={{ marginRight: 4, color: 'var(--semi-color-info)' }}> </Button> 新增书籍 </div> </div> </div> ); }; ``` ### 单选或多选 你可以通过组合使用 Radio 或 Checkbox 将 List 增强为一个列表选择器 ```jsx live=true dir="column" hideInDSM import React, { useState } from 'react'; import { List, Input, Button, Checkbox, Radio, RadioGroup, CheckboxGroup } from '@douyinfe/semi-ui'; () => { const data = [ '围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数', ]; const [page, onPageChange] = useState(1); const [checkboxVal, setCV] = useState([...data[0]]); const [radioVal, setRV] = useState(data[0]); let pageSize = 8; const getData = (page) => { let start = (page - 1) * pageSize; let end = page * pageSize; return data.slice(start, end); }; return ( <div style={{ display: 'flex' }}> <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}> <CheckboxGroup value={checkboxVal} onChange={(value) => setCV(value)}> <List dataSource={getData(page)} className='component-list-demo-booklist' split={false} size='small' style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }} renderItem={item => <List.Item className='list-item'><Checkbox value={item}>{item}</Checkbox></List.Item>} /> </CheckboxGroup> </div> <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap' }}> <RadioGroup value={radioVal} onChange={(e) => setRV(e.target.value)}> <List className='component-list-demo-booklist' dataSource={getData(page)} split={false} size='small' style={{ border: '1px solid var(--semi-color-border)', flexBasis: '100%', flexShrink: 0 }} renderItem={item => <List.Item className='list-item'><Radio value={item}>{item}</Radio></List.Item>} /> </RadioGroup> </div> </div> ); }; ``` ### 响应键盘事件 你可以自行监听对应按键的键盘事件,实现不同 Item 的选择。如下面这个例子,可以使用上下方向键选择不同Item ```jsx live=true dir="column" hideInDSM import React, { useState, useRef } from 'react'; import { List, Input, Button } from '@douyinfe/semi-ui'; () => { const data = [ '围城', '平凡的世界(全三册)', '三体(全集)', '雪中悍刀行(全集)', '撒哈拉的故事', '明朝那些事', '一禅小和尚', '沙丘', '被讨厌的勇气', '罪与罚', '月亮与六便士', '沉默的大多数', '第一人称单数', ]; const [list, setList] = useState(data.slice(0, 10)); const [hoverIndex, setHi] = useState(-1); const i = useRef(-1); let changeIndex = (offset) => { let currentIndex = i.current; let index = currentIndex + offset; if (index < 0) { index = list.length - 1; } if (index >= list.length) { index = 0; } i.current = index; setHi(index); }; useEffect(() => { let keydownHandler = (event) => { let key = event.keyCode; switch (key) { case 38: // KeyCode.UP event.preventDefault(); changeIndex(-1); break; case 40: // KeyCode.DOWN event.preventDefault(); changeIndex(1); break; default: break; } }; window.addEventListener('keydown', keydownHandler); return () => { window.removeEventListener('keydown', keydownHandler); }; }, []); return ( <div> <div style={{ marginRight: 16, width: 280, display: 'flex', flexWrap: 'wrap', border: '1px solid var(--semi-color-border)' }}> <List className='component-list-demo-booklist' dataSource={list} split={false} size='small' style={{ flexBasis: '100%', flexShrink: 0, borderBottom: '1px solid var(--semi-color-border)' }} renderItem={(item, index) => <List.Item className={index === hoverIndex ? 'component-list-demo-booklist-active-item' : ''}>{item}</List.Item> } /> </div> </div> ); }; ``` 以上书单例子的Demo中涉及到的自定义样式如下 ```scss .component-list-demo-booklist { .list-item { &:hover { background-color: var(--semi-color-fill-0); } &:active { background-color: var(--semi-color-fill-1); } } } body > .component-list-demo-drag-item { font-size: 14px; } .component-list-demo-booklist-active-item { background-color: var(--semi-color-fill-0); } ``` ## API 参考 ### List | 属性 | 说明 | 类型 | 默认值 | | ------------ | -------------------------------------------------------- | -------------------------------- | ---------- | | bordered | 是否显示边框 | boolean | `false` | | className | 自定义样式类名 | string | - | | dataSource | 列表数据源 | any[] | - | | emptyContent | 空列表的展示内容 | ReactNode | - | | footer | 列表底部 | ReactNode | - | | grid | 列表栅格配置 | [Grid](/zh-CN/basic/grid#API参考) | - | | header | 列表头部 | ReactNode | - | | layout | 列表布局,支持`vertical`, `horizontal` | string | `vertical` | | loadMore | 加载更多的按钮 | ReactNode | - | | loading | 是否处于加载中,为`true`时会显示 spin | boolean | `false` | | renderItem | 当使用 dataSource 时,可以用 renderItem 自定义渲染列表项 | (item, ind) => ReactNode | - | | size | 列表尺寸,支持 `small`, `default`, `large` | string | `default` | | split | 是否展示分割线 | boolean | `true` | | style | 自定义样式对象 | CSSProperties | - | | onClick | 点击回调事件 | (e: event) => void | - | | onRightClick | 右键点击回调事件 | (e: event) => void | - | ### List grid props 其他 grid 参数,请参考 [Grid](/zh-CN/basic/grid) | 属性 | 说明 | 类型 | 默认值 | | ------ | -------------------------------------------------------- | -------------- | ------ | | span | 栅格占位格数 | number | - | | gutter | 栅格间隔 | number | 0 | | xs | `<576px` 响应式栅格,可为栅格数或一个包含其他属性的对象 | number\|object | - | | sm | `≥576px` 响应式栅格,可为栅格数或一个包含其他属性的对象 | number\|object | - | | md | `≥768px` 响应式栅格,可为栅格数或一个包含其他属性的对象 | number\|object | - | | lg | `≥992px` 响应式栅格,可为栅格数或一个包含其他属性的对象 | number\|object | - | | xl | `≥1200px` 响应式栅格,可为栅格数或一个包含其他属性的对象 | number\|object | - | | xxl | `≥1600px` 响应式栅格,可为栅格数或一个包含其他属性的对象 | number\|object | - | ### List.Item | 属性 | 说明 | 类型 | 默认值 | | ------------ | --------------------------------------------------------------------------------------------------- | --------- | ------------ | | align | 列表项头内容和主体内容的垂直对齐方式,支持 `flex-start`, `flex-end`, `center`, `baseline`, `stretch` | string | `flex-start` | | className | 自定义样式类名 | string | - | | extra | 列表项附加内容 | ReactNode | - | | header | 列表项头内容 | ReactNode | - | | main | 列表项主体内容 | ReactNode | - | | style | 自定义样式对象 | CSSProperties | - | | onClick | 点击回调事件 | (e: event) => void | - | | onRightClick | 右键点击回调事件 | (e: event) => void | - | ## 文案规范 - 首字母大写 - 结尾不跟随标点符号 - 语法平行:如主动态与被动态、陈述句与祈使句混合使用 ## 设计变量 <DesignToken/>