perf: page ui (#5469)

* perf: page ui

* fix: icon

* limit chat items

* limit chat items
This commit is contained in:
Archer
2025-08-15 17:56:49 +08:00
committed by GitHub
parent 76dc23c2e4
commit ce36230285
36 changed files with 619 additions and 322 deletions

View File

@@ -52,6 +52,7 @@ export const iconPaths = {
'common/errorFill': () => import('./icons/common/errorFill.svg'),
'common/file/move': () => import('./icons/common/file/move.svg'),
'common/fileNotFound': () => import('./icons/common/fileNotFound.svg'),
'common/first_page': () => import('./icons/common/first_page.svg'),
'common/folderFill': () => import('./icons/common/folderFill.svg'),
'common/folderImport': () => import('./icons/common/folderImport.svg'),
'common/fullScreenLight': () => import('./icons/common/fullScreenLight.svg'),
@@ -67,6 +68,7 @@ export const iconPaths = {
'common/language/China': () => import('./icons/common/language/China.svg'),
'common/language/en': () => import('./icons/common/language/en.svg'),
'common/language/zh': () => import('./icons/common/language/zh.svg'),
'common/latest_page': () => import('./icons/common/latest_page.svg'),
'common/layer': () => import('./icons/common/layer.svg'),
'common/leftArrowLight': () => import('./icons/common/leftArrowLight.svg'),
'common/line': () => import('./icons/common/line.svg'),

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 6" fill="none">
<path d="M6.705 5.295L4.41 3L6.705 0.705L6 0L3 3L6 6L6.705 5.295ZM0.5 0H1.5V6H0.5V0Z" />
</svg>

After

Width:  |  Height:  |  Size: 169 B

View File

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 7 6" fill="none">
<path d="M0.295044 0.705L2.59004 3L0.295044 5.295L1.00004 6L4.00004 3L1.00004 0L0.295044 0.705ZM5.50004 0H6.50004V6H5.50004V0Z" />
</svg>

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -1,4 +1,3 @@
<svg viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path fill-rule="evenodd" clip-rule="evenodd"
d="M15.7071 5.79289C16.0976 6.18342 16.0976 6.81658 15.7071 7.20711L10.4142 12.5L15.7071 17.7929C16.0976 18.1834 16.0976 18.8166 15.7071 19.2071C15.3166 19.5976 14.6834 19.5976 14.2929 19.2071L8.29289 13.2071C7.90237 12.8166 7.90237 12.1834 8.29289 11.7929L14.2929 5.79289C14.6834 5.40237 15.3166 5.40237 15.7071 5.79289Z" />
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 8" fill="none">
<path d="M1.914 3.99987L4.389 6.47487L3.682 7.18187L0.5 3.99987L3.682 0.817871L4.389 1.52487L1.914 3.99987Z" />
</svg>

Before

Width:  |  Height:  |  Size: 463 B

After

Width:  |  Height:  |  Size: 192 B

View File

@@ -1,6 +1,3 @@
<svg t="1706532590649" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
p-id="3146">
<path
d="M236.552013 926.853955a55.805997 55.805997 0 0 0 0 80.454996 59.682997 59.682997 0 0 0 82.794996 0l468.099978-455.081978a55.805997 55.805997 0 0 0 0-80.453996L319.348009 16.689999a59.682997 59.682997 0 0 0-82.794996 0 55.805997 55.805997 0 0 0 0 80.454996L663.401993 511.999975 236.624013 926.853955z"
p-id="3147"></path>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 5 8" fill="none">
<path d="M3.08596 3.99987L0.610962 1.52487L1.31796 0.817871L4.49996 3.99987L1.31796 7.18187L0.610962 6.47487L3.08596 3.99987Z" />
</svg>

Before

Width:  |  Height:  |  Size: 483 B

After

Width:  |  Height:  |  Size: 210 B

View File

@@ -1,12 +1,22 @@
import { useRef, useState, useCallback, type RefObject, type ReactNode, useMemo } from 'react';
import { IconButton, Flex, Box, Input, type BoxProps } from '@chakra-ui/react';
import { ArrowBackIcon, ArrowForwardIcon } from '@chakra-ui/icons';
import {
useRef,
useState,
useCallback,
type RefObject,
type ReactNode,
useMemo,
useEffect
} from 'react';
import type { FlexProps } from '@chakra-ui/react';
import { Flex, Box, type BoxProps } from '@chakra-ui/react';
import MyIcon from '../components/common/Icon';
import type { IconNameType } from '../components/common/Icon/type';
import { useTranslation } from 'next-i18next';
import { useToast } from './useToast';
import { getErrText } from '@fastgpt/global/common/error/utils';
import {
useBoolean,
useLockFn,
useCreation,
useMemoizedFn,
useRequest,
useScroll,
@@ -14,13 +24,16 @@ import {
} from 'ahooks';
import { type PaginationProps, type PaginationResponse } from '../common/fetch/type';
import MyMenu from '../components/common/MyMenu';
import { useSystem } from './useSystem';
const thresholdVal = 200;
export function usePagination<DataT, ResT = {}>(
api: (data: PaginationProps<DataT>) => Promise<PaginationResponse<ResT>>,
{
pageSize = 10,
defaultPageSize = 10,
pageSizeOptions: defaultPageSizeOptions,
params,
defaultRequest = true,
type = 'button',
@@ -31,7 +44,8 @@ export function usePagination<DataT, ResT = {}>(
pollingInterval,
pollingWhenHidden = false
}: {
pageSize?: number;
defaultPageSize?: number;
pageSizeOptions?: number[];
params?: DataT;
defaultRequest?: boolean;
type?: 'button' | 'scroll';
@@ -45,11 +59,18 @@ export function usePagination<DataT, ResT = {}>(
}
) {
const { toast } = useToast();
const { isPc } = useSystem();
const { t } = useTranslation();
const [isLoading, { setTrue, setFalse }] = useBoolean(false);
const [pageNum, setPageNum] = useState(1);
const [pageSize, setPageSize] = useState(defaultPageSize);
const pageSizeOptions = useCreation(
() => defaultPageSizeOptions || [10, 20, 50, 100],
[defaultPageSizeOptions]
);
const [total, setTotal] = useState(0);
const [data, setData] = useState<ResT[]>([]);
const totalDataLength = useMemo(() => Math.max(total, data.length), [total, data.length]);
@@ -119,67 +140,110 @@ export function usePagination<DataT, ResT = {}>(
const Pagination = useCallback(() => {
const maxPage = Math.ceil(totalDataLength / pageSize);
return (
<Flex alignItems={'center'} justifyContent={'end'}>
<IconButton
isDisabled={pageNum === 1}
icon={<ArrowBackIcon />}
aria-label={'left'}
size={'smSquare'}
isLoading={isLoading}
onClick={() => fetchData(pageNum - 1)}
/>
<Flex mx={2} alignItems={'center'}>
<Input
defaultValue={pageNum}
w={'50px'}
h={'30px'}
size={'xs'}
type={'number'}
min={1}
max={maxPage}
onBlur={(e) => {
const val = +e.target.value;
if (val === pageNum) return;
if (val >= maxPage) {
fetchData(maxPage);
} else if (val < 1) {
fetchData(1);
} else {
fetchData(+e.target.value);
const IconButton = ({
icon,
isDisabled,
onClick,
...props
}: {
icon: IconNameType;
isDisabled?: boolean;
onClick: () => void;
} & FlexProps) => {
isDisabled = isDisabled || isLoading;
return (
<Flex
alignItems={'center'}
justifyContent={'center'}
borderRadius={'full'}
w={'24px'}
h={'24px'}
cursor={'pointer'}
bg={'myGray.150'}
{...(isDisabled
? {
opacity: 0.5
}
}}
onKeyDown={(e) => {
// @ts-ignore
const val = +e.target.value;
if (val && e.key === 'Enter') {
if (val === pageNum) return;
if (val >= maxPage) {
fetchData(maxPage);
} else if (val < 1) {
fetchData(1);
} else {
fetchData(val);
}
}
}}
/>
<Box mx={2}>/</Box>
{maxPage}
: {
onClick
})}
{...props}
>
<MyIcon name={icon} w={'6px'} color={'myGray.900'} />
</Flex>
<IconButton
isDisabled={pageNum === maxPage}
icon={<ArrowForwardIcon />}
aria-label={'left'}
size={'sm'}
isLoading={isLoading}
w={'28px'}
h={'28px'}
onClick={() => fetchData(pageNum + 1)}
/>
);
};
return (
<Flex alignItems={'center'} justifyContent={'center'} fontSize={'sm'} userSelect={'none'}>
{isPc && <Box color={'myGray.500'}>{t('common:total_num', { num: totalDataLength })}</Box>}
<Flex alignItems={'center'} ml={6} mr={4}>
{isPc && (
<IconButton
mr={2}
isDisabled={pageNum === 1}
icon="common/first_page"
onClick={() => fetchData(1)}
/>
)}
<IconButton
isDisabled={pageNum === 1}
icon="common/leftArrowLight"
onClick={() => fetchData(pageNum - 1)}
/>
<Box ml={4} color={'myGray.500'}>
{pageNum}
</Box>
<Box mx={1} color={'myGray.500'}>
/
</Box>
<Box mr={4} color={'myGray.900'}>
{maxPage}
</Box>
<IconButton
isDisabled={pageNum === maxPage}
icon="common/rightArrowLight"
onClick={() => fetchData(pageNum + 1)}
/>
{isPc && (
<IconButton
ml={2}
isDisabled={pageNum === maxPage}
icon="common/latest_page"
onClick={() => fetchData(maxPage)}
/>
)}
</Flex>
{isPc && (
<MyMenu
menuList={[
{
label: '',
children: pageSizeOptions.map((item) => ({
label: `${item}`,
isActive: pageSize === item,
onClick: () => setPageSize(item)
}))
}
]}
Button={
<Flex alignItems={'center'} cursor={'pointer'}>
<Box color={'myGray.900'}>{pageSize}</Box>
<Box mx={1} color={'myGray.500'}>
/
</Box>
<Box color={'myGray.500'}>{t('common:page')}</Box>
<MyIcon ml={1} name={'core/chat/chevronDown'} w={'14px'} color={'myGray.900'} />
</Flex>
}
/>
)}
</Flex>
);
}, [isLoading, totalDataLength, pageSize, fetchData, pageNum]);
}, [totalDataLength, isPc, pageSize, t, pageNum, pageSizeOptions, isLoading, fetchData]);
// Scroll pagination
const DefaultRef = useRef<HTMLDivElement>(null);
@@ -259,6 +323,9 @@ export function usePagination<DataT, ResT = {}>(
throttleWait: 100
}
);
useEffect(() => {
data.length > 0 && fetchData();
}, [pageSize]);
useRequest(
async () => {

View File

@@ -0,0 +1,98 @@
import type { ReactNode } from 'react';
import React, { useState, useCallback, useMemo } from 'react';
import type { FlexProps } from '@chakra-ui/react';
import { Box, Checkbox, Flex } from '@chakra-ui/react';
import { useTranslation } from 'next-i18next';
export type TableMultipleSelectHookProps<T = any> = {
list: T[];
getItemId: (item: T) => string | number;
};
export const useTableMultipleSelect = <T = any,>({
list,
getItemId
}: TableMultipleSelectHookProps<T>) => {
const { t } = useTranslation();
const [selectedItems, setSelectedItems] = useState<T[]>([]);
// Toggle single item selection
const toggleSelect = useCallback(
(item: T) => {
const itemId = getItemId(item);
setSelectedItems((prev) => {
const isSelected = prev.some((selected) => getItemId(selected) === itemId);
if (isSelected) {
return prev.filter((selected) => getItemId(selected) !== itemId);
} else {
return [...prev, item];
}
});
},
[getItemId]
);
// Check if item is selected
const isSelected = useCallback(
(item: T) => {
const itemId = getItemId(item);
return selectedItems.some((selected) => getItemId(selected) === itemId);
},
[selectedItems, getItemId]
);
const isSelecteAll = useMemo(() => {
return list.length > 0 && list.every((item) => isSelected(item));
}, [list, isSelected]);
// Select all items
const selectAllTrigger = useCallback(() => {
if (isSelecteAll) {
setSelectedItems([]);
} else {
setSelectedItems((pre) => [...pre, ...list.filter((item) => !isSelected(item))]);
}
}, [isSelecteAll, list, isSelected]);
const selectedCount = selectedItems.length;
// Check if has selections
const hasSelections = selectedCount > 0;
// Floating Action Bar component
const FloatingActionBar = useCallback(
({
children,
Controler,
...props
}: { children: ReactNode; Controler: ReactNode } & FlexProps) => {
return (
<Flex w={'100%'} bg="white" px={6} pt={4} pb={2} alignItems="center" {...props}>
{hasSelections && (
<>
<Checkbox size="sm" isChecked={isSelecteAll} onChange={selectAllTrigger} />
<Box ml={2} fontSize="sm" color="gray.600">
{t('common:select_count_num', { num: selectedCount })}
</Box>
<Box flex={'1 0 0'} ml={4}>
{Controler}
</Box>
</>
)}
<Box flex={hasSelections ? '' : '1 0 0'}>{children}</Box>
</Flex>
);
},
[hasSelections, isSelecteAll, selectAllTrigger, selectedCount, t]
);
return {
selectedItems,
isSelecteAll,
selectAllTrigger,
hasSelections,
toggleSelect,
isSelected,
FloatingActionBar,
setSelectedItems
};
};

View File

@@ -939,6 +939,7 @@
"not_yet_introduced": "No Introduction Yet",
"open_folder": "Open Folder",
"option": "Option",
"page": "Page",
"page_center": "Page Center",
"pay.amount": "Amount",
"pay.error_desc": "There was a problem when converting payment routes",
@@ -1024,6 +1025,7 @@
"search_tool": "Search Tools",
"secret_key": "Secret",
"secret_tips": "The value will not return plaintext again after saving",
"select_count_num": "{{num}} item selected",
"select_file_failed": "File Selection Failed",
"select_reference_variable": "Select Reference Variable",
"select_template": "Select Template",
@@ -1238,6 +1240,7 @@
"template_market": "Template Market",
"textarea_variable_picker_tip": "Enter \"/\" to select a variable",
"to_dataset": "To dataset",
"total_num": "Total: {{num}}",
"ui.textarea.Magnifying": "Magnifying",
"un_used": "Unused",
"unauth_token": "The certificate has expired, please log in again",

View File

@@ -14,6 +14,7 @@
"backup_dataset_tip": "You can reimport the downloaded csv file when exporting the knowledge base.",
"backup_mode": "Backup import",
"backup_template_invalid": "The backup file format is incorrect, it should be the csv file with the first column as q,a,indexes",
"batch_delete": "Delete",
"chunk_max_tokens": "max_tokens",
"chunk_process_params": "Block processing parameters",
"chunk_size": "Block size",
@@ -41,6 +42,7 @@
"common_dataset_desc": "Building a knowledge base by importing files, web page links, or manual entry",
"condition": "condition",
"config_sync_schedule": "Configure scheduled synchronization",
"confirm_delete_collection": "Confirm to delete {{num }} files?",
"confirm_import_images": "Total {{num}} | Confirm create",
"confirm_to_rebuild_embedding_tip": "Are you sure you want to switch the index for the Dataset?\nSwitching the index is a significant operation that requires re-indexing all data in your Dataset, which may take a long time. Please ensure your account has sufficient remaining points.\n\nAdditionally, you need to update the applications that use this Dataset to avoid conflicts with other indexed model Datasets.",
"core.dataset.Image collection": "Image dataset",

View File

@@ -665,10 +665,10 @@
"core.module.template.System Plugin": "系统插件",
"core.module.template.System input module": "系统输入",
"core.module.template.Team app": "团队应用",
"core.module.template.all_team_app": "全部",
"core.module.template.UnKnow Module": "未知模块",
"core.module.template.ai_chat": "AI 对话",
"core.module.template.ai_chat_intro": "AI 大模型对话",
"core.module.template.all_team_app": "全部",
"core.module.template.config_params": "可以配置应用的系统参数",
"core.module.template.empty_plugin": "空白插件",
"core.module.template.empty_workflow": "空白工作流",
@@ -939,6 +939,7 @@
"not_yet_introduced": "暂无介绍",
"open_folder": "打开文件夹",
"option": "选项",
"page": "页",
"page_center": "页面居中",
"pay.amount": "金额",
"pay.error_desc": "转换支付途径时出现了问题",
@@ -1024,6 +1025,7 @@
"search_tool": "搜索工具",
"secret_key": "密钥",
"secret_tips": "值保存后不会再次明文返回",
"select_count_num": "已选 {{num}} 项",
"select_file_failed": "选择文件异常",
"select_reference_variable": "选择引用变量",
"select_template": "选择模板",
@@ -1239,6 +1241,7 @@
"template_market": "模板市场",
"textarea_variable_picker_tip": "输入\"/\"可选择变量",
"to_dataset": "前往知识库",
"total_num": "总数: {{num}}",
"ui.textarea.Magnifying": "放大",
"un_used": "未使用",
"unauth_token": "凭证已过期,请重新登录",

View File

@@ -14,6 +14,7 @@
"backup_dataset_tip": "可以将导出知识库时,下载的 csv 文件重新导入。",
"backup_mode": "备份导入",
"backup_template_invalid": "备份文件格式不正确,应该是首列为 q,a,indexes 的 csv 文件",
"batch_delete": "批量删除",
"chunk_max_tokens": "分块上限",
"chunk_process_params": "分块处理参数",
"chunk_size": "分块大小",
@@ -41,6 +42,7 @@
"common_dataset_desc": "通过导入文件、网页链接或手动录入形式构建知识库",
"condition": "条件",
"config_sync_schedule": "配置定时同步",
"confirm_delete_collection": "确认删除 {{num }} 个文件?",
"confirm_import_images": "共 {{num}} 张图片 | 确认创建",
"confirm_to_rebuild_embedding_tip": "确认为知识库切换索引?\n切换索引是一个非常重量的操作需要对您知识库内所有数据进行重新索引时间可能较长请确保账号内剩余积分充足。\n\n此外你还需要注意修改选择该知识库的应用避免它们与其他索引模型知识库混用。",
"core.dataset.Image collection": "图片数据集",

View File

@@ -938,6 +938,7 @@
"not_yet_introduced": "暫無介紹",
"open_folder": "開啟資料夾",
"option": "選項",
"page": "頁",
"page_center": "頁面置中",
"pay.amount": "金額",
"pay.error_desc": "轉換支付途徑時出現了問題",
@@ -1023,6 +1024,7 @@
"search_tool": "搜索工具",
"secret_key": "密鑰",
"secret_tips": "值保存後不會再次明文返回",
"select_count_num": "已選 {{num}} 項",
"select_file_failed": "選擇檔案失敗",
"select_reference_variable": "選擇引用變數",
"select_template": "選擇範本",
@@ -1237,6 +1239,7 @@
"template_market": "模板市場",
"textarea_variable_picker_tip": "輸入「/」以選擇變數",
"to_dataset": "前往知識庫",
"total_num": "總數: {{num}}",
"ui.textarea.Magnifying": "放大",
"un_used": "未使用",
"unauth_token": "憑證已過期,請重新登入",

View File

@@ -14,6 +14,7 @@
"backup_dataset_tip": "可以將導出知識庫時,下載的 csv 文件重新導入。",
"backup_mode": "備份導入",
"backup_template_invalid": "備份文件格式不正確,應該是首列為 q,a,indexes 的 csv 文件",
"batch_delete": "批量刪除",
"chunk_max_tokens": "分塊上限",
"chunk_process_params": "分塊處理參數",
"chunk_size": "分塊大小",
@@ -41,6 +42,7 @@
"common_dataset_desc": "通過導入文件、網頁鏈接或手動錄入形式構建知識庫",
"condition": "條件",
"config_sync_schedule": "設定定時同步",
"confirm_delete_collection": "確認刪除 {{num }} 個文件?",
"confirm_import_images": "共 {{num}} 張圖片 | 確認創建",
"confirm_to_rebuild_embedding_tip": "確定要為資料集切換索引嗎?\n切換索引是一個重要的操作需要對您資料集內所有資料重新建立索引可能需要較長時間請確保帳號內剩餘點數充足。\n\n此外您還需要注意修改使用此資料集的應用程式避免與其他索引模型資料集混用。",
"core.dataset.Image collection": "圖片數據集",

View File

@@ -549,25 +549,37 @@ const Checkbox = checkBoxMultiStyle({
sizes: {
sm: checkBoxPart({
control: {
width: '18px',
height: '18px',
width: '16px',
height: '16px',
borderWidth: '2px'
},
icon: {
fontSize: '10px'
}
}),
md: checkBoxPart({
control: {
width: '20px',
height: '20px',
width: '18px',
height: '18px',
borderWidth: '2px'
},
icon: {
fontSize: '12px'
}
}),
lg: checkBoxPart({
control: {
width: '24px',
height: '24px',
width: '20px',
height: '20px',
borderWidth: '2px'
},
icon: {
fontSize: '14px'
}
})
},
defaultProps: {
size: 'sm'
}
});