V4.8.18 feature (#3565)

* feat: org CRUD (#3380)

* feat: add org schema

* feat: org manage UI

* feat: OrgInfoModal

* feat: org tree view

* feat: org management

* fix: init root org

* feat: org permission for app

* feat: org support for dataset

* fix: disable org role control

* styles: opt type signatures

* fix: remove unused permission

* feat: delete org collaborator

* perf: Team org ui (#3499)

* perf: org ui

* perf: org ui

* feat: org auth for app & dataset (#3498)

* feat: auth org resource permission

* feat: org auth support for app & dataset

* perf: org permission check (#3500)

* i18n (#3501)

* name

* i18n

* feat: support dataset changeOwner (#3483)

* feat: support dataset changeOwner

* chore: update dataset change owner api

* feat: permission manage UI for org (#3503)

* perf: password check;perf: image upload check;perf: sso login check (#3509)

* perf: password check

* perf: image upload check

* perf: sso login check

* force show update notification modal & fix login page text (#3512)

* fix login page English text

* update notification modal

* perf: notify account (#3515)

* perf(plugin): improve searXNG empty result handling and documentation (#3507)

* perf(plugin): improve searXNG empty result handling and documentation

* 修改了文档和代码部分无搜索的结果的反馈

* refactor: org pathId (#3516)

* optimize payment process (#3517)

* feat: support wecom sso (#3518)

* feat: support wecom sso

* chore: remove unused wecom js-sdk dependency

* fix qrcode script (#3520)

* fix qrcode script

* i18n

* perf: full text collection and search code;perf: rename function (#3519)

* perf: full text collection and search code

* perf: rename function

* perf: notify modal

* remove invalid code

* perf: sso login

* perf: pay process

* 4.8.18 test (#3524)

* perf: remove local token

* perf: index

* perf: file encoding;perf: leave team code;@c121914yu perf: full text search code (#3528)

* perf: text encoding

* perf: leave team code

* perf: full text search code

* fix: http status

* perf: embedding search and vector avatar

* perf: async read file (#3531)

* refactor: team permission  manager (#3535)

* perf: classify org, group and member

* refactor: team per manager

* fix: missing functions

* 4.8.18 test (#3543)

* perf: login check

* doc

* perf: llm model config

* perf: team clb config

* fix: MemberModal UI (#3553)

* fix: adapt MemberModal title and icon

* fix: adapt member modal

* fix: search input placeholder

* fix: add button text

* perf: org permission (#3556)

* docs:用户答疑的官方文档补充 (#3540)

* docs:用户答疑的官方文档补充

* 问题回答的内容修补

* share link random avatar (#3541)

* share link random avatar

* fix

* delete unused code

* share page avatar (#3558)

* feat: init 4818

* share page avatar

* feat: tmp upgrade code (#3559)

* feat: tmp upgrade code

* fulltext search test

* update action

* full text tmp code (#3561)

* full text tmp code

* fix: init

* fix: init

* remove tmp code

* remove tmp code

* 4818-alpha

* 4.8.18 test (#3562)

* full text tmp code

* fix: init

* upgrade code

* account log

* account log

* perf: dockerfile

* upgrade code

* chore: update docs app template submission (#3564)

---------

Co-authored-by: a.e. <49438478+I-Info@users.noreply.github.com>
Co-authored-by: Finley Ge <32237950+FinleyGe@users.noreply.github.com>
Co-authored-by: heheer <heheer@sealos.io>
Co-authored-by: Jiangween <145003935+Jiangween@users.noreply.github.com>
This commit is contained in:
Archer
2025-01-11 15:15:38 +08:00
committed by GitHub
parent bb669ca3ff
commit 10d8c56e23
205 changed files with 5305 additions and 2428 deletions

View File

@@ -19,6 +19,9 @@ const UpdateInviteModal = dynamic(() => import('@/components/support/user/team/U
const NotSufficientModal = dynamic(() => import('@/components/support/wallet/NotSufficientModal'));
const SystemMsgModal = dynamic(() => import('@/components/support/user/inform/SystemMsgModal'));
const ImportantInform = dynamic(() => import('@/components/support/user/inform/ImportantInform'));
const UpdateNotification = dynamic(
() => import('@/components/support/user/inform/UpdateNotificationModal')
);
const pcUnShowLayoutRoute: Record<string, boolean> = {
'/': true,
@@ -48,9 +51,9 @@ export const navbarWidth = '64px';
const Layout = ({ children }: { children: JSX.Element }) => {
const router = useRouter();
const { Loading } = useLoading();
const { loading, feConfigs, isNotSufficientModal } = useSystemStore();
const { loading, feConfigs, notSufficientModalType } = useSystemStore();
const { isPc } = useSystem();
const { userInfo } = useUserStore();
const { userInfo, isUpdateNotification, setIsUpdateNotification } = useUserStore();
const { setUserDefaultLng } = useI18nLng();
const isChatPage = useMemo(
@@ -68,6 +71,11 @@ const Layout = ({ children }: { children: JSX.Element }) => {
const isHideNavbar = !!pcUnShowLayoutRoute[router.pathname];
const showUpdateNotification =
isUpdateNotification &&
!userInfo?.team.notificationAccount &&
!!userInfo?.team.permission.isOwner;
useMount(() => {
setUserDefaultLng();
});
@@ -113,8 +121,11 @@ const Layout = ({ children }: { children: JSX.Element }) => {
{feConfigs?.isPlus && (
<>
{!!userInfo && <UpdateInviteModal />}
{isNotSufficientModal && <NotSufficientModal />}
{notSufficientModalType && <NotSufficientModal type={notSufficientModalType} />}
{!!userInfo && <SystemMsgModal />}
{showUpdateNotification && (
<UpdateNotification onClose={() => setIsUpdateNotification(false)} />
)}
{!!userInfo && importantInforms.length > 0 && (
<ImportantInform informs={importantInforms} refetch={refetchUnRead} />
)}

View File

@@ -1,17 +1,13 @@
import React, { useCallback } from 'react';
import React from 'react';
import { ModalFooter, ModalBody, Input, Button, Box, Textarea, HStack } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal/index';
import { useTranslation } from 'next-i18next';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useForm } from 'react-hook-form';
import { compressImgFileAndUpload } from '@/web/common/file/controller';
import { MongoImageTypeEnum } from '@fastgpt/global/common/file/image/constants';
import { useSelectFile } from '@/web/common/file/hooks/useSelectFile';
import { getErrText } from '@fastgpt/global/common/error/utils';
import MyTooltip from '@fastgpt/web/components/common/MyTooltip';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useToast } from '@fastgpt/web/hooks/useToast';
export type EditResourceInfoFormType = {
id: string;
@@ -31,7 +27,6 @@ const EditResourceModal = ({
onEdit: (data: EditResourceInfoFormType) => any;
}) => {
const { t } = useTranslation();
const { toast } = useToast();
const { register, watch, setValue, handleSubmit } = useForm<EditResourceInfoFormType>({
defaultValues: defaultForm
});
@@ -46,31 +41,14 @@ const EditResourceModal = ({
}
);
const { File, onOpen: onOpenSelectFile } = useSelectFile({
const {
File,
onOpen: onOpenSelectFile,
onSelectImage
} = useSelectFile({
fileType: '.jpg,.png',
multiple: false
});
const onSelectFile = useCallback(
async (e: File[]) => {
const file = e[0];
if (!file) return;
try {
const src = await compressImgFileAndUpload({
type: MongoImageTypeEnum.appAvatar,
file,
maxW: 300,
maxH: 300
});
setValue('avatar', src);
} catch (err: any) {
toast({
title: getErrText(err, t('common:common.error.Select avatar failed')),
status: 'warning'
});
}
},
[setValue, t, toast]
);
return (
<MyModal isOpen onClose={onClose} iconSrc={avatar} title={title}>
@@ -108,7 +86,15 @@ const EditResourceModal = ({
</Button>
</ModalFooter>
<File onSelect={onSelectFile} />
<File
onSelect={(e) =>
onSelectImage(e, {
maxH: 300,
maxW: 300,
callback: (e) => setValue('avatar', e)
})
}
/>
</MyModal>
);
};

View File

@@ -7,7 +7,6 @@ import MyDivider from '@fastgpt/web/components/common/MyDivider';
import { useTranslation } from 'next-i18next';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { PermissionValueType } from '@fastgpt/global/support/permission/type';
import DefaultPermissionList from '@/components/support/permission/DefaultPerList';
import CollaboratorContextProvider, {
MemberManagerInputPropsType
} from '../../support/permission/MemberManager/context';
@@ -24,7 +23,6 @@ const FolderSlideCard = ({
deleteTip,
onDelete,
defaultPer,
managePer,
isInheritPermission,
resumeInheritPermission,
@@ -39,11 +37,6 @@ const FolderSlideCard = ({
deleteTip: string;
onDelete: () => void;
defaultPer?: {
value: PermissionValueType;
defaultValue: PermissionValueType;
onChange: (v: PermissionValueType) => Promise<any>;
};
managePer: MemberManagerInputPropsType;
isInheritPermission?: boolean;

View File

@@ -104,14 +104,9 @@ const ChatBox = ({
showVoiceIcon = true,
showEmptyIntro = false,
active = true,
shareId,
outLinkUid,
teamId,
teamToken,
onStartChat
}: Props) => {
const ScrollContainerRef = useRef<HTMLDivElement>(null);
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const { feConfigs } = useSystemStore();
@@ -925,10 +920,6 @@ const ChatBox = ({
isLastChild={index === chatRecords.length - 1}
{...{
showVoiceIcon,
shareId,
outLinkUid,
teamId,
teamToken,
statusBoxData,
questionGuides,
onMark: onMark(
@@ -1004,17 +995,13 @@ const ChatBox = ({
onCloseUserLike,
onMark,
onReadUserDislike,
outLinkUid,
questionGuides,
retryInput,
shareId,
showEmpty,
showMarkIcon,
showVoiceIcon,
statusBoxData,
t,
teamId,
teamToken,
userAvatar,
variableList?.length,
welcomeText

View File

@@ -250,7 +250,7 @@ const ApiKeyTable = ({ tips, appId }: { tips: string; appId?: string }) => {
<MyModal
isOpen={!!apiKey}
w={['400px', '600px']}
iconSrc="/imgs/modal/key.svg"
iconSrc="keyPrimary"
title={
<Box>
<Box fontWeight={'bold'}>{t('common:support.openapi.New api key')}</Box>
@@ -330,7 +330,7 @@ function EditKeyModal({
return (
<MyModal
isOpen={true}
iconSrc="/imgs/modal/key.svg"
iconSrc="keyPrimary"
title={isEdit ? t('publish:edit_api_key') : t('publish:create_api_key')}
>
<ModalBody>

View File

@@ -44,7 +44,7 @@ const ConfigPerModal = ({
<>
<MyModal
isOpen
iconSrc="/imgs/modal/key.svg"
iconSrc="keyPrimary"
onClose={onClose}
title={t('common:permission.Permission config')}
>

View File

@@ -1,308 +0,0 @@
import {
Flex,
Box,
ModalBody,
Checkbox,
ModalFooter,
Button,
Grid,
HStack
} from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useContextSelector } from 'use-context-selector';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import { useMemo, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import { CollaboratorContext } from './context';
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
export type AddModalPropsType = {
onClose: () => void;
mode?: 'member' | 'all';
};
function AddMemberModal({ onClose, mode = 'member' }: AddModalPropsType) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, myGroups } = useUserStore();
const { permissionList, collaboratorList, onUpdateCollaborators, getPerLabelList, permission } =
useContextSelector(CollaboratorContext, (v) => v);
const [searchText, setSearchText] = useState<string>('');
const { data: [members = [], groups = []] = [], loading: loadingMembersAndGroups } = useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return await Promise.all([loadAndGetTeamMembers(true), loadAndGetGroups(true)]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const filterMembers = useMemo(() => {
return members.filter((item) => {
if (item.tmbId === userInfo?.team?.tmbId) return false;
if (!searchText) return true;
return item.memberName.includes(searchText);
});
}, [members, searchText, userInfo?.team?.tmbId]);
const filterGroups = useMemo(() => {
if (mode !== 'all') return [];
return groups.filter((item) => {
if (permission.isOwner) return true; // owner can see all groups
if (myGroups.find((i) => String(i._id) === String(item._id))) return false;
if (!searchText) return true;
return item.name.includes(searchText);
});
}, [groups, searchText, myGroups, mode, permission]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const [selectedPermission, setSelectedPermission] = useState(permissionList['read'].value);
const perLabel = useMemo(() => {
return getPerLabelList(selectedPermission).join('、');
}, [getPerLabelList, selectedPermission]);
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() =>
onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
permission: selectedPermission
}),
{
successToast: t('common:common.Add Success'),
errorToast: 'Error',
onSuccess() {
onClose();
}
}
);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc="modal/AddClb"
title={t('user:team.add_collaborator')}
minW="800px"
h={'100%'}
isCentered
isLoading={loadingMembersAndGroups}
>
<ModalBody flex={'1'}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex flexDirection="column" borderRight="1px solid" borderColor="myGray.200" p="4">
<SearchInput
placeholder={t('user:search_user')}
bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)}
/>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{filterGroups.map((group) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
const collaborator = collaboratorList.find((v) => v.groupId === group._id);
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedGroupIdList.includes(group._id)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<Checkbox isChecked={selectedGroupIdList.includes(group._id)} />
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
{filterMembers.map((member) => {
const onChange = () => {
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
const collaborator = collaboratorList.find((v) => v.tmbId === member.tmbId);
return (
<HStack
justifyContent="space-between"
key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedMemberIdList.includes(member.tmbId)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<Checkbox
isChecked={selectedMemberIdList.includes(member.tmbId)}
icon={<MyIcon name={'common/check'} w={'12px'} />}
/>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
{!!collaborator && (
<PermissionTags permission={collaborator.permission.value} />
)}
</HStack>
);
})}
</Flex>
</Flex>
<Flex p="4" flexDirection="column">
<Box>
{t('user:has_chosen') + ': '}{' '}
{selectedMemberIdList.length + selectedGroupIdList.length}
</Box>
<Flex flexDirection="column" mt="2" overflow={'auto'} maxH="400px">
{selectedGroupIdList.map((groupId) => {
const onChange = () => {
setSelectedGroupIdList((state) => {
if (state.includes(groupId)) {
return state.filter((v) => v !== groupId);
}
return [...state, groupId];
});
};
const group = groups.find((v) => String(v._id) === groupId);
return (
<HStack
justifyContent="space-between"
key={groupId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={{
bgColor: 'myGray.50',
cursor: 'pointer',
...(!selectedGroupIdList.includes(groupId)
? { svg: { color: 'myGray.50' } }
: {})
}}
onClick={onChange}
>
<MyAvatar src={group?.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{group?.name === DefaultGroupName ? userInfo?.team.teamName : group?.name}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
);
})}
{selectedMemberIdList.map((tmbId) => {
const member = filterMembers.find((v) => v.tmbId === tmbId);
return member ? (
<HStack
justifyContent="space-between"
key={tmbId}
alignItems="center"
py="2"
px={3}
borderRadius={'md'}
_hover={{ bg: 'myGray.50' }}
onClick={() =>
setSelectedMembers(selectedMemberIdList.filter((v) => v !== tmbId))
}
>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius="50%" />
<Box w="full" ml={2}>
{member.memberName}
</Box>
<MyIcon
name="common/closeLight"
w="16px"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
/>
</HStack>
) : null;
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
<PermissionSelect
value={selectedPermission}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{t(perLabel as any)}
<ChevronDownIcon fontSize={'md'} />
</Flex>
}
onChange={(v) => setSelectedPermission(v)}
/>
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default AddMemberModal;

View File

@@ -1,19 +1,19 @@
import { ModalBody, Table, TableContainer, Tbody, Th, Thead, Tr, Td, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Flex, ModalBody, Table, TableContainer, Tbody, Td, Th, Thead, Tr } from '@chakra-ui/react';
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import MyIcon from '@fastgpt/web/components/common/Icon';
import Loading from '@fastgpt/web/components/common/MyLoading';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { CollaboratorContext } from './context';
import MyIcon from '@fastgpt/web/components/common/Icon';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useUserStore } from '@/web/support/user/useUserStore';
import EmptyTip from '@fastgpt/web/components/common/EmptyTip';
import Loading from '@fastgpt/web/components/common/MyLoading';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
export type ManageModalProps = {
onClose: () => void;
};
@@ -65,7 +65,7 @@ function ManageModal({ onClose }: ManageModalProps) {
>
<Td border="none">
<Flex alignItems="center">
<Avatar src={item.avatar} w="24px" mr={2} />
<Avatar src={item.avatar} rounded={'50%'} w="24px" mr={2} />
{item.name === DefaultGroupName ? userInfo?.team.teamName : item.name}
</Flex>
</Td>
@@ -85,14 +85,20 @@ function ManageModal({ onClose }: ManageModalProps) {
onUpdate({
members: item.tmbId ? [item.tmbId] : undefined,
groups: item.groupId ? [item.groupId] : undefined,
orgs: item.orgId ? [item.orgId] : undefined,
permission
});
}}
onDelete={() => {
onDelete({
tmbId: item.tmbId,
groupId: item.groupId
} as RequireOnlyOne<{ tmbId: string; groupId: string }>);
groupId: item.groupId,
orgId: item.orgId
} as RequireOnlyOne<{
tmbId: string;
groupId: string;
orgId: string;
}>);
}}
/>
)}

View File

@@ -1,13 +1,13 @@
import { Box, BoxProps, Flex } from '@chakra-ui/react';
import { useUserStore } from '@/web/support/user/useUserStore';
import { Box, type BoxProps, Flex } from '@chakra-ui/react';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import Avatar from '@fastgpt/web/components/common/Avatar';
import MyBox from '@fastgpt/web/components/common/MyBox';
import Tag, { type TagProps } from '@fastgpt/web/components/common/Tag';
import { useTranslation } from 'next-i18next';
import React from 'react';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
import Tag, { TagProps } from '@fastgpt/web/components/common/Tag';
import Avatar from '@fastgpt/web/components/common/Avatar';
import { useTranslation } from 'next-i18next';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import { useUserStore } from '@/web/support/user/useUserStore';
export type MemberListCardProps = BoxProps & { tagStyle?: Omit<TagProps, 'children'> };
@@ -31,12 +31,12 @@ const MemberListCard = ({ tagStyle, ...props }: MemberListCardProps) => {
{collaboratorList?.map((member) => {
return (
<Tag
key={member.tmbId || member.groupId}
key={member.tmbId || member.groupId || member.orgId}
type={'fill'}
colorSchema="white"
{...tagStyle}
>
<Avatar src={member.avatar} w="1.25rem" />
<Avatar src={member.avatar} w="1.25rem" rounded={'50%'} />
<Box fontSize={'sm'} ml={1}>
{member.name === DefaultGroupName ? userInfo?.team.teamName : member.name}
</Box>

View File

@@ -0,0 +1,512 @@
import { useUserStore } from '@/web/support/user/useUserStore';
import { ChevronDownIcon } from '@chakra-ui/icons';
import {
Box,
Button,
Checkbox,
Flex,
Grid,
HStack,
ModalBody,
ModalFooter,
Tag,
Text
} from '@chakra-ui/react';
import { DefaultGroupName } from '@fastgpt/global/support/user/team/group/constant';
import MyAvatar from '@fastgpt/web/components/common/Avatar';
import MyIcon from '@fastgpt/web/components/common/Icon';
import SearchInput from '@fastgpt/web/components/common/Input/SearchInput';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useTranslation } from 'next-i18next';
import { useMemo, useRef, useState } from 'react';
import PermissionSelect from './PermissionSelect';
import PermissionTags from './PermissionTags';
import {
DEFAULT_ORG_AVATAR,
DEFAULT_TEAM_AVATAR,
DEFAULT_USER_AVATAR
} from '@fastgpt/global/common/system/constants';
import Path from '@/components/common/folder/Path';
import { getOrgChildrenPath } from '@fastgpt/global/support/user/team/org/constant';
import { ParentTreePathItemType } from '@fastgpt/global/common/parentFolder/type';
import { OrgType } from '@fastgpt/global/support/user/team/org/type';
import { useContextSelector } from 'use-context-selector';
import { CollaboratorContext } from './context';
const HoverBoxStyle = {
bgColor: 'myGray.50',
cursor: 'pointer'
};
function MemberModal({
onClose,
addPermissionOnly: addOnly = false
}: {
onClose: () => void;
addPermissionOnly?: boolean;
}) {
const { t } = useTranslation();
const { userInfo, loadAndGetTeamMembers, loadAndGetGroups, loadAndGetOrgs } = useUserStore();
const collaboratorList = useContextSelector(CollaboratorContext, (v) => v.collaboratorList);
const [searchText, setSearchText] = useState<string>('');
const [filterClass, setFilterClass] = useState<'member' | 'org' | 'group'>();
const { data: [members = [], groups = [], orgs = []] = [], loading: loadingMembersAndGroups } =
useRequest2(
async () => {
if (!userInfo?.team?.teamId) return [[], []];
return Promise.all([
loadAndGetTeamMembers(true),
loadAndGetGroups(true),
loadAndGetOrgs(true)
]);
},
{
manual: false,
refreshDeps: [userInfo?.team?.teamId]
}
);
const [parentPath, setParentPath] = useState('');
const paths = useMemo(() => {
const splitPath = parentPath.split('/').filter(Boolean);
return splitPath
.map((id) => {
const org = orgs.find((org) => org.pathId === id)!;
if (org.path === '') return;
return {
parentId: getOrgChildrenPath(org),
parentName: org.name
};
})
.filter(Boolean) as ParentTreePathItemType[];
}, [parentPath, orgs]);
const [selectedOrgIdList, setSelectedOrgIdList] = useState<string[]>([]);
const currentOrg = useMemo(() => {
const splitPath = parentPath.split('/');
const currentOrgId = splitPath[splitPath.length - 1];
if (!currentOrgId) return;
return orgs.find((org) => org.pathId === currentOrgId);
}, [orgs, parentPath]);
const filterOrgs: (OrgType & { count?: number })[] = useMemo(() => {
if (searchText) return orgs.filter((item) => item.name.includes(searchText));
if (!searchText && filterClass !== 'org') return [];
if (parentPath === '') {
setParentPath(`/${orgs[0].pathId}`);
return [];
}
return orgs
.filter((org) => org.path === parentPath)
.map((item) => ({
...item,
count:
item.members.length + orgs.filter((org) => org.path === getOrgChildrenPath(item)).length
}));
}, [orgs, searchText, filterClass, parentPath]);
const [selectedMemberIdList, setSelectedMembers] = useState<string[]>([]);
const filterMembers = useMemo(() => {
if (searchText) return members.filter((item) => item.memberName.includes(searchText));
if (!searchText && filterClass !== 'member' && filterClass !== 'org') return [];
if (currentOrg && filterClass === 'org') {
return members.filter((item) => currentOrg.members.find((v) => v.tmbId === item.tmbId));
}
return members;
}, [members, searchText, filterClass, currentOrg]);
const [selectedGroupIdList, setSelectedGroupIdList] = useState<string[]>([]);
const filterGroups = useMemo(() => {
if (searchText) return groups.filter((item) => item.name.includes(searchText));
if (!searchText && filterClass !== 'group') return [];
return groups;
}, [groups, searchText, filterClass]);
const permissionList = useContextSelector(CollaboratorContext, (v) => v.permissionList);
const getPerLabelList = useContextSelector(CollaboratorContext, (v) => v.getPerLabelList);
const [selectedPermission, setSelectedPermission] = useState<number | undefined>(
permissionList?.read?.value
);
const perLabel = useMemo(() => {
if (selectedPermission === undefined) return '';
return getPerLabelList(selectedPermission!).join('、');
}, [getPerLabelList, selectedPermission]);
const onUpdateCollaborators = useContextSelector(
CollaboratorContext,
(v) => v.onUpdateCollaborators
);
const { runAsync: onConfirm, loading: isUpdating } = useRequest2(
() =>
onUpdateCollaborators({
members: selectedMemberIdList,
groups: selectedGroupIdList,
orgs: selectedOrgIdList,
permission: selectedPermission!
}),
{
successToast: t('common:common.Add Success'),
onSuccess() {
onClose();
}
}
);
const entryList = useRef([
{ label: t('user:team.group.members'), icon: DEFAULT_USER_AVATAR, value: 'member' },
{ label: t('user:team.org.org'), icon: DEFAULT_ORG_AVATAR, value: 'org' },
{ label: t('user:team.group.group'), icon: DEFAULT_TEAM_AVATAR, value: 'group' }
]);
const selectedList = useMemo(() => {
const selectedOrgs = orgs.filter((org) => selectedOrgIdList.includes(org._id));
const selectedGroups = groups.filter((group) => selectedGroupIdList.includes(group._id));
const selectedMembers = members.filter((member) => selectedMemberIdList.includes(member.tmbId));
return [
...selectedOrgs.map((item) => ({
id: `org-${item._id}`,
avatar: item.avatar,
name: item.name,
onDelete: () => setSelectedOrgIdList(selectedOrgIdList.filter((v) => v !== item._id))
})),
...selectedGroups.map((item) => ({
id: `group-${item._id}`,
avatar: item.avatar,
name: item.name === DefaultGroupName ? userInfo?.team.teamName : item.name,
onDelete: () => setSelectedGroupIdList(selectedGroupIdList.filter((v) => v !== item._id))
})),
...selectedMembers.map((item) => ({
id: `member-${item.tmbId}`,
avatar: item.avatar,
name: item.memberName,
onDelete: () => setSelectedMembers(selectedMemberIdList.filter((v) => v !== item.tmbId))
}))
];
}, [
orgs,
groups,
members,
selectedOrgIdList,
selectedGroupIdList,
selectedMemberIdList,
userInfo?.team.teamName
]);
return (
<MyModal
isOpen
onClose={onClose}
iconSrc={addOnly ? 'keyPrimary' : 'modal/AddClb'}
title={addOnly ? t('user:team.add_permission') : t('user:team.add_collaborator')}
minW="800px"
h={'100%'}
maxH={'90vh'}
isCentered
isLoading={loadingMembersAndGroups}
>
<ModalBody flex={'1'}>
<Grid
border="1px solid"
borderColor="myGray.200"
borderRadius="0.5rem"
gridTemplateColumns="1fr 1fr"
h={'100%'}
>
<Flex
h={'100%'}
flexDirection="column"
borderRight="1px solid"
borderColor="myGray.200"
p="4"
>
<SearchInput
placeholder={t('user:search_group_org_user')}
bgColor="myGray.50"
onChange={(e) => setSearchText(e.target.value)}
/>
<Flex flexDirection="column" mt="3" overflow={'auto'} flex={'1 0 0'} h={0}>
{!searchText && !filterClass && (
<>
{entryList.current.map((item) => {
return (
<HStack
key={item.value}
justifyContent="space-between"
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
_notLast={{ mb: 1 }}
onClick={() => setFilterClass(item.value as any)}
>
<MyAvatar src={item.icon} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{item.label}
</Box>
<MyIcon name="core/chat/chevronRight" w="16px" />
</HStack>
);
})}
</>
)}
{/* Path */}
{!searchText && filterClass && (
<Box mb={1}>
<Path
paths={[
{
parentId: filterClass,
parentName:
filterClass === 'member'
? t('user:team.group.members')
: filterClass === 'org'
? t('user:team.org.org')
: t('user:team.group.group')
},
...paths
]}
onClick={(parentId) => {
if (parentId === '') {
setFilterClass(undefined);
setParentPath('');
} else if (
parentId === 'member' ||
parentId === 'org' ||
parentId === 'group'
) {
setFilterClass(parentId);
setParentPath('');
} else {
setParentPath(parentId);
}
}}
rootName={t('common:common.Team')}
/>
</Box>
)}
<Flex flexDirection={'column'} gap={1} userSelect={'none'}>
{filterMembers.map((member) => {
const collaborator = collaboratorList?.find((v) => v.tmbId === member.tmbId);
const disabled = addOnly && collaborator !== undefined;
const onChange = () => {
if (disabled) return;
setSelectedMembers((state) => {
if (state.includes(member.tmbId)) {
return state.filter((v) => v !== member.tmbId);
}
return [...state, member.tmbId];
});
};
return (
<HStack
justifyContent="space-between"
key={member.tmbId}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isDisabled={disabled}
isChecked={disabled || selectedMemberIdList.includes(member.tmbId)}
pointerEvents="none"
/>
<MyAvatar src={member.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{member.memberName}
</Box>
<PermissionTags
permission={addOnly ? undefined : collaborator?.permission.value}
/>
</HStack>
);
})}
{filterOrgs.map((org) => {
const collaborator = collaboratorList?.find((v) => v.orgId === org._id);
const disabled = addOnly && collaborator !== undefined;
const onChange = () => {
if (disabled) return;
setSelectedOrgIdList((state) => {
if (state.includes(org._id)) {
return state.filter((v) => v !== org._id);
}
return [...state, org._id];
});
};
return (
<HStack
justifyContent="space-between"
key={org._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isDisabled={disabled}
isChecked={disabled || selectedOrgIdList.includes(org._id)}
pointerEvents="none"
/>
<MyAvatar src={org.avatar} w="1.5rem" borderRadius={'50%'} />
<HStack ml="2" w="full" gap="5px">
<Text>{org.name}</Text>
{org.count && (
<Tag size="sm" my="auto">
{org.count}
</Tag>
)}
</HStack>
<PermissionTags
permission={addOnly ? undefined : collaborator?.permission.value}
/>
{org.count && (
<MyIcon
name="core/chat/chevronRight"
w="16px"
p="4px"
rounded={'6px'}
_hover={{
bgColor: 'myGray.200'
}}
onClick={(e) => {
e.stopPropagation();
setParentPath(getOrgChildrenPath(org));
}}
/>
)}
</HStack>
);
})}
{filterGroups.map((group) => {
const collaborator = collaboratorList?.find((v) => v.groupId === group._id);
const disabled = addOnly && collaborator !== undefined;
const onChange = () => {
if (disabled) return;
setSelectedGroupIdList((state) => {
if (state.includes(group._id)) {
return state.filter((v) => v !== group._id);
}
return [...state, group._id];
});
};
return (
<HStack
justifyContent="space-between"
key={group._id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
onClick={onChange}
>
<Checkbox
isDisabled={disabled}
isChecked={disabled || selectedGroupIdList.includes(group._id)}
pointerEvents="none"
/>
<MyAvatar src={group.avatar} w="1.5rem" borderRadius={'50%'} />
<Box ml="2" w="full">
{group.name === DefaultGroupName ? userInfo?.team.teamName : group.name}
</Box>
<PermissionTags
permission={addOnly ? undefined : collaborator?.permission.value}
/>
</HStack>
);
})}
</Flex>
</Flex>
</Flex>
<Flex h={'100%'} p="4" flexDirection="column">
<Box>
{`${t('user:has_chosen')}: `}
{selectedMemberIdList.length + selectedGroupIdList.length + selectedOrgIdList.length}
</Box>
<Flex flexDirection="column" mt="2" gap={1} overflow={'auto'} flex={'1 0 0'} h={0}>
{selectedList.map((item) => {
return (
<HStack
justifyContent="space-between"
key={item.id}
py="2"
px="3"
borderRadius="sm"
alignItems="center"
_hover={HoverBoxStyle}
>
<MyAvatar src={item.avatar} w="1.5rem" borderRadius={'50%'} />
<Box w="full" ml="2">
{item.name}
</Box>
<MyIcon
name="common/closeLight"
w="1rem"
cursor={'pointer'}
_hover={{
color: 'red.600'
}}
onClick={item.onDelete}
/>
</HStack>
);
})}
</Flex>
</Flex>
</Grid>
</ModalBody>
<ModalFooter>
{!addOnly && !!permissionList && (
<PermissionSelect
value={selectedPermission}
Button={
<Flex
alignItems={'center'}
bg={'myGray.50'}
border="base"
fontSize={'sm'}
px={3}
borderRadius={'md'}
h={'32px'}
>
{t(perLabel as any)}
<ChevronDownIcon fontSize={'md'} />
</Flex>
}
onChange={(v) => setSelectedPermission(v)}
/>
)}
{addOnly && (
<HStack bg={'blue.50'} color={'blue.600'} padding={'6px 12px'} rounded={'5px'}>
<MyIcon name="common/info" w="1rem" h="1rem" />
<Text fontSize="12px">{t('user:permission_add_tip')}</Text>
</HStack>
)}
<Button isLoading={isUpdating} ml="4" h={'32px'} onClick={onConfirm}>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
);
}
export default MemberModal;

View File

@@ -49,13 +49,16 @@ function PermissionSelect({
onDelete
}: PermissionSelectProps) {
const { t } = useTranslation();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const ref = useRef<HTMLButtonElement>(null);
const closeTimer = useRef<NodeJS.Timeout>();
const { permission, permissionList } = useContextSelector(CollaboratorContext, (v) => v);
const [isOpen, setIsOpen] = useState(false);
const permissionSelectList = useMemo(() => {
if (!permissionList) return { singleCheckBoxList: [], multipleCheckBoxList: [] };
const list = Object.entries(permissionList).map(([_, value]) => {
return {
name: value.name,
@@ -77,6 +80,8 @@ function PermissionSelect({
};
}, [permission.isOwner, permissionList]);
const selectedSingleValue = useMemo(() => {
if (!permissionList) return undefined;
const per = new Permission({ per: value });
if (per.hasManagePer) return permissionList['manage'].value;
@@ -107,7 +112,7 @@ function PermissionSelect({
}
});
return (
return selectedSingleValue !== undefined ? (
<Menu offset={offset} isOpen={isOpen} autoSelect={false} direction={'ltr'}>
<Box
w="fit-content"
@@ -241,7 +246,7 @@ function PermissionSelect({
</MenuList>
</Box>
</Menu>
);
) : null;
}
export default React.memo(PermissionSelect);

View File

@@ -7,12 +7,15 @@ import { CollaboratorContext } from './context';
import { useTranslation } from 'next-i18next';
export type PermissionTagsProp = {
permission: PermissionValueType;
permission?: PermissionValueType;
};
function PermissionTags({ permission }: PermissionTagsProp) {
const { getPerLabelList } = useContextSelector(CollaboratorContext, (v) => v);
const { t } = useTranslation();
if (permission === undefined) return null;
const perTagList = getPerLabelList(permission);
return (

View File

@@ -1,32 +1,37 @@
import { useDisclosure } from '@chakra-ui/react';
import {
import type {
CollaboratorItemType,
UpdateClbPermissionProps
} from '@fastgpt/global/support/permission/collaborator';
import { PermissionList } from '@fastgpt/global/support/permission/constant';
import { Permission } from '@fastgpt/global/support/permission/controller';
import { PermissionListType, PermissionValueType } from '@fastgpt/global/support/permission/type';
import { ReactNode, useCallback } from 'react';
import type {
PermissionListType,
PermissionValueType
} from '@fastgpt/global/support/permission/type';
import { type ReactNode, useCallback } from 'react';
import { createContext } from 'use-context-selector';
import dynamic from 'next/dynamic';
import MemberListCard, { MemberListCardProps } from './MemberListCard';
import MemberListCard, { type MemberListCardProps } from './MemberListCard';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { useConfirm } from '@fastgpt/web/hooks/useConfirm';
import { useI18n } from '@/web/context/I18n';
import { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const AddMemberModal = dynamic(() => import('./AddMemberModal'));
import type { RequireOnlyOne } from '@fastgpt/global/common/type/utils';
const MemberModal = dynamic(() => import('./MemberModal'));
const ManageModal = dynamic(() => import('./ManageModal'));
export type MemberManagerInputPropsType = {
permission: Permission;
onGetCollaboratorList: () => Promise<CollaboratorItemType[]>;
permissionList: PermissionListType;
permissionList?: PermissionListType;
onUpdateCollaborators: (props: UpdateClbPermissionProps) => Promise<any>;
onDelOneCollaborator: (props: RequireOnlyOne<{ tmbId: string; groupId: string }>) => Promise<any>;
onDelOneCollaborator: (
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => Promise<any>;
refreshDeps?: any[];
mode?: 'member' | 'all';
};
export type MemberManagerPropsType = MemberManagerInputPropsType & {
@@ -46,19 +51,19 @@ type CollaboratorContextType = MemberManagerPropsType & {};
export const CollaboratorContext = createContext<CollaboratorContextType>({
collaboratorList: [],
permissionList: PermissionList,
onUpdateCollaborators: function () {
onUpdateCollaborators: () => {
throw new Error('Function not implemented.');
},
onDelOneCollaborator: function () {
onDelOneCollaborator: () => {
throw new Error('Function not implemented.');
},
getPerLabelList: function (): string[] {
getPerLabelList: (): string[] => {
throw new Error('Function not implemented.');
},
refetchCollaboratorList: function (): void {
refetchCollaboratorList: (): void => {
throw new Error('Function not implemented.');
},
onGetCollaboratorList: function (): Promise<CollaboratorItemType[]> {
onGetCollaboratorList: (): Promise<CollaboratorItemType[]> => {
throw new Error('Function not implemented.');
},
isFetchingCollaborator: false,
@@ -76,19 +81,20 @@ const CollaboratorContextProvider = ({
refreshDeps = [],
isInheritPermission,
hasParent,
mode = 'member'
addPermissionOnly
}: MemberManagerInputPropsType & {
children: (props: ChildrenProps) => ReactNode;
refetchResource?: () => void;
isInheritPermission?: boolean;
hasParent?: boolean;
addPermissionOnly?: boolean;
}) => {
const onUpdateCollaboratorsThen = async (props: UpdateClbPermissionProps) => {
await onUpdateCollaborators(props);
refetchCollaboratorList();
};
const onDelOneCollaboratorThen = async (
props: RequireOnlyOne<{ tmbId: string; groupId: string }>
props: RequireOnlyOne<{ tmbId: string; groupId: string; orgId: string }>
) => {
await onDelOneCollaborator(props);
refetchCollaboratorList();
@@ -116,6 +122,8 @@ const CollaboratorContextProvider = ({
const getPerLabelList = useCallback(
(per: PermissionValueType) => {
if (!permissionList) return [];
const Per = new Permission({ per });
const labels: string[] = [];
@@ -123,7 +131,7 @@ const CollaboratorContextProvider = ({
labels.push(permissionList['manage'].name);
} else if (Per.hasWritePer) {
labels.push(permissionList['write'].name);
} else {
} else if (Per.hasReadPer) {
labels.push(permissionList['read'].name);
}
@@ -198,12 +206,12 @@ const CollaboratorContextProvider = ({
MemberListCard
})}
{isOpenAddMember && (
<AddMemberModal
<MemberModal
onClose={() => {
onCloseAddMember();
refetchResource?.();
}}
mode={mode}
addPermissionOnly={addPermissionOnly}
/>
)}
{isOpenManageModal && (

View File

@@ -0,0 +1,111 @@
import React from 'react';
import { ModalBody, Box, Flex, Input, ModalFooter, Button, HStack } from '@chakra-ui/react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { useForm } from 'react-hook-form';
import { useRequest2 } from '@fastgpt/web/hooks/useRequest';
import { updateNotificationAccount } from '@/web/support/user/api';
import Icon from '@fastgpt/web/components/common/Icon';
import { useSendCode } from '@/web/support/user/hooks/useSendCode';
import { useUserStore } from '@/web/support/user/useUserStore';
import { useSystemStore } from '@/web/common/system/useSystemStore';
type FormType = {
account: string;
verifyCode: string;
};
const UpdateNotificationModal = ({ onClose }: { onClose: () => void }) => {
const { t } = useTranslation();
const { initUserInfo } = useUserStore();
const { feConfigs } = useSystemStore();
const { register, handleSubmit, watch } = useForm<FormType>({
defaultValues: {
account: '',
verifyCode: ''
}
});
const account = watch('account');
const verifyCode = watch('verifyCode');
const { runAsync: onSubmit, loading: isLoading } = useRequest2(
(data: FormType) => {
return updateNotificationAccount(data);
},
{
onSuccess() {
initUserInfo();
onClose();
},
successToast: t('common:support.user.info.bind_notification_success'),
errorToast: t('common:support.user.info.bind_notification_error')
}
);
const { SendCodeBox } = useSendCode({ type: 'bindNotification' });
const placeholder = feConfigs?.bind_notification_method
?.map((item) => {
switch (item) {
case 'email':
return t('common:support.user.login.Email');
case 'phone':
return t('common:support.user.login.Phone number');
}
})
.join('/');
return (
<>
<MyModal
isOpen
iconSrc="common/settingLight"
w={'32rem'}
title={t('common:support.user.info.notification_receiving_hint')}
>
<ModalBody px={10}>
<Flex flexDirection="column">
<HStack px="6" py="3" color="primary.600" bgColor="primary.50" borderRadius="md">
<Icon name="common/info" w="1rem" />
<Box fontSize={'sm'}>{t('common:support.user.info.bind_notification_hint')}</Box>
</HStack>
<Flex mt="4" alignItems="center">
<Box flex={'0 0 70px'}>{t('common:user.Account')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('account', { required: true })}
placeholder={placeholder}
></Input>
</Flex>
<Flex mt="6" alignItems="center" position={'relative'}>
<Box flex={'0 0 70px'}>{t('common:support.user.info.verification_code')}</Box>
<Input
flex={1}
bg={'myGray.50'}
{...register('verifyCode', { required: true })}
placeholder={t('common:support.user.info.code_required')}
></Input>
<SendCodeBox username={account} />
</Flex>
</Flex>
</ModalBody>
<ModalFooter>
<Button mr={3} variant={'whiteBase'} onClick={onClose}>
{t('common:common.Cancel')}
</Button>
<Button
isLoading={isLoading}
isDisabled={!account || !verifyCode}
onClick={handleSubmit((data) => onSubmit(data))}
>
{t('common:common.Confirm')}
</Button>
</ModalFooter>
</MyModal>
</>
);
};
export default UpdateNotificationModal;

View File

@@ -1,35 +1,153 @@
import React from 'react';
import React, { useMemo, useState } from 'react';
import MyModal from '@fastgpt/web/components/common/MyModal';
import { useTranslation } from 'next-i18next';
import { Button, ModalBody, ModalFooter } from '@chakra-ui/react';
import { useRouter } from 'next/router';
import { useSystemStore } from '@/web/common/system/useSystemStore';
import { Box, Button, Flex, ModalBody, ModalFooter, useDisclosure } from '@chakra-ui/react';
import { NotSufficientModalType, useSystemStore } from '@/web/common/system/useSystemStore';
import ExtraPlan from '@/pages/price/components/ExtraPlan';
import StandardPlan from '@/pages/price/components/Standard';
import FillRowTabs from '@fastgpt/web/components/common/Tabs/FillRowTabs';
import FormLabel from '@fastgpt/web/components/common/MyBox/FormLabel';
import { useUserStore } from '@/web/support/user/useUserStore';
import { standardSubLevelMap } from '@fastgpt/global/support/wallet/sub/constants';
import { TeamErrEnum } from '@fastgpt/global/common/error/code/team';
import { useMount } from 'ahooks';
const NotSufficientModal = () => {
const NotSufficientModal = ({ type }: { type: NotSufficientModalType }) => {
const { t } = useTranslation();
const router = useRouter();
const { setIsNotSufficientModal } = useSystemStore();
const { setNotSufficientModalType } = useSystemStore();
const onClose = () => setIsNotSufficientModal(false);
const onClose = () => setNotSufficientModalType(undefined);
const {
isOpen: isRechargeModalOpen,
onOpen: onRechargeModalOpen,
onClose: onRechargeModalClose
} = useDisclosure();
const textMap = {
[TeamErrEnum.aiPointsNotEnough]: t('common:support.wallet.Not sufficient'),
[TeamErrEnum.datasetSizeNotEnough]: t('common:support.wallet.Dataset_not_sufficient'),
[TeamErrEnum.datasetAmountNotEnough]: t('common:support.wallet.Dataset_amount_not_sufficient'),
[TeamErrEnum.teamMemberOverSize]: t('common:support.wallet.Team_member_over_size'),
[TeamErrEnum.appAmountNotEnough]: t('common:support.wallet.App_amount_not_sufficient')
};
return (
<MyModal isOpen iconSrc="common/confirm/deleteTip" title={t('common:common.Warning')}>
<ModalBody>{t('common:support.wallet.Not sufficient')}</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
onClick={() => {
router.push('/account/info');
onClose();
}}
>
{t('common:support.wallet.To read plan')}
</Button>
</ModalFooter>
</MyModal>
<>
<MyModal
isOpen
iconSrc="common/confirm/deleteTip"
title={t('common:common.Warning')}
w={'420px'}
>
<ModalBody>{textMap[type]}</ModalBody>
<ModalFooter>
<Button variant={'whiteBase'} mr={2} onClick={onClose}>
{t('common:common.Close')}
</Button>
<Button
onClick={() => {
onRechargeModalOpen();
}}
>
{t('common:support.wallet.To read plan')}
</Button>
</ModalFooter>
</MyModal>
{isRechargeModalOpen && (
<RechargeModal onClose={onRechargeModalClose} onPaySuccess={onClose} />
)}
</>
);
};
export default NotSufficientModal;
const RechargeModal = ({
onClose,
onPaySuccess
}: {
onClose: () => void;
onPaySuccess: () => void;
}) => {
const { t } = useTranslation();
const { teamPlanStatus, initTeamPlanStatus } = useUserStore();
useMount(() => {
initTeamPlanStatus();
});
const planName = useMemo(() => {
if (!teamPlanStatus?.standard?.currentSubLevel) return '';
return standardSubLevelMap[teamPlanStatus.standard.currentSubLevel].label;
}, [teamPlanStatus?.standard?.currentSubLevel]);
const [tab, setTab] = useState<'standard' | 'extra'>('standard');
return (
<MyModal
isOpen
iconSrc="common/wallet"
iconColor={'primary.600'}
title={t('common:user.Pay')}
onClose={onClose}
isCentered
minW={'90%'}
maxH={'90%'}
>
<ModalBody px={'52px'}>
<Flex alignItems={'center'} mb={6}>
<FormLabel fontSize={'16px'} fontWeight={'medium'}>
{t('common:support.wallet.subscription.Current plan')}
</FormLabel>
<Box fontSize={'14px'} ml={5} color={'myGray.900'}>
{t(planName as any)}
</Box>
</Flex>
<Flex alignItems={'center'} mb={6}>
<FormLabel fontSize={'16px'} fontWeight={'medium'}>
{t('common:info.resource')}
</FormLabel>
<Flex fontSize={'14px'} ml={5} color={'myGray.900'}>
<Box>{`${t('common:support.user.team.Dataset usage')}:`}</Box>
<Box
ml={2}
>{`${teamPlanStatus?.usedDatasetSize} / ${teamPlanStatus?.datasetMaxSize || t('account_info:unlimited')}`}</Box>
<Box ml={5}>{`${t('common:support.wallet.subscription.AI points usage')}:`}</Box>
<Box
ml={2}
>{`${Math.round(teamPlanStatus?.usedPoints || 0)} / ${teamPlanStatus?.totalPoints || t('account_info:unlimited')}`}</Box>
</Flex>
</Flex>
<FillRowTabs
list={[
{ label: t('common:support.wallet.subscription.Sub plan'), value: 'standard' },
{ label: t('common:support.wallet.subscription.Extra plan'), value: 'extra' }
]}
value={tab}
onChange={(e) => {
setTab(e as 'standard' | 'extra');
}}
/>
<Box
mt={3}
p={8}
bg={'myGray.50'}
border={'1px solid'}
borderColor={'myGray.200'}
rounded={'12px'}
>
{tab === 'standard' ? (
<StandardPlan standardPlan={teamPlanStatus?.standard} onPaySuccess={onPaySuccess} />
) : (
<ExtraPlan onPaySuccess={onPaySuccess} />
)}
</Box>
</ModalBody>
</MyModal>
);
};

View File

@@ -1,5 +1,5 @@
import MyModal from '@fastgpt/web/components/common/MyModal';
import React, { useEffect, useRef } from 'react';
import React, { useCallback, useEffect, useRef } from 'react';
import { useTranslation } from 'next-i18next';
import { Box, ModalBody } from '@chakra-ui/react';
import { checkBalancePayResult } from '@/web/support/wallet/bill/api';
@@ -7,6 +7,8 @@ import { useToast } from '@fastgpt/web/hooks/useToast';
import { useRouter } from 'next/router';
import { getErrText } from '@fastgpt/global/common/error/utils';
import LightTip from '@fastgpt/web/components/common/LightTip';
import Script from 'next/script';
import { getWebReqUrl } from '@fastgpt/web/common/system/utils';
export type QRPayProps = {
readPrice: number;
@@ -23,25 +25,25 @@ const QRCodePayModal = ({
billId,
onSuccess
}: QRPayProps & { tip?: string; onSuccess?: () => any }) => {
const router = useRouter();
const { t } = useTranslation();
const { toast } = useToast();
const dom = useRef<HTMLDivElement>(null);
const drawCode = useCallback(() => {
if (dom.current && window.QRCode && !dom.current.innerHTML) {
new window.QRCode(dom.current, {
text: codeUrl,
width: qrCodeSize,
height: qrCodeSize,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
}
}, [codeUrl]);
useEffect(() => {
let timer: NodeJS.Timeout;
const drawCode = () => {
if (dom.current && window.QRCode && !dom.current.innerHTML) {
new window.QRCode(dom.current, {
text: codeUrl,
width: qrCodeSize,
height: qrCodeSize,
colorDark: '#000000',
colorLight: '#ffffff',
correctLevel: window.QRCode.CorrectLevel.H
});
}
};
const check = async () => {
try {
const res = await checkBalancePayResult(billId);
@@ -52,9 +54,6 @@ const QRCodePayModal = ({
title: res,
status: 'success'
});
setTimeout(() => {
router.reload();
}, 1000);
return;
} catch (error) {
toast({
@@ -73,18 +72,26 @@ const QRCodePayModal = ({
check();
return () => clearTimeout(timer);
}, [billId, onSuccess, toast]);
}, [billId, drawCode, onSuccess, toast]);
return (
<MyModal isOpen title={t('common:user.Pay')} iconSrc="/imgs/modal/pay.svg">
<ModalBody textAlign={'center'} pb={10} whiteSpace={'pre-wrap'}>
{tip && <LightTip text={tip} mb={8} textAlign={'left'} />}
<Box ref={dom} id={'payQRCode'} display={'inline-block'} h={`${qrCodeSize}px`}></Box>
<Box mt={5} textAlign={'center'}>
{t('common:pay.wechat', { price: readPrice })}
</Box>
</ModalBody>
</MyModal>
<>
<Script
src={getWebReqUrl('/js/qrcode.min.js')}
strategy="lazyOnload"
onLoad={drawCode}
></Script>
<MyModal isOpen title={t('common:user.Pay')} iconSrc="/imgs/modal/pay.svg">
<ModalBody textAlign={'center'} pb={10} whiteSpace={'pre-wrap'}>
{tip && <LightTip text={tip} mb={8} textAlign={'left'} />}
<Box ref={dom} id={'payQRCode'} display={'inline-block'} h={`${qrCodeSize}px`}></Box>
<Box mt={5} textAlign={'center'}>
{t('common:pay.wechat', { price: readPrice })}
</Box>
</ModalBody>
</MyModal>
</>
);
};