refactor: The PageScrollList component supports two generic parameters

This commit is contained in:
zhaoying
2026-01-29 15:57:52 +08:00
parent 9c3e0b5541
commit 4fe32b7dbc
4 changed files with 31 additions and 29 deletions

View File

@@ -1,13 +1,14 @@
import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react'; import React, { useEffect, useState, useRef, forwardRef, useImperativeHandle } from 'react';
import { List, Skeleton} from 'antd'; import { List } from 'antd';
import InfiniteScroll from 'react-infinite-scroll-component'; import InfiniteScroll from 'react-infinite-scroll-component';
import { request } from '@/utils/request'; import { request } from '@/utils/request';
import Empty from '@/components/Empty'; import PageEmpty from '@/components/Empty/PageEmpty'
import PageLoading from '@/components/Empty/PageLoading'
const PAGE_SIZE = 20; const PAGE_SIZE = 20;
interface ApiResponse { interface ApiResponse<T> {
items?: Record<string, unknown>[]; items?: T[];
page: { page: {
page: number; page: number;
pagesize: number; pagesize: number;
@@ -19,26 +20,25 @@ export interface PageScrollListRef {
refresh: () => void; refresh: () => void;
} }
interface PageScrollListProps { interface PageScrollListProps<T, Q = Record<string, unknown>> {
url: string; url: string;
renderItem: (item: Record<string, unknown>) => React.ReactNode; renderItem: (item: T) => React.ReactNode;
query?: Record<string, unknown>; query?: Q;
column?: number; column?: number;
className?: string; className?: string;
} }
const PageScrollList = forwardRef(<T, Q = Record<string, unknown>>({
const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
renderItem, renderItem,
query, query,
url, url,
column = 4, column = 4,
className = '', className = '',
}, ref) => { }: PageScrollListProps<T, Q>, ref: React.Ref<PageScrollListRef>) => {
useImperativeHandle(ref, () => ({ useImperativeHandle(ref, () => ({
refresh, refresh,
})); }));
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [data, setData] = useState<Record<string, unknown>[]>([]); const [data, setData] = useState<T[]>([]);
const [page, setPage] = useState(1); const [page, setPage] = useState(1);
const [hasMore, setHasMore] = useState(true); const [hasMore, setHasMore] = useState(true);
const scrollRef = useRef<HTMLDivElement>(null); const scrollRef = useRef<HTMLDivElement>(null);
@@ -54,8 +54,8 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
...(query||{}), ...(query||{}),
}) })
.then((res) => { .then((res) => {
const response = res as ApiResponse; const response = res as ApiResponse<T>;
const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response : []; const results = Array.isArray(response.items) ? response.items : Array.isArray(response) ? response as T[] : [];
if (flag) { if (flag) {
setData(results); setData(results);
} else { } else {
@@ -104,7 +104,7 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
dataLength={data.length} dataLength={data.length}
next={loadMoreData} next={loadMoreData}
hasMore={hasMore} hasMore={hasMore}
loader={<Skeleton active />} loader={<PageLoading />}
// endMessage={<Divider plain>It is all, nothing more 🤐</Divider>} // endMessage={<Divider plain>It is all, nothing more 🤐</Divider>}
scrollableTarget="scrollableDiv" scrollableTarget="scrollableDiv"
> >
@@ -118,11 +118,11 @@ const PageScrollList = forwardRef<PageScrollListRef, PageScrollListProps>(({
</List.Item> </List.Item>
)} )}
/> />
) : !loading ? <Empty /> : null} ) : !loading ? <PageEmpty /> : null}
</InfiniteScroll> </InfiniteScroll>
</div> </div>
</> </>
); );
}); }) as <T = Record<string, unknown>, Q = Record<string, unknown>>(props: PageScrollListProps<T, Q> & { ref?: React.Ref<PageScrollListRef> }) => React.ReactElement;
export default PageScrollList; export default PageScrollList;

View File

@@ -58,13 +58,12 @@ const ApiKeyManagement: React.FC = () => {
</Button> </Button>
</div> </div>
<PageScrollList <PageScrollList<ApiKey, { is_active: boolean; type: string }>
ref={scrollListRef} ref={scrollListRef}
url={getApiKeyListUrl} url={getApiKeyListUrl}
query={{ is_active: true, type: 'service' }} query={{ is_active: true, type: 'service' }}
column={2} column={2}
renderItem={(item: Record<string, unknown>) => { renderItem={(apiKeyItem) => {
let apiKeyItem = item as unknown as ApiKey
return ( return (
<RbCard <RbCard
title={apiKeyItem.name} title={apiKeyItem.name}

View File

@@ -3,7 +3,7 @@ import { useTranslation } from 'react-i18next';
import { Button, Row, Col, App } from 'antd'; import { Button, Row, Col, App } from 'antd';
import clsx from 'clsx'; import clsx from 'clsx';
import { DeleteOutlined } from '@ant-design/icons'; import { DeleteOutlined } from '@ant-design/icons';
import type { Application, ApplicationModalRef } from './types'; import type { Application, ApplicationModalRef, Query } from './types';
import ApplicationModal from './components/ApplicationModal'; import ApplicationModal from './components/ApplicationModal';
import SearchInput from '@/components/SearchInput' import SearchInput from '@/components/SearchInput'
import RbCard from '@/components/RbCard/Card' import RbCard from '@/components/RbCard/Card'
@@ -14,7 +14,7 @@ import { formatDateTime } from '@/utils/format';
const ApplicationManagement: React.FC = () => { const ApplicationManagement: React.FC = () => {
const { t } = useTranslation(); const { t } = useTranslation();
const { modal } = App.useApp(); const { modal } = App.useApp();
const [query, setQuery] = useState({}); const [query, setQuery] = useState<Query>({} as Query);
const applicationModalRef = useRef<ApplicationModalRef>(null); const applicationModalRef = useRef<ApplicationModalRef>(null);
const scrollListRef = useRef<PageScrollListRef>(null) const scrollListRef = useRef<PageScrollListRef>(null)
@@ -47,7 +47,7 @@ const ApplicationManagement: React.FC = () => {
} }
return ( return (
<> <>
<Row gutter={16} className="rb:mb-[16px]"> <Row gutter={16} className="rb:mb-4">
<Col span={12}> <Col span={12}>
<SearchInput <SearchInput
placeholder={t('application.searchPlaceholder')} placeholder={t('application.searchPlaceholder')}
@@ -62,22 +62,22 @@ const ApplicationManagement: React.FC = () => {
</Col> </Col>
</Row> </Row>
<PageScrollList <PageScrollList<Application, Query>
ref={scrollListRef} ref={scrollListRef}
url={getApplicationListUrl} url={getApplicationListUrl}
query={query} query={query}
renderItem={(item: Application) => ( renderItem={(item) => (
<RbCard <RbCard
title={item.name} title={item.name}
avatar={ avatar={
<div className="rb:w-[48px] rb:h-[48px] rb:rounded-[8px] rb:mr-[13px] rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]"> <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
{item.name[0]} {item.name[0]}
</div> </div>
} }
> >
{['type', 'source', 'created_at'].map((key, index) => ( {['type', 'source', 'created_at'].map((key, index) => (
<div key={key} className={clsx("rb:flex rb:justify-between rb:gap-[20px] rb:font-regular rb:text-[14px]", { <div key={key} className={clsx("rb:flex rb:justify-between rb:gap-5 rb:font-regular rb:text-[14px]", {
'rb:mt-[12px]': index !== 0 'rb:mt-3': index !== 0
})}> })}>
<span className="rb:text-[#5B6167]">{t(`application.${key}`)}</span> <span className="rb:text-[#5B6167]">{t(`application.${key}`)}</span>
<span className={clsx({ <span className={clsx({
@@ -89,14 +89,14 @@ const ApplicationManagement: React.FC = () => {
: key === 'source' && !item.is_shared : key === 'source' && !item.is_shared
? t('application.configuration') ? t('application.configuration')
: key === 'created_at' : key === 'created_at'
? formatDateTime(item[key as keyof Application], 'YYYY-MM-DD HH:mm:ss') ? formatDateTime(item.created_at, 'YYYY-MM-DD HH:mm:ss')
: t(`application.${item[key as keyof Application]}`) : t(`application.${item[key as keyof Application]}`)
} }
</span> </span>
</div> </div>
))} ))}
<div className="rb:mt-[20px] rb:flex rb:justify-between rb:gap-[10px]"> <div className="rb:mt-5 rb:flex rb:justify-between rb:gap-2.5">
<Button type="primary" ghost className="rb:w-[calc(100%-46px)]" onClick={() => handleEdit(item)}>{t('application.configuration')}</Button> <Button type="primary" ghost className="rb:w-[calc(100%-46px)]" onClick={() => handleEdit(item)}>{t('application.configuration')}</Button>
<Button icon={<DeleteOutlined />} onClick={() => handleDelete(item)}></Button> <Button icon={<DeleteOutlined />} onClick={() => handleDelete(item)}></Button>
</div> </div>

View File

@@ -1,4 +1,7 @@
// 应用数据类型 // 应用数据类型
export interface Query {
search: string;
}
export interface Application { export interface Application {
id: string; id: string;
workspace_id: string; workspace_id: string;