perf: scroll page code

This commit is contained in:
archer 2025-01-13 13:40:42 +08:00
parent ec0cef09a2
commit 62bcff2ff0
No known key found for this signature in database
GPG Key ID: 4446499B846D4A9E
14 changed files with 302 additions and 250 deletions

View File

@ -20,6 +20,10 @@ const TeamMemberSchema = new Schema({
ref: userCollectionName, ref: userCollectionName,
required: true required: true
}, },
avatar: {
type: String,
default: () => getRandomUserAvatar()
},
name: { name: {
type: String, type: String,
default: 'Member' default: 'Member'
@ -36,10 +40,6 @@ const TeamMemberSchema = new Schema({
type: Boolean, type: Boolean,
default: false default: false
}, },
avatar: {
type: String,
default: getRandomUserAvatar()
},
// Abandoned // Abandoned
role: { role: {

View File

@ -97,6 +97,46 @@ const MySelect = <T = any,>(
const isSelecting = loading || isLoading; const isSelecting = loading || isLoading;
const ListRender = useMemo(() => {
return (
<>
{list.map((item, i) => (
<Box key={i}>
<MenuItem
{...menuItemStyles}
{...(value === item.value
? {
ref: SelectedItemRef,
color: 'primary.700',
bg: 'myGray.100',
fontWeight: '600'
}
: {
color: 'myGray.900'
})}
onClick={() => {
if (onChange && value !== item.value) {
onChange(item.value);
}
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
display={'block'}
>
<Box>{item.label}</Box>
{item.description && (
<Box color={'myGray.500'} fontSize={'xs'}>
{item.description}
</Box>
)}
</MenuItem>
{item.showBorder && <MyDivider my={2} />}
</Box>
))}
</>
);
}, []);
return ( return (
<Box <Box
css={css({ css={css({
@ -164,45 +204,7 @@ const MySelect = <T = any,>(
maxH={'40vh'} maxH={'40vh'}
overflowY={'auto'} overflowY={'auto'}
> >
{(() => { {ScrollData ? <ScrollData>{ListRender}</ScrollData> : ListRender}
const component = list.map((item, i) => (
<Box key={i}>
<MenuItem
{...menuItemStyles}
{...(value === item.value
? {
ref: SelectedItemRef,
color: 'primary.700',
bg: 'myGray.100',
fontWeight: '600'
}
: {
color: 'myGray.900'
})}
onClick={() => {
if (onChange && value !== item.value) {
onChange(item.value);
}
}}
whiteSpace={'pre-wrap'}
fontSize={'sm'}
display={'block'}
>
<Box>{item.label}</Box>
{item.description && (
<Box color={'myGray.500'} fontSize={'xs'}>
{item.description}
</Box>
)}
</MenuItem>
{item.showBorder && <MyDivider my={2} />}
</Box>
));
if (ScrollData) {
return <ScrollData>{component}</ScrollData>;
}
return component;
})()}
</MenuList> </MenuList>
</Menu> </Menu>
</Box> </Box>

View File

@ -15,7 +15,7 @@ function UserBox({ sourceMember, avatarSize = '1.25rem', ...props }: UserBoxProp
<HStack space="1" {...props}> <HStack space="1" {...props}>
<Avatar src={sourceMember.avatar} w={avatarSize} /> <Avatar src={sourceMember.avatar} w={avatarSize} />
<Box>{sourceMember.name}</Box> <Box>{sourceMember.name}</Box>
{sourceMember.status === 'leave' && <Tag color="gray">{t('account_team:leaved')}</Tag>} {sourceMember.status === 'leave' && <Tag color="gray">{t('common:user_leaved')}</Tag>}
</HStack> </HStack>
); );
} }

View File

@ -269,8 +269,10 @@ export function useScrollPagination<
({ ({
children, children,
ScrollContainerRef, ScrollContainerRef,
isLoading,
...props ...props
}: { }: {
isLoading?: boolean;
children: ReactNode; children: ReactNode;
ScrollContainerRef?: RefObject<HTMLDivElement>; ScrollContainerRef?: RefObject<HTMLDivElement>;
} & BoxProps) => { } & BoxProps) => {
@ -302,7 +304,7 @@ export function useScrollPagination<
); );
return ( return (
<Box {...props} ref={ref} overflow={'overlay'}> <MyBox {...props} ref={ref} overflow={'overlay'} isLoading={isLoading}>
{scrollLoadType === 'top' && total > 0 && isLoading && ( {scrollLoadType === 'top' && total > 0 && isLoading && (
<Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}> <Box mt={2} fontSize={'xs'} color={'blackAlpha.500'} textAlign={'center'}>
{t('common:common.is_requesting')} {t('common:common.is_requesting')}
@ -325,7 +327,7 @@ export function useScrollPagination<
</Box> </Box>
)} )}
{isEmpty && EmptyTip} {isEmpty && EmptyTip}
</Box> </MyBox>
); );
} }
); );

View File

@ -39,6 +39,7 @@
"classification": "Classification", "classification": "Classification",
"click_to_resume": "Click to Resume", "click_to_resume": "Click to Resume",
"code_editor": "Code Editor", "code_editor": "Code Editor",
"code_error.account_error": "Incorrect account name or password",
"code_error.app_error.invalid_app_type": "Invalid Application Type", "code_error.app_error.invalid_app_type": "Invalid Application Type",
"code_error.app_error.invalid_owner": "Unauthorized Application Owner", "code_error.app_error.invalid_owner": "Unauthorized Application Owner",
"code_error.app_error.not_exist": "Application Does Not Exist", "code_error.app_error.not_exist": "Application Does Not Exist",
@ -95,7 +96,6 @@
"code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync", "code_error.team_error.website_sync_not_enough": "Unauthorized to Use Website Sync",
"code_error.token_error_code.403": "Invalid Login Status, Please Re-login", "code_error.token_error_code.403": "Invalid Login Status, Please Re-login",
"code_error.user_error.balance_not_enough": "Insufficient Account Balance", "code_error.user_error.balance_not_enough": "Insufficient Account Balance",
"code_error.account_error": "Incorrect account name or password",
"code_error.user_error.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate", "code_error.user_error.bin_visitor_guest": "You Are Currently a Guest, Unauthorized to Operate",
"code_error.user_error.un_auth_user": "User Not Found", "code_error.user_error.un_auth_user": "User Not Found",
"common.Action": "Action", "common.Action": "Action",
@ -1273,6 +1273,7 @@
"user.team.role.Visitor": "visitor", "user.team.role.Visitor": "visitor",
"user.team.role.writer": "writable member", "user.team.role.writer": "writable member",
"user.type": "Type", "user.type": "Type",
"user_leaved": "Leaved",
"verification": "Verification", "verification": "Verification",
"workflow.template.communication": "Communication", "workflow.template.communication": "Communication",
"xx_search_result": "{{key}} Search Results", "xx_search_result": "{{key}} Search Results",

View File

@ -35,7 +35,6 @@
"user_team_invite_member": "邀请成员", "user_team_invite_member": "邀请成员",
"user_team_leave_team": "离开团队", "user_team_leave_team": "离开团队",
"user_team_leave_team_failed": "离开团队失败", "user_team_leave_team_failed": "离开团队失败",
"leaved": "已离职",
"waiting": "待接受", "waiting": "待接受",
"sync_immediately": "立即同步", "sync_immediately": "立即同步",
"sync_member_failed": "同步成员失败", "sync_member_failed": "同步成员失败",

View File

@ -43,6 +43,7 @@
"classification": "分类", "classification": "分类",
"click_to_resume": "点击恢复", "click_to_resume": "点击恢复",
"code_editor": "代码编辑", "code_editor": "代码编辑",
"code_error.account_error": "账号名或密码错误",
"code_error.app_error.invalid_app_type": "错误的应用类型", "code_error.app_error.invalid_app_type": "错误的应用类型",
"code_error.app_error.invalid_owner": "非法的应用所有者", "code_error.app_error.invalid_owner": "非法的应用所有者",
"code_error.app_error.not_exist": "应用不存在", "code_error.app_error.not_exist": "应用不存在",
@ -99,7 +100,6 @@
"code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~", "code_error.team_error.website_sync_not_enough": "无权使用Web站点同步~",
"code_error.token_error_code.403": "登录状态无效,请重新登录", "code_error.token_error_code.403": "登录状态无效,请重新登录",
"code_error.user_error.balance_not_enough": "账号余额不足~", "code_error.user_error.balance_not_enough": "账号余额不足~",
"code_error.account_error": "账号名或密码错误",
"code_error.user_error.bin_visitor_guest": "您当前身份为游客,无权操作", "code_error.user_error.bin_visitor_guest": "您当前身份为游客,无权操作",
"code_error.user_error.un_auth_user": "找不到该用户", "code_error.user_error.un_auth_user": "找不到该用户",
"common.Action": "操作", "common.Action": "操作",
@ -1268,6 +1268,7 @@
"user.team.role.Visitor": "访客", "user.team.role.Visitor": "访客",
"user.team.role.writer": "可写成员", "user.team.role.writer": "可写成员",
"user.type": "类型", "user.type": "类型",
"user_leaved": "已离开",
"verification": "验证", "verification": "验证",
"workflow.template.communication": "通信", "workflow.template.communication": "通信",
"xx_search_result": "{{key}} 的搜索结果", "xx_search_result": "{{key}} 的搜索结果",

View File

@ -39,6 +39,7 @@
"classification": "分類", "classification": "分類",
"click_to_resume": "點選繼續", "click_to_resume": "點選繼續",
"code_editor": "程式碼編輯器", "code_editor": "程式碼編輯器",
"code_error.account_error": "帳號名稱或密碼錯誤",
"code_error.app_error.invalid_app_type": "無效的應用程式類型", "code_error.app_error.invalid_app_type": "無效的應用程式類型",
"code_error.app_error.invalid_owner": "非法的應用程式擁有者", "code_error.app_error.invalid_owner": "非法的應用程式擁有者",
"code_error.app_error.not_exist": "應用程式不存在", "code_error.app_error.not_exist": "應用程式不存在",
@ -95,7 +96,6 @@
"code_error.team_error.website_sync_not_enough": "無權使用網站同步", "code_error.team_error.website_sync_not_enough": "無權使用網站同步",
"code_error.token_error_code.403": "登入狀態無效,請重新登入", "code_error.token_error_code.403": "登入狀態無效,請重新登入",
"code_error.user_error.balance_not_enough": "帳戶餘額不足", "code_error.user_error.balance_not_enough": "帳戶餘額不足",
"code_error.account_error": "帳號名稱或密碼錯誤",
"code_error.user_error.bin_visitor_guest": "您目前身份為訪客,無權操作", "code_error.user_error.bin_visitor_guest": "您目前身份為訪客,無權操作",
"code_error.user_error.un_auth_user": "找不到此使用者", "code_error.user_error.un_auth_user": "找不到此使用者",
"common.Action": "操作", "common.Action": "操作",
@ -1273,6 +1273,7 @@
"user.team.role.Visitor": "訪客", "user.team.role.Visitor": "訪客",
"user.team.role.writer": "可寫入成員", "user.team.role.writer": "可寫入成員",
"user.type": "類型", "user.type": "類型",
"user_leaved": "已離開",
"verification": "驗證", "verification": "驗證",
"workflow.template.communication": "通訊", "workflow.template.communication": "通訊",
"xx_search_result": "{{key}} 的搜尋結果", "xx_search_result": "{{key}} 的搜尋結果",

View File

@ -31,6 +31,7 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { delLeaveTeam } from '@/web/support/user/team/api'; import { delLeaveTeam } from '@/web/support/user/team/api';
import { postSyncMembers } from '@/web/support/user/api'; import { postSyncMembers } from '@/web/support/user/api';
import MyLoading from '@fastgpt/web/components/common/MyLoading'; import MyLoading from '@fastgpt/web/components/common/MyLoading';
import { TeamMemberRoleEnum } from '@fastgpt/global/support/user/team/constant';
const InviteModal = dynamic(() => import('./InviteModal')); const InviteModal = dynamic(() => import('./InviteModal'));
const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal')); const TeamTagModal = dynamic(() => import('@/components/support/user/team/TeamTagModal'));
@ -169,84 +170,83 @@ function MemberTable({ Tabs }: { Tabs: React.ReactNode }) {
<Box flex={'1 0 0'} overflow={'auto'}> <Box flex={'1 0 0'} overflow={'auto'}>
<TableContainer overflow={'unset'} fontSize={'sm'}> <TableContainer overflow={'unset'} fontSize={'sm'}>
{MemberScrollData && ( <MemberScrollData>
<MemberScrollData> <Table overflow={'unset'}>
<Table overflow={'unset'}> <Thead>
<Thead> <Tr bgColor={'white !important'}>
<Tr bgColor={'white !important'}> <Th borderLeftRadius="6px" bgColor="myGray.100">
<Th borderLeftRadius="6px" bgColor="myGray.100"> {t('account_team:user_name')}
{t('account_team:user_name')} </Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th>
{!isSyncMember && (
<Th borderRightRadius="6px" bgColor="myGray.100">
{t('common:common.Action')}
</Th> </Th>
<Th bgColor="myGray.100">{t('account_team:member_group')}</Th> )}
</Tr>
</Thead>
<Tbody>
{members?.map((item) => (
<Tr key={item.userId} overflow={'unset'}>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) =>
group.members.map((m) => m.tmbId).includes(item.tmbId)
)
.map((g) => g.name)}
max={3}
/>
</Td>
{!isSyncMember && ( {!isSyncMember && (
<Th borderRightRadius="6px" bgColor="myGray.100"> <Td>
{t('common:common.Action')} {userInfo?.team.permission.hasManagePer &&
</Th> item.role !== TeamMemberRoleEnum.owner &&
item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)}
</Td>
)} )}
</Tr> </Tr>
</Thead> ))}
<Tbody> </Tbody>
{members?.map((item) => ( </Table>
<Tr key={item.userId} overflow={'unset'}> </MemberScrollData>
<Td>
<HStack>
<Avatar src={item.avatar} w={['18px', '22px']} borderRadius={'50%'} />
<Box className={'textEllipsis'}>
{item.memberName}
{item.status === 'waiting' && (
<Tag ml="2" colorSchema="yellow">
{t('account_team:waiting')}
</Tag>
)}
</Box>
</HStack>
</Td>
<Td maxW={'300px'}>
<GroupTags
names={groups
?.filter((group) =>
group.members.map((m) => m.tmbId).includes(item.tmbId)
)
.map((g) => g.name)}
max={3}
/>
</Td>
{!isSyncMember && (
<Td>
userInfo?.team.permission.hasManagePer && item.role !==
TeamMemberRoleEnum.owner && item.tmbId !== userInfo?.team.tmbId && (
<Icon
name={'common/trash'}
cursor={'pointer'}
w="1rem"
p="1"
borderRadius="sm"
_hover={{
color: 'red.600',
bgColor: 'myGray.100'
}}
onClick={() => {
openRemoveMember(
() =>
delRemoveMember(item.tmbId).then(() =>
Promise.all([refetchGroups(), refetchMembers()])
),
undefined,
t('account_team:remove_tip', {
username: item.memberName
})
)();
}}
/>
)
</Td>
)}
</Tr>
))}
</Tbody>
</Table>
</MemberScrollData>
)}
<ConfirmRemoveMemberModal /> <ConfirmRemoveMemberModal />
</TableContainer> </TableContainer>
</Box> </Box>

View File

@ -163,14 +163,20 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
<Flex justify={'space-between'} align={'center'} pb={'1rem'}> <Flex justify={'space-between'} align={'center'} pb={'1rem'}>
{Tabs} {Tabs}
</Flex> </Flex>
<MyBox flex={'1 0 0'} overflow={'auto'} isLoading={isLoadingOrgs}> <MyBox
flex={'1 0 0'}
h={0}
display={'flex'}
flexDirection={'column'}
isLoading={isLoadingOrgs}
>
<Box mb={3}> <Box mb={3}>
<Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} /> <Path paths={paths} rootName={userInfo?.team?.teamName} onClick={setParentPath} />
</Box> </Box>
<Flex w={'100%'} gap={'4'}> <Flex flex={'1 0 0'} h={0} w={'100%'} gap={'4'}>
{/* Table */} {/* Table */}
<TableContainer overflow={'unset'} fontSize={'sm'} flexGrow={1}> <TableContainer h={'100%'} overflowY={'auto'} fontSize={'sm'} flexGrow={1}>
<Table overflow={'unset'}> <Table>
<Thead> <Thead>
<Tr bg={'white !important'}> <Tr bg={'white !important'}>
<Th bg="myGray.100" borderLeftRadius="6px"> <Th bg="myGray.100" borderLeftRadius="6px">
@ -326,33 +332,33 @@ function OrgTable({ Tabs }: { Tabs: React.ReactNode }) {
</VStack> </VStack>
)} )}
</Flex> </Flex>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</MyBox> </MyBox>
{!!editOrg && (
<OrgInfoModal
editOrg={editOrg}
onClose={() => setEditOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!movingOrg && (
<OrgMoveModal
orgs={orgs}
movingOrg={movingOrg}
onClose={() => setMovingOrg(undefined)}
onSuccess={refetchOrgs}
/>
)}
{!!manageMemberOrg && (
<OrgMemberManageModal
currentOrg={manageMemberOrg}
refetchOrgs={refetchOrgs}
onClose={() => setManageMemberOrg(undefined)}
/>
)}
<ConfirmDeleteOrgModal />
<ConfirmDeleteMember />
</> </>
); );
} }

View File

@ -26,7 +26,7 @@ type TeamModalContextType = {
refetchTeams: () => void; refetchTeams: () => void;
refetchGroups: () => void; refetchGroups: () => void;
teamSize: number; teamSize: number;
MemberScrollData?: ReturnType<typeof useScrollPagination>['ScrollData']; MemberScrollData: ReturnType<typeof useScrollPagination>['ScrollData'];
}; };
export const TeamContext = createContext<TeamModalContextType>({ export const TeamContext = createContext<TeamModalContextType>({
@ -51,7 +51,7 @@ export const TeamContext = createContext<TeamModalContextType>({
}, },
teamSize: 0, teamSize: 0,
MemberScrollData: undefined MemberScrollData: () => <></>
}); });
export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => { export const TeamModalContextProvider = ({ children }: { children: ReactNode }) => {

View File

@ -61,72 +61,81 @@ const Team = () => {
return ( return (
<AccountContainer isLoading={isLoading}> <AccountContainer isLoading={isLoading}>
{/* header */} <Flex h={'100%'} flexDirection={'column'}>
<Flex {/* header */}
w={'100%'} <Flex
h={'3.5rem'} w={'100%'}
px={'1.56rem'} h={'3.5rem'}
py={'0.56rem'} px={'1.56rem'}
borderBottom={'1px solid'} py={'0.56rem'}
borderColor={'myGray.200'} borderBottom={'1px solid'}
bg={'myGray.25'} borderColor={'myGray.200'}
align={'center'} bg={'myGray.25'}
gap={6} align={'center'}
justify={'space-between'} gap={6}
> justify={'space-between'}
<Flex align={'center'}> >
<Flex gap={2} color={'myGray.900'}> <Flex align={'center'}>
<Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} /> <Flex gap={2} color={'myGray.900'}>
<Box fontWeight={'500'} fontSize={'1rem'}> <Icon name="support/user/usersLight" w={'1.25rem'} h={'1.25rem'} />
{t('account:team')} <Box fontWeight={'500'} fontSize={'1rem'}>
</Box> {t('account:team')}
</Flex> </Box>
<Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"
w="18px"
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
</Flex> </Flex>
)} <Flex align={'center'} ml={6}>
<TeamSelector height={'28px'} />
</Flex>
{userInfo?.team?.role === TeamMemberRoleEnum.owner && (
<Flex align={'center'} justify={'center'} ml={2} p={'0.44rem'}>
<MyIcon
name="edit"
w="18px"
cursor="pointer"
_hover={{
color: 'primary.500'
}}
onClick={() => {
if (!userInfo?.team) return;
setEditTeamData({
id: userInfo.team.teamId,
name: userInfo.team.teamName,
avatar: userInfo.team.avatar
});
}}
/>
</Flex>
)}
</Flex>
<Box
float={'right'}
color={'myGray.900'}
h={'1.25rem'}
px={'0.5rem'}
py={'0.125rem'}
fontSize={'0.75rem'}
borderRadius={'1.25rem'}
bg={'myGray.150'}
>
{t('account_team:total_team_members', { amount: teamSize })}
</Box>
</Flex> </Flex>
{/* table */}
<Box <Box
float={'right'} py={'1.5rem'}
color={'myGray.900'} px={'2rem'}
h={'1.25rem'} flex={'1 0 0'}
px={'0.5rem'} display={'flex'}
py={'0.125rem'} flexDirection={'column'}
fontSize={'0.75rem'} overflow={'auto'}
borderRadius={'1.25rem'}
bg={'myGray.150'}
> >
{t('account_team:total_team_members', { amount: teamSize })} {teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box> </Box>
</Flex> </Flex>
{/* table */}
<Box py={'1.5rem'} px={'2rem'}>
{teamTab === TeamTabEnum.member && <MemberTable Tabs={Tabs} />}
{teamTab === TeamTabEnum.org && <OrgManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.group && <GroupManage Tabs={Tabs} />}
{teamTab === TeamTabEnum.permission && <PermissionManage Tabs={Tabs} />}
</Box>
</AccountContainer> </AccountContainer>
); );
}; };

View File

@ -1,4 +1,5 @@
import { NextAPI } from '@/service/middleware/entry'; import { NextAPI } from '@/service/middleware/entry';
import { mongoSessionRun } from '@fastgpt/service/common/mongo/sessionRun';
import { authCert } from '@fastgpt/service/support/permission/auth/common'; import { authCert } from '@fastgpt/service/support/permission/auth/common';
import { MongoUser } from '@fastgpt/service/support/user/schema'; import { MongoUser } from '@fastgpt/service/support/user/schema';
import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema'; import { MongoTeamMember } from '@fastgpt/service/support/user/team/teamMemberSchema';
@ -19,18 +20,35 @@ export default NextAPI(handler);
const moveUserAvatar = async () => { const moveUserAvatar = async () => {
try { try {
const users = await MongoUser.find({}); const users = await MongoUser.find({}, '_id avatar');
console.log('Total users:', users.length);
let success = 0;
for await (const user of users) { for await (const user of users) {
await MongoTeamMember.updateOne( // @ts-ignore
{ if (!user.avatar) continue;
_id: user._id try {
}, await mongoSessionRun(async (session) => {
{ await MongoTeamMember.updateOne(
avatar: (user as any).avatar // 删除 avatar 字段, 因为 Type 改了,所以这里不能直接写 user.avatar {
} userId: user._id
); },
{
$set: {
avatar: (user as any).avatar // 删除 avatar 字段, 因为 Type 改了,所以这里不能直接写 user.avatar
}
},
{ session }
);
// @ts-ignore
user.avatar = undefined;
await user.save({ session });
});
success++;
console.log('Move avatar success:', success);
} catch (error) {
console.error(error);
}
} }
console.log('Move avatar success:', users.length);
} catch (error) { } catch (error) {
console.error(error); console.error(error);
} }

View File

@ -4,7 +4,7 @@ import {
getWorkflowVersionList, getWorkflowVersionList,
updateAppVersion updateAppVersion
} from '@/web/core/app/api/version'; } from '@/web/core/app/api/version';
import { useVirtualScrollPagination } from '@fastgpt/web/hooks/useScrollPagination'; import { useScrollPagination } from '@fastgpt/web/hooks/useScrollPagination';
import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer'; import CustomRightDrawer from '@fastgpt/web/components/common/MyDrawer/CustomRightDrawer';
import { useTranslation } from 'next-i18next'; import { useTranslation } from 'next-i18next';
import { Box, BoxProps, Button, Flex, Input } from '@chakra-ui/react'; import { Box, BoxProps, Button, Flex, Input } from '@chakra-ui/react';
@ -22,7 +22,6 @@ import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useToast } from '@fastgpt/web/hooks/useToast'; import { useToast } from '@fastgpt/web/hooks/useToast';
import type { AppVersionSchemaType, VersionListItemType } from '@fastgpt/global/core/app/version'; import type { AppVersionSchemaType, VersionListItemType } from '@fastgpt/global/core/app/version';
import type { SimpleAppSnapshotType } from './SimpleApp/useSnapshots'; import type { SimpleAppSnapshotType } from './SimpleApp/useSnapshots';
import UserBox from '@fastgpt/web/components/common/UserBox';
const PublishHistoriesSlider = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({ const PublishHistoriesSlider = <T extends SimpleAppSnapshotType | WorkflowSnapshotsType>({
onClose, onClose,
@ -183,18 +182,18 @@ const TeamCloud = ({
const { t } = useTranslation(); const { t } = useTranslation();
const { appDetail } = useContextSelector(AppContext, (v) => v); const { appDetail } = useContextSelector(AppContext, (v) => v);
const { scrollDataList, ScrollList, isLoading, setData } = useVirtualScrollPagination( const {
getWorkflowVersionList, ScrollData,
{ data: scrollDataList,
itemHeight: 40, setData,
overscan: 20, isLoading
} = useScrollPagination(getWorkflowVersionList, {
pageSize: 30, pageSize: 30,
defaultParams: { params: {
appId: appDetail._id appId: appDetail._id
} },
} refreshDeps: [appDetail._id]
); });
const [editIndex, setEditIndex] = useState<number | undefined>(undefined); const [editIndex, setEditIndex] = useState<number | undefined>(undefined);
const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined); const [hoveredIndex, setHoveredIndex] = useState<number | undefined>(undefined);
@ -231,14 +230,13 @@ const TeamCloud = ({
); );
return ( return (
<ScrollList isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}> <ScrollData isLoading={isLoading || isLoadingVersion} flex={'1 0 0'} px={5}>
{scrollDataList.map((data, index) => { {scrollDataList.map((item, index) => {
const item = data.data; const firstPublishedIndex = scrollDataList.findIndex((data) => data.isPublish);
const firstPublishedIndex = scrollDataList.findIndex((data) => data.data.isPublish);
return ( return (
<Flex <Flex
key={data.index} key={item._id}
alignItems={'center'} alignItems={'center'}
py={editIndex !== index ? 2 : 1} py={editIndex !== index ? 2 : 1}
px={3} px={3}
@ -260,7 +258,7 @@ const TeamCloud = ({
Trigger={ Trigger={
<Box> <Box>
<Avatar <Avatar
src={data.data.sourceMember.avatar} src={item.sourceMember.avatar}
borderRadius={'50%'} borderRadius={'50%'}
w={'24px'} w={'24px'}
h={'24px'} h={'24px'}
@ -269,10 +267,25 @@ const TeamCloud = ({
} }
> >
{() => ( {() => (
<Flex alignItems={'center'} h={'full'} pl={5} gap={3}> <Flex alignItems={'center'} h={'full'} pl={5} gap={2}>
<UserBox sourceMember={data.data.sourceMember} avatarSize="36px" fontSize="sm" /> <Box>
<Box fontSize={'12px'} color={'myGray.500'}> <Avatar
{formatTime2YMDHMS(item.time)} src={item.sourceMember.avatar}
borderRadius={'50%'}
w={'36px'}
h={'36px'}
/>
</Box>
<Box>
<Flex gap={1} fontSize={'sm'} color={'myGray.900'}>
<Box>{item.sourceMember.name}</Box>
{item.sourceMember.status === 'leave' && (
<Tag color="gray">{t('common:user_leaved')}</Tag>
)}
</Flex>
<Box fontSize={'xs'} mt={2} color={'myGray.500'}>
{formatTime2YMDHMS(item.time)}
</Box>
</Box> </Box>
</Flex> </Flex>
)} )}
@ -340,6 +353,6 @@ const TeamCloud = ({
</Flex> </Flex>
); );
})} })}
</ScrollList> </ScrollData>
); );
}; };